@@ -17,6 +17,16 @@ export enum Tiebreak {
17
17
* The sum of the scores of each of the opponents of a participant.
18
18
*/
19
19
BUCHHOLZ = "BH" ,
20
+
21
+ /**
22
+ * Buchholz with the least significant opponent cut. Note that voluntarily
23
+ * unplayed rounds will be cut first if FIDE_2023 regulations are applied.
24
+ */
25
+ BUCHHOLZ_CUT1 = "BH-C1" ,
26
+
27
+ BUCHHOLZ_CUT2 = "BH-C2" ,
28
+ BUCHHOLZ_MEDIAN1 = "BH-M1" ,
29
+ BUCHHOLZ_MEDIAN2 = "BH-M2" ,
20
30
}
21
31
22
32
export enum UnplayedRoundsAdjustment {
@@ -46,31 +56,19 @@ export enum UnplayedRoundsAdjustment {
46
56
FIDE_2009 = "FIDE_2009" ,
47
57
}
48
58
49
- /**
50
- * Modifiers for tiebreaks that are based on a sum of values, such as Buchholz
51
- * and Sonneborn-Berger. These modifiers are defined in the FIDE Tiebreak Regulations,
52
- * Article 14.
53
- */
54
- export enum Modifier {
55
- // 14.1 Cut-1: Cut the Least Significant Value
56
- CUT_1 = "Cut-1" ,
57
-
58
- // 14.2 Cut-2: Cut the two Least Significant Values
59
- CUT_2 = "Cut-2" ,
60
-
61
- // 14.3 Median1: Cut the Least and the Most Significant Values (in that order)
62
- MEDIAN_1 = "Median1" ,
63
-
64
- // 14.4 Median2: Cut the two Least and the two Most Significant Values (in that order)
65
- MEDIAN_2 = "Median2" ,
66
- }
67
-
68
59
export interface PlayerRanking {
69
60
rank : number
70
61
playerId : PlayerId
71
62
scores : number [ ]
72
63
}
73
64
65
+ interface AdjustedGame {
66
+ round : number
67
+ gameScore : number
68
+ opponentScore : number
69
+ isVur : boolean
70
+ }
71
+
74
72
/**
75
73
* Calculates tiebreaks for a tournament with the given results and configuration.
76
74
*/
@@ -130,6 +128,14 @@ export class Tiebreaker {
130
128
131
129
case Tiebreak . BUCHHOLZ :
132
130
return this . buchholz ( player , round )
131
+ case Tiebreak . BUCHHOLZ_CUT1 :
132
+ return this . buchholz ( player , round , 1 , 0 )
133
+ case Tiebreak . BUCHHOLZ_CUT2 :
134
+ return this . buchholz ( player , round , 2 , 0 )
135
+ case Tiebreak . BUCHHOLZ_MEDIAN1 :
136
+ return this . buchholz ( player , round , 1 , 1 )
137
+ case Tiebreak . BUCHHOLZ_MEDIAN2 :
138
+ return this . buchholz ( player , round , 2 , 2 )
133
139
}
134
140
}
135
141
@@ -186,36 +192,64 @@ export class Tiebreaker {
186
192
}
187
193
188
194
/**
189
- * Buchholz score. Note that unplayed games are adjusted according to the configured UnplayedRoundsAdjustment.
195
+ * Returns all opponents of the given player with their adjusted scores for the purpose of
196
+ * calculating tiebreaks like Buchholz and SoBerg.
190
197
*/
191
- public buchholz ( player : PlayerId , round : number , modifier ?: Modifier ) : number {
192
- const opponentScores = this . results . getAll ( player , round ) . map ( ( result , index ) => {
193
- const currentRound = index + 1
194
- switch ( this . unplayedRoundsAdjustment ) {
195
- case UnplayedRoundsAdjustment . NONE :
196
- return isPaired ( result ) ? this . adjustedScore ( result . opponent , round ) : 0
197
-
198
- case UnplayedRoundsAdjustment . FIDE_2023 :
199
- if ( isPlayed ( result ) ) {
200
- return this . adjustedScore ( result . opponent , round )
201
- } else {
202
- // Use a dummy opponent with same score.
203
- return this . score ( player , round )
204
- }
205
-
206
- case UnplayedRoundsAdjustment . FIDE_2009 :
207
- if ( isPlayed ( result ) ) {
208
- return this . adjustedScore ( result . opponent , round )
209
- } else {
210
- // Use a virtual opponent.
211
- const initialScore = this . score ( player , currentRound - 1 )
212
- const gameScore = 1 - this . scoreForResult ( result )
213
- const virtualPoints = ( round - currentRound ) * 0.5
214
- return initialScore + gameScore + virtualPoints
215
- }
198
+ private adjustedGames ( player : PlayerId , round : number ) : AdjustedGame [ ] {
199
+ return this . results . getAll ( player , round ) . map ( ( result , index ) => {
200
+ const game : AdjustedGame = {
201
+ round : index + 1 ,
202
+ gameScore : this . scoreForResult ( result ) ,
203
+ opponentScore : isPaired ( result ) ? this . adjustedScore ( result . opponent , round ) : 0 ,
204
+ isVur : isVoluntarilyUnplayedRound ( result ) ,
205
+ }
206
+
207
+ // Apply unplayed round adjustments.
208
+ if ( ! isPlayed ( result ) ) {
209
+ // 2023: Use a dummy opponent with same score as the player.
210
+ if ( this . unplayedRoundsAdjustment === UnplayedRoundsAdjustment . FIDE_2023 ) {
211
+ game . opponentScore = this . score ( player , round )
212
+ }
213
+
214
+ // 2009: Use a virtual opponent that starts with the same score.
215
+ if ( this . unplayedRoundsAdjustment === UnplayedRoundsAdjustment . FIDE_2009 ) {
216
+ const initialScore = this . score ( player , game . round - 1 )
217
+ const opponentGameScore = 1 - game . gameScore
218
+ const virtualPoints = ( round - game . round ) * 0.5
219
+ game . opponentScore = initialScore + opponentGameScore + virtualPoints
220
+ }
216
221
}
222
+ return game
217
223
} )
218
- return this . sumWithModifier ( opponentScores , modifier )
224
+ }
225
+
226
+ /**
227
+ * Buchholz score. Note that unplayed games are adjusted according to the configured UnplayedRoundsAdjustment.
228
+ */
229
+ public buchholz (
230
+ player : PlayerId ,
231
+ round : number ,
232
+ cutLowest : number = 0 ,
233
+ cutHighest : number = 0 ,
234
+ ) : number {
235
+ let games = this . adjustedGames ( player , round )
236
+
237
+ if ( cutLowest || cutHighest ) {
238
+ games . sort ( ( a , b ) => {
239
+ // Since 2023, voluntarily unplayed rounds should get cut first.
240
+ if (
241
+ this . unplayedRoundsAdjustment === UnplayedRoundsAdjustment . FIDE_2023 &&
242
+ a . isVur !== b . isVur
243
+ ) {
244
+ return Number ( a . isVur ) - Number ( b . isVur )
245
+ }
246
+ return b . opponentScore - a . opponentScore
247
+ } )
248
+
249
+ games = games . slice ( cutHighest , - cutLowest )
250
+ }
251
+
252
+ return this . sum ( games . map ( ( g ) => g . opponentScore ) )
219
253
}
220
254
221
255
// TODO: Maybe turn PlayerResult into a class which returns the score?
@@ -232,20 +266,6 @@ export class Tiebreaker {
232
266
}
233
267
}
234
268
235
- /**
236
- * Applies the given modifier to the values (e.g. cutting least significant values)
237
- * and returns the sum of the remaining values.
238
- */
239
- private sumWithModifier ( values : number [ ] , modifier ?: Modifier ) : number {
240
- values . sort ( )
241
- switch ( modifier ) {
242
- case Modifier . CUT_1 :
243
- values = values . splice ( 1 )
244
- break
245
- }
246
- return this . sum ( values )
247
- }
248
-
249
269
private sum ( numbers : number [ ] ) : number {
250
270
return numbers . reduce ( ( prev , curr ) => prev + curr , 0 )
251
271
}
0 commit comments