Skip to content

Commit 71e40b9

Browse files
Merge pull request #4 from marceljuenemann/sonneborn
Sonneborn-Berger
2 parents 8bc14ab + 53e9a3a commit 71e40b9

File tree

3 files changed

+87
-15
lines changed

3 files changed

+87
-15
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Tiebreak is a TypeScript library for calculating various tournament tiebreak sco
1212

1313
- Buchholz
1414
- Buchholz with modifiers (Cut-1, Cut-2, Median-1 etc.)
15+
- Sonneborn-Berger
1516
- (more to come)
1617

1718
**Configurable adjustments for unplayed games:**

src/__tests__/tiebreak.test.ts

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,66 @@ describe("TiebreakCalculation", () => {
189189
})
190190

191191
/*
192-
TODO: Test more modifiers with old FIDE unplayed rounds adjustments
193-
- Cut-1
194-
- Median-1
195-
- Cut-2
196-
- Very high cut numbers
192+
TODO: Test more modifiers with old FIDE unplayed rounds adjustments
193+
- Cut-1
194+
- Median-1
195+
- Cut-2
196+
- Very high cut numbers
197197
198-
describe("with modifiers", () => {
198+
describe("with modifiers", () => {
199199
200-
200+
})
201+
*/
202+
})
203+
204+
describe("sonnebornBerger", () => {
205+
describe("with FIDE_2023 unplayed rounds adjustment", () => {
206+
it("should pass FIDE exercise 11 (2023)", async () => {
207+
const rounds = await readTestCases("fide-exercise-2023")
208+
const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
209+
expect(tiebreak.sonnebornBerger("1", 5)).toEqual(8)
210+
expect(tiebreak.sonnebornBerger("3", 5)).toEqual(10.5)
211+
expect(tiebreak.sonnebornBerger("16", 5)).toEqual(7.25)
212+
expect(tiebreak.sonnebornBerger("4", 5)).toEqual(9.75)
213+
})
201214

215+
it("should pass FIDE exercise 12 (2023)", async () => {
216+
const rounds = await readTestCases("fide-exercise-2023")
217+
const tiebreak = new Tiebreaker(rounds, UnplayedRoundsAdjustment.FIDE_2023)
218+
expect(tiebreak.adjustedScore("2", 5)).toEqual(4)
219+
expect(tiebreak.sonnebornBerger("2", 5)).toEqual(9.5)
220+
expect(tiebreak.adjustedScore("3", 5)).toEqual(3.5)
221+
expect(tiebreak.sonnebornBerger("3", 5)).toEqual(10.5)
222+
expect(tiebreak.adjustedScore("4", 5)).toEqual(3.5)
223+
expect(tiebreak.sonnebornBerger("4", 5)).toEqual(9.75)
224+
expect(tiebreak.adjustedScore("1", 5)).toEqual(3.5)
225+
expect(tiebreak.sonnebornBerger("1", 5)).toEqual(8)
226+
expect(tiebreak.adjustedScore("16", 5)).toEqual(3.5)
227+
expect(tiebreak.sonnebornBerger("16", 5)).toEqual(7.25)
228+
expect(tiebreak.adjustedScore("6", 5)).toEqual(3)
229+
expect(tiebreak.sonnebornBerger("6", 5)).toEqual(6.5)
230+
expect(tiebreak.adjustedScore("11", 5)).toEqual(2.5)
231+
expect(tiebreak.sonnebornBerger("11", 5)).toEqual(5.75)
232+
expect(tiebreak.adjustedScore("8", 5)).toEqual(2.5)
233+
expect(tiebreak.sonnebornBerger("8", 5)).toEqual(5.25)
234+
expect(tiebreak.adjustedScore("5", 5)).toEqual(2.5)
235+
expect(tiebreak.sonnebornBerger("5", 5)).toEqual(4.25)
236+
expect(tiebreak.adjustedScore("14", 5)).toEqual(2)
237+
expect(tiebreak.sonnebornBerger("14", 5)).toEqual(4.5)
238+
expect(tiebreak.adjustedScore("12", 5)).toEqual(3.0)
239+
expect(tiebreak.sonnebornBerger("12", 5)).toEqual(4.0)
240+
expect(tiebreak.adjustedScore("15", 5)).toEqual(2)
241+
expect(tiebreak.sonnebornBerger("15", 5)).toEqual(3.5)
242+
expect(tiebreak.adjustedScore("13", 5)).toEqual(1.5)
243+
expect(tiebreak.sonnebornBerger("13", 5)).toEqual(4.25)
244+
expect(tiebreak.adjustedScore("7", 5)).toEqual(1.5)
245+
expect(tiebreak.sonnebornBerger("7", 5)).toEqual(3.25)
246+
expect(tiebreak.adjustedScore("10", 5)).toEqual(1.0)
247+
expect(tiebreak.sonnebornBerger("10", 5)).toEqual(1.5)
248+
})
202249
})
203-
*/
250+
251+
// TODO: Test with FIDE_2009 and NONE unplayed rounds adjustments.
204252
})
205253

