API Reference

import {
  rating,
  rate,
  ordinal,
  predictWin,
  predictDraw,
  predictRank,
} from 'openskill'

rating(options?)

Create a new rating.

rating(options?: { mu?: number; sigma?: number }): Rating
rating()                          // { mu: 25, sigma: 8.333... }
rating({ mu: 1500, sigma: 200 })  // { mu: 1500, sigma: 200 }

rate(teams, options?)

Update ratings after a match. teams is an array of arrays — each inner array is one team. By default the team listed first won; use rank or score for arbitrary orderings.

rate(
  teams: Rating[][],
  options?: {
    model?: typeof plackettLuce
      | typeof bradleyTerryFull
      | typeof bradleyTerryPart
      | typeof thurstoneMostellerFull
      | typeof thurstoneMostellerPart
    rank?: number[]
    score?: number[]
    margin?: number
    weight?: number[][]
    weightBounds?: [number, number] | null
    tau?: number
    limitSigma?: boolean
    balance?: boolean
    gamma?: Gamma
    mu?: number
    sigma?: number
    beta?: number
    epsilon?: number
    kappa?: number
    windowSize?: number
  }
): Rating[][]

Default usage — first team wins

const [[a1n, a2n], [b1n, b2n]] = rate([
  [a1, a2],
  [b1, b2],
])

With ranks

Lower rank = better placement. Equal ranks are ties.

rate([[a1], [b1], [c1], [d1]], { rank: [4, 1, 3, 2] })
// → 🐌 🥇 🥉 🥈

With scores

Higher score = better. Equal scores are ties.

rate([[a1], [b1], [c1], [d1]], { score: [37, 19, 37, 42] })
// → 🥈 🐌 🥈 🥇

With victory margins

When both score and margin are given, rating updates are amplified for wins by more than margin points. A 20-point blowout moves ratings more than a 1-point squeaker; the amplification grows logarithmically, so extreme blowouts don't produce runaway swings. Score gaps within the margin update as ordinary wins/losses.

rate(
  [
    [a1, a2],
    [b1, b2],
  ],
  { score: [37, 12], margin: 10 }
)

With partial-play weights

weight scales each player's rating update by their contribution to the match — useful when a player only played part of a game, or you have a per-player contribution metric (damage dealt, objectives captured, minutes played). It's shaped like teams: one number per player.

rate(
  [
    [a1, a2],
    [b1, b2],
  ],
  {
    weight: [
      [1, 0.5], // a2 only played half the game
      [1, 1],
    ],
  }
)

By default each team's weights are normalized into [1, 2], so only the relative differences within a team matter — matching openskill.py bit-for-bit. Change the range with weightBounds: [min, max], or pass weightBounds: null to apply your raw weights as-is.

With tau and limitSigma

tau (default 25/300) is an additive dynamics factor that keeps sigma from collapsing to zero over many games, so ratings stay responsive to skill changes. A side effect is that sigma can increase, which can make a player's ordinal drop even after a win. Set limitSigma: true to clamp sigma so it never rises.

rate(teams, { tau: 0.3, limitSigma: true })

With balance

balance: true weights weaker players' contributions to their team's rating more heavily, emphasizing skill disparity within a team.

rate(teams, { balance: true })

With windowSize

By default the partial-pairing models (bradleyTerryPart and thurstoneMostellerPart) compare each team only with the teams directly above and below it in the ranking, and add those updates. In this mode, tied teams can get different updates, and the result can change with the order in which you list the tied teams.

Set windowSize to use the sliding window from openskill.py instead. Each team is then compared with the teams at most windowSize places above or below it, and the model averages those comparisons. openskill.py uses 4, so set windowSize: 4 to get the same results. A larger window compares more teams, so its accuracy is nearer to a full-pairing model, but it costs more time. The other models ignore this option.

When the window reaches all teams (windowSize is teams.length - 1 or more), tied teams with the same ratings get the same update, and the order of tied teams has no effect, apart from floating-point rounding. In a larger game, tied teams can still get different updates, because their windows cover different teams. The next major version will make windowSize: 4 the default.

rate(teams, { model: bradleyTerryPart, windowSize: 4 })

Custom gamma

Advanced: pass a gamma function to override how much each team's sigma shrinks per update. The default is sigma / c (where c is the total uncertainty in the match).

ordinal(rating, options?)

Reduce a rating to a single number for display or sorting.

ordinal(r: Rating, options?: { z?: number; alpha?: number; target?: number }): number
ordinal({ mu: 25, sigma: 8.333 }) // ≈ 0.0

predictWin(teams, options?)

Returns an array of probabilities, one per team, that sums to 1.

const a1 = rating()
const b1 = rating({ mu: 33.564, sigma: 1.123 })

predictWin([[a1], [b1]])
// [0.2021..., 0.7978...]

predictDraw(teams, options?)

Returns the relative likelihood that a match between these teams ends in a draw. Useful for matchmaking quality scores.

predictDraw([[a1], [b1]])
// 0.2164...

predictRank(teams, options?)

Returns, for each team, its predicted placement and the probability of that placement.

const a1 = rating()
const b1 = rating({ mu: 30, sigma: 5 })
const c1 = rating({ mu: 20, sigma: 8 })

predictRank([[a1], [b1], [c1]])
// [[2, 0.3276...], [1, 0.4867...], [3, 0.1855...]]

All three predictors accept the same hyperparameter options as rate (mu, sigma, beta, balance, …) so predictions stay consistent with however you rate.

Selecting a model

import { rate } from 'openskill'
import { bradleyTerryFull } from 'openskill/models'

rate(teams, { model: bradleyTerryFull })

See Models for guidance on choosing one.