ELO is a small TypeScript library for calculating Elo ratings — the system used in chess and many other competitive games to measure relative skill.
It follows FIDE rules out of the box: K-factors by age, games played, and rating tier; a 400-point rating difference cap; and performance rating calculation. Zero runtime dependencies.
Most Elo libraries on npm give you the basic formula and nothing else. If you're building a chess platform or any app that follows FIDE rules, you'd have to implement the rest yourself. This library ships it all out of the box:
If you don't need FIDE compliance, any Elo library will do. If you do, this is the one.
npm install @echecs/elo
import { update } from '@echecs/elo';
// Two players both rated 1400. Player A wins.
const [playerA, playerB] = update(1400, 1400, 1);
console.log(playerA); // 1410
console.log(playerB); // 1390
Basic — when you just need new ratings after a game:
import { update } from '@echecs/elo';
const [newRatingA, newRatingB] = update(1400, 1400, 1); // → [1410, 1390]
Player options — pass a player object when you need to apply FIDE K-factor rules for age or experience:
import { update } from '@echecs/elo';
// Young player (age < 18, rating < 2300) gets K=40
const [newRatingA, newRatingB] = update({ age: 15, rating: 1400 }, 1400, 1); // → [1420, 1390]
// New player (≤ 30 games played) also gets K=40
const [newRatingC, newRatingD] = update(
{ gamesPlayed: 10, rating: 1400 },
1400,
1,
); // → [1420, 1390]
Game options — pass a game object as the third argument when the game type affects the K-factor:
import { update } from '@echecs/elo';
// Blitz and rapid games always use K=20
const [newRatingA, newRatingB] = update(2400, 2400, {
gameType: 'blitz',
result: 1,
}); // → [2410, 2390]
K-factor cap — pass gamesInPeriod when a player has played many games in
the current rating period and the §8.3.3 cap applies:
import { update } from '@echecs/elo';
// New player (K=40) with 18 games played in the period — K is capped to 38
const [newRatingA, newRatingB] = update(
{ gamesInPeriod: 18, gamesPlayed: 0, rating: 1400 },
1400,
1,
); // → [1419, 1390]
Performance rating — use performance() to calculate a player's FIDE
performance rating over a tournament:
import { performance } from '@echecs/elo';
const rating = performance([
{ opponentRating: 1600, result: 1 },
{ opponentRating: 1600, result: 0.5 },
{ opponentRating: 1600, result: 0 },
]); // 1600
Initial rating — use initial() to calculate a new (unrated) player's first
FIDE rating. If the player scored zero points across their entire first event,
omit all games from that event before calling (§8.2.1):
import { initial } from '@echecs/elo';
const rating = initial([
{ opponentRating: 1600, result: 1 },
{ opponentRating: 1500, result: 0.5 },
{ opponentRating: 1700, result: 0 },
{ opponentRating: 1600, result: 1 },
{ opponentRating: 1550, result: 1 },
]); // → 1752
Full API reference is available at https://echecsjs.github.io/elo/
update(a, b, resultOrOptions)Updates the Elo ratings of two players after a game. Returns a tuple
[ratingA, ratingB] with the new ratings rounded per FIDE §8.3.4.
function update(
a: number | PlayerOptions,
b: number | PlayerOptions,
resultOrOptions: Result | GameOptions,
): [ratingA: number, ratingB: number];
kFactor(options)Returns the FIDE K-factor for a player given their profile. Decision order:
gameType === 'blitz' | 'rapid' → 20gamesPlayed <= 30 or (age < 18 && rating < 2300) → 40rating >= 2400 || everHigher2400 → 10If gamesInPeriod is set and K × gamesInPeriod > 700, K is reduced to
Math.floor(700 / gamesInPeriod) per §8.3.3.
function kFactor(options: KFactorOptions): number;
expected(a, b)Returns the expected win probability for player A against player B, using the FIDE §8.1.2 discrete lookup table. Rating differences are clamped to ±400 unless either player is rated ≥ 2650 (§8.3.1).
function expected(a: number, b: number): number;
delta(actual, expected, k)Returns the raw rating change: k * (actual - expected). Use this when you need
the unrounded delta before applying it to a rating.
function delta(actual: number, expected: number, k: number): number;
performance(games)Calculates a player's FIDE performance rating (§8.2.3) over a series of games.
Throws RangeError if games is empty or contains invalid result values.
function performance(games: ResultAndOpponent[]): number;
initial(games)Calculates an unrated player's first FIDE published rating (§8.2). Injects two
hypothetical 1800-rated draws, caps the result at 2200. Throws RangeError if
games is empty.
function initial(games: ResultAndOpponent[]): number;
Resulttype Result = 0 | 0.5 | 1;
Game outcome from the perspective of player A: 1 = win, 0.5 = draw, 0 =
loss.
GameTypetype GameType = 'blitz' | 'rapid' | 'standard';
GameOptionsOptions for a single game passed as the third argument to update().
interface GameOptions {
gameType?: GameType;
result: Result;
}
PlayerOptionsPer-player options for update(). All fields except rating are optional.
interface PlayerOptions {
age?: number; // player's age — affects K-factor for juniors (< 18)
everHigher2400?: boolean; // whether the player has ever been rated ≥ 2400
gamesInPeriod?: number; // games played this rating period — used for §8.3.3 cap
gamesPlayed?: number; // total career games — new players (≤ 30) get K=40
k?: number; // override computed K-factor entirely
rating: number; // current rating
}
The k field bypasses all FIDE K-factor logic and uses the supplied value
directly.
KFactorOptionsOptions for kFactor(). Mirrors PlayerOptions minus the k override.
interface KFactorOptions {
age?: number;
everHigher2400?: boolean;
gameType?: GameType;
gamesInPeriod?: number;
gamesPlayed?: number;
rating: number;
}
ResultAndOpponentA single game result used by performance() and initial().
interface ResultAndOpponent {
opponentRating: number;
result: Result;
}