您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Predicts Codeforces rating changes, original by meooow25 (https://github.com/meooow25/carrot), ported to Tampermonkey by RimuruChan
当前为
// ==UserScript== // @name carrot-script // @namespace https://gf.zukizuki.org/zh-CN/users/1182955 // @version 0.1.0 // @author meooow25 & RimuruChan // @description Predicts Codeforces rating changes, original by meooow25 (https://github.com/meooow25/carrot), ported to Tampermonkey by RimuruChan // @license MIT // @icon https://aowuucdn.oss-accelerate.aliyuncs.com/codeforces.png // @homepageURL https://github.com/RimuruChan/carrot-userscript // @match https://codeforces.com/* // @grant GM.deleteValue // @grant GM_addStyle // @grant GM_deleteValue // @grant GM_getValue // @grant GM_listValues // @grant GM_registerMenuCommand // @grant GM_setValue // ==/UserScript== (function () { 'use strict'; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); class Api { constructor(fetchFromContentScript2) { __publicField(this, "fetchFromContentScript"); this.fetchFromContentScript = fetchFromContentScript2; } async fetch(path, queryParams) { let queryParamList = []; for (const [key, value] of Object.entries(queryParams)) { if (value !== void 0) { queryParamList.push([key, value]); } } return await this.fetchFromContentScript(path, queryParamList); } async contestList(gym = void 0) { return await this.fetch("contest.list", { gym }); } async contestStandings(contestId, from = void 0, count = void 0, handles = void 0, room = void 0, showUnofficial = void 0) { return await this.fetch("contest.standings", { contestId, from, count, handles: handles && handles.length ? handles.join(";") : void 0, room, showUnofficial }); } async contestRatingChanges(contestId) { return await this.fetch("contest.ratingChanges", { contestId }); } async userRatedList(activeOnly = false) { return await this.fetch("user.ratedList", { activeOnly }); } } class FFTConv { constructor(n) { __publicField(this, "n"); __publicField(this, "wr"); __publicField(this, "wi"); __publicField(this, "rev"); let k = 1; while (1 << k < n) { k++; } this.n = 1 << k; const n2 = this.n >> 1; this.wr = []; this.wi = []; const ang = 2 * Math.PI / this.n; for (let i = 0; i < n2; i++) { this.wr[i] = Math.cos(i * ang); this.wi[i] = Math.sin(i * ang); } this.rev = [0]; for (let i = 1; i < this.n; i++) { this.rev[i] = this.rev[i >> 1] >> 1 | (i & 1) << k - 1; } } reverse(a) { for (let i = 1; i < this.n; i++) { if (i < this.rev[i]) { const tmp = a[i]; a[i] = a[this.rev[i]]; a[this.rev[i]] = tmp; } } } transform(ar, ai) { this.reverse(ar); this.reverse(ai); const wr = this.wr; const wi = this.wi; for (let len = 2; len <= this.n; len <<= 1) { const half = len >> 1; const diff = this.n / len; for (let i = 0; i < this.n; i += len) { let pw = 0; for (let j = i; j < i + half; j++) { const k = j + half; const vr = ar[k] * wr[pw] - ai[k] * wi[pw]; const vi = ar[k] * wi[pw] + ai[k] * wr[pw]; ar[k] = ar[j] - vr; ai[k] = ai[j] - vi; ar[j] += vr; ai[j] += vi; pw += diff; } } } } convolve(a, b) { if (a.length === 0 || b.length === 0) { return []; } const n = this.n; const resLen = a.length + b.length - 1; if (resLen > n) { throw new Error( `a.length + b.length - 1 is ${a.length} + ${b.length} - 1 = ${resLen}, expected <= ${n}` ); } const cr = new Array(n).fill(0); const ci = new Array(n).fill(0); cr.splice(0, a.length, ...a); ci.splice(0, b.length, ...b); this.transform(cr, ci); cr[0] = 4 * cr[0] * ci[0]; ci[0] = 0; for (let i = 1, j = n - 1; i <= j; i++, j--) { const ar = cr[i] + cr[j]; const ai = ci[i] - ci[j]; const br = ci[j] + ci[i]; const bi = cr[j] - cr[i]; cr[i] = ar * br - ai * bi; ci[i] = ar * bi + ai * br; cr[j] = cr[i]; ci[j] = -ci[i]; } this.transform(cr, ci); const res = []; res[0] = cr[0] / (4 * n); for (let i = 1, j = n - 1; i <= j; i++, j--) { res[i] = cr[j] / (4 * n); res[j] = cr[i] / (4 * n); } res.splice(resLen); return res; } } function binarySearch(left, right, predicate) { if (left > right) { throw new Error(`left ${left} must be <= right ${right}`); } while (left < right) { const mid = Math.floor((left + right) / 2); if (predicate(mid)) { right = mid; } else { left = mid + 1; } } return left; } const DEFAULT_RATING = 1400; class Contestant { constructor(handle, points, penalty, rating) { __publicField(this, "handle"); __publicField(this, "points"); __publicField(this, "penalty"); __publicField(this, "rating"); __publicField(this, "effectiveRating"); __publicField(this, "rank"); __publicField(this, "delta"); __publicField(this, "performance"); this.handle = handle; this.points = points; this.penalty = penalty; this.rating = rating; this.effectiveRating = rating == null ? DEFAULT_RATING : rating; this.rank = null; this.delta = null; this.performance = null; } } class PredictResult { constructor(handle, rating, delta, performance2) { __publicField(this, "handle"); __publicField(this, "rating"); __publicField(this, "delta"); __publicField(this, "performance"); this.handle = handle; this.rating = rating; this.delta = delta; this.performance = performance2; } get effectiveRating() { return this.rating == null ? DEFAULT_RATING : this.rating; } } const MAX_RATING_LIMIT = 6e3; const MIN_RATING_LIMIT = -500; const RATING_RANGE_LEN = MAX_RATING_LIMIT - MIN_RATING_LIMIT; const ELO_OFFSET = RATING_RANGE_LEN; const RATING_OFFSET = -MIN_RATING_LIMIT; const ELO_WIN_PROB = new Array(2 * RATING_RANGE_LEN + 1); for (let i = -RATING_RANGE_LEN; i <= RATING_RANGE_LEN; i++) { ELO_WIN_PROB[i + ELO_OFFSET] = 1 / (1 + Math.pow(10, i / 400)); } const fftConv = new FFTConv(ELO_WIN_PROB.length + RATING_RANGE_LEN - 1); class RatingCalculator { constructor(contestants) { __publicField(this, "contestants"); __publicField(this, "seed"); __publicField(this, "adjustment"); this.contestants = contestants; this.seed = null; this.adjustment = null; } calculateDeltas(calcPerfs = false) { performance.now(); this.calcSeed(); this.reassignRanks(); this.calcDeltas(); this.adjustDeltas(); if (calcPerfs) { this.calcPerfs(); } performance.now(); } calcSeed() { const counts = new Array(RATING_RANGE_LEN).fill(0); for (const c of this.contestants) { counts[c.effectiveRating + RATING_OFFSET] += 1; } this.seed = fftConv.convolve(ELO_WIN_PROB, counts); for (let i = 0; i < this.seed.length; i++) { this.seed[i] += 1; } } getSeed(r, exclude) { return this.seed[r + ELO_OFFSET + RATING_OFFSET] - ELO_WIN_PROB[r - exclude + ELO_OFFSET]; } reassignRanks() { this.contestants.sort( (a, b) => a.points !== b.points ? b.points - a.points : a.penalty - b.penalty ); let lastPoints, lastPenalty, rank; for (let i = this.contestants.length - 1; i >= 0; i--) { const c = this.contestants[i]; if (c.points !== lastPoints || c.penalty !== lastPenalty) { lastPoints = c.points; lastPenalty = c.penalty; rank = i + 1; } c.rank = rank; } } calcDelta(contestant, assumedRating) { const c = contestant; const seed = this.getSeed(assumedRating, c.effectiveRating); const midRank = Math.sqrt(c.rank * seed); const needRating = this.rankToRating(midRank, c.effectiveRating); const delta = Math.trunc((needRating - assumedRating) / 2); return delta; } calcDeltas() { for (const c of this.contestants) { c.delta = this.calcDelta(c, c.effectiveRating); } } rankToRating(rank, selfRating) { return binarySearch( 2, MAX_RATING_LIMIT, (rating) => this.getSeed(rating, selfRating) < rank ) - 1; } adjustDeltas() { this.contestants.sort((a, b) => b.effectiveRating - a.effectiveRating); const n = this.contestants.length; { const deltaSum = this.contestants.reduce((a, b) => a + b.delta, 0); const inc = Math.trunc(-deltaSum / n) - 1; this.adjustment = inc; for (const c of this.contestants) { c.delta += inc; } } { const zeroSumCount = Math.min(4 * Math.round(Math.sqrt(n)), n); const deltaSum = this.contestants.slice(0, zeroSumCount).reduce((a, b) => a + b.delta, 0); const inc = Math.min(Math.max(Math.trunc(-deltaSum / zeroSumCount), -10), 0); this.adjustment += inc; for (const c of this.contestants) { c.delta += inc; } } } calcPerfs() { for (const c of this.contestants) { if (c.rank === 1) { c.performance = Infinity; } else { c.performance = binarySearch( MIN_RATING_LIMIT, MAX_RATING_LIMIT, (assumedRating) => this.calcDelta(c, assumedRating) + this.adjustment <= 0 ); } } } } function predict$1(contestants, calcPerfs = false) { new RatingCalculator(contestants).calculateDeltas(calcPerfs); return contestants.map((c) => new PredictResult(c.handle, c.rating, c.delta, c.performance)); } const _Rank = class _Rank { constructor(name, abbr, low, high, colorClass) { __publicField(this, "name"); __publicField(this, "abbr"); __publicField(this, "low"); __publicField(this, "high"); __publicField(this, "colorClass"); this.name = name; this.abbr = abbr; this.low = low; this.high = high; this.colorClass = colorClass; } static forRating(rating) { if (rating == null) { return _Rank.UNRATED; } for (const rank of _Rank.RATED) { if (rating < rank.high) { return rank; } } return _Rank.RATED[_Rank.RATED.length - 1]; } }; __publicField(_Rank, "UNRATED"); __publicField(_Rank, "RATED"); let Rank = _Rank; Rank.UNRATED = new Rank("Unrated", "U", -Infinity, null, null); Rank.RATED = [ new Rank("Newbie", "N", -Infinity, 1200, "user-gray"), new Rank("Pupil", "P", 1200, 1400, "user-green"), new Rank("Specialist", "S", 1400, 1600, "user-cyan"), new Rank("Expert", "E", 1600, 1900, "user-blue"), new Rank("Candidate Master", "CM", 1900, 2100, "user-violet"), new Rank("Master", "M", 2100, 2300, "user-orange"), new Rank("International Master", "IM", 2300, 2400, "user-orange"), new Rank("Grandmaster", "GM", 2400, 2600, "user-red"), new Rank("International Grandmaster", "IGM", 2600, 3e3, "user-red"), new Rank("Legendary Grandmaster", "LGM", 3e3, 4e3, "user-legendary"), new Rank("Tourist", "T", 4e3, Infinity, "user-4000") ]; class PredictResponseRow { constructor(delta, rank, performance2, newRank, deltaReqForRankUp, nextRank) { __publicField(this, "delta"); __publicField(this, "rank"); __publicField(this, "performance"); // For FINAL __publicField(this, "newRank"); // For PREDICTED __publicField(this, "deltaReqForRankUp"); __publicField(this, "nextRank"); this.delta = delta; this.rank = rank; this.performance = performance2; this.newRank = newRank; this.deltaReqForRankUp = deltaReqForRankUp; this.nextRank = nextRank; } } const _PredictResponse = class _PredictResponse { constructor(predictResults, type, fetchTime) { __publicField(this, "rowMap"); __publicField(this, "type"); __publicField(this, "fetchTime"); _PredictResponse.assertTypeOk(type); this.rowMap = {}; this.type = type; this.fetchTime = fetchTime; this.populateMap(predictResults); } populateMap(predictResults) { for (const result of predictResults) { let rank, newRank, deltaReqForRankUp, nextRank; switch (this.type) { case _PredictResponse.TYPE_PREDICTED: rank = Rank.forRating(result.rating); const effectiveRank = Rank.forRating(result.effectiveRating); deltaReqForRankUp = effectiveRank.high - result.effectiveRating; nextRank = Rank.RATED[Rank.RATED.indexOf(effectiveRank) + 1] || null; break; case _PredictResponse.TYPE_FINAL: rank = Rank.forRating(result.rating); newRank = Rank.forRating(result.effectiveRating + result.delta); break; default: throw new Error("Unknown prediction type"); } const performance2 = { value: result.performance === Infinity ? "Infinity" : result.performance, colorClass: Rank.forRating(result.performance).colorClass }; this.rowMap[result.handle] = new PredictResponseRow( result.delta, rank, performance2, newRank, deltaReqForRankUp, nextRank ); } } static assertTypeOk(type) { if (!_PredictResponse.TYPES.includes(type)) { throw new Error("Unknown prediction type: " + type); } } }; __publicField(_PredictResponse, "TYPE_PREDICTED", "PREDICTED"); __publicField(_PredictResponse, "TYPE_FINAL", "FINAL"); __publicField(_PredictResponse, "TYPES", [_PredictResponse.TYPE_PREDICTED, _PredictResponse.TYPE_FINAL]); let PredictResponse = _PredictResponse; class Lock { constructor() { __publicField(this, "queue"); __publicField(this, "locked"); this.queue = []; this.locked = false; } async acquire() { if (this.locked) { await new Promise((resolve) => { this.queue.push(resolve); }); } this.locked = true; } release() { if (!this.locked) { throw new Error("The lock must be acquired before release"); } this.locked = false; if (this.queue.length) { const resolve = this.queue.shift(); resolve(); } } async execute(asyncFunc) { await this.acquire(); try { return await asyncFunc(); } finally { this.release(); } } } const REFRESH_INTERVAL = 6 * 60 * 60 * 1e3; const CONTESTS$1 = "cache.contests"; const CONTESTS_TIMESTAMP = "cache.contests.timestamp"; class Contests { constructor(api, storage) { __publicField(this, "api"); __publicField(this, "storage"); __publicField(this, "lock"); this.api = api; this.storage = storage; this.lock = new Lock(); } async getLastAttemptTime() { return await this.storage.get(CONTESTS_TIMESTAMP, 0); } async setLastAttemptTime(time) { await this.storage.set(CONTESTS_TIMESTAMP, time); } async getContestMap() { let res = await this.storage.get(CONTESTS$1, {}); res = new Map(Object.entries(res).map(([k, v]) => [parseInt(k), v])); return res; } async setContestMap(contestMap) { const obj = Object.fromEntries(contestMap); await this.storage.set(CONTESTS$1, obj); } async maybeRefreshCache() { const inner = async () => { const now = Date.now(); const refresh = now - await this.getLastAttemptTime() > REFRESH_INTERVAL; if (!refresh) { return; } await this.setLastAttemptTime(now); try { const contests = await this.api.contestList(); await this.setContestMap(new Map(contests.map((c) => [c.id, c]))); } catch (er) { console.warn("Unable to fetch contest list: " + er); } }; await this.lock.execute(inner); } async list() { return Array.from((await this.getContestMap()).values()); } async hasCached(contestId) { return (await this.getContestMap()).has(contestId); } async getCached(contestId) { return (await this.getContestMap()).get(contestId); } async update(contest) { const contestMap = await this.getContestMap(); contestMap.set(contest.id, contest); await this.setContestMap(contestMap); } } const PREFETCH_INTERVAL = 60 * 60 * 1e3; const RATINGS_TIMESTAMP = "cache.ratings.timestamp"; const RATINGS$1 = "cache.ratings"; class Ratings { constructor(api, storage) { __publicField(this, "api"); __publicField(this, "storage"); __publicField(this, "lock"); this.api = api; this.storage = storage; this.lock = new Lock(); } async maybeRefreshCache(contestStartMs) { const inner = async () => { const timeLeft = contestStartMs - Date.now(); if (timeLeft > PREFETCH_INTERVAL) { return; } const timeLeftAfterLastFetch = contestStartMs - await this.storage.get(RATINGS_TIMESTAMP, 0); if (timeLeftAfterLastFetch > PREFETCH_INTERVAL) { await this.cacheRatings(); } }; await this.lock.execute(inner); } async fetchCurrentRatings(contestStartMs) { if (Date.now() < contestStartMs) { throw new Error("getCurrentRatings should be called after contest start"); } await this.maybeRefreshCache(contestStartMs); const ratings = await this.storage.get(RATINGS$1); return new Map(Object.entries(ratings)); } async cacheRatings() { const users = await this.api.userRatedList(false); const ratings = Object.fromEntries(users.map((u) => [u.handle, u.rating])); await this.storage.set(RATINGS$1, ratings); await this.storage.set(RATINGS_TIMESTAMP, Date.now()); } } const _Contest = class _Contest { constructor(contest, problems, rows, ratingChanges, oldRatings, fetchTime, isRated) { __publicField(this, "contest"); __publicField(this, "problems"); __publicField(this, "rows"); __publicField(this, "ratingChanges"); __publicField(this, "oldRatings"); __publicField(this, "performances"); __publicField(this, "fetchTime"); __publicField(this, "isRated"); __publicField(this, "startTimeSeconds"); __publicField(this, "durationSeconds"); this.contest = contest; this.problems = problems; this.rows = rows; this.ratingChanges = ratingChanges; this.oldRatings = oldRatings; this.fetchTime = fetchTime; this.isRated = isRated; this.performances = null; this.startTimeSeconds = 0; this.durationSeconds = 0; } toPlainObject() { return { contest: this.contest, problems: this.problems, rows: this.rows, ratingChanges: this.ratingChanges, oldRatings: Array.from(this.oldRatings), fetchTime: this.fetchTime, isRated: this.isRated }; } static fromPlainObject(obj) { const c = new _Contest( obj.contest, obj.problems, obj.rows, obj.ratingChanges, new Map(obj.oldRatings), obj.fetchTime, obj.isRated ); return c; } }; __publicField(_Contest, "IsRated", { YES: "YES", NO: "NO", LIKELY: "LIKELY" }); let Contest = _Contest; const MAGIC_CACHE_DURATION = 5 * 60 * 1e3; const RATING_PENDING_MAX_DAYS = 3; function isOldContest(contest) { const daysSinceContestEnd = (Date.now() / 1e3 - contest.startTimeSeconds - contest.durationSeconds) / (60 * 60 * 24); return daysSinceContestEnd > RATING_PENDING_MAX_DAYS; } function isMagicOn() { let now = /* @__PURE__ */ new Date(); return now.getMonth() === 11 && now.getDate() >= 24 || now.getMonth() === 0 && now.getDate() <= 11; } const MAX_FINISHED_CONTESTS_TO_CACHE = 15; const CONTESTS_COMPLETE$1 = "cache.contests_complete"; const CONTESTS_COMPLETE_IDS = "cache.contests_complete.ids"; const CONTESTS_COMPLETE_TIMESTAMP = "cache.contests_complete.timestamp"; class ContestsComplete { constructor(api, storage) { __publicField(this, "api"); __publicField(this, "storage"); this.api = api; this.storage = storage; } async getContests() { let res = await this.storage.get(CONTESTS_COMPLETE$1, {}); res = new Map(Object.entries(res).map(([k, v]) => [parseInt(k), Contest.fromPlainObject(v)])); return res; } async setContests(contests) { const obj = Object.fromEntries([...contests.entries()].map(([k, v]) => [k, v.toPlainObject()])); await this.storage.set(CONTESTS_COMPLETE$1, obj); } async getContestIds() { return await this.storage.get(CONTESTS_COMPLETE_IDS, []); } async setContestIds(contestIds) { await this.storage.set(CONTESTS_COMPLETE_IDS, contestIds); } async getContestTimestamp() { let res = await this.storage.get(CONTESTS_COMPLETE_TIMESTAMP, {}); res = new Map(Object.entries(res)); return res; } async setContestTimestamp(contestTimestamp) { const obj = Object.fromEntries(contestTimestamp); await this.storage.set(CONTESTS_COMPLETE_TIMESTAMP, obj); } async fetch(contestId) { const cachedContests = await this.getContests(); if (cachedContests.has(contestId)) { console.log("Returning cached contest"); return cachedContests.get(contestId); } const { contest, problems, rows } = await this.api.contestStandings(contestId); let ratingChanges; let oldRatings; let isRated = Contest.IsRated.LIKELY; if (contest.phase === "FINISHED") { try { ratingChanges = await this.api.contestRatingChanges(contestId); if (ratingChanges) { if (ratingChanges.length > 0) { isRated = Contest.IsRated.YES; oldRatings = adjustOldRatings(contestId, ratingChanges); } else { ratingChanges = void 0; } } } catch (er) { if (er.message.includes("Rating changes are unavailable for this contest")) { isRated = Contest.IsRated.NO; } } } if (isRated === Contest.IsRated.LIKELY && isOldContest(contest)) { isRated = Contest.IsRated.NO; } const isFinished = isRated === Contest.IsRated.NO || isRated === Contest.IsRated.YES; const c = new Contest(contest, problems, rows, ratingChanges, oldRatings, Date.now(), isRated); if (isFinished) { const contests = await this.getContests(); contests.set(contestId, c); let contestIds = await this.getContestIds(); contestIds.push(contestId); if (contestIds.length > MAX_FINISHED_CONTESTS_TO_CACHE) { contests.delete(contestIds.shift()); } if (isMagicOn()) { const contestTimestamp = await this.getContestTimestamp(); for (const [cid, timestamp] of contestTimestamp) { if (Date.now() - timestamp > MAGIC_CACHE_DURATION) { contestTimestamp.delete(cid); contests.delete(cid); contestIds = contestIds.filter((c2) => c2 !== cid); } } contestTimestamp.set(contestId, Date.now()); await this.setContestTimestamp(contestTimestamp); } await this.setContests(contests); await this.setContestIds(contestIds); } return c; } } const FAKE_RATINGS_SINCE_CONTEST = 1360; const NEW_DEFAULT_RATING = 1400; function adjustOldRatings(contestId, ratingChanges) { const oldRatings = /* @__PURE__ */ new Map(); if (contestId < FAKE_RATINGS_SINCE_CONTEST) { for (const change of ratingChanges) { oldRatings.set(change.handle, change.oldRating); } } else { for (const change of ratingChanges) { oldRatings.set(change.handle, change.oldRating == 0 ? NEW_DEFAULT_RATING : change.oldRating); } } return oldRatings; } var _GM_addStyle = /* @__PURE__ */ (() => typeof GM_addStyle != "undefined" ? GM_addStyle : void 0)(); var _GM_deleteValue = /* @__PURE__ */ (() => typeof GM_deleteValue != "undefined" ? GM_deleteValue : void 0)(); var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)(); var _GM_listValues = /* @__PURE__ */ (() => typeof GM_listValues != "undefined" ? GM_listValues : void 0)(); var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)(); var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)(); class StorageWrapper { constructor(storageName) { __publicField(this, "storageName"); this.storageName = storageName; } async get(key, defaultValue = void 0) { return await _GM_getValue(`${this.storageName}.${key}`, defaultValue); } async set(key, value) { return await _GM_setValue(`${this.storageName}.${key}`, value); } } const LOCAL = new StorageWrapper("LOCAL"); const SYNC = new StorageWrapper("SYNC"); function boolSetterGetter(key, defaultValue) { return async (value) => { if (value === void 0) { return await SYNC.get(key, defaultValue); } return await SYNC.set(key, value); }; } const enablePredictDeltas = boolSetterGetter("settings.enablePredictDeltas", true); const enableFinalDeltas = boolSetterGetter("settings.enableFetchDeltas", true); const enablePrefetchRatings = boolSetterGetter("settings.enablePrefetchRatings", true); const showColCurrentPerformance = boolSetterGetter("settings.showColCurrentPerformance", true); const showColPredictedDelta = boolSetterGetter("settings.showColPredictedDelta", true); const showColRankUpDelta = boolSetterGetter("settings.showColRankUpDelta", true); const showColFinalPerformance = boolSetterGetter("settings.showColFinalPerformance", true); const showColFinalDelta = boolSetterGetter("settings.showColFinalDelta", true); const showColRankChange = boolSetterGetter("settings.showColRankChange", true); async function getPrefs() { return { enablePredictDeltas: await enablePredictDeltas(), enableFinalDeltas: await enableFinalDeltas(), enablePrefetchRatings: await enablePrefetchRatings(), showColCurrentPerformance: await showColCurrentPerformance(), showColPredictedDelta: await showColPredictedDelta(), showColRankUpDelta: await showColRankUpDelta(), showColFinalPerformance: await showColFinalPerformance(), showColFinalDelta: await showColFinalDelta(), showColRankChange: await showColRankChange() }; } const UNRATED_HINTS = ["unrated", "fools", "q#", "kotlin", "marathon", "teams"]; const EDU_ROUND_RATED_THRESHOLD = 2100; const API = new Api(fetchFromContentScript); const CONTESTS = new Contests(API, LOCAL); const RATINGS = new Ratings(API, LOCAL); const CONTESTS_COMPLETE = new ContestsComplete(API, LOCAL); const API_PATH = "/api/"; async function fetchFromContentScript(path, queryParamList) { const url = new URL(location.origin + API_PATH + path); for (const [key, value] of queryParamList) { url.searchParams.append(key, value); } const resp = await fetch(url); const text = await resp.text(); if (resp.status !== 200) { throw new Error(`CF API: HTTP error ${resp.status}: ${text}`); } let json; try { json = JSON.parse(text); } catch (_) { throw new Error(`CF API: Invalid JSON: ${text}`); } if (json.status !== "OK" || json.result === void 0) { throw new Error(`CF API: Error: ${text}`); } return json.result; } function isUnratedByName(contestName) { const lower = contestName.toLowerCase(); return UNRATED_HINTS.some((hint) => lower.includes(hint)); } function anyRowHasTeam(rows) { return rows.some((row) => row.party.teamId != null || row.party.teamName != null); } async function getDeltas(contestId) { const prefs = await getPrefs(); return await calcDeltas(contestId, prefs); } async function calcDeltas(contestId, prefs) { if (!prefs.enablePredictDeltas && !prefs.enableFinalDeltas) { return { result: "DISABLED" }; } if (await CONTESTS.hasCached(contestId)) { const contest2 = await CONTESTS.getCached(contestId); if (isUnratedByName(contest2.name)) { return { result: "UNRATED_CONTEST" }; } } const contest = await CONTESTS_COMPLETE.fetch(contestId); CONTESTS.update(contest.contest); if (contest.isRated === Contest.IsRated.NO) { return { result: "UNRATED_CONTEST" }; } if (contest.isRated === Contest.IsRated.YES) { if (!prefs.enableFinalDeltas) { return { result: "DISABLED" }; } return { result: "OK", prefs, predictResponse: getFinal(contest) }; } if (isUnratedByName(contest.contest.name)) { return { result: "UNRATED_CONTEST" }; } if (anyRowHasTeam(contest.rows)) { return { result: "UNRATED_CONTEST" }; } if (!prefs.enablePredictDeltas) { return { result: "DISABLED" }; } return { result: "OK", prefs, predictResponse: await getPredicted(contest) }; } function predictForRows(rows, ratingBeforeContest) { const contestants = rows.map((row) => { const handle = row.party.members[0].handle; return new Contestant(handle, row.points, row.penalty, ratingBeforeContest.get(handle) ?? null); }); return predict$1(contestants, true); } function getFinal(contest) { if (contest.performances === null) { const ratingBeforeContest = new Map( contest.ratingChanges.map((c) => [c.handle, contest.oldRatings.get(c.handle)]) ); const rows = contest.rows.filter((row) => { const handle = row.party.members[0].handle; return ratingBeforeContest.has(handle); }); const predictResultsForPerf = predictForRows(rows, ratingBeforeContest); contest.performances = new Map(predictResultsForPerf.map((r) => [r.handle, r.performance])); } const predictResults = []; for (const change of contest.ratingChanges) { predictResults.push( new PredictResult( change.handle, change.oldRating, change.newRating - change.oldRating, contest.performances.get(change.handle) ) ); } return new PredictResponse(predictResults, PredictResponse.TYPE_FINAL, contest.fetchTime); } async function getPredicted(contest) { const ratingMap = await RATINGS.fetchCurrentRatings(contest.contest.startTimeSeconds * 1e3); const isEduRound = contest.contest.name.toLowerCase().includes("educational"); let rows = contest.rows; if (isEduRound) { rows = contest.rows.filter((row) => { const handle = row.party.members[0].handle; return !ratingMap.has(handle) || ratingMap.get(handle) < EDU_ROUND_RATED_THRESHOLD; }); } const predictResults = predictForRows(rows, ratingMap); return new PredictResponse(predictResults, PredictResponse.TYPE_PREDICTED, contest.fetchTime); } async function predictDeltas(contestId) { return await getDeltas(contestId); } async function maybeUpdateContestList() { const prefs = await getPrefs(); if (!prefs.enablePredictDeltas && !prefs.enableFinalDeltas) { return; } await CONTESTS.maybeRefreshCache(); } async function getNearestUpcomingRatedContestStartTime() { let nearest = null; const now = Date.now(); for (const c of await CONTESTS.list()) { const start = (c.startTimeSeconds || 0) * 1e3; if (start < now || isUnratedByName(c.name)) { continue; } if (nearest === null || start < nearest) { nearest = start; } } return nearest; } async function maybeUpdateRatings() { const prefs = await getPrefs(); if (!prefs.enablePredictDeltas || !prefs.enablePrefetchRatings) { return; } const startTimeMs = await getNearestUpcomingRatedContestStartTime(); if (startTimeMs !== null) { await RATINGS.maybeRefreshCache(startTimeMs); } } const contentCss = ".carrot-display-none {\n display: none;\n}\n"; const PING_INTERVAL = 3 * 60 * 1e3; const PREDICT_TEXT_ID = "carrot-predict-text"; const DISPLAY_NONE_CLS = "carrot-display-none"; const Unicode = { BLACK_CURVED_RIGHTWARDS_AND_UPWARDS_ARROW: "⮭", GREEK_CAPITAL_DELTA: "Δ", GREEK_CAPITAL_PI: "Π", INFINITY: "∞", SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL: "⭜", BACKSLANTED_SOUTH_ARROW_WITH_HORIZONTAL_TAIL: "⭝" }; const PREDICT_COLUMNS = [ { text: "current performance", id: "carrot-current-performance", setting: "showColCurrentPerformance" }, { text: "predicted delta", id: "carrot-predicted-delta", setting: "showColPredictedDelta" }, { text: "delta required to rank up", id: "carrot-rank-up-delta", setting: "showColRankUpDelta" } ]; const FINAL_COLUMNS = [ { text: "final performance", id: "carrot-final-performance", setting: "showColFinalPerformance" }, { text: "final delta", id: "carrot-final-delta", setting: "showColFinalDelta" }, { text: "rank change", id: "carrot-rank-change", setting: "showColRankChange" } ]; const ALL_COLUMNS = PREDICT_COLUMNS.concat(FINAL_COLUMNS); function makeGreySpan(text, title) { const span = document.createElement("span"); span.style.fontWeight = "bold"; span.style.color = "lightgrey"; span.textContent = text; if (title) { span.title = title; } span.classList.add("small"); return span; } function makePerformanceSpan(performance2) { const span = document.createElement("span"); if (performance2.value === "Infinity") { span.textContent = Unicode.INFINITY; } else { span.textContent = performance2.value; span.classList.add(performance2.colorClass); } span.style.fontWeight = "bold"; span.style.display = "inline-block"; return span; } function makeRankSpan(rank) { const span = document.createElement("span"); if (rank.colorClass) { span.classList.add(rank.colorClass); } span.style.verticalAlign = "middle"; span.textContent = rank.abbr; span.title = rank.name; span.style.display = "inline-block"; return span; } function makeArrowSpan(arrow) { const span = document.createElement("span"); span.classList.add("small"); span.style.verticalAlign = "middle"; span.style.paddingLeft = "0.5em"; span.style.paddingRight = "0.5em"; span.textContent = arrow; return span; } function makeDeltaSpan(delta) { const span = document.createElement("span"); span.style.fontWeight = "bold"; span.style.verticalAlign = "middle"; if (delta > 0) { span.style.color = "green"; span.textContent = `+${delta}`; } else { span.style.color = "gray"; span.textContent = delta.toString(); } return span; } function makeFinalRankUpSpan(rank, newRank, arrow) { const span = document.createElement("span"); span.style.fontWeight = "bold"; span.appendChild(makeRankSpan(rank)); span.appendChild(makeArrowSpan(arrow)); span.appendChild(makeRankSpan(newRank)); return span; } function makePredictedRankUpSpan(rank, deltaReqForRankUp, nextRank) { const span = document.createElement("span"); span.style.fontWeight = "bold"; if (nextRank === null) { span.appendChild(makeRankSpan(rank)); return span; } span.appendChild(makeDeltaSpan(deltaReqForRankUp)); span.appendChild(makeArrowSpan(Unicode.SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL)); span.appendChild(makeRankSpan(nextRank)); return span; } function makePerfHeaderCell() { const cell = document.createElement("th"); cell.classList.add("top"); cell.style.width = "4em"; { const span = document.createElement("span"); span.textContent = Unicode.GREEK_CAPITAL_PI; span.title = "Performance"; cell.appendChild(span); } return cell; } function makeDeltaHeaderCell(deltaColTitle) { const cell = document.createElement("th"); cell.classList.add("top"); cell.style.width = "4.5em"; { const span = document.createElement("span"); span.textContent = Unicode.GREEK_CAPITAL_DELTA; span.title = deltaColTitle; cell.appendChild(span); } cell.appendChild(document.createElement("br")); { const span = document.createElement("span"); span.classList.add("small"); span.id = PREDICT_TEXT_ID; cell.appendChild(span); } return cell; } function makeRankUpHeaderCell(rankUpColWidth, rankUpColTitle) { const cell = document.createElement("th"); cell.classList.add("top", "right"); cell.style.width = rankUpColWidth; { const span = document.createElement("span"); span.textContent = Unicode.BLACK_CURVED_RIGHTWARDS_AND_UPWARDS_ARROW; span.title = rankUpColTitle; cell.appendChild(span); } return cell; } function makeDataCell(bottom = false, right = false) { const cell = document.createElement("td"); if (bottom) { cell.classList.add("bottom"); } if (right) { cell.classList.add("right"); } return cell; } function populateCells(row, type, rankUpTint, perfCell, deltaCell, rankUpCell) { if (row === void 0) { perfCell.appendChild(makeGreySpan("N/A", "Not applicable")); deltaCell.appendChild(makeGreySpan("N/A", "Not applicable")); rankUpCell.appendChild(makeGreySpan("N/A", "Not applicable")); return; } perfCell.appendChild(makePerformanceSpan(row.performance)); deltaCell.appendChild(makeDeltaSpan(row.delta)); switch (type) { case "FINAL": if (row.rank.abbr === row.newRank.abbr) { rankUpCell.appendChild(makeGreySpan("N/C", "No change")); } else { const arrow = row.delta > 0 ? Unicode.SLANTED_NORTH_ARROW_WITH_HORIZONTAL_TAIL : Unicode.BACKSLANTED_SOUTH_ARROW_WITH_HORIZONTAL_TAIL; rankUpCell.appendChild(makeFinalRankUpSpan(row.rank, row.newRank, arrow)); } break; case "PREDICTED": rankUpCell.appendChild( makePredictedRankUpSpan(row.rank, row.deltaReqForRankUp, row.nextRank) ); if (row.delta >= row.deltaReqForRankUp) { const [color, priority] = rankUpTint; rankUpCell.style.setProperty("background-color", color ?? null, priority); } break; default: throw new Error("Unknown prediction type"); } } function updateStandings(resp) { let deltaColTitle, rankUpColWidth, rankUpColTitle, columns; switch (resp.type) { case "FINAL": deltaColTitle = "Final rating change"; rankUpColWidth = "6.5em"; rankUpColTitle = "Rank change"; columns = FINAL_COLUMNS; break; case "PREDICTED": deltaColTitle = "Predicted rating change"; rankUpColWidth = "7.5em"; rankUpColTitle = "Rating change for rank up"; columns = PREDICT_COLUMNS; break; default: throw new Error("Unknown prediction type"); } const rows = Array.from(document.querySelectorAll("table.standings tbody tr")); for (const [idx, tableRow] of rows.entries()) { tableRow.querySelector("th:last-child, td:last-child").classList.remove("right"); let perfCell, deltaCell, rankUpCell; if (idx === 0) { perfCell = makePerfHeaderCell(); deltaCell = makeDeltaHeaderCell(deltaColTitle); rankUpCell = makeRankUpHeaderCell(rankUpColWidth, rankUpColTitle); } else if (idx === rows.length - 1) { perfCell = makeDataCell(true); deltaCell = makeDataCell(true); rankUpCell = makeDataCell(true, true); } else { perfCell = makeDataCell(); deltaCell = makeDataCell(); rankUpCell = makeDataCell(false, true); const handle = tableRow.querySelector("td.contestant-cell").textContent.trim(); let rankUpTint; if (tableRow.classList.contains("highlighted-row")) { rankUpTint = ["#d1eef2", "important"]; } else { rankUpTint = [idx % 2 ? "#ebf8eb" : "#f2fff2", void 0]; } populateCells(resp.rowMap[handle], resp.type, rankUpTint, perfCell, deltaCell, rankUpCell); } const cells = [perfCell, deltaCell, rankUpCell]; for (let i = 0; i < cells.length; i++) { const cell = cells[i]; if (idx % 2) { cell.classList.add("dark"); } cell.classList.add(columns[i].id, DISPLAY_NONE_CLS); tableRow.appendChild(cell); } } return columns; } function updateColumnVisibility(prefs) { for (const col of ALL_COLUMNS) { const showCol = prefs[col.setting]; const func = showCol ? (cell) => cell.classList.remove(DISPLAY_NONE_CLS) : (cell) => cell.classList.add(DISPLAY_NONE_CLS); document.querySelectorAll(`.${col.id}`).forEach(func); } } function showFinal() { const predictTextSpan = document.getElementById(PREDICT_TEXT_ID); predictTextSpan.textContent = "Final"; } function showTimer(fetchTime) { const predictTextSpan = document.getElementById(PREDICT_TEXT_ID); function update() { const secSincePredict = Math.floor((Date.now() - fetchTime) / 1e3); if (secSincePredict < 30) { predictTextSpan.textContent = "Just now"; } else if (secSincePredict < 60) { predictTextSpan.textContent = "<1m old"; } else { predictTextSpan.textContent = Math.floor(secSincePredict / 60) + "m old"; } } update(); setInterval(update, 1e3); } async function predict(contestId) { const response = await predictDeltas(contestId); switch (response.result) { case "OK": break; case "UNRATED_CONTEST": console.info("[Carrot] Unrated contest, not displaying delta column."); return; case "DISABLED": console.info("[Carrot] Deltas for this contest are disabled according to user settings."); return; default: throw new Error("Unknown result"); } const columns = updateStandings(response.predictResponse); switch (response.predictResponse.type) { case "FINAL": showFinal(); break; case "PREDICTED": showTimer(response.predictResponse.fetchTime); break; default: throw new Error("Unknown prediction type"); } updateColumnVisibility(response.prefs); return columns; } function main() { _GM_addStyle(contentCss); const matches = location.pathname.match(/contest\/(\d+)\/standings/); const contestId = matches ? matches[1] : null; if (contestId && document.querySelector("table.standings")) { predict(Number.parseInt(contestId)).then((columns) => { }).catch((er) => { console.error("[Carrot] Predict error: %o", er); er.toString(); }); } const ping = async () => { await Promise.all([maybeUpdateContestList(), maybeUpdateRatings()]); }; setInterval(ping, PING_INTERVAL); } 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>'; 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"; async function setup() { const predict2 = document.querySelector("#enable-predict-deltas"); const final = document.querySelector("#enable-final-deltas"); const prefetch = document.querySelector("#enable-prefetch-ratings"); async function update() { predict2.checked = await enablePredictDeltas(); final.checked = await enableFinalDeltas(); prefetch.checked = await enablePrefetchRatings(); prefetch.disabled = !predict2.checked; } predict2.addEventListener("input", async () => { await enablePredictDeltas(predict2.checked); await update(); }); final.addEventListener("input", async () => { await enableFinalDeltas(final.checked); await update(); }); prefetch.addEventListener("input", async () => { await enablePrefetchRatings(prefetch.checked); await update(); }); await update(); } function initOptions() { $("body").append(optionsHtml); _GM_addStyle(optionsCss); _GM_registerMenuCommand("Open options", () => { const dialog = document.querySelector("#options-dialog"); dialog.showModal(); }); _GM_registerMenuCommand("Clear cache", () => { const list = _GM_listValues(); for (const key of list) { if (key.startsWith("LOCAL.")) { _GM_deleteValue(key); } } }); $("#close-options").on("click", () => { const dialog = document.querySelector("#options-dialog"); dialog.close(); }); setup(); } initOptions(); main(); })();