Skip to content

Commit

Permalink
Merge branch 'main' of github.com:marceljuenemann/tiebreak
Browse files Browse the repository at this point in the history
  • Loading branch information
marceljuenemann committed Dec 11, 2024
2 parents 35317a6 + 71e40b9 commit ab3986e
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 15 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
79 changes: 66 additions & 13 deletions src/__tests__/tiebreak.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -221,12 +269,17 @@ 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.SONNEBORN_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
])
})

Expand Down
22 changes: 20 additions & 2 deletions src/tiebreak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
PlayerId,
PlayerResult,
Results,
Score,
isPaired,
isPlayed,
isVoluntarilyUnplayedRound,
Expand All @@ -27,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.
*/
SONNEBORN_BERGER = "SB",
}

export enum UnplayedRoundsAdjustment {
Expand Down Expand Up @@ -64,7 +71,7 @@ export interface PlayerRanking {

interface AdjustedGame {
round: number
gameScore: number
gameScore: Score
opponentScore: number
isVur: boolean
}
Expand Down Expand Up @@ -136,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.SONNEBORN_BERGER:
return this.sonnebornBerger(player, round)
}
}

Expand Down Expand Up @@ -252,8 +262,16 @@ 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 {
const 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 {
private scoreForResult(result: PlayerResult): Score {
switch (result) {
case "unpaired":
return 0
Expand Down

0 comments on commit ab3986e

Please sign in to comment.