carrot-script

Predicts Codeforces rating changes, original by meooow25 (https://github.com/meooow25/carrot), ported to Tampermonkey by RimuruChan

  1. // ==UserScript==
  2. // @name carrot-script
  3. // @namespace https://gf.qytechs.cn/zh-CN/users/1182955
  4. // @version 0.1.1
  5. // @author meooow25 & RimuruChan
  6. // @description Predicts Codeforces rating changes, original by meooow25 (https://github.com/meooow25/carrot), ported to Tampermonkey by RimuruChan
  7. // @license MIT
  8. // @icon https://aowuucdn.oss-accelerate.aliyuncs.com/codeforces.png
  9. // @homepageURL https://github.com/RimuruChan/carrot-userscript
  10. // @match https://codeforces.com/*
  11. // @grant GM.deleteValue
  12. // @grant GM_addStyle
  13. // @grant GM_deleteValue
  14. // @grant GM_getValue
  15. // @grant GM_listValues
  16. // @grant GM_registerMenuCommand
  17. // @grant GM_setValue
  18. // ==/UserScript==
  19.  
  20. (function () {
  21. 'use strict';
  22.  
  23. var __defProp = Object.defineProperty;
  24. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  25. var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  26. class Api {
  27. constructor(fetchFromContentScript2) {
  28. __publicField(this, "fetchFromContentScript");
  29. this.fetchFromContentScript = fetchFromContentScript2;
  30. }
  31. async fetch(path, queryParams) {
  32. let queryParamList = [];
  33. for (const [key, value] of Object.entries(queryParams)) {
  34. if (value !== void 0) {
  35. queryParamList.push([key, value]);
  36. }
  37. }
  38. return await this.fetchFromContentScript(path, queryParamList);
  39. }
  40. async contestList(gym = void 0) {
  41. return await this.fetch("contest.list", { gym });
  42. }
  43. async contestStandings(contestId, from = void 0, count = void 0, handles = void 0, room = void 0, showUnofficial = void 0) {
  44. return await this.fetch("contest.standings", {
  45. contestId,
  46. from,
  47. count,
  48. handles: handles && handles.length ? handles.join(";") : void 0,
  49. room,
  50. showUnofficial
  51. });
  52. }
  53. async contestRatingChanges(contestId) {
  54. return await this.fetch("contest.ratingChanges", { contestId });
  55. }
  56. async userRatedList(activeOnly = false) {
  57. return await this.fetch("user.ratedList", { activeOnly });
  58. }
  59. }
  60. class FFTConv {
  61. constructor(n) {
  62. __publicField(this, "n");
  63. __publicField(this, "wr");
  64. __publicField(this, "wi");
  65. __publicField(this, "rev");
  66. let k = 1;
  67. while (1 << k < n) {
  68. k++;
  69. }
  70. this.n = 1 << k;
  71. const n2 = this.n >> 1;
  72. this.wr = [];
  73. this.wi = [];
  74. const ang = 2 * Math.PI / this.n;
  75. for (let i = 0; i < n2; i++) {
  76. this.wr[i] = Math.cos(i * ang);
  77. this.wi[i] = Math.sin(i * ang);
  78. }
  79. this.rev = [0];
  80. for (let i = 1; i < this.n; i++) {
  81. this.rev[i] = this.rev[i >> 1] >> 1 | (i & 1) << k - 1;
  82. }
  83. }
  84. reverse(a) {
  85. for (let i = 1; i < this.n; i++) {
  86. if (i < this.rev[i]) {
  87. const tmp = a[i];
  88. a[i] = a[this.rev[i]];
  89. a[this.rev[i]] = tmp;
  90. }
  91. }
  92. }
  93. transform(ar, ai) {
  94. this.reverse(ar);
  95. this.reverse(ai);
  96. const wr = this.wr;
  97. const wi = this.wi;
  98. for (let len = 2; len <= this.n; len <<= 1) {
  99. const half = len >> 1;
  100. const diff = this.n / len;
  101. for (let i = 0; i < this.n; i += len) {
  102. let pw = 0;
  103. for (let j = i; j < i + half; j++) {
  104. const k = j + half;
  105. const vr = ar[k] * wr[pw] - ai[k] * wi[pw];
  106. const vi = ar[k] * wi[pw] + ai[k] * wr[pw];
  107. ar[k] = ar[j] - vr;
  108. ai[k] = ai[j] - vi;
  109. ar[j] += vr;
  110. ai[j] += vi;
  111. pw += diff;
  112. }
  113. }
  114. }
  115. }
  116. convolve(a, b) {
  117. if (a.length === 0 || b.length === 0) {
  118. return [];
  119. }
  120. const n = this.n;
  121. const resLen = a.length + b.length - 1;
  122. if (resLen > n) {
  123. throw new Error(
  124. `a.length + b.length - 1 is ${a.length} + ${b.length} - 1 = ${resLen}, expected <= ${n}`
  125. );
  126. }
  127. const cr = new Array(n).fill(0);
  128. const ci = new Array(n).fill(0);
  129. cr.splice(0, a.length, ...a);
  130. ci.splice(0, b.length, ...b);
  131. this.transform(cr, ci);
  132. cr[0] = 4 * cr[0] * ci[0];
  133. ci[0] = 0;
  134. for (let i = 1, j = n - 1; i <= j; i++, j--) {
  135. const ar = cr[i] + cr[j];
  136. const ai = ci[i] - ci[j];
  137. const br = ci[j] + ci[i];
  138. const bi = cr[j] - cr[i];
  139. cr[i] = ar * br - ai * bi;
  140. ci[i] = ar * bi + ai * br;
  141. cr[j] = cr[i];
  142. ci[j] = -ci[i];
  143. }
  144. this.transform(cr, ci);
  145. const res = [];
  146. res[0] = cr[0] / (4 * n);
  147. for (let i = 1, j = n - 1; i <= j; i++, j--) {
  148. res[i] = cr[j] / (4 * n);
  149. res[j] = cr[i] / (4 * n);
  150. }
  151. res.splice(resLen);
  152. return res;
  153. }
  154. }
  155. function binarySearch(left, right, predicate) {
  156. if (left > right) {
  157. throw new Error(`left ${left} must be <= right ${right}`);
  158. }
  159. while (left < right) {
  160. const mid = Math.floor((left + right) / 2);
  161. if (predicate(mid)) {
  162. right = mid;
  163. } else {
  164. left = mid + 1;
  165. }
  166. }
  167. return left;
  168. }
  169. const DEFAULT_RATING = 1400;
  170. class Contestant {
  171. constructor(handle, points, penalty, rating) {
  172. __publicField(this, "handle");
  173. __publicField(this, "points");
  174. __publicField(this, "penalty");
  175. __publicField(this, "rating");
  176. __publicField(this, "effectiveRating");
  177. __publicField(this, "rank");
  178. __publicField(this, "delta");
  179. __publicField(this, "performance");
  180. this.handle = handle;
  181. this.points = points;
  182. this.penalty = penalty;
  183. this.rating = rating;
  184. this.effectiveRating = rating == null ? DEFAULT_RATING : rating;
  185. this.rank = null;
  186. this.delta = null;
  187. this.performance = null;
  188. }
  189. }
  190. class PredictResult {
  191. constructor(handle, rating, delta, performance2) {
  192. __publicField(this, "handle");
  193. __publicField(this, "rating");
  194. __publicField(this, "delta");
  195. __publicField(this, "performance");
  196. this.handle = handle;
  197. this.rating = rating;
  198. this.delta = delta;
  199. this.performance = performance2;
  200. }
  201. get effectiveRating() {
  202. return this.rating == null ? DEFAULT_RATING : this.rating;
  203. }
  204. }
  205. const MAX_RATING_LIMIT = 6e3;
  206. const MIN_RATING_LIMIT = -500;
  207. const RATING_RANGE_LEN = MAX_RATING_LIMIT - MIN_RATING_LIMIT;
  208. const ELO_OFFSET = RATING_RANGE_LEN;
  209. const RATING_OFFSET = -MIN_RATING_LIMIT;
  210. const ELO_WIN_PROB = new Array(2 * RATING_RANGE_LEN + 1);
  211. for (let i = -RATING_RANGE_LEN; i <= RATING_RANGE_LEN; i++) {
  212. ELO_WIN_PROB[i + ELO_OFFSET] = 1 / (1 + Math.pow(10, i / 400));
  213. }
  214. const fftConv = new FFTConv(ELO_WIN_PROB.length + RATING_RANGE_LEN - 1);
  215. class RatingCalculator {
  216. constructor(contestants) {
  217. __publicField(this, "contestants");
  218. __publicField(this, "seed");
  219. __publicField(this, "adjustment");
  220. this.contestants = contestants;
  221. this.seed = null;
  222. this.adjustment = null;
  223. }
  224. calculateDeltas(calcPerfs = false) {
  225. performance.now();
  226. this.calcSeed();
  227. this.reassignRanks();
  228. this.calcDeltas();
  229. this.adjustDeltas();
  230. if (calcPerfs) {
  231. this.calcPerfs();
  232. }
  233. performance.now();
  234. }
  235. calcSeed() {
  236. const counts = new Array(RATING_RANGE_LEN).fill(0);
  237. for (const c of this.contestants) {
  238. counts[c.effectiveRating + RATING_OFFSET] += 1;
  239. }
  240. this.seed = fftConv.convolve(ELO_WIN_PROB, counts);
  241. for (let i = 0; i < this.seed.length; i++) {
  242. this.seed[i] += 1;
  243. }
  244. }
  245. getSeed(r, exclude) {
  246. return this.seed[r + ELO_OFFSET + RATING_OFFSET] - ELO_WIN_PROB[r - exclude + ELO_OFFSET];
  247. }
  248. reassignRanks() {
  249. this.contestants.sort(
  250. (a, b) => a.points !== b.points ? b.points - a.points : a.penalty - b.penalty
  251. );
  252. let lastPoints, lastPenalty, rank;
  253. for (let i = this.contestants.length - 1; i >= 0; i--) {
  254. const c = this.contestants[i];
  255. if (c.points !== lastPoints || c.penalty !== lastPenalty) {
  256. lastPoints = c.points;
  257. lastPenalty = c.penalty;
  258. rank = i + 1;
  259. }
  260. c.rank = rank;
  261. }
  262. }
  263. calcDelta(contestant, assumedRating) {
  264. const c = contestant;
  265. const seed = this.getSeed(assumedRating, c.effectiveRating);
  266. const midRank = Math.sqrt(c.rank * seed);
  267. const needRating = this.rankToRating(midRank, c.effectiveRating);
  268. const delta = Math.trunc((needRating - assumedRating) / 2);
  269. return delta;
  270. }
  271. calcDeltas() {
  272. for (const c of this.contestants) {
  273. c.delta = this.calcDelta(c, c.effectiveRating);
  274. }
  275. }
  276. rankToRating(rank, selfRating) {
  277. return binarySearch(
  278. 2,
  279. MAX_RATING_LIMIT,
  280. (rating) => this.getSeed(rating, selfRating) < rank
  281. ) - 1;
  282. }
  283. adjustDeltas() {
  284. this.contestants.sort((a, b) => b.effectiveRating - a.effectiveRating);
  285. const n = this.contestants.length;
  286. {
  287. const deltaSum = this.contestants.reduce((a, b) => a + b.delta, 0);
  288. const inc = Math.trunc(-deltaSum / n) - 1;
  289. this.adjustment = inc;
  290. for (const c of this.contestants) {
  291. c.delta += inc;
  292. }
  293. }
  294. {
  295. const zeroSumCount = Math.min(4 * Math.round(Math.sqrt(n)), n);
  296. const deltaSum = this.contestants.slice(0, zeroSumCount).reduce((a, b) => a + b.delta, 0);
  297. const inc = Math.min(Math.max(Math.trunc(-deltaSum / zeroSumCount), -10), 0);
  298. this.adjustment += inc;
  299. for (const c of this.contestants) {
  300. c.delta += inc;
  301. }
  302. }
  303. }
  304. calcPerfs() {
  305. for (const c of this.contestants) {
  306. if (c.rank === 1) {
  307. c.performance = Infinity;
  308. } else {
  309. c.performance = binarySearch(
  310. MIN_RATING_LIMIT,
  311. MAX_RATING_LIMIT,
  312. (assumedRating) => this.calcDelta(c, assumedRating) + this.adjustment <= 0
  313. );
  314. }
  315. }
  316. }
  317. }
  318. function predict$1(contestants, calcPerfs = false) {
  319. new RatingCalculator(contestants).calculateDeltas(calcPerfs);
  320. return contestants.map((c) => new PredictResult(c.handle, c.rating, c.delta, c.performance));
  321. }
  322. const _Rank = class _Rank {
  323. constructor(name, abbr, low, high, colorClass) {
  324. __publicField(this, "name");
  325. __publicField(this, "abbr");
  326. __publicField(this, "low");
  327. __publicField(this, "high");
  328. __publicField(this, "colorClass");
  329. this.name = name;
  330. this.abbr = abbr;
  331. this.low = low;
  332. this.high = high;
  333. this.colorClass = colorClass;
  334. }
  335. static forRating(rating) {
  336. if (rating == null) {
  337. return _Rank.UNRATED;
  338. }
  339. for (const rank of _Rank.RATED) {
  340. if (rating < rank.high) {
  341. return rank;
  342. }
  343. }
  344. return _Rank.RATED[_Rank.RATED.length - 1];
  345. }
  346. };
  347. __publicField(_Rank, "UNRATED");
  348. __publicField(_Rank, "RATED");
  349. let Rank = _Rank;
  350. Rank.UNRATED = new Rank("Unrated", "U", -Infinity, null, null);
  351. Rank.RATED = [
  352. new Rank("Newbie", "N", -Infinity, 1200, "user-gray"),
  353. new Rank("Pupil", "P", 1200, 1400, "user-green"),
  354. new Rank("Specialist", "S", 1400, 1600, "user-cyan"),
  355. new Rank("Expert", "E", 1600, 1900, "user-blue"),
  356. new Rank("Candidate Master", "CM", 1900, 2100, "user-violet"),
  357. new Rank("Master", "M", 2100, 2300, "user-orange"),
  358. new Rank("International Master", "IM", 2300, 2400, "user-orange"),
  359. new Rank("Grandmaster", "GM", 2400, 2600, "user-red"),
  360. new Rank("International Grandmaster", "IGM", 2600, 3e3, "user-red"),
  361. new Rank("Legendary Grandmaster", "LGM", 3e3, 4e3, "user-legendary"),
  362. new Rank("Tourist", "T", 4e3, Infinity, "user-4000")
  363. ];
  364. class PredictResponseRow {
  365. constructor(delta, rank, performance2, newRank, deltaReqForRankUp, nextRank) {
  366. __publicField(this, "delta");
  367. __publicField(this, "rank");
  368. __publicField(this, "performance");
  369. // For FINAL
  370. __publicField(this, "newRank");
  371. // For PREDICTED
  372. __publicField(this, "deltaReqForRankUp");
  373. __publicField(this, "nextRank");
  374. this.delta = delta;
  375. this.rank = rank;
  376. this.performance = performance2;
  377. this.newRank = newRank;
  378. this.deltaReqForRankUp = deltaReqForRankUp;
  379. this.nextRank = nextRank;
  380. }
  381. }
  382. const _PredictResponse = class _PredictResponse {
  383. constructor(predictResults, type, fetchTime) {
  384. __publicField(this, "rowMap");
  385. __publicField(this, "type");
  386. __publicField(this, "fetchTime");
  387. _PredictResponse.assertTypeOk(type);
  388. this.rowMap = {};
  389. this.type = type;
  390. this.fetchTime = fetchTime;
  391. this.populateMap(predictResults);
  392. }
  393. populateMap(predictResults) {
  394. for (const result of predictResults) {
  395. let rank, newRank, deltaReqForRankUp, nextRank;
  396. switch (this.type) {
  397. case _PredictResponse.TYPE_PREDICTED:
  398. rank = Rank.forRating(result.rating);
  399. const effectiveRank = Rank.forRating(result.effectiveRating);
  400. deltaReqForRankUp = effectiveRank.high - result.effectiveRating;
  401. nextRank = Rank.RATED[Rank.RATED.indexOf(effectiveRank) + 1] || null;
  402. break;
  403. case _PredictResponse.TYPE_FINAL:
  404. rank = Rank.forRating(result.rating);
  405. newRank = Rank.forRating(result.effectiveRating + result.delta);
  406. break;
  407. default:
  408. throw new Error("Unknown prediction type");
  409. }
  410. const performance2 = {
  411. value: result.performance === Infinity ? "Infinity" : result.performance,
  412. colorClass: Rank.forRating(result.performance).colorClass
  413. };
  414. this.rowMap[result.handle] = new PredictResponseRow(
  415. result.delta,
  416. rank,
  417. performance2,
  418. newRank,
  419. deltaReqForRankUp,
  420. nextRank
  421. );
  422. }
  423. }
  424. static assertTypeOk(type) {
  425. if (!_PredictResponse.TYPES.includes(type)) {
  426. throw new Error("Unknown prediction type: " + type);
  427. }
  428. }
  429. };
  430. __publicField(_PredictResponse, "TYPE_PREDICTED", "PREDICTED");
  431. __publicField(_PredictResponse, "TYPE_FINAL", "FINAL");
  432. __publicField(_PredictResponse, "TYPES", [_PredictResponse.TYPE_PREDICTED, _PredictResponse.TYPE_FINAL]);
  433. let PredictResponse = _PredictResponse;
  434. class Lock {
  435. constructor() {
  436. __publicField(this, "queue");
  437. __publicField(this, "locked");
  438. this.queue = [];
  439. this.locked = false;
  440. }
  441. async acquire() {
  442. if (this.locked) {
  443. await new Promise((resolve) => {
  444. this.queue.push(resolve);
  445. });
  446. }
  447. this.locked = true;
  448. }
  449. release() {
  450. if (!this.locked) {
  451. throw new Error("The lock must be acquired before release");
  452. }
  453. this.locked = false;
  454. if (this.queue.length) {
  455. const resolve = this.queue.shift();
  456. resolve();
  457. }
  458. }
  459. async execute(asyncFunc) {
  460. await this.acquire();
  461. try {
  462. return await asyncFunc();
  463. } finally {
  464. this.release();
  465. }
  466. }
  467. }
  468. const REFRESH_INTERVAL = 6 * 60 * 60 * 1e3;
  469. const CONTESTS$1 = "cache.contests";
  470. const CONTESTS_TIMESTAMP = "cache.contests.timestamp";
  471. class Contests {
  472. constructor(api, storage) {
  473. __publicField(this, "api");
  474. __publicField(this, "storage");
  475. __publicField(this, "lock");
  476. this.api = api;
  477. this.storage = storage;
  478. this.lock = new Lock();
  479. }
  480. async getLastAttemptTime() {
  481. return await this.storage.get(CONTESTS_TIMESTAMP, 0);
  482. }
  483. async setLastAttemptTime(time) {
  484. await this.storage.set(CONTESTS_TIMESTAMP, time);
  485. }
  486. async getContestMap() {
  487. let res = await this.storage.get(CONTESTS$1, {});
  488. res = new Map(Object.entries(res).map(([k, v]) => [parseInt(k), v]));
  489. return res;
  490. }
  491. async setContestMap(contestMap) {
  492. const obj = Object.fromEntries(contestMap);
  493. await this.storage.set(CONTESTS$1, obj);
  494. }
  495. async maybeRefreshCache() {
  496. const inner = async () => {
  497. const now = Date.now();
  498. const refresh = now - await this.getLastAttemptTime() > REFRESH_INTERVAL;
  499. if (!refresh) {
  500. return;
  501. }
  502. await this.setLastAttemptTime(now);
  503. try {
  504. const contests = await this.api.contestList();
  505. await this.setContestMap(new Map(contests.map((c) => [c.id, c])));
  506. } catch (er) {
  507. console.warn("Unable to fetch contest list: " + er);
  508. }
  509. };
  510. await this.lock.execute(inner);
  511. }
  512. async list() {
  513. return Array.from((await this.getContestMap()).values());
  514. }
  515. async hasCached(contestId) {
  516. return (await this.getContestMap()).has(contestId);
  517. }
  518. async getCached(contestId) {
  519. return (await this.getContestMap()).get(contestId);
  520. }
  521. async update(contest) {
  522. const contestMap = await this.getContestMap();
  523. contestMap.set(contest.id, contest);
  524. await this.setContestMap(contestMap);
  525. }
  526. }
  527. const PREFETCH_INTERVAL = 60 * 60 * 1e3;
  528. const RATINGS_TIMESTAMP = "cache.ratings.timestamp";
  529. const RATINGS$1 = "cache.ratings";
  530. class Ratings {
  531. constructor(api, storage) {
  532. __publicField(this, "api");
  533. __publicField(this, "storage");
  534. __publicField(this, "lock");
  535. this.api = api;
  536. this.storage = storage;
  537. this.lock = new Lock();
  538. }
  539. async maybeRefreshCache(contestStartMs) {
  540. const inner = async () => {
  541. const timeLeft = contestStartMs - Date.now();
  542. if (timeLeft > PREFETCH_INTERVAL) {
  543. return;
  544. }
  545. const timeLeftAfterLastFetch = contestStartMs - await this.storage.get(RATINGS_TIMESTAMP, 0);
  546. if (timeLeftAfterLastFetch > PREFETCH_INTERVAL) {
  547. await this.cacheRatings();
  548. }
  549. };
  550. await this.lock.execute(inner);
  551. }
  552. async fetchCurrentRatings(contestStartMs) {
  553. if (Date.now() < contestStartMs) {
  554. throw new Error("getCurrentRatings should be called after contest start");
  555. }
  556. await this.maybeRefreshCache(contestStartMs);
  557. const ratings = await this.storage.get(RATINGS$1);
  558. return new Map(Object.entries(ratings));
  559. }
  560. async cacheRatings() {
  561. const users = await this.api.userRatedList(false);
  562. const ratings = Object.fromEntries(users.map((u) => [u.handle, u.rating]));
  563. await this.storage.set(RATINGS$1, ratings);
  564. await this.storage.set(RATINGS_TIMESTAMP, Date.now());
  565. }
  566. }
  567. const _Contest = class _Contest {
  568. constructor(contest, problems, rows, ratingChanges, oldRatings, fetchTime, isRated) {
  569. __publicField(this, "contest");
  570. __publicField(this, "problems");
  571. __publicField(this, "rows");
  572. __publicField(this, "ratingChanges");
  573. __publicField(this, "oldRatings");
  574. __publicField(this, "performances");
  575. __publicField(this, "fetchTime");
  576. __publicField(this, "isRated");
  577. __publicField(this, "startTimeSeconds");
  578. __publicField(this, "durationSeconds");
  579. this.contest = contest;
  580. this.problems = problems;
  581. this.rows = rows;
  582. this.ratingChanges = ratingChanges;
  583. this.oldRatings = oldRatings;
  584. this.fetchTime = fetchTime;
  585. this.isRated = isRated;
  586. this.performances = null;
  587. this.startTimeSeconds = 0;
  588. this.durationSeconds = 0;
  589. }
  590. toPlainObject() {
  591. return {
  592. contest: this.contest,
  593. problems: this.problems,
  594. rows: this.rows,
  595. ratingChanges: this.ratingChanges,
  596. oldRatings: Array.from(this.oldRatings),
  597. fetchTime: this.fetchTime,
  598. isRated: this.isRated
  599. };
  600. }
  601. static fromPlainObject(obj) {
  602. const c = new _Contest(
  603. obj.contest,
  604. obj.problems,
  605. obj.rows,
  606. obj.ratingChanges,
  607. new Map(obj.oldRatings),
  608. obj.fetchTime,
  609. obj.isRated
  610. );
  611. return c;
  612. }
  613. };
  614. __publicField(_Contest, "IsRated", {
  615. YES: "YES",
  616. NO: "NO",
  617. LIKELY: "LIKELY"
  618. });
  619. let Contest = _Contest;
  620. const MAGIC_CACHE_DURATION = 5 * 60 * 1e3;
  621. const RATING_PENDING_MAX_DAYS = 3;
  622. function isOldContest(contest) {
  623. const daysSinceContestEnd = (Date.now() / 1e3 - contest.startTimeSeconds - contest.durationSeconds) / (60 * 60 * 24);
  624. return daysSinceContestEnd > RATING_PENDING_MAX_DAYS;
  625. }
  626. function isMagicOn() {
  627. let now = /* @__PURE__ */ new Date();
  628. return now.getMonth() === 11 && now.getDate() >= 24 || now.getMonth() === 0 && now.getDate() <= 11;
  629. }
  630. const MAX_FINISHED_CONTESTS_TO_CACHE = 5;
  631. const CONTESTS_COMPLETE$1 = "cache.contests_complete";
  632. const CONTESTS_COMPLETE_IDS = "cache.contests_complete.ids";
  633. const CONTESTS_COMPLETE_TIMESTAMP = "cache.contests_complete.timestamp";
  634. class ContestsComplete {
  635. constructor(api, storage) {
  636. __publicField(this, "api");
  637. __publicField(this, "storage");
  638. this.api = api;
  639. this.storage = storage;
  640. }
  641. async getContests() {
  642. let res = await this.storage.get(CONTESTS_COMPLETE$1, {});
  643. res = new Map(Object.entries(res).map(([k, v]) => [parseInt(k), Contest.fromPlainObject(v)]));
  644. return res;
  645. }
  646. async setContests(contests) {
  647. const obj = Object.fromEntries([...contests.entries()].map(([k, v]) => [k, v.toPlainObject()]));
  648. await this.storage.set(CONTESTS_COMPLETE$1, obj);
  649. }
  650. async getContestIds() {
  651. return await this.storage.get(CONTESTS_COMPLETE_IDS, []);
  652. }
  653. async setContestIds(contestIds) {
  654. await this.storage.set(CONTESTS_COMPLETE_IDS, contestIds);
  655. }
  656. async getContestTimestamp() {
  657. let res = await this.storage.get(CONTESTS_COMPLETE_TIMESTAMP, {});
  658. res = new Map(Object.entries(res));
  659. return res;
  660. }
  661. async setContestTimestamp(contestTimestamp) {
  662. const obj = Object.fromEntries(contestTimestamp);
  663. await this.storage.set(CONTESTS_COMPLETE_TIMESTAMP, obj);
  664. }
  665. async fetch(contestId) {
  666. const cachedContests = await this.getContests();
  667. if (cachedContests.has(contestId)) {
  668. console.log("Returning cached contest");
  669. return cachedContests.get(contestId);
  670. }
  671. const { contest, problems, rows } = await this.api.contestStandings(contestId);
  672. let ratingChanges;
  673. let oldRatings;
  674. let isRated = Contest.IsRated.LIKELY;
  675. if (contest.phase === "FINISHED") {
  676. try {
  677. ratingChanges = await this.api.contestRatingChanges(contestId);
  678. if (ratingChanges) {
  679. if (ratingChanges.length > 0) {
  680. isRated = Contest.IsRated.YES;
  681. oldRatings = adjustOldRatings(contestId, ratingChanges);
  682. } else {
  683. ratingChanges = void 0;
  684. }
  685. }
  686. } catch (er) {
  687. if (er.message.includes("Rating changes are unavailable for this contest")) {
  688. isRated = Contest.IsRated.NO;
  689. }
  690. }
  691. }
  692. if (isRated === Contest.IsRated.LIKELY && isOldContest(contest)) {
  693. isRated = Contest.IsRated.NO;
  694. }
  695. const isFinished = isRated === Contest.IsRated.NO || isRated === Contest.IsRated.YES;
  696. const c = new Contest(contest, problems, rows, ratingChanges, oldRatings, Date.now(), isRated);
  697. if (isFinished) {
  698. const contests = await this.getContests();
  699. contests.set(contestId, c);
  700. let contestIds = await this.getContestIds();
  701. contestIds.push(contestId);
  702. while (contestIds.length > MAX_FINISHED_CONTESTS_TO_CACHE) {
  703. contests.delete(contestIds.shift());
  704. }
  705. if (isMagicOn()) {
  706. const contestTimestamp = await this.getContestTimestamp();
  707. for (const [cid, timestamp] of contestTimestamp) {
  708. if (Date.now() - timestamp > MAGIC_CACHE_DURATION) {
  709. contestTimestamp.delete(cid);
  710. contests.delete(cid);
  711. contestIds = contestIds.filter((c2) => c2 !== cid);
  712. }
  713. }
  714. contestTimestamp.set(contestId, Date.now());
  715. await this.setContestTimestamp(contestTimestamp);
  716. }
  717. await this.setContests(contests);
  718. await this.setContestIds(contestIds);
  719. }
  720. return c;
  721. }
  722. }
  723. const FAKE_RATINGS_SINCE_CONTEST = 1360;
  724. const NEW_DEFAULT_RATING = 1400;
  725. function adjustOldRatings(contestId, ratingChanges) {
  726. const oldRatings = /* @__PURE__ */ new Map();
  727. if (contestId < FAKE_RATINGS_SINCE_CONTEST) {
  728. for (const change of ratingChanges) {
  729. oldRatings.set(change.handle, change.oldRating);
  730. }
  731. } else {
  732. for (const change of ratingChanges) {
  733. oldRatings.set(change.handle, change.oldRating == 0 ? NEW_DEFAULT_RATING : change.oldRating);
  734. }
  735. }
  736. return oldRatings;
  737. }
  738. var _GM_addStyle = /* @__PURE__ */ (() => typeof GM_addStyle != "undefined" ? GM_addStyle : void 0)();
  739. var _GM_deleteValue = /* @__PURE__ */ (() => typeof GM_deleteValue != "undefined" ? GM_deleteValue : void 0)();
  740. var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  741. var _GM_listValues = /* @__PURE__ */ (() => typeof GM_listValues != "undefined" ? GM_listValues : void 0)();
  742. var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
  743. var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  744. class StorageWrapper {
  745. constructor(storageName) {
  746. __publicField(this, "storageName");
  747. this.storageName = storageName;
  748. }
  749. async get(key, defaultValue = void 0) {
  750. return await _GM_getValue(`${this.storageName}.${key}`, defaultValue);
  751. }
  752. async set(key, value) {
  753. return await _GM_setValue(`${this.storageName}.${key}`, value);
  754. }
  755. }
  756. const LOCAL = new StorageWrapper("LOCAL");
  757. const SYNC = new StorageWrapper("SYNC");
  758. function boolSetterGetter(key, defaultValue) {
  759. return async (value) => {
  760. if (value === void 0) {
  761. return await SYNC.get(key, defaultValue);
  762. }
  763. return await SYNC.set(key, value);
  764. };
  765. }
  766. const enablePredictDeltas = boolSetterGetter("settings.enablePredictDeltas", true);
  767. const enableFinalDeltas = boolSetterGetter("settings.enableFetchDeltas", true);
  768. const enablePrefetchRatings = boolSetterGetter("settings.enablePrefetchRatings", true);
  769. const showColCurrentPerformance = boolSetterGetter("settings.showColCurrentPerformance", true);
  770. const showColPredictedDelta = boolSetterGetter("settings.showColPredictedDelta", true);
  771. const showColRankUpDelta = boolSetterGetter("settings.showColRankUpDelta", true);
  772. const showColFinalPerformance = boolSetterGetter("settings.showColFinalPerformance", true);
  773. const showColFinalDelta = boolSetterGetter("settings.showColFinalDelta", true);
  774. const showColRankChange = boolSetterGetter("settings.showColRankChange", true);
  775. async function getPrefs() {
  776. return {
  777. enablePredictDeltas: await enablePredictDeltas(),
  778. enableFinalDeltas: await enableFinalDeltas(),
  779. enablePrefetchRatings: await enablePrefetchRatings(),
  780. showColCurrentPerformance: await showColCurrentPerformance(),
  781. showColPredictedDelta: await showColPredictedDelta(),
  782. showColRankUpDelta: await showColRankUpDelta(),
  783. showColFinalPerformance: await showColFinalPerformance(),
  784. showColFinalDelta: await showColFinalDelta(),
  785. showColRankChange: await showColRankChange()
  786. };
  787. }
  788. const UNRATED_HINTS = ["unrated", "fools", "q#", "kotlin", "marathon", "teams"];
  789. const EDU_ROUND_RATED_THRESHOLD = 2100;
  790. const API = new Api(fetchFromContentScript);
  791. const CONTESTS = new Contests(API, LOCAL);
  792. const RATINGS = new Ratings(API, LOCAL);
  793. const CONTESTS_COMPLETE = new ContestsComplete(API, LOCAL);
  794. const API_PATH = "/api/";
  795. async function fetchFromContentScript(path, queryParamList) {
  796. const url = new URL(location.origin + API_PATH + path);
  797. for (const [key, value] of queryParamList) {
  798. url.searchParams.append(key, value);
  799. }
  800. const resp = await fetch(url);
  801. const text = await resp.text();
  802. if (resp.status !== 200) {
  803. throw new Error(`CF API: HTTP error ${resp.status}: ${text}`);
  804. }
  805. let json;
  806. try {
  807. json = JSON.parse(text);
  808. } catch (_) {
  809. throw new Error(`CF API: Invalid JSON: ${text}`);
  810. }
  811. if (json.status !== "OK" || json.result === void 0) {
  812. throw new Error(`CF API: Error: ${text}`);
  813. }
  814. return json.result;
  815. }
  816. function isUnratedByName(contestName) {
  817. const lower = contestName.toLowerCase();
  818. return UNRATED_HINTS.some((hint) => lower.includes(hint));
  819. }
  820. function anyRowHasTeam(rows) {
  821. return rows.some((row) => row.party.teamId != null || row.party.teamName != null);
  822. }
  823. async function getDeltas(contestId) {
  824. const prefs = await getPrefs();
  825. return await calcDeltas(contestId, prefs);
  826. }
  827. async function calcDeltas(contestId, prefs) {
  828. if (!prefs.enablePredictDeltas && !prefs.enableFinalDeltas) {
  829. return { result: "DISABLED" };
  830. }
  831. if (await CONTESTS.hasCached(contestId)) {
  832. const contest2 = await CONTESTS.getCached(contestId);
  833. if (isUnratedByName(contest2.name)) {
  834. return { result: "UNRATED_CONTEST" };
  835. }
  836. }
  837. const contest = await CONTESTS_COMPLETE.fetch(contestId);
  838. CONTESTS.update(contest.contest);
  839. if (contest.isRated === Contest.IsRated.NO) {
  840. return { result: "UNRATED_CONTEST" };
  841. }
  842. if (contest.isRated === Contest.IsRated.YES) {
  843. if (!prefs.enableFinalDeltas) {
  844. return { result: "DISABLED" };
  845. }
  846. return {
  847. result: "OK",
  848. prefs,
  849. predictResponse: getFinal(contest)
  850. };
  851. }
  852. if (isUnratedByName(contest.contest.name)) {
  853. return { result: "UNRATED_CONTEST" };
  854. }
  855. if (anyRowHasTeam(contest.rows)) {
  856. return { result: "UNRATED_CONTEST" };
  857. }
  858. if (!prefs.enablePredictDeltas) {
  859. return { result: "DISABLED" };
  860. }
  861. return {
  862. result: "OK",
  863. prefs,
  864. predictResponse: await getPredicted(contest)
  865. };
  866. }
  867. function predictForRows(rows, ratingBeforeContest) {
  868. const contestants = rows.map((row) => {
  869. const handle = row.party.members[0].handle;
  870. return new Contestant(handle, row.points, row.penalty, ratingBeforeContest.get(handle) ?? null);
  871. });
  872. return predict$1(contestants, true);
  873. }
  874. function getFinal(contest) {
  875. if (contest.performances === null) {
  876. const ratingBeforeContest = new Map(
  877. contest.ratingChanges.map((c) => [c.handle, contest.oldRatings.get(c.handle)])
  878. );
  879. const rows = contest.rows.filter((row) => {
  880. const handle = row.party.members[0].handle;
  881. return ratingBeforeContest.has(handle);
  882. });
  883. const predictResultsForPerf = predictForRows(rows, ratingBeforeContest);
  884. contest.performances = new Map(predictResultsForPerf.map((r) => [r.handle, r.performance]));
  885. }
  886. const predictResults = [];
  887. for (const change of contest.ratingChanges) {
  888. predictResults.push(
  889. new PredictResult(
  890. change.handle,
  891. change.oldRating,
  892. change.newRating - change.oldRating,
  893. contest.performances.get(change.handle)
  894. )
  895. );
  896. }
  897. return new PredictResponse(predictResults, PredictResponse.TYPE_FINAL, contest.fetchTime);
  898. }
  899. async function getPredicted(contest) {
  900. const ratingMap = await RATINGS.fetchCurrentRatings(contest.contest.startTimeSeconds * 1e3);
  901. const isEduRound = contest.contest.name.toLowerCase().includes("educational");
  902. let rows = contest.rows;
  903. if (isEduRound) {
  904. rows = contest.rows.filter((row) => {
  905. const handle = row.party.members[0].handle;
  906. return !ratingMap.has(handle) || ratingMap.get(handle) < EDU_ROUND_RATED_THRESHOLD;
  907. });
  908. }
  909. const predictResults = predictForRows(rows, ratingMap);
  910. return new PredictResponse(predictResults, PredictResponse.TYPE_PREDICTED, contest.fetchTime);
  911. }
  912. async function predictDeltas(contestId) {
  913. return await getDeltas(contestId);
  914. }
  915. async function maybeUpdateContestList() {
  916. const prefs = await getPrefs();
  917. if (!prefs.enablePredictDeltas && !prefs.enableFinalDeltas) {
  918. return;
  919. }
  920. await CONTESTS.maybeRefreshCache();
  921. }
  922. async function getNearestUpcomingRatedContestStartTime() {
  923. let nearest = null;
  924. const now = Date.now();
  925. for (const c of await CONTESTS.list()) {
  926. const start = (c.startTimeSeconds || 0) * 1e3;
  927. if (start < now || isUnratedByName(c.name)) {
  928. continue;
  929. }
  930. if (nearest === null || start < nearest) {
  931. nearest = start;
  932. }
  933. }
  934. return nearest;
  935. }
  936. async function maybeUpdateRatings() {
  937. const prefs = await getPrefs();
  938. if (!prefs.enablePredictDeltas || !prefs.enablePrefetchRatings) {
  939. return;
  940. }
  941. const startTimeMs = await getNearestUpcomingRatedContestStartTime();
  942. if (startTimeMs !== null) {
  943. await RATINGS.maybeRefreshCache(startTimeMs);
  944. }
  945. }
  946. const contentCss = ".carrot-display-none {\n display: none;\n}\n";
  947. const PING_INTERVAL = 3 * 60 * 1e3;
  948. const PREDICT_TEXT_ID = "carrot-predict-text";
  949. const DISPLAY_NONE_CLS = "carrot-display-none";
  950. const Unicode = {
  951. BLACK_CURVED_RIGHTWARDS_AND_UPWARDS_ARROW: "⮭",
  952. GREEK_CAPITAL_DELTA: "Δ",
  953. GREEK_CAPITAL_PI: "Π",
  954. INFINITY: "∞",
  955. SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL: "⭜",
  956. BACKSLANTED_SOUTH_ARROW_WITH_HORIZONTAL_TAIL: "⭝"
  957. };
  958. const PREDICT_COLUMNS = [
  959. {
  960. text: "current performance",
  961. id: "carrot-current-performance",
  962. setting: "showColCurrentPerformance"
  963. },
  964. {
  965. text: "predicted delta",
  966. id: "carrot-predicted-delta",
  967. setting: "showColPredictedDelta"
  968. },
  969. {
  970. text: "delta required to rank up",
  971. id: "carrot-rank-up-delta",
  972. setting: "showColRankUpDelta"
  973. }
  974. ];
  975. const FINAL_COLUMNS = [
  976. {
  977. text: "final performance",
  978. id: "carrot-final-performance",
  979. setting: "showColFinalPerformance"
  980. },
  981. {
  982. text: "final delta",
  983. id: "carrot-final-delta",
  984. setting: "showColFinalDelta"
  985. },
  986. {
  987. text: "rank change",
  988. id: "carrot-rank-change",
  989. setting: "showColRankChange"
  990. }
  991. ];
  992. const ALL_COLUMNS = PREDICT_COLUMNS.concat(FINAL_COLUMNS);
  993. function makeGreySpan(text, title) {
  994. const span = document.createElement("span");
  995. span.style.fontWeight = "bold";
  996. span.style.color = "lightgrey";
  997. span.textContent = text;
  998. if (title) {
  999. span.title = title;
  1000. }
  1001. span.classList.add("small");
  1002. return span;
  1003. }
  1004. function makePerformanceSpan(performance2) {
  1005. const span = document.createElement("span");
  1006. if (performance2.value === "Infinity") {
  1007. span.textContent = Unicode.INFINITY;
  1008. } else {
  1009. span.textContent = performance2.value;
  1010. span.classList.add(performance2.colorClass);
  1011. }
  1012. span.style.fontWeight = "bold";
  1013. span.style.display = "inline-block";
  1014. return span;
  1015. }
  1016. function makeRankSpan(rank) {
  1017. const span = document.createElement("span");
  1018. if (rank.colorClass) {
  1019. span.classList.add(rank.colorClass);
  1020. }
  1021. span.style.verticalAlign = "middle";
  1022. span.textContent = rank.abbr;
  1023. span.title = rank.name;
  1024. span.style.display = "inline-block";
  1025. return span;
  1026. }
  1027. function makeArrowSpan(arrow) {
  1028. const span = document.createElement("span");
  1029. span.classList.add("small");
  1030. span.style.verticalAlign = "middle";
  1031. span.style.paddingLeft = "0.5em";
  1032. span.style.paddingRight = "0.5em";
  1033. span.textContent = arrow;
  1034. return span;
  1035. }
  1036. function makeDeltaSpan(delta) {
  1037. const span = document.createElement("span");
  1038. span.style.fontWeight = "bold";
  1039. span.style.verticalAlign = "middle";
  1040. if (delta > 0) {
  1041. span.style.color = "green";
  1042. span.textContent = `+${delta}`;
  1043. } else {
  1044. span.style.color = "gray";
  1045. span.textContent = delta.toString();
  1046. }
  1047. return span;
  1048. }
  1049. function makeFinalRankUpSpan(rank, newRank, arrow) {
  1050. const span = document.createElement("span");
  1051. span.style.fontWeight = "bold";
  1052. span.appendChild(makeRankSpan(rank));
  1053. span.appendChild(makeArrowSpan(arrow));
  1054. span.appendChild(makeRankSpan(newRank));
  1055. return span;
  1056. }
  1057. function makePredictedRankUpSpan(rank, deltaReqForRankUp, nextRank) {
  1058. const span = document.createElement("span");
  1059. span.style.fontWeight = "bold";
  1060. if (nextRank === null) {
  1061. span.appendChild(makeRankSpan(rank));
  1062. return span;
  1063. }
  1064. span.appendChild(makeDeltaSpan(deltaReqForRankUp));
  1065. span.appendChild(makeArrowSpan(Unicode.SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL));
  1066. span.appendChild(makeRankSpan(nextRank));
  1067. return span;
  1068. }
  1069. function makePerfHeaderCell() {
  1070. const cell = document.createElement("th");
  1071. cell.classList.add("top");
  1072. cell.style.width = "4em";
  1073. {
  1074. const span = document.createElement("span");
  1075. span.textContent = Unicode.GREEK_CAPITAL_PI;
  1076. span.title = "Performance";
  1077. cell.appendChild(span);
  1078. }
  1079. return cell;
  1080. }
  1081. function makeDeltaHeaderCell(deltaColTitle) {
  1082. const cell = document.createElement("th");
  1083. cell.classList.add("top");
  1084. cell.style.width = "4.5em";
  1085. {
  1086. const span = document.createElement("span");
  1087. span.textContent = Unicode.GREEK_CAPITAL_DELTA;
  1088. span.title = deltaColTitle;
  1089. cell.appendChild(span);
  1090. }
  1091. cell.appendChild(document.createElement("br"));
  1092. {
  1093. const span = document.createElement("span");
  1094. span.classList.add("small");
  1095. span.id = PREDICT_TEXT_ID;
  1096. cell.appendChild(span);
  1097. }
  1098. return cell;
  1099. }
  1100. function makeRankUpHeaderCell(rankUpColWidth, rankUpColTitle) {
  1101. const cell = document.createElement("th");
  1102. cell.classList.add("top", "right");
  1103. cell.style.width = rankUpColWidth;
  1104. {
  1105. const span = document.createElement("span");
  1106. span.textContent = Unicode.BLACK_CURVED_RIGHTWARDS_AND_UPWARDS_ARROW;
  1107. span.title = rankUpColTitle;
  1108. cell.appendChild(span);
  1109. }
  1110. return cell;
  1111. }
  1112. function makeDataCell(bottom = false, right = false) {
  1113. const cell = document.createElement("td");
  1114. if (bottom) {
  1115. cell.classList.add("bottom");
  1116. }
  1117. if (right) {
  1118. cell.classList.add("right");
  1119. }
  1120. return cell;
  1121. }
  1122. function populateCells(row, type, rankUpTint, perfCell, deltaCell, rankUpCell) {
  1123. if (row === void 0) {
  1124. perfCell.appendChild(makeGreySpan("N/A", "Not applicable"));
  1125. deltaCell.appendChild(makeGreySpan("N/A", "Not applicable"));
  1126. rankUpCell.appendChild(makeGreySpan("N/A", "Not applicable"));
  1127. return;
  1128. }
  1129. perfCell.appendChild(makePerformanceSpan(row.performance));
  1130. deltaCell.appendChild(makeDeltaSpan(row.delta));
  1131. switch (type) {
  1132. case "FINAL":
  1133. if (row.rank.abbr === row.newRank.abbr) {
  1134. rankUpCell.appendChild(makeGreySpan("N/C", "No change"));
  1135. } else {
  1136. const arrow = row.delta > 0 ? Unicode.SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL : Unicode.BACKSLANTED_SOUTH_ARROW_WITH_HORIZONTAL_TAIL;
  1137. rankUpCell.appendChild(makeFinalRankUpSpan(row.rank, row.newRank, arrow));
  1138. }
  1139. break;
  1140. case "PREDICTED":
  1141. rankUpCell.appendChild(
  1142. makePredictedRankUpSpan(row.rank, row.deltaReqForRankUp, row.nextRank)
  1143. );
  1144. if (row.delta >= row.deltaReqForRankUp) {
  1145. const [color, priority] = rankUpTint;
  1146. rankUpCell.style.setProperty("background-color", color ?? null, priority);
  1147. }
  1148. break;
  1149. default:
  1150. throw new Error("Unknown prediction type");
  1151. }
  1152. }
  1153. function updateStandings(resp) {
  1154. let deltaColTitle, rankUpColWidth, rankUpColTitle, columns;
  1155. switch (resp.type) {
  1156. case "FINAL":
  1157. deltaColTitle = "Final rating change";
  1158. rankUpColWidth = "6.5em";
  1159. rankUpColTitle = "Rank change";
  1160. columns = FINAL_COLUMNS;
  1161. break;
  1162. case "PREDICTED":
  1163. deltaColTitle = "Predicted rating change";
  1164. rankUpColWidth = "7.5em";
  1165. rankUpColTitle = "Rating change for rank up";
  1166. columns = PREDICT_COLUMNS;
  1167. break;
  1168. default:
  1169. throw new Error("Unknown prediction type");
  1170. }
  1171. const rows = Array.from(document.querySelectorAll("table.standings tbody tr"));
  1172. for (const [idx, tableRow] of rows.entries()) {
  1173. tableRow.querySelector("th:last-child, td:last-child").classList.remove("right");
  1174. let perfCell, deltaCell, rankUpCell;
  1175. if (idx === 0) {
  1176. perfCell = makePerfHeaderCell();
  1177. deltaCell = makeDeltaHeaderCell(deltaColTitle);
  1178. rankUpCell = makeRankUpHeaderCell(rankUpColWidth, rankUpColTitle);
  1179. } else if (idx === rows.length - 1) {
  1180. perfCell = makeDataCell(true);
  1181. deltaCell = makeDataCell(true);
  1182. rankUpCell = makeDataCell(true, true);
  1183. } else {
  1184. perfCell = makeDataCell();
  1185. deltaCell = makeDataCell();
  1186. rankUpCell = makeDataCell(false, true);
  1187. const handle = tableRow.querySelector("td.contestant-cell").textContent.trim();
  1188. let rankUpTint;
  1189. if (tableRow.classList.contains("highlighted-row")) {
  1190. rankUpTint = ["#d1eef2", "important"];
  1191. } else {
  1192. rankUpTint = [idx % 2 ? "#ebf8eb" : "#f2fff2", void 0];
  1193. }
  1194. populateCells(resp.rowMap[handle], resp.type, rankUpTint, perfCell, deltaCell, rankUpCell);
  1195. }
  1196. const cells = [perfCell, deltaCell, rankUpCell];
  1197. for (let i = 0; i < cells.length; i++) {
  1198. const cell = cells[i];
  1199. if (idx % 2) {
  1200. cell.classList.add("dark");
  1201. }
  1202. cell.classList.add(columns[i].id, DISPLAY_NONE_CLS);
  1203. tableRow.appendChild(cell);
  1204. }
  1205. }
  1206. return columns;
  1207. }
  1208. function updateColumnVisibility(prefs) {
  1209. for (const col of ALL_COLUMNS) {
  1210. const showCol = prefs[col.setting];
  1211. const func = showCol ? (cell) => cell.classList.remove(DISPLAY_NONE_CLS) : (cell) => cell.classList.add(DISPLAY_NONE_CLS);
  1212. document.querySelectorAll(`.${col.id}`).forEach(func);
  1213. }
  1214. }
  1215. function showFinal() {
  1216. const predictTextSpan = document.getElementById(PREDICT_TEXT_ID);
  1217. predictTextSpan.textContent = "Final";
  1218. }
  1219. function showTimer(fetchTime) {
  1220. const predictTextSpan = document.getElementById(PREDICT_TEXT_ID);
  1221. function update() {
  1222. const secSincePredict = Math.floor((Date.now() - fetchTime) / 1e3);
  1223. if (secSincePredict < 30) {
  1224. predictTextSpan.textContent = "Just now";
  1225. } else if (secSincePredict < 60) {
  1226. predictTextSpan.textContent = "<1m old";
  1227. } else {
  1228. predictTextSpan.textContent = Math.floor(secSincePredict / 60) + "m old";
  1229. }
  1230. }
  1231. update();
  1232. setInterval(update, 1e3);
  1233. }
  1234. async function predict(contestId) {
  1235. const response = await predictDeltas(contestId);
  1236. switch (response.result) {
  1237. case "OK":
  1238. break;
  1239. case "UNRATED_CONTEST":
  1240. console.info("[Carrot] Unrated contest, not displaying delta column.");
  1241. return;
  1242. case "DISABLED":
  1243. console.info("[Carrot] Deltas for this contest are disabled according to user settings.");
  1244. return;
  1245. default:
  1246. throw new Error("Unknown result");
  1247. }
  1248. const columns = updateStandings(response.predictResponse);
  1249. switch (response.predictResponse.type) {
  1250. case "FINAL":
  1251. showFinal();
  1252. break;
  1253. case "PREDICTED":
  1254. showTimer(response.predictResponse.fetchTime);
  1255. break;
  1256. default:
  1257. throw new Error("Unknown prediction type");
  1258. }
  1259. updateColumnVisibility(response.prefs);
  1260. return columns;
  1261. }
  1262. function main() {
  1263. _GM_addStyle(contentCss);
  1264. const matches = location.pathname.match(/contest\/(\d+)\/standings/);
  1265. const contestId = matches ? matches[1] : null;
  1266. if (contestId && document.querySelector("table.standings")) {
  1267. predict(Number.parseInt(contestId)).then((columns) => {
  1268. }).catch((er) => {
  1269. console.error("[Carrot] Predict error: %o", er);
  1270. er.toString();
  1271. });
  1272. }
  1273. const ping = async () => {
  1274. await Promise.all([maybeUpdateContestList(), maybeUpdateRatings()]);
  1275. };
  1276. setInterval(ping, PING_INTERVAL);
  1277. }
  1278. const optionsHtml = '<dialog id="options-dialog">\n <h1>Options</h1>\n <div id="options-content">\n <ul>\n <li>\n <input type="checkbox" id="enable-predict-deltas">\n <label for="enable-predict-deltas">\n Predict and show deltas for running contests and recently finished contests\n </label>\n <ul class="inner-ul">\n <li>\n <details>\n <summary>\n TL;DR: Disable this if you are on a data capped network\n </summary>\n If you are on Codeforces and a contest starts in less than an hour, having this\n option enabled will prefetch user ratings (around 7MB of data) which is required for\n delta prediction. This is a one-time fetch for the contest. Disabling this will fetch\n the ratings when you open the ranklist for the first time.\n </details>\n <input type="checkbox" id="enable-prefetch-ratings">\n <label for="enable-prefetch-ratings">Prefetch ratings</label>\n </li>\n </ul>\n </li>\n <li>\n <input type="checkbox" id="enable-final-deltas">\n <label for="enable-final-deltas">\n Show final deltas for finished rated contests\n </label>\n </li>\n <button id="close-options">Close</button>\n </ul>\n </div>\n</dialog>';
  1279. const optionsCss = "#options-dialog {\n min-width: 500px;\n min-height: 180px;\n\n margin: 0 auto;\n padding: 10px;\n border: 1px solid #ccc;\n border-radius: 5px;\n background-color: #f9f9f9;\n top: 50%;\n left: 50%;\n -webkit-transform: translateX(-50%) translateY(-50%);\n -moz-transform: translateX(-50%) translateY(-50%);\n -ms-transform: translateX(-50%) translateY(-50%);\n transform: translateX(-50%) translateY(-50%);\n}\n\n#options-dialog ul {\n list-style-type: none;\n}\n\n#options-dialog li {\n padding-top: 5px;\n}\n\n#options-dialog .inner-ul {\n padding-left: 25px;\n}\n\n#options-dialog details {\n margin-left: 10px;\n}\n\n#options-dialog details > summary {\n margin-left: -10px;\n cursor: pointer;\n}\n";
  1280. async function setup() {
  1281. const predict2 = document.querySelector("#enable-predict-deltas");
  1282. const final = document.querySelector("#enable-final-deltas");
  1283. const prefetch = document.querySelector("#enable-prefetch-ratings");
  1284. async function update() {
  1285. predict2.checked = await enablePredictDeltas();
  1286. final.checked = await enableFinalDeltas();
  1287. prefetch.checked = await enablePrefetchRatings();
  1288. prefetch.disabled = !predict2.checked;
  1289. }
  1290. predict2.addEventListener("input", async () => {
  1291. await enablePredictDeltas(predict2.checked);
  1292. await update();
  1293. });
  1294. final.addEventListener("input", async () => {
  1295. await enableFinalDeltas(final.checked);
  1296. await update();
  1297. });
  1298. prefetch.addEventListener("input", async () => {
  1299. await enablePrefetchRatings(prefetch.checked);
  1300. await update();
  1301. });
  1302. await update();
  1303. }
  1304. function initOptions() {
  1305. $("body").append(optionsHtml);
  1306. _GM_addStyle(optionsCss);
  1307. _GM_registerMenuCommand("Open options", () => {
  1308. const dialog = document.querySelector("#options-dialog");
  1309. dialog.showModal();
  1310. });
  1311. _GM_registerMenuCommand("Clear cache", () => {
  1312. const list = _GM_listValues();
  1313. for (const key of list) {
  1314. if (key.startsWith("LOCAL.")) {
  1315. _GM_deleteValue(key);
  1316. }
  1317. }
  1318. });
  1319. $("#close-options").on("click", () => {
  1320. const dialog = document.querySelector("#options-dialog");
  1321. dialog.close();
  1322. });
  1323. setup();
  1324. }
  1325. initOptions();
  1326. main();
  1327.  
  1328. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址