UNPKG

tiebreak

Version:

Calculation of chess tournament tiebreaks compliant with FIDE regulations

182 lines (181 loc) 6.23 kB
import { isPaired, isPlayed, isVoluntarilyUnplayedRound } from "./results.js"; var Tiebreak = /* @__PURE__ */ ((Tiebreak2) => { Tiebreak2["SCORE"] = "SCORE"; Tiebreak2["BUCHHOLZ"] = "BH"; Tiebreak2["BUCHHOLZ_CUT1"] = "BH-C1"; Tiebreak2["BUCHHOLZ_CUT2"] = "BH-C2"; Tiebreak2["BUCHHOLZ_MEDIAN1"] = "BH-M1"; Tiebreak2["BUCHHOLZ_MEDIAN2"] = "BH-M2"; return Tiebreak2; })(Tiebreak || {}); var UnplayedRoundsAdjustment = /* @__PURE__ */ ((UnplayedRoundsAdjustment2) => { UnplayedRoundsAdjustment2["NONE"] = "NONE"; UnplayedRoundsAdjustment2["FIDE_2023"] = "FIDE_2023"; UnplayedRoundsAdjustment2["FIDE_2009"] = "FIDE_2009"; return UnplayedRoundsAdjustment2; })(UnplayedRoundsAdjustment || {}); class Tiebreaker { constructor(results, unplayedRoundsAdjustment) { this.results = results; this.unplayedRoundsAdjustment = unplayedRoundsAdjustment; } /** * Calculates a full ranking based on the given tiebreaks. The returned array is sorted * by rank. */ ranking(round, tiebreaks) { const ranking = this.results.allPlayers().map((playerId) => { const scores = tiebreaks.map((t) => this.tiebreak(t, playerId, round)); return { playerId, scores, rank: 1 }; }); const compareScores = (a, b) => { for (let i = 0; i < a.scores.length && i < b.scores.length; i++) { if (a.scores[i] !== b.scores[i]) { return b.scores[i] - a.scores[i]; } } return 0; }; ranking.sort((a, b) => { const cmp = compareScores(a, b); return cmp === 0 ? a.playerId.toString().localeCompare(b.playerId.toString()) : cmp; }); for (let i = 1; i < ranking.length; i++) { if (compareScores(ranking[i - 1], ranking[i]) === 0) { ranking[i].rank = ranking[i - 1].rank; } else { ranking[i].rank = i + 1; } } return ranking; } /** * Calculates the specified tiebreak. This is just a more generic short-hand method for calling * tiebreak methods directly. */ tiebreak(tiebreak, player, round) { switch (tiebreak) { case "SCORE" /* SCORE */: return this.score(player, round); case "BH" /* BUCHHOLZ */: return this.buchholz(player, round); case "BH-C1" /* BUCHHOLZ_CUT1 */: return this.buchholz(player, round, 1, 0); case "BH-C2" /* BUCHHOLZ_CUT2 */: return this.buchholz(player, round, 2, 0); case "BH-M1" /* BUCHHOLZ_MEDIAN1 */: return this.buchholz(player, round, 1, 1); case "BH-M2" /* BUCHHOLZ_MEDIAN2 */: return this.buchholz(player, round, 2, 2); } } /** * Returns the total points the given player scored by the given round. */ score(player, round) { return this.sum( this.results.getAll(player, round).map((result) => { return this.scoreForResult(result); }) ); } /** * Returns the players score with unplayed rounds adjusted according to the configured UnplayedRoundsAdjustment. */ adjustedScore(player, round) { switch (this.unplayedRoundsAdjustment) { case "NONE" /* NONE */: return this.score(player, round); case "FIDE_2023" /* FIDE_2023 */: { const lastAvailableToPlayRound = this.lastAvailableToPlayRound(player, round); return this.score(player, lastAvailableToPlayRound) + (round - lastAvailableToPlayRound) * 0.5; } case "FIDE_2009" /* FIDE_2009 */: return this.sum( this.results.getAll(player, round).map((result) => { return isPlayed(result) ? result.score : 0.5; }) ); } } /** * Returns the highest round number in which the player was "available to play", i.e. they didn't * voluntarily not play the round. Returns 0 if the player was never available to play. */ lastAvailableToPlayRound(player, maxRound) { for (let round = maxRound; round >= 1; round--) { if (!isVoluntarilyUnplayedRound(this.results.get(player, round))) { return round; } } return 0; } /** * Returns all opponents of the given player with their adjusted scores for the purpose of * calculating tiebreaks like Buchholz and SoBerg. */ adjustedGames(player, round) { return this.results.getAll(player, round).map((result, index) => { const game = { round: index + 1, gameScore: this.scoreForResult(result), opponentScore: isPaired(result) ? this.adjustedScore(result.opponent, round) : 0, isVur: isVoluntarilyUnplayedRound(result) }; if (!isPlayed(result)) { if (this.unplayedRoundsAdjustment === "FIDE_2023" /* FIDE_2023 */) { game.opponentScore = this.score(player, round); } if (this.unplayedRoundsAdjustment === "FIDE_2009" /* FIDE_2009 */) { const initialScore = this.score(player, game.round - 1); const opponentGameScore = 1 - game.gameScore; const virtualPoints = (round - game.round) * 0.5; game.opponentScore = initialScore + opponentGameScore + virtualPoints; } } return game; }); } /** * Buchholz score. Note that unplayed games are adjusted according to the configured UnplayedRoundsAdjustment. */ buchholz(player, round, cutLowest = 0, cutHighest = 0) { let games = this.adjustedGames(player, round); if (cutLowest || cutHighest) { games.sort((a, b) => { if (this.unplayedRoundsAdjustment === "FIDE_2023" /* FIDE_2023 */ && a.isVur !== b.isVur) { return Number(a.isVur) - Number(b.isVur); } return b.opponentScore - a.opponentScore; }); games = games.slice(cutHighest, -cutLowest); } return this.sum(games.map((g) => g.opponentScore)); } // TODO: Maybe turn PlayerResult into a class which returns the score? scoreForResult(result) { switch (result) { case "unpaired": return 0; case "allocated-bye": return 1; case "half-point-bye": return 0.5; default: return result.score; } } sum(numbers) { return numbers.reduce((prev, curr) => prev + curr, 0); } } export { Tiebreak, Tiebreaker, UnplayedRoundsAdjustment }; //# sourceMappingURL=tiebreak.js.map