/
rank_utils.ts
404 lines (364 loc) · 12.2 KB
/
rank_utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
/*
* Copyright (C) 2012-2022 Online-Go.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { _, interpolate, pgettext } from "translate";
export interface IRankInfo {
rank: number;
label: string;
}
class Rating {
unset: boolean;
rating: number;
deviation: number;
volatility: number;
provisional: boolean;
rank: number;
rank_label: string;
partial_rank: number;
partial_rank_label: string;
rank_deviation_labels: Array<string>;
rank_deviation: number;
professional: boolean;
bounded_rank: number;
bounded_rank_label: string;
partial_bounded_rank: number;
partial_bounded_rank_label: string;
}
export const MinRank = 5;
export const MaxRank = 38;
export const PROVISIONAL_RATING_CUTOFF = 160;
const MIN_RATING = 100;
const MAX_RATING = 6000;
const A = 525;
const C = 23.15;
interface CompactRatingType {
rating: number;
deviation: number;
volatility: number;
}
interface RatingsType {
overall: CompactRatingType;
}
interface UserType {
ranking?: number;
rank?: number;
pro?: boolean;
professional?: boolean;
ratings?: RatingsType;
}
type UserOrRank = UserType | number;
/** Returns the Glicko2 rating corresponding to OGS rank. */
export function rank_to_rating(rank: number): number {
return A * Math.exp(rank / C);
}
/** Returns the OGS rank corresponding to the Glicko2 rating */
export function rating_to_rank(rating: number): number {
return Math.log(Math.min(MAX_RATING, Math.max(MIN_RATING, rating)) / A) * C;
}
/** Calculates OGS rank deviation from the Glicko2 rating and deviation */
export function rank_deviation(rating: number, deviation: number): number {
// Suggestion: use the uncertainty propagation formula for log transforms:
// https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulae
// - bpj
return rating_to_rank(rating + deviation) - rating_to_rank(rating);
}
function get_handicap_adjustment(rating: number, handicap: number): number {
return rank_to_rating(rating_to_rank(rating) + handicap) - rating;
}
function overall_rank(user_or_rank: UserOrRank): number {
let rank = null;
if (typeof user_or_rank === "number") {
rank = user_or_rank;
} else {
rank = getUserRating(user_or_rank, "overall", 0).rank;
}
return rank;
}
/** Returns true if user is below 25k */
export function is_novice(user_or_rank: UserOrRank): boolean {
return overall_rank(user_or_rank) < MinRank;
}
/** Returns true if user is below 25k or above 9d */
export function is_rank_bounded(user_or_rank: UserOrRank): boolean {
const rank = overall_rank(user_or_rank);
return rank < MinRank || rank > MaxRank;
}
/** Returns rank clamped to the bounds [25k, 9d] */
export function bounded_rank(user_or_rank: UserOrRank): number {
const rank = overall_rank(user_or_rank);
return Math.min(MaxRank, Math.max(MinRank, rank));
}
/**
* Returns true if the user's rank deviation is too large.
*
* This determines whether rank shows up as [?] around OGS
*/
export function is_provisional(user: { ratings?: RatingsType }): boolean {
const ratings = user.ratings || {};
const rating = ratings["overall"] || {
rating: 1500,
deviation: 350,
volatility: 0.06,
};
return rating.deviation >= PROVISIONAL_RATING_CUTOFF;
}
/**
* Computes ratings data for a user for a given size and speed.
*/
export function getUserRating(
user: UserType,
speed: "overall" | "blitz" | "live" | "correspondence" = "overall",
size: 0 | 9 | 13 | 19 = 0,
): Rating {
const ret = new Rating();
const ratings = user.ratings || {};
ret.professional = user.pro || user.professional;
let key: string = speed;
if (size > 0) {
if (speed !== "overall") {
key += `-${size}x${size}`;
} else {
key = `${size}x${size}`;
}
}
let rating = {
rating: 1500,
deviation: 350,
volatility: 0.06,
};
ret.unset = true;
if (key in ratings) {
ret.unset = false;
rating = ratings[key];
}
ret.rating = rating.rating;
ret.deviation = rating.deviation;
ret.provisional = rating.deviation >= PROVISIONAL_RATING_CUTOFF;
ret.volatility = rating.volatility;
ret.rank = Math.floor(rating_to_rank(ret.rating));
ret.rank_deviation = rating_to_rank(ret.rating + ret.deviation) - rating_to_rank(ret.rating);
ret.partial_rank = rating_to_rank(ret.rating);
ret.rank_label = rankString(ret.rank, false);
ret.partial_rank_label = rankString(ret.partial_rank, true);
ret.rank_deviation_labels = [
rankString(rating_to_rank(ret.rating - ret.deviation), true),
rankString(rating_to_rank(ret.rating + ret.deviation), true),
];
ret.bounded_rank = Math.max(MinRank, Math.min(MaxRank, ret.rank));
ret.bounded_rank_label = rankString(ret.bounded_rank);
ret.partial_bounded_rank = Math.max(MinRank, Math.min(MaxRank, ret.partial_rank));
ret.partial_bounded_rank_label = rankString(ret.partial_bounded_rank, true);
if (ret.rank > MaxRank + 1) {
ret.bounded_rank_label += "+";
ret.partial_bounded_rank_label += "+";
}
if (ret.professional) {
ret.rank_label = rankString(user);
ret.bounded_rank_label = rankString(user);
ret.partial_rank_label = ret.rank_label;
ret.rank_deviation_labels = ["", ""];
}
return ret;
}
/** Like rankString, but clamped to the range [25k, 9d] */
export function boundedRankString(r: UserOrRank, with_tenths?: boolean): string {
return rankString(bounded_rank(r), with_tenths);
}
/**
* Returns a concise, localized string representing a user's kyu/dan rank
*
* @param r If a user type, the users overall rating will be pulled off the user.
* If a number, it will be treated as the OGS rank.
* @param with_tenths If true, 1 decimal of precision will be added to the output.
* @returns a string representing the rank (e.g. "7.1k", "4d", "9p")
*/
export function rankString(r: UserOrRank, with_tenths?: boolean): string {
let provisional = false;
if (typeof r === "object") {
provisional = is_provisional(r);
const ranking = "ranking" in r ? r.ranking : r.rank;
if (r.pro || r.professional) {
if (ranking > 900) {
return interpolate(pgettext("Pro", "%sp"), [ranking - 1000 - 36]);
} else {
return interpolate(pgettext("Pro", "%sp"), [ranking - 36]);
}
}
if ("ratings" in r) {
r = overall_rank(r);
} else {
provisional = false;
r = ranking;
}
}
if (r > 900) {
return interpolate(pgettext("Pro", "%sp"), [r - 1000 - 36]);
}
if (r < -900) {
provisional = true;
}
if (provisional) {
return "?";
}
if (r < 30) {
if (with_tenths) {
(r as any) = (Math.ceil((30 - r) * 10) / 10).toFixed(1);
} else {
r = Math.ceil(30 - r);
}
return interpolate(pgettext("Kyu", "%sk"), [r]);
}
if (with_tenths) {
(r as any) = (Math.floor((r - 29) * 10) / 10).toFixed(1);
} else {
r = Math.floor(r - 29);
}
return interpolate(pgettext("Dan", "%sd"), [r]);
}
/**
* Returns a localized string representing a user's kyu/dan rank
*
* @param r If a user type, the users overall rating will be pulled off the user.
* If a number, it will be treated as the OGS rank.
* @returns a string representing the rank (e.g. "7.1 Kyu", "4.36 Dan", "9 Pro")
*/
export function longRankString(r: UserOrRank): string {
let provisional = false;
if (typeof r === "object") {
provisional = is_provisional(r);
const ranking = "ranking" in r ? r.ranking : r.rank;
if (r.pro || r.professional) {
return interpolate(_("%s Pro"), [ranking - 36]);
}
if ("ratings" in r) {
r = overall_rank(r);
} else {
r = ranking;
}
}
if (r > 900) {
return interpolate(_("%s Pro"), [r - 1000 - 36]);
}
if (r < -900) {
provisional = true;
}
if (provisional) {
return "?";
}
if (r < 30) {
return interpolate(_("%s Kyu"), [30 - r]);
}
return interpolate(_("%s Dan"), [r - 30 + 1]);
}
/**
* Returns a list of OGS ranks and labels in the range [minRank, maxRank]
* @param minRank the first rank in the list
* @param maxRank the last rank in the list
* @param usePlusOnLast if true, the last entry will have a plus (e.g. "1d+")
*/
export function rankList(
minRank: number = 0,
maxRank: number = MaxRank,
usePlusOnLast: boolean = false,
): Array<IRankInfo> {
const result = [];
for (let i = minRank; i <= maxRank; ++i) {
let label = longRankString(i);
if (usePlusOnLast && i === maxRank) {
label += "+";
}
result.push({
rank: i,
label: label,
});
}
return result;
}
/**
* Returns a list of all possible pro ranks and their labels.
* @param bigranknums if true, ranks will start at 1037
*/
export function proRankList(bigranknums: boolean = true): Array<IRankInfo> {
const result = [];
for (let i = 37; i <= 45; ++i) {
result.push({
rank: i + (bigranknums ? 1000 : 0),
label: longRankString(i + 1000),
});
}
return result;
}
/** Returns all ranks with labels in the range [25k, 9d] */
export function amateurRanks(): IRankInfo[] {
return rankList(MinRank, MaxRank, true);
}
/** Returns all available ranks on OGS */
export function allRanks(): IRankInfo[] {
return rankList().concat(proRankList());
}
/**
* For new players we pretend their rating is lower than it actually is for the purposes of
* matchmaking and the like. See:
* https://forums.online-go.com/t/i-think-the-13k-default-rank-is-doing-harm/13480/192
* for the history surounding that.
*/
export function humble_rating(rating: number, deviation: number): number {
return (
rating -
((Math.min(350, Math.max(PROVISIONAL_RATING_CUTOFF, deviation)) -
PROVISIONAL_RATING_CUTOFF) /
(350 - PROVISIONAL_RATING_CUTOFF)) *
deviation
);
}
export interface EffectiveOutcome {
black_real_rating: number;
white_real_rating: number;
black_real_stronger: boolean;
white_real_stronger: boolean;
handicap: number;
black_effective_rating: number;
white_effective_rating: number;
black_effective_stronger: boolean;
white_effective_stronger: boolean;
}
/**
* Returns a ratings object containing ratings adjusted for the handicap.
*/
export function effective_outcome(
black_rating: number,
white_rating: number,
handicap: number,
): EffectiveOutcome {
// Note: it seems like black_effective_stronger and white_effective_stronger
// are the only values that get used in the calling function. Would it be
// appropriate to remove all the other stuff that gets added to this object?
// - BPJ
const black_effective_rating: number =
black_rating + get_handicap_adjustment(black_rating, handicap);
const white_effective_rating: number = white_rating;
return {
black_real_rating: black_rating,
white_real_rating: white_rating,
handicap: handicap,
black_effective_rating: black_effective_rating,
white_effective_rating: white_effective_rating,
black_real_stronger: black_rating > white_rating,
black_effective_stronger: black_effective_rating > white_effective_rating,
white_real_stronger: white_rating >= black_rating,
white_effective_stronger: white_effective_rating >= black_effective_rating,
};
}