206254
describe("ranking", () => {
@@ -221,12 +269,17 @@ describe("TiebreakCalculation", () => {
221269
])
222270
const tiebreak = new Tiebreaker(results, UnplayedRoundsAdjustment.FIDE_2023)
223271
expect(
224-
tiebreak.ranking(2, [Tiebreak.SCORE, Tiebreak.BUCHHOLZ, Tiebreak.BUCHHOLZ_CUT1]),
272+
tiebreak.ranking(2, [
273+
Tiebreak.SCORE,
274+
Tiebreak.BUCHHOLZ,
275+
Tiebreak.BUCHHOLZ_CUT1,
276+
Tiebreak.SONNEBORN_BERGER,
277+
]),
225278
).toEqual([
226-
{ playerId: "B", rank: 1, scores: [2, 2.5, 2] }, // BH: 2 for unplayed + 0.5 for A
227-
{ playerId: "C", rank: 2, scores: [1.5, 2, 1.5] }, // BH: 0.5 for A + 1.5 for bye
228-
{ playerId: "A", rank: 3, scores: [0.5, 3.5, 2] }, // BH: 1.5 for C + 2 for B
229-
{ playerId: "D", rank: 4, scores: [0, 0, 0] }, // BH: 2 * 0 for unplayed rounds
279+
{ playerId: "B", rank: 1, scores: [2, 2.5, 2, 2.5] }, // BH: 2 for unplayed + 0.5 for A
280+
{ playerId: "C", rank: 2, scores: [1.5, 2, 1.5, 1.75] }, // BH: 0.5 for A + 1.5 for bye
281+
{ playerId: "A", rank: 3, scores: [0.5, 3.5, 2, 0.75] }, // BH: 1.5 for C + 2 for B
282+
{ playerId: "D", rank: 4, scores: [0, 0, 0, 0] }, // BH: 2 * 0 for unplayed rounds
230283
])
231284
})
232285

src/tiebreak.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
PlayerId,
33
PlayerResult,
44
Results,
5+
Score,
56
isPaired,
67
isPlayed,
78
isVoluntarilyUnplayedRound,
@@ -27,6 +28,12 @@ export enum Tiebreak {
2728
BUCHHOLZ_CUT2 = "BH-C2",
2829
BUCHHOLZ_MEDIAN1 = "BH-M1",
2930
BUCHHOLZ_MEDIAN2 = "BH-M2",
31+
32+
/**
33+
* Calculated by adding, for each round, a value given by multiplying the final
34+
* score of the opponents by the points scored against them.
35+
*/
36+
SONNEBORN_BERGER = "SB",
3037
}
3138

3239
export enum UnplayedRoundsAdjustment {
@@ -64,7 +71,7 @@ export interface PlayerRanking {
6471

6572
interface AdjustedGame {
6673
round: number
67-
gameScore: number
74+
gameScore: Score
6875
opponentScore: number
6976
isVur: boolean
7077
}
@@ -136,6 +143,9 @@ export class Tiebreaker {
136143
return this.buchholz(player, round, 1, 1)
137144
case Tiebreak.BUCHHOLZ_MEDIAN2:
138145
return this.buchholz(player, round, 2, 2)
146+
147+
case Tiebreak.SONNEBORN_BERGER:
148+
return this.sonnebornBerger(player, round)
139149
}
140150
}
141151

@@ -252,8 +262,16 @@ export class Tiebreaker {
252262
return this.sum(games.map((g) => g.opponentScore))
253263
}
254264

265+
/**
266+
* Sonneborn-Berger score. Note that unplayed games are adjusted according to the configured UnplayedRoundsAdjustment.
267+
*/
268+
public sonnebornBerger(player: PlayerId, round: number): number {
269+
const games = this.adjustedGames(player, round)
270+
return this.sum(games.map((g) => g.opponentScore * g.gameScore))
271+
}
272+
255273
// TODO: Maybe turn PlayerResult into a class which returns the score?
256-
private scoreForResult(result: PlayerResult): number {
274+
private scoreForResult(result: PlayerResult): Score {
257275
switch (result) {
258276
case "unpaired":
259277
return 0

0 commit comments

Comments
 (0)