From 2e440966eec72953947c5730e1a32c28b0bd455b Mon Sep 17 00:00:00 2001 From: Marcel Juenemann Date: Wed, 11 Dec 2024 16:08:13 +0000 Subject: [PATCH 1/4] Sonneborn-Berger implemented and passing FIDE test cases --- src/__tests__/tiebreak.test.ts | 64 +++++++++++++++++++++++++++++----- src/tiebreak.ts | 17 +++++++-- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/__tests__/tiebreak.test.ts b/src/__tests__/tiebreak.test.ts index 73b98e6..2c33cb9 100644 --- a/src/__tests__/tiebreak.test.ts +++ b/src/__tests__/tiebreak.test.ts @@ -189,18 +189,66 @@ describe("TiebreakCalculation", () => { }) /* - TODO: Test more modifiers with old FIDE unplayed rounds adjustments - - Cut-1 - - Median-1 - - Cut-2 - - Very high cut numbers + TODO: Test more modifiers with old FIDE unplayed rounds adjustments + - Cut-1 + - Median-1 + - Cut-2 + - Very high cut numbers - describe("with modifiers", () => { + describe("with modifiers", () => { - + }) + */ + }) + describe("sonnebornBerger", () => { + describe("with FIDE_2023 unplayed rounds adjustment", () => { + it("should pass FIDE exercise 11 (2023)", async () => { + const rounds = await readTestCases("fide-exercise-2023") + const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023) + expect(tiebreak.sonnebornBerger("1", 5)).toEqual(8) + expect(tiebreak.sonnebornBerger("3", 5)).toEqual(10.5) + expect(tiebreak.sonnebornBerger("16", 5)).toEqual(7.25) + expect(tiebreak.sonnebornBerger("4", 5)).toEqual(9.75) + }) + + it("should pass FIDE exercise 12 (2023)", async () => { + const rounds = await readTestCases("fide-exercise-2023") + const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023) + expect(tiebreak.adjustedScore("2", 5)).toEqual(4) + expect(tiebreak.sonnebornBerger("2", 5)).toEqual(9.5) + expect(tiebreak.adjustedScore("3", 5)).toEqual(3.5) + expect(tiebreak.sonnebornBerger("3", 5)).toEqual(10.5) + expect(tiebreak.adjustedScore("4", 5)).toEqual(3.5) + expect(tiebreak.sonnebornBerger("4", 5)).toEqual(9.75) + expect(tiebreak.adjustedScore("1", 5)).toEqual(3.5) + expect(tiebreak.sonnebornBerger("1", 5)).toEqual(8) + expect(tiebreak.adjustedScore("16", 5)).toEqual(3.5) + expect(tiebreak.sonnebornBerger("16", 5)).toEqual(7.25) + expect(tiebreak.adjustedScore("6", 5)).toEqual(3) + expect(tiebreak.sonnebornBerger("6", 5)).toEqual(6.5) + expect(tiebreak.adjustedScore("11", 5)).toEqual(2.5) + expect(tiebreak.sonnebornBerger("11", 5)).toEqual(5.75) + expect(tiebreak.adjustedScore("8", 5)).toEqual(2.5) + expect(tiebreak.sonnebornBerger("8", 5)).toEqual(5.25) + expect(tiebreak.adjustedScore("5", 5)).toEqual(2.5) + expect(tiebreak.sonnebornBerger("5", 5)).toEqual(4.25) + expect(tiebreak.adjustedScore("14", 5)).toEqual(2) + expect(tiebreak.sonnebornBerger("14", 5)).toEqual(4.5) + expect(tiebreak.adjustedScore("12", 5)).toEqual(3.0) + expect(tiebreak.sonnebornBerger("12", 5)).toEqual(4.0) + expect(tiebreak.adjustedScore("15", 5)).toEqual(2) + expect(tiebreak.sonnebornBerger("15", 5)).toEqual(3.5) + expect(tiebreak.adjustedScore("13", 5)).toEqual(1.5) + expect(tiebreak.sonnebornBerger("13", 5)).toEqual(4.25) + expect(tiebreak.adjustedScore("7", 5)).toEqual(1.5) + expect(tiebreak.sonnebornBerger("7", 5)).toEqual(3.25) + expect(tiebreak.adjustedScore("10", 5)).toEqual(1.0) + expect(tiebreak.sonnebornBerger("10", 5)).toEqual(1.5) + }) }) - */ + + // TODO: Test with FIDE_2009 and NONE unplayed rounds adjustments. }) describe("ranking", () => { diff --git a/src/tiebreak.ts b/src/tiebreak.ts index 0020f75..47146f4 100644 --- a/src/tiebreak.ts +++ b/src/tiebreak.ts @@ -2,6 +2,7 @@ import { PlayerId, PlayerResult, Results, + Score, isPaired, isPlayed, isVoluntarilyUnplayedRound, @@ -64,8 +65,8 @@ export interface PlayerRanking { interface AdjustedGame { round: number - gameScore: number - opponentScore: number + gameScore: Score + opponentScore: Score isVur: boolean } @@ -252,6 +253,18 @@ export class Tiebreaker { return this.sum(games.map((g) => g.opponentScore)) } + + /** + * Sonneborn-Berger score. Note that unplayed games are adjusted according to the configured UnplayedRoundsAdjustment. + */ + public sonnebornBerger( + player: PlayerId, + round: number + ): number { + let games = this.adjustedGames(player, round) + return this.sum(games.map((g) => g.opponentScore * g.gameScore)) + } + // TODO: Maybe turn PlayerResult into a class which returns the score? private scoreForResult(result: PlayerResult): number { switch (result) { From 4d0110e24ef90a007077194038f0e4dab7d17a08 Mon Sep 17 00:00:00 2001 From: Marcel Juenemann Date: Wed, 11 Dec 2024 16:26:32 +0000 Subject: [PATCH 2/4] Sonneborn-Beger added to Ranking --- src/__tests__/tiebreak.test.ts | 10 +++++----- src/tiebreak.ts | 9 +++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/__tests__/tiebreak.test.ts b/src/__tests__/tiebreak.test.ts index 2c33cb9..fe286ae 100644 --- a/src/__tests__/tiebreak.test.ts +++ b/src/__tests__/tiebreak.test.ts @@ -269,12 +269,12 @@ describe("TiebreakCalculation", () => { ]) const tiebreak = new Tiebreaker(results, UnplayedRoundsAdjustment.FIDE_2023) expect( - tiebreak.ranking(2, [Tiebreak.SCORE, Tiebreak.BUCHHOLZ, Tiebreak.BUCHHOLZ_CUT1]), + tiebreak.ranking(2, [Tiebreak.SCORE, Tiebreak.BUCHHOLZ, Tiebreak.BUCHHOLZ_CUT1, Tiebreak.SONNEBORG_BERGER]), ).toEqual([ - { playerId: "B", rank: 1, scores: [2, 2.5, 2] }, // BH: 2 for unplayed + 0.5 for A - { playerId: "C", rank: 2, scores: [1.5, 2, 1.5] }, // BH: 0.5 for A + 1.5 for bye - { playerId: "A", rank: 3, scores: [0.5, 3.5, 2] }, // BH: 1.5 for C + 2 for B - { playerId: "D", rank: 4, scores: [0, 0, 0] }, // BH: 2 * 0 for unplayed rounds + { playerId: "B", rank: 1, scores: [2, 2.5, 2, 2.5] }, // BH: 2 for unplayed + 0.5 for A + { playerId: "C", rank: 2, scores: [1.5, 2, 1.5, 1.75] }, // BH: 0.5 for A + 1.5 for bye + { playerId: "A", rank: 3, scores: [0.5, 3.5, 2, 0.75] }, // BH: 1.5 for C + 2 for B + { playerId: "D", rank: 4, scores: [0, 0, 0, 0] }, // BH: 2 * 0 for unplayed rounds ]) }) diff --git a/src/tiebreak.ts b/src/tiebreak.ts index 47146f4..fdb218d 100644 --- a/src/tiebreak.ts +++ b/src/tiebreak.ts @@ -28,6 +28,12 @@ export enum Tiebreak { BUCHHOLZ_CUT2 = "BH-C2", BUCHHOLZ_MEDIAN1 = "BH-M1", BUCHHOLZ_MEDIAN2 = "BH-M2", + + /** + * Calculated by adding, for each round, a value given by multiplying the final + * score of the opponents by the points scored against them. + */ + SONNEBORG_BERGER = "SB", } export enum UnplayedRoundsAdjustment { @@ -137,6 +143,9 @@ export class Tiebreaker { return this.buchholz(player, round, 1, 1) case Tiebreak.BUCHHOLZ_MEDIAN2: return this.buchholz(player, round, 2, 2) + + case Tiebreak.SONNEBORG_BERGER: + return this.sonnebornBerger(player, round) } } From 6454f187a132770d11974c09568c108e10adbe0d Mon Sep 17 00:00:00 2001 From: Marcel Juenemann Date: Wed, 11 Dec 2024 16:29:45 +0000 Subject: [PATCH 3/4] Sonneborn-Berger: cleanup --- README.md | 1 + src/__tests__/tiebreak.test.ts | 7 ++++++- src/tiebreak.ts | 12 ++++-------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a220983..9d33960 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Tiebreak is a TypeScript library for calculating various tournament tiebreak sco - Buchholz - Buchholz with modifiers (Cut-1, Cut-2, Median-1 etc.) +- Sonneborn-Berger - (more to come) **Configurable adjustments for unplayed games:** diff --git a/src/__tests__/tiebreak.test.ts b/src/__tests__/tiebreak.test.ts index fe286ae..c216d1f 100644 --- a/src/__tests__/tiebreak.test.ts +++ b/src/__tests__/tiebreak.test.ts @@ -269,7 +269,12 @@ describe("TiebreakCalculation", () => { ]) const tiebreak = new Tiebreaker(results, UnplayedRoundsAdjustment.FIDE_2023) expect( - tiebreak.ranking(2, [Tiebreak.SCORE, Tiebreak.BUCHHOLZ, Tiebreak.BUCHHOLZ_CUT1, Tiebreak.SONNEBORG_BERGER]), + tiebreak.ranking(2, [ + Tiebreak.SCORE, + Tiebreak.BUCHHOLZ, + Tiebreak.BUCHHOLZ_CUT1, + Tiebreak.SONNEBORN_BERGER, + ]), ).toEqual([ { playerId: "B", rank: 1, scores: [2, 2.5, 2, 2.5] }, // BH: 2 for unplayed + 0.5 for A { playerId: "C", rank: 2, scores: [1.5, 2, 1.5, 1.75] }, // BH: 0.5 for A + 1.5 for bye diff --git a/src/tiebreak.ts b/src/tiebreak.ts index fdb218d..19f242b 100644 --- a/src/tiebreak.ts +++ b/src/tiebreak.ts @@ -33,7 +33,7 @@ export enum Tiebreak { * Calculated by adding, for each round, a value given by multiplying the final * score of the opponents by the points scored against them. */ - SONNEBORG_BERGER = "SB", + SONNEBORN_BERGER = "SB", } export enum UnplayedRoundsAdjustment { @@ -144,7 +144,7 @@ export class Tiebreaker { case Tiebreak.BUCHHOLZ_MEDIAN2: return this.buchholz(player, round, 2, 2) - case Tiebreak.SONNEBORG_BERGER: + case Tiebreak.SONNEBORN_BERGER: return this.sonnebornBerger(player, round) } } @@ -262,15 +262,11 @@ export class Tiebreaker { return this.sum(games.map((g) => g.opponentScore)) } - /** * Sonneborn-Berger score. Note that unplayed games are adjusted according to the configured UnplayedRoundsAdjustment. */ - public sonnebornBerger( - player: PlayerId, - round: number - ): number { - let games = this.adjustedGames(player, round) + public sonnebornBerger(player: PlayerId, round: number): number { + const games = this.adjustedGames(player, round) return this.sum(games.map((g) => g.opponentScore * g.gameScore)) } From 53e9a3ae4e683997795be27999296bbe5d0ddc80 Mon Sep 17 00:00:00 2001 From: Marcel Juenemann Date: Wed, 11 Dec 2024 16:32:32 +0000 Subject: [PATCH 4/4] Sonneborn-Berger: build fix --- src/tiebreak.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tiebreak.ts b/src/tiebreak.ts index 19f242b..ef00665 100644 --- a/src/tiebreak.ts +++ b/src/tiebreak.ts @@ -72,7 +72,7 @@ export interface PlayerRanking { interface AdjustedGame { round: number gameScore: Score - opponentScore: Score + opponentScore: number isVur: boolean } @@ -271,7 +271,7 @@ export class Tiebreaker { } // TODO: Maybe turn PlayerResult into a class which returns the score? - private scoreForResult(result: PlayerResult): number { + private scoreForResult(result: PlayerResult): Score { switch (result) { case "unpaired": return 0