UNPKG

tiebreak

Version:

Calculation of chess tournament tiebreaks compliant with FIDE regulations

141 lines (122 loc) 4.02 kB
export type Score = 0 | 0.5 | 1 /** * Unique identifier for a player. Exact format is up to the caller. */ export type PlayerId = string | number /** * Results of a tournament round. */ export type RoundResults = { /** * Pairings for this round. */ pairings: Pairing[] /** * IDs of players who were allocated a bye by the pairing algorithm. This is usually * one player if the number of players is odd and no player if the number of players * is even. */ pairingAllocatedByes?: PlayerId[] /** * Some tournaments have a half-point bye system where players can request a bye and receive * half a point. */ halfPointByes?: PlayerId[] } export type Pairing = { white: PlayerId black: PlayerId scoreWhite: Score scoreBlack: Score forfeited: boolean } export enum TournamentType { // TODO: Add support for round robin tournaments (where no unplayed games adjustments are made). SWISS = "SWISS", } /** * Immutable representation of a tournament's pairings and results. */ export class Results { private pairings: Map<PlayerId, Map<number, PlayerResult>> = new Map() constructor( public readonly tournamentType: TournamentType, results: RoundResults[], ) { for (const [roundIndex, roundResults] of results.entries()) { for (const pairing of roundResults.pairings) { this.addToMap(pairing.white, roundIndex + 1, { score: pairing.scoreWhite, opponentScore: pairing.scoreBlack, opponent: pairing.black, forfeited: pairing.forfeited, }) this.addToMap(pairing.black, roundIndex + 1, { score: pairing.scoreBlack, opponentScore: pairing.scoreWhite, opponent: pairing.white, forfeited: pairing.forfeited, }) } for (const player of roundResults.pairingAllocatedByes ?? []) { this.addToMap(player, roundIndex + 1, "allocated-bye") } for (const player of roundResults.halfPointByes ?? []) { this.addToMap(player, roundIndex + 1, "half-point-bye") } } } private addToMap(playerId: PlayerId, round: number, result: PlayerResult) { if (!this.pairings.has(playerId)) { this.pairings.set(playerId, new Map()) } if (this.pairings.get(playerId)?.has(round)) { throw new Error(`Multiple results in round ${round} for player ${playerId}`) } this.pairings.get(playerId)?.set(round, result) } /** * Returns the result for the given player and round. */ public get(playerId: PlayerId, round: number): PlayerResult { return this.pairings.get(playerId)?.get(round) ?? "unpaired" } /** * Returns results for a player for each round up to the given maxRound. */ public getAll(playerId: PlayerId, maxRound: number): PlayerResult[] { return Array.from(Array(maxRound).keys()).map((i) => this.get(playerId, i + 1)) } public allPlayers(): PlayerId[] { return Array.from(this.pairings.keys()) } } /** * Pairing from the view of the player. */ export interface PlayerPairing { score: Score opponentScore: Score opponent: PlayerId forfeited: boolean } /** * A round's result from the view of the player. */ export type PlayerResult = PlayerPairing | "allocated-bye" | "half-point-bye" | "unpaired" export function isPaired(result: PlayerResult): result is PlayerPairing { return typeof result === "object" } export function isPlayed(result: PlayerResult): result is PlayerPairing { return isPaired(result) && !result.forfeited } export function isForfeitLoss(result: PlayerResult): boolean { return isPaired(result) && result.forfeited && result.score === 0 } /** * Voluntarily Unplayed Rounds (VUR) are defined in the FIDE regulations and in some cases * handled differently from rounds in which the player was "available to play". */ export function isVoluntarilyUnplayedRound(result: PlayerResult): boolean { return result === "unpaired" || result === "half-point-bye" || isForfeitLoss(result) }