@xcpcio/core 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/rank.ts DELETED
@@ -1,613 +0,0 @@
1
- import type { SubmissionStatus } from "@xcpcio/types";
2
- import type { Balloons } from "./balloon";
3
- import type { SelectOptionItem } from "./basic-types";
4
- import type { Contest } from "./contest";
5
- import type { Submissions } from "./submission";
6
-
7
- import type { Teams } from "./team";
8
-
9
- import _ from "lodash";
10
- import { Award, MedalType } from "./award";
11
- import { Balloon } from "./balloon";
12
- import { BattleOfGiants } from "./battle-of-giants";
13
- import { TeamProblemStatistics } from "./problem";
14
- import { RankStatistics } from "./rank-statistics";
15
- import { Submission } from "./submission";
16
- import { Team } from "./team";
17
-
18
- export class RankOptions {
19
- enableFilterSubmissionsByTimestamp: boolean;
20
- width: number;
21
- timestamp: number;
22
-
23
- enableFilterTeamsByGroup: boolean;
24
- group: string;
25
-
26
- filterOrganizations: Array<SelectOptionItem>;
27
- filterOrganizationMap: Map<string, SelectOptionItem>;
28
- filterTeams: Array<SelectOptionItem>;
29
- filterTeamMap: Map<string, SelectOptionItem>;
30
-
31
- enableAnimatedSubmissions: boolean;
32
-
33
- battleOfGiants: BattleOfGiants;
34
-
35
- constructor() {
36
- this.enableFilterSubmissionsByTimestamp = false;
37
- this.width = 0;
38
- this.timestamp = 0;
39
-
40
- this.enableFilterTeamsByGroup = false;
41
- this.group = "all";
42
-
43
- this.filterOrganizations = [];
44
- this.filterOrganizationMap = new Map<string, SelectOptionItem>();
45
-
46
- this.filterTeams = [];
47
- this.filterTeamMap = new Map<string, SelectOptionItem>();
48
-
49
- this.enableAnimatedSubmissions = false;
50
-
51
- this.battleOfGiants = new BattleOfGiants();
52
- }
53
-
54
- setSelf(self: RankOptions) {
55
- this.enableFilterSubmissionsByTimestamp = self.enableFilterSubmissionsByTimestamp;
56
- this.width = self.width;
57
- this.timestamp = self.timestamp;
58
-
59
- this.enableFilterTeamsByGroup = self.enableFilterTeamsByGroup;
60
- this.group = self.group;
61
-
62
- this.filterOrganizations = self.filterOrganizations;
63
- this.filterOrganizationMap = self.filterOrganizationMap;
64
-
65
- this.filterTeams = self.filterTeams;
66
- this.filterTeamMap = self.filterTeamMap;
67
-
68
- this.enableAnimatedSubmissions = self.enableAnimatedSubmissions;
69
-
70
- this.battleOfGiants = self.battleOfGiants;
71
- }
72
-
73
- setWidth(width: number, contest: Contest) {
74
- this.width = width;
75
- this.timestamp = Math.floor((contest.getEndTime().unix() - contest.getStartTime().unix()) * this.width * 0.0001);
76
- this.enableFilterSubmissionsByTimestamp = true;
77
- }
78
-
79
- disableFilterSubmissionByTimestamp() {
80
- this.enableFilterSubmissionsByTimestamp = false;
81
- }
82
-
83
- setGroup(group: string) {
84
- this.group = group;
85
- this.enableFilterTeamsByGroup = true;
86
-
87
- if (this.group === "all") {
88
- this.disableFilterTeamsByGroup();
89
- }
90
- }
91
-
92
- disableFilterTeamsByGroup() {
93
- this.enableFilterTeamsByGroup = false;
94
- this.group = "all";
95
- }
96
-
97
- setFilterOrganizations(filterOrganizations: Array<SelectOptionItem>) {
98
- const m = new Map<string, SelectOptionItem>();
99
- filterOrganizations.forEach((item) => {
100
- m.set(item.value, item);
101
- });
102
-
103
- this.filterOrganizations = filterOrganizations;
104
- this.filterOrganizationMap = m;
105
- }
106
-
107
- setFilterTeams(filterTeams: Array<SelectOptionItem>) {
108
- const m = new Map<string, SelectOptionItem>();
109
- filterTeams.forEach((item) => {
110
- m.set(item.value, item);
111
- });
112
-
113
- this.filterTeams = filterTeams;
114
- this.filterTeamMap = m;
115
- }
116
-
117
- isNeedReBuildRank(nextRankOptions: RankOptions): boolean {
118
- if (this.enableFilterSubmissionsByTimestamp !== nextRankOptions.enableFilterSubmissionsByTimestamp) {
119
- return true;
120
- }
121
-
122
- if (this.width !== nextRankOptions.width) {
123
- return true;
124
- }
125
-
126
- if (this.timestamp !== nextRankOptions.timestamp) {
127
- return true;
128
- }
129
-
130
- if (this.enableFilterTeamsByGroup !== nextRankOptions.enableFilterTeamsByGroup) {
131
- return true;
132
- }
133
-
134
- if (this.group !== nextRankOptions.group) {
135
- return true;
136
- }
137
-
138
- return false;
139
- }
140
- }
141
-
142
- export class Rank {
143
- readonly contest: Contest;
144
-
145
- teams: Teams;
146
- teamsMap: Map<string /* teamId */, Team>;
147
-
148
- submissions: Submissions;
149
- submissionsMap: Map<string /* submissionId */, Submission>;
150
-
151
- organizations: Array<string>;
152
- originTeams: Teams;
153
-
154
- rankStatistics: RankStatistics;
155
-
156
- options: RankOptions;
157
-
158
- balloons: Balloons;
159
-
160
- languages: Array<string>;
161
- statuses: Array<SubmissionStatus>;
162
-
163
- constructor(contest: Contest, teams: Teams, submissions: Submissions) {
164
- this.contest = contest;
165
-
166
- this.teams = _.cloneDeep(teams);
167
- this.teamsMap = new Map(this.teams.map(t => [t.id, t]));
168
-
169
- this.submissions = _.cloneDeep(submissions).sort(Submission.compare);
170
-
171
- {
172
- const o = this.contest.options;
173
- const timestampUnit = _.cloneDeep(o.submissionTimestampUnit);
174
- this.submissions.forEach((s) => {
175
- s.timestampUnit = timestampUnit;
176
-
177
- if (s.time) {
178
- o.submissionHasTimeField = true;
179
- }
180
-
181
- if (s.language) {
182
- o.submissionHasLanguageField = true;
183
- }
184
- });
185
- }
186
-
187
- this.submissionsMap = new Map(this.submissions.map(s => [s.id, s]));
188
-
189
- this.organizations = this.buildOrganizations();
190
- this.originTeams = this.teams.map(t => t);
191
- this.originTeams.sort(Team.compare);
192
-
193
- this.rankStatistics = new RankStatistics();
194
-
195
- this.options = new RankOptions();
196
-
197
- this.balloons = [];
198
-
199
- {
200
- const se = new Set<string>();
201
-
202
- this.submissions.forEach((s) => {
203
- if (s.language) {
204
- se.add(s.language);
205
- }
206
- });
207
-
208
- this.languages = [...se].sort();
209
- }
210
-
211
- {
212
- const se = new Set<SubmissionStatus>();
213
-
214
- this.submissions.forEach((s) => {
215
- se.add(s.status);
216
- });
217
-
218
- this.statuses = [...se].sort();
219
- }
220
- }
221
-
222
- cleanRank() {
223
- (() => {
224
- this.teams = [];
225
-
226
- for (const [_k, v] of this.teamsMap) {
227
- if (this.filterTeamByOrg(v)) {
228
- continue;
229
- }
230
-
231
- this.teams.push(v);
232
- }
233
- })();
234
-
235
- for (const t of this.teams) {
236
- t.reset();
237
-
238
- t.problemStatistics = this.contest.problems.map((p) => {
239
- const ps = new TeamProblemStatistics();
240
- ps.problem = p;
241
- ps.contestPenalty = this.contest.penalty;
242
-
243
- return ps;
244
- });
245
-
246
- t.problemStatisticsMap = new Map(t.problemStatistics.map(ps => [ps.problem.id, ps]));
247
- }
248
-
249
- this.contest.problems.forEach((p) => {
250
- p.statistics.reset();
251
- });
252
- }
253
-
254
- buildRank() {
255
- (() => {
256
- this.cleanRank();
257
-
258
- this.teams.forEach(t =>
259
- t.placeChartPoints.push({
260
- timePoint: 0,
261
- rank: 1,
262
- lastSolvedProblem: null,
263
- }),
264
- );
265
-
266
- (() => {
267
- this.rankStatistics.reset();
268
- this.rankStatistics.teamSolvedNum = Array.from({ length: this.contest.problems.length + 1 }).fill(0) as number[];
269
- this.rankStatistics.teamSolvedNumIndex = Array.from({ length: this.contest.problems.length + 1 }).fill(0) as number[];
270
- })();
271
-
272
- let preSubmissionTimestampToMinute = 0;
273
- const allSubmissions = this.getSubmissions();
274
-
275
- for (let ix = 0; ix < allSubmissions.length; ix++) {
276
- const s = allSubmissions[ix];
277
-
278
- const teamId = s.teamId;
279
- const problemId = s.problemId;
280
- const team = this.teamsMap.get(teamId);
281
- const problem = this.contest.problemsMap.get(problemId);
282
-
283
- (() => {
284
- if (team === undefined || this.filterTeamByOrg(team) || problem === undefined) {
285
- return;
286
- }
287
-
288
- const problemStatistics = team.problemStatisticsMap.get(problemId) as TeamProblemStatistics;
289
- const submissions = problemStatistics.submissions;
290
-
291
- submissions.push(s);
292
- team.submissions.push(s);
293
- problem.statistics.submittedNum++;
294
-
295
- if (problemStatistics.isSolved) {
296
- s.isSolved = false;
297
- s.isFirstSolved = false;
298
- return;
299
- }
300
-
301
- if (s.isIgnore || s.isNotCalculatedPenaltyStatus()) {
302
- problem.statistics.ignoreNum++;
303
- problemStatistics.ignoreCount++;
304
- return;
305
- }
306
-
307
- problemStatistics.isSubmitted = true;
308
- problemStatistics.lastSubmitTimestamp = s.timestampToSecond;
309
- problemStatistics.totalCount++;
310
-
311
- if (s.isAccepted()) {
312
- s.isSolved = true;
313
-
314
- problemStatistics.isSolved = true;
315
- problemStatistics.solvedTimestamp = s.timestampToSecond;
316
-
317
- problem.statistics.acceptedNum++;
318
- problem.statistics.attemptedNum += problemStatistics.failedCount + 1;
319
-
320
- if (
321
- problem.statistics.firstSolveSubmissions.length === 0
322
- || problem.statistics.firstSolveSubmissions[problem.statistics.firstSolveSubmissions.length - 1].timestamp === s.timestamp
323
- ) {
324
- s.isFirstSolved = true;
325
- problemStatistics.isFirstSolved = true;
326
- problem.statistics.firstSolveSubmissions.push(s);
327
- }
328
-
329
- while (problem.statistics.lastSolveSubmissions.length > 0) {
330
- problem.statistics.lastSolveSubmissions.pop();
331
- }
332
-
333
- problem.statistics.lastSolveSubmissions.push(s);
334
-
335
- team.lastSolvedProblem = problem;
336
- team.lastSolvedProblemStatistics = problemStatistics;
337
- }
338
-
339
- if (s.isRejected()) {
340
- problemStatistics.failedCount++;
341
- problem.statistics.rejectedNum++;
342
- }
343
-
344
- if (s.isPending()) {
345
- problemStatistics.pendingCount++;
346
- problem.statistics.pendingNum++;
347
- }
348
- })();
349
-
350
- if (s.timestampToMinute > preSubmissionTimestampToMinute || ix === allSubmissions.length - 1) {
351
- this.teams.forEach(t => t.calcSolvedData(this.contest.options));
352
- this.teams.sort(Team.compare);
353
- this.buildTeamRank();
354
-
355
- this.teams.forEach(t =>
356
- t.placeChartPoints.push(
357
- {
358
- timePoint: s.timestampToMinute,
359
- rank: t.rank,
360
- lastSolvedProblem: t.lastSolvedProblem,
361
- },
362
- ),
363
- );
364
- }
365
-
366
- preSubmissionTimestampToMinute = s.timestampToMinute;
367
- }
368
-
369
- this.teams.forEach(t => t.calcSolvedData(this.contest.options));
370
- this.teams.sort(Team.compare);
371
- this.buildTeamRank();
372
- this.buildOrgRank();
373
-
374
- this.rankStatistics.effectiveTeamNum = this.teams.filter(t => t.isEffectiveTeam).length;
375
- this.rankStatistics.totalTeamNum = this.teams.length;
376
- this.teams.forEach(t => t.calcSE(this.rankStatistics.totalTeamNum));
377
- this.contest.problems.forEach(p => p.statistics.calcSE(this.rankStatistics.totalTeamNum));
378
- this.buildAwards();
379
-
380
- this.teams.forEach(t => t.calcAwards(this.contest.awards?.get(this.options.group)));
381
- this.teams.forEach(t => t.postProcessPlaceChartPoints());
382
- })();
383
-
384
- (() => {
385
- for (const t of this.teams) {
386
- this.rankStatistics.teamSolvedNum[t.solvedProblemNum]++;
387
- }
388
-
389
- {
390
- let current = 0;
391
- const teamSolvedNum = this.rankStatistics.teamSolvedNum;
392
- const teamSolvedNumIndex = this.rankStatistics.teamSolvedNumIndex;
393
-
394
- for (let i = teamSolvedNumIndex.length - 1; i >= 0; i--) {
395
- current += (teamSolvedNum[i] > 0 ? 1 : 0);
396
- teamSolvedNumIndex[i] = current;
397
- }
398
- }
399
-
400
- if (this.teams.length > 0) {
401
- this.rankStatistics.maxSolvedProblems = this.teams[0].solvedProblemNum;
402
- }
403
- })();
404
-
405
- return this;
406
- }
407
-
408
- buildTeamRank() {
409
- let rank = 1;
410
- let originalRank = 1;
411
- let preTeam = null;
412
- for (const t of this.teams) {
413
- t.rank = rank++;
414
- t.originalRank = originalRank++;
415
-
416
- if (preTeam !== null) {
417
- if (t.isEqualRank(preTeam)) {
418
- t.rank = preTeam.rank;
419
- }
420
- }
421
-
422
- preTeam = t;
423
- }
424
- }
425
-
426
- buildOrgRank() {
427
- if (!this.contest.organization) {
428
- return;
429
- }
430
-
431
- let rank = 1;
432
- let preTeam = null;
433
-
434
- const se = new Set<string>();
435
-
436
- for (const t of this.teams) {
437
- const org = t.organization;
438
- if (se.has(org)) {
439
- continue;
440
- }
441
-
442
- se.add(org);
443
- t.organizationRank = rank++;
444
-
445
- if (preTeam !== null) {
446
- if (t.isEqualRank(preTeam)) {
447
- t.organizationRank = preTeam.organizationRank;
448
- }
449
- }
450
-
451
- preTeam = t;
452
- }
453
- }
454
-
455
- buildOrganizations() {
456
- if (!this.contest.organization) {
457
- return [];
458
- }
459
-
460
- const res = new Array<string>();
461
- const se = new Set<string>();
462
-
463
- this.teams.forEach((t) => {
464
- const org = t.organization;
465
- if (se.has(org)) {
466
- return;
467
- }
468
-
469
- res.push(org);
470
- se.add(org);
471
- });
472
-
473
- res.sort();
474
-
475
- return res;
476
- }
477
-
478
- buildAwards() {
479
- if (this.contest.medal === "ccpc") {
480
- this.contest.awards = new Map<string, Award[]>();
481
-
482
- const tot = this.rankStatistics.effectiveTeamNum;
483
- const award: Award[] = [];
484
-
485
- const gold = new Award();
486
- const silver = new Award();
487
- const bronze = new Award();
488
- const honorable = new Award();
489
-
490
- {
491
- gold.medalType = MedalType.GOLD;
492
- gold.minRank = 1;
493
- gold.maxRank = Math.ceil(tot * 0.1);
494
- if (gold.maxRank >= gold.minRank) {
495
- award.push(gold);
496
- }
497
- }
498
-
499
- {
500
- silver.medalType = MedalType.SILVER;
501
- silver.minRank = gold.maxRank + 1;
502
- silver.maxRank = Math.ceil(tot * 0.3);
503
- if (silver.maxRank >= silver.minRank) {
504
- award.push(silver);
505
- }
506
- }
507
-
508
- {
509
- bronze.medalType = MedalType.BRONZE;
510
- bronze.minRank = silver.maxRank + 1;
511
- bronze.maxRank = Math.ceil(tot * 0.6);
512
- if (bronze.maxRank >= bronze.minRank) {
513
- award.push(bronze);
514
- }
515
- }
516
-
517
- {
518
- honorable.medalType = MedalType.HONORABLE;
519
- honorable.minRank = bronze.maxRank + 1;
520
- this.teams.forEach((t) => {
521
- if (t.solvedProblemNum > 0) {
522
- honorable.maxRank = Math.max(honorable.maxRank, t.rank);
523
- }
524
- });
525
- if (honorable.maxRank >= honorable.minRank) {
526
- award.push(honorable);
527
- }
528
- }
529
-
530
- this.contest.awards.set("official", award);
531
- }
532
- }
533
-
534
- filterTeamByOrg(team: Team) {
535
- const o = this.options;
536
-
537
- if (o.enableFilterTeamsByGroup) {
538
- if (!team.group?.includes(o.group)) {
539
- return true;
540
- }
541
- }
542
-
543
- return false;
544
- }
545
-
546
- getSubmissions() {
547
- if (this.contest.replayContestStartTimestamp === undefined && this.options.enableFilterSubmissionsByTimestamp === false) {
548
- return this.submissions;
549
- }
550
-
551
- return this.submissions.filter((s) => {
552
- if (this.contest.replayContestStartTimestamp !== undefined) {
553
- if (s.timestampToSecond > this.contest.replayContestStartTimestamp) {
554
- return false;
555
- }
556
- }
557
-
558
- if (this.options.enableFilterSubmissionsByTimestamp) {
559
- if (s.timestampToSecond > this.options.timestamp) {
560
- return false;
561
- }
562
- }
563
-
564
- return true;
565
- });
566
- }
567
-
568
- buildBalloons() {
569
- this.balloons = [];
570
- this.cleanRank();
571
-
572
- const allSubmissions = this.getSubmissions();
573
-
574
- for (let ix = 0; ix < allSubmissions.length; ix++) {
575
- const s = allSubmissions[ix];
576
-
577
- const teamId = s.teamId;
578
- const problemId = s.problemId;
579
- const team = this.teamsMap.get(teamId);
580
- const problem = this.contest.problemsMap.get(problemId);
581
-
582
- (() => {
583
- if (team === undefined || problem === undefined) {
584
- return;
585
- }
586
-
587
- const problemStatistics = team.problemStatisticsMap.get(problemId) as TeamProblemStatistics;
588
-
589
- if (problemStatistics.isSolved) {
590
- return;
591
- }
592
-
593
- if (s.isAccepted()) {
594
- problemStatistics.isSolved = true;
595
- problemStatistics.solvedTimestamp = s.timestampToSecond;
596
-
597
- const b = new Balloon();
598
- b.team = team;
599
- b.problem = problem;
600
- b.submission = s;
601
-
602
- this.balloons.push(b);
603
- }
604
- })();
605
- }
606
- }
607
-
608
- setReplayTime(replayStartTimestamp: number) {
609
- this.contest.setReplayTime(replayStartTimestamp);
610
- }
611
- }
612
-
613
- export type Ranks = Array<Rank>;
@@ -1,5 +0,0 @@
1
- export * from "./rating";
2
- export * from "./rating-calculator";
3
- export * from "./rating-history";
4
- export * from "./rating-user";
5
- export * from "./rating-utility";
@@ -1,100 +0,0 @@
1
- import type { RatingUsers } from "./rating-user";
2
- import { RatingUser } from "./rating-user";
3
-
4
- // https://www.wikiwand.com/en/Elo_rating_system
5
- export class RatingCalculator {
6
- users: RatingUsers;
7
-
8
- constructor() {
9
- this.users = [];
10
- }
11
-
12
- calculate() {
13
- this.calculateInternal();
14
- }
15
-
16
- private calcP(userA: RatingUser, userB: RatingUser) {
17
- return 1.0 / (1.0 + 10 ** ((userB.oldRating - userA.oldRating) / 400.0));
18
- }
19
-
20
- private getExSeed(users: RatingUsers, rating: number, ownUser: RatingUser) {
21
- const exUser = new RatingUser();
22
- exUser.oldRating = rating;
23
-
24
- let res = 0;
25
-
26
- users.forEach((user) => {
27
- if (user.id !== ownUser.id) {
28
- res += this.calcP(user, exUser);
29
- }
30
- });
31
-
32
- return res;
33
- }
34
-
35
- private calcRating(users: RatingUsers, rank: number, user: RatingUser) {
36
- let left = 1;
37
- let right = 8000;
38
-
39
- while (right - left > 1) {
40
- const mid = Math.floor((left + right) / 2);
41
- if (this.getExSeed(users, mid, user) < rank) {
42
- right = mid;
43
- } else {
44
- left = mid;
45
- }
46
- }
47
-
48
- return left;
49
- }
50
-
51
- private calculateInternal() {
52
- // Calculate seed
53
- for (let i = 0; i < this.users.length; i++) {
54
- const u = this.users[i];
55
- u.seed = 1.0;
56
- for (let j = 0; j < this.users.length; j++) {
57
- if (i !== j) {
58
- const otherUser = this.users[j];
59
- u.seed += this.calcP(otherUser, u);
60
- }
61
- }
62
- }
63
-
64
- // Calculate initial delta and sumDelta
65
- let sumDelta = 0;
66
- for (let i = 0; i < this.users.length; i++) {
67
- const u = this.users[i];
68
- u.delta = Math.floor(
69
- (this.calcRating(this.users, Math.sqrt(u.rank * u.seed), u)
70
- - u.oldRating)
71
- / 2,
72
- );
73
- sumDelta += u.delta;
74
- }
75
-
76
- // Calculate first inc
77
- let inc = Math.floor(-sumDelta / this.users.length) - 1;
78
- for (let i = 0; i < this.users.length; i++) {
79
- const u = this.users[i];
80
- u.delta += inc;
81
- }
82
-
83
- // Calculate second inc
84
- this.users = this.users.sort((a, b) => b.oldRating - a.oldRating);
85
- const s = Math.min(this.users.length, Math.floor(4 * Math.round(Math.sqrt(this.users.length))));
86
- let sumS = 0;
87
- for (let i = 0; i < s; i++) {
88
- sumS += this.users[i].delta;
89
- }
90
- inc = Math.min(Math.max(Math.floor(-sumS / s), -10), 0);
91
-
92
- // Calculate new rating
93
- this.users.forEach((u) => {
94
- u.delta += inc;
95
- u.UpdateRating(u.oldRating + u.delta);
96
- });
97
-
98
- this.users = this.users.sort((a, b) => a.rank - b.rank);
99
- }
100
- }