コンテスト中にAtCoderのパフォーマンスを予測します
// ==UserScript== // @name ac-predictor // @namespace http://ac-predictor.azurewebsites.net/ // @version 2.0.8 // @description コンテスト中にAtCoderのパフォーマンスを予測します // @author keymoon // @license MIT // @match https://atcoder.jp/* // @exclude /^https://atcoder\.jp/[^#?]*/json/ // @grant none // ==/UserScript== var config_header_text$1 = "ac-predictor 設定"; var config_hideDuringContest_label$1 = "コンテスト中に予測を非表示にする"; var config_hideUntilFixed_label$1 = "パフォーマンスが確定するまで予測を非表示にする"; var config_useFinalR###ltOnVirtual_label$1 = "バーチャル参加時のパフォーマンス計算に最終結果を用いる"; var config_useFinalR###ltOnVirtual_description$1 = "チェックを入れると、当時の参加者が既にコンテストを終えているものとしてパフォーマンスを計算します。"; var config_dropdown$1 = "ac-predictor 設定"; var standings_performance_column_label$1 = "perf"; var standings_rate_change_column_label$1 = "レート変化"; var standings_click_to_compute_label$1 = "クリックして計算"; var standings_not_provided_label$1 = "提供不可"; var jaJson = { config_header_text: config_header_text$1, config_hideDuringContest_label: config_hideDuringContest_label$1, config_hideUntilFixed_label: config_hideUntilFixed_label$1, config_useFinalR###ltOnVirtual_label: config_useFinalR###ltOnVirtual_label$1, config_useFinalR###ltOnVirtual_description: config_useFinalR###ltOnVirtual_description$1, config_dropdown: config_dropdown$1, standings_performance_column_label: standings_performance_column_label$1, standings_rate_change_column_label: standings_rate_change_column_label$1, standings_click_to_compute_label: standings_click_to_compute_label$1, standings_not_provided_label: standings_not_provided_label$1 }; var config_header_text = "ac-predictor settings"; var config_hideDuringContest_label = "hide prediction during contests"; var config_hideUntilFixed_label = "hide prediction until performances are fixed"; var config_useFinalR###ltOnVirtual_label = "use final r###lt as a performance reference during the virtual participation"; var config_useFinalR###ltOnVirtual_description = "If enabled, the performance is calculated as if the original participant had already done the contest."; var config_dropdown = "ac-predictor"; var standings_performance_column_label = "perf"; var standings_rate_change_column_label = "rating delta"; var standings_click_to_compute_label = "click to compute"; var standings_not_provided_label = "not provided"; var enJson = { config_header_text: config_header_text, config_hideDuringContest_label: config_hideDuringContest_label, config_hideUntilFixed_label: config_hideUntilFixed_label, config_useFinalR###ltOnVirtual_label: config_useFinalR###ltOnVirtual_label, config_useFinalR###ltOnVirtual_description: config_useFinalR###ltOnVirtual_description, config_dropdown: config_dropdown, standings_performance_column_label: standings_performance_column_label, standings_rate_change_column_label: standings_rate_change_column_label, standings_click_to_compute_label: standings_click_to_compute_label, standings_not_provided_label: standings_not_provided_label }; // should not be here function getCurrentLanguage() { const elems = document.querySelectorAll("#navbar-collapse .dropdown > a"); if (elems.length == 0) return "JA"; for (let i = 0; i < elems.length; i++) { if (elems[i].textContent?.includes("English")) return "EN"; if (elems[i].textContent?.includes("日本語")) return "JA"; } console.warn("language detection failed. fallback to English"); return "EN"; } const language = getCurrentLanguage(); const currentJson = { "EN": enJson, "JA": jaJson }[language]; function getTranslation(label) { return currentJson[label]; } function substitute(input) { for (const key in currentJson) { // @ts-ignore input = input.replaceAll(`{${key}}`, currentJson[key]); } return input; } const configKey = "ac-predictor-config"; const defaultConfig = { useR###lts: true, hideDuringContest: false, isDebug: false, hideUntilFixed: false, useFinalR###ltOnVirtual: false }; function getConfigObj() { const val = localStorage.getItem(configKey) ?? "{}"; let config; try { config = JSON.parse(val); } catch { console.warn("invalid config found", val); config = {}; } return { ...defaultConfig, ...config }; } function storeConfigObj(config) { localStorage.setItem(configKey, JSON.stringify(config)); } function getConfig(configKey) { return getConfigObj()[configKey]; } function setConfig(key, value) { const config = getConfigObj(); config[key] = value; storeConfigObj(config); } const isDebug = location.hash.includes("ac-predictor-debug") || getConfig("isDebug"); function isDebugMode() { return isDebug; } var modalHTML = "<div id=\"modal-ac-predictor-settings\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n\t<div class=\"modal-dialog\" role=\"document\">\n\t<div class=\"modal-content\">\n\t\t<div class=\"modal-header\">\n\t\t\t<button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\n\t\t\t<h4 class=\"modal-title\">{config_header_text}</h4>\n\t\t</div>\n\t\t<div class=\"modal-body\">\n\t\t\t<div class=\"container-fluid\">\n\t\t\t\t<div class=\"settings-row\" class=\"row\">\n\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"modal-footer\">\n\t\t\t<button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">close</button>\n\t\t</div>\n\t</div>\n</div>\n</div>"; var newDropdownElem = "<li><a id=\"ac-predictor-settings-dropdown-button\" data-toggle=\"modal\" data-target=\"#modal-ac-predictor-settings\" style=\"cursor : pointer;\"><i class=\"a-icon a-icon-setting\"></i> {config_dropdown}</a></li>\n"; var legacyDropdownElem = "<li><a id=\"ac-predictor-settings-dropdown-button\" data-toggle=\"modal\" data-target=\"#modal-ac-predictor-settings\" style=\"cursor : pointer;\"><span class=\"glyphicon glyphicon-wrench\" aria-hidden=\"true\"></span> {config_dropdown}</a></li>\n"; class ConfigView { modalElement; constructor(modalElement) { this.modalElement = modalElement; } addCheckbox(label, val, description, handler) { const settingsRow = this.getSettingsRow(); const div = document.createElement("div"); div.classList.add("checkbox"); const labelElem = document.createElement("label"); const input = document.createElement("input"); input.type = "checkbox"; input.checked = val; labelElem.append(input); labelElem.append(label); if (description) { const descriptionDiv = document.createElement("div"); descriptionDiv.append(description); descriptionDiv.classList.add("small"); descriptionDiv.classList.add("gray"); labelElem.append(descriptionDiv); } div.append(labelElem); settingsRow.append(div); input.addEventListener("change", () => { handler(input.checked); }); } addHeader(level, content) { const settingsRow = this.getSettingsRow(); const div = document.createElement(`h${level}`); div.textContent = content; settingsRow.append(div); } getSettingsRow() { return this.modalElement.querySelector(".settings-row"); } static Create() { document.querySelector("body")?.insertAdjacentHTML("afterbegin", substitute(modalHTML)); document.querySelector(".header-mypage_list li:nth-last-child(1)")?.insertAdjacentHTML("beforebegin", substitute(newDropdownElem)); document.querySelector(".navbar-right .dropdown-menu .divider:nth-last-child(2)")?.insertAdjacentHTML("beforebegin", substitute(legacyDropdownElem)); const element = document.querySelector("#modal-ac-predictor-settings"); if (element === null) { throw new Error("settings modal not found"); } return new ConfigView(element); } } class ConfigController { register() { const configView = ConfigView.Create(); // TODO: 流石に処理をまとめたい configView.addCheckbox(getTranslation("config_useFinalR###ltOnVirtual_label"), getConfig("useFinalR###ltOnVirtual"), getTranslation("config_useFinalR###ltOnVirtual_description"), val => setConfig("useFinalR###ltOnVirtual", val)); configView.addCheckbox(getTranslation("config_hideDuringContest_label"), getConfig("hideDuringContest"), null, val => setConfig("hideDuringContest", val)); configView.addCheckbox(getTranslation("config_hideUntilFixed_label"), getConfig("hideUntilFixed"), null, val => setConfig("hideUntilFixed", val)); if (isDebugMode()) { configView.addCheckbox("[DEBUG] enable debug mode", getConfig("isDebug"), null, val => setConfig("isDebug", val)); configView.addCheckbox("[DEBUG] use r###lts", getConfig("useR###lts"), null, val => setConfig("useR###lts", val)); } } } async function getAPerfs(contestScreenName) { const r###lt = await fetch(`https://data.ac-predictor.com/aperfs/${contestScreenName}.json`); if (!r###lt.ok) { throw new Error(`Failed to fetch aperfs: ${r###lt.status}`); } return await r###lt.json(); } // [start, end] class Range { start; end; constructor(start, end) { this.start = start; this.end = end; } contains(val) { return this.start <= val && val <= this.end; } hasValue() { return this.start <= this.end; } } class ContestDetails { contestName; contestScreenName; contestType; startTime; duration; ratedrange; constructor(contestName, contestScreenName, contestType, startTime, duration, ratedRange) { this.contestName = contestName; this.contestScreenName = contestScreenName; this.contestType = contestType; this.startTime = startTime; this.duration = duration; this.ratedrange = ratedRange; } get endTime() { return new Date(this.startTime.getTime() + this.duration * 1000); } get defaultAPerf() { if (this.contestType == "heuristic") return 1000; if (!this.ratedrange.hasValue()) { throw new Error("unrated contest"); } if (this.ratedrange.end == 1199) return 800; if (this.ratedrange.end == 1999) return 800; if (this.ratedrange.end == 2399) return 800; // value is not relevant as it is never used const DEFAULT_CHANGED_AT = new Date("2019-05-25"); // maybe wrong if (this.ratedrange.end == 2799) { if (this.startTime < DEFAULT_CHANGED_AT) return 1600; else return 1000; } if (4000 <= this.ratedrange.end) { if (this.startTime < DEFAULT_CHANGED_AT) return 1600; else return 1200; } throw new Error("unknown contest type"); } get performanceCap() { if (this.contestType == "heuristic") return Infinity; if (!this.ratedrange.hasValue()) { throw new Error("unrated contest"); } if (4000 <= this.ratedrange.end) return Infinity; if (this.ratedrange.end % 400 != 399) { throw new Error("unknown contest type"); } return this.ratedrange.end + 1 + 400; } beforeContest(dateTime) { return dateTime < this.startTime; } duringContest(dateTime) { return this.startTime < dateTime && dateTime < this.endTime; } isOver(dateTime) { return this.endTime < dateTime; } } async function getContestDetails() { const r###lt = await fetch(`https://data.ac-predictor.com/contest-details.json`); if (!r###lt.ok) { throw new Error(`Failed to fetch contest details: ${r###lt.status}`); } const parsed = await r###lt.json(); const res = []; for (const elem of parsed) { if (typeof elem !== "object") throw new Error("invalid object returned"); if (typeof elem.contestName !== "string") throw new Error("invalid object returned"); const contestName = elem.contestName; if (typeof elem.contestScreenName !== "string") throw new Error("invalid object returned"); const contestScreenName = elem.contestScreenName; if (elem.contestType !== "algorithm" && elem.contestType !== "heuristic") throw new Error("invalid object returned"); const contestType = elem.contestType; if (typeof elem.startTime !== "number") throw new Error("invalid object returned"); const startTime = new Date(elem.startTime * 1000); if (typeof elem.duration !== "number") throw new Error("invalid object returned"); const duration = elem.duration; if (typeof elem.ratedrange !== "object" || typeof elem.ratedrange[0] !== "number" || typeof elem.ratedrange[1] !== "number") throw new Error("invalid object returned"); const ratedRange = new Range(elem.ratedrange[0], elem.ratedrange[1]); res.push(new ContestDetails(contestName, contestScreenName, contestType, startTime, duration, ratedRange)); } return res; } class Cache { cacheDuration; cacheExpires = new Map(); cacheData = new Map(); constructor(cacheDuration) { this.cacheDuration = cacheDuration; } has(key) { return this.cacheExpires.has(key) || Date.now() <= this.cacheExpires.get(key); } set(key, content) { const expire = Date.now() + this.cacheDuration; this.cacheExpires.set(key, expire); this.cacheData.set(key, content); } get(key) { if (!this.has(key)) { throw new Error(`invalid key: ${key}`); } return this.cacheData.get(key); } } const handlers = []; function addHandler(handler) { handlers.push(handler); } // absurd hack to steal ajax response data for caching // @ts-ignore $(document).on("ajaxComplete", (_, xhr, settings) => { if (xhr.status == 200) { for (const handler of handlers) { handler(xhr.responseText, settings.url); } } }); let StandingsWrapper$2 = class StandingsWrapper { data; constructor(data) { this.data = data; } toRanks(onlyRated = false, contestType = "algorithm") { const res = new Map(); for (const data of this.data.StandingsData) { if (onlyRated && !this.isRated(data, contestType)) continue; const userScreenName = typeof (data.Additional["standings.extendedContestRank"]) == "undefined" ? `extended:${data.UserScreenName}` : data.UserScreenName; res.set(userScreenName, data.Rank); } return res; } toRatedUsers(contestType) { const res = []; for (const data of this.data.StandingsData) { if (this.isRated(data, contestType)) { res.push(data.UserScreenName); } } return res; } toScores() { const res = new Map(); for (const data of this.data.StandingsData) { const userScreenName = typeof (data.Additional["standings.extendedContestRank"]) == "undefined" ? `extended:${data.UserScreenName}` : data.UserScreenName; res.set(userScreenName, { score: data.TotalR###lt.Score, penalty: data.TotalR###lt.Elapsed }); } return res; } isRated(data, contestType) { if (contestType === "algorithm") { return data.IsRated && typeof (data.Additional["standings.extendedContestRank"]) != "undefined"; } else { return data.IsRated && typeof (data.Additional["standings.extendedContestRank"]) != "undefined" && data.TotalR###lt.Count !== 0; } } }; const STANDINGS_CACHE_DURATION$2 = 10 * 1000; const cache$4 = new Cache(STANDINGS_CACHE_DURATION$2); async function getExtendedStandings(contestScreenName) { if (!cache$4.has(contestScreenName)) { const r###lt = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/extended/json`); if (!r###lt.ok) { throw new Error(`Failed to fetch extended standings: ${r###lt.status}`); } cache$4.set(contestScreenName, await r###lt.json()); } return new StandingsWrapper$2(cache$4.get(contestScreenName)); } addHandler((content, path) => { const match = path.match(/^\/contests\/([^/]*)\/standings\/extended\/json$/); if (!match) return; const contestScreenName = match[1]; cache$4.set(contestScreenName, JSON.parse(content)); }); class EloPerformanceProvider { ranks; ratings; cap; rankMemo = new Map(); constructor(ranks, ratings, cap) { this.ranks = ranks; this.ratings = ratings; this.cap = cap; } availableFor(userScreenName) { return this.ranks.has(userScreenName); } getPerformance(userScreenName) { if (!this.availableFor(userScreenName)) { throw new Error(`User ${userScreenName} not found`); } const rank = this.ranks.get(userScreenName); return this.getPerformanceForRank(rank); } getPerformances() { const performances = new Map(); for (const userScreenName of this.ranks.keys()) { performances.set(userScreenName, this.getPerformance(userScreenName)); } return performances; } getPerformanceForRank(rank) { let upper = 6144; let lower = -2048; while (upper - lower > 0.5) { const mid = (upper + lower) / 2; if (rank > this.getRankForPerformance(mid)) upper = mid; else lower = mid; } return Math.min(this.cap, Math.round((upper + lower) / 2)); } getRankForPerformance(performance) { if (this.rankMemo.has(performance)) return this.rankMemo.get(performance); const res = this.ratings.reduce((val, APerf) => val + 1.0 / (1.0 + Math.pow(6.0, (performance - APerf) / 400.0)), 0.5); this.rankMemo.set(performance, res); return res; } } function getRankToUsers(ranks) { const rankToUsers = new Map(); for (const [userScreenName, rank] of ranks) { if (!rankToUsers.has(rank)) rankToUsers.set(rank, []); rankToUsers.get(rank).push(userScreenName); } return rankToUsers; } function getMaxRank(ranks) { return Math.max(...ranks.values()); } class InterpolatePerformanceProvider { ranks; maxRank; rankToUsers; baseProvider; constructor(ranks, baseProvider) { this.ranks = ranks; this.maxRank = getMaxRank(ranks); this.rankToUsers = getRankToUsers(ranks); this.baseProvider = baseProvider; } availableFor(userScreenName) { return this.ranks.has(userScreenName); } getPerformance(userScreenName) { if (!this.availableFor(userScreenName)) { throw new Error(`User ${userScreenName} not found`); } if (this.performanceCache.has(userScreenName)) return this.performanceCache.get(userScreenName); let rank = this.ranks.get(userScreenName); while (rank <= this.maxRank) { const perf = this.getPerformanceIfAvailable(rank); if (perf !== null) { return perf; } rank++; } this.performanceCache.set(userScreenName, -Infinity); return -Infinity; } performanceCache = new Map(); getPerformances() { let currentPerformance = -Infinity; const res = new Map(); for (let rank = this.maxRank; rank >= 0; rank--) { const users = this.rankToUsers.get(rank); if (users === undefined) continue; const perf = this.getPerformanceIfAvailable(rank); if (perf !== null) currentPerformance = perf; for (const userScreenName of users) { res.set(userScreenName, currentPerformance); } } this.performanceCache = res; return res; } cacheForRank = new Map(); getPerformanceIfAvailable(rank) { if (!this.rankToUsers.has(rank)) return null; if (this.cacheForRank.has(rank)) return this.cacheForRank.get(rank); for (const userScreenName of this.rankToUsers.get(rank)) { if (!this.baseProvider.availableFor(userScreenName)) continue; const perf = this.baseProvider.getPerformance(userScreenName); this.cacheForRank.set(rank, perf); return perf; } return null; } } function normalizeRank(ranks) { const rankValues = [...new Set(ranks.values()).values()]; const rankToUsers = new Map(); for (const [userScreenName, rank] of ranks) { if (!rankToUsers.has(rank)) rankToUsers.set(rank, []); rankToUsers.get(rank).push(userScreenName); } rankValues.sort((a, b) => a - b); const res = new Map(); let currentRank = 1; for (const rank of rankValues) { const users = rankToUsers.get(rank); const averageRank = currentRank + (users.length - 1) / 2; for (const userScreenName of users) { res.set(userScreenName, averageRank); } currentRank += users.length; } return res; } //Copyright © 2017 koba-e964. //from : https://github.com/koba-e964/atcoder-rating-estimator const finf = bigf(400); function bigf(n) { let pow1 = 1; let pow2 = 1; let numerator = 0; let denominator = 0; for (let i = 0; i < n; ++i) { pow1 *= 0.81; pow2 *= 0.9; numerator += pow1; denominator += pow2; } return Math.sqrt(numerator) / denominator; } function f(n) { return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0; } /** * calculate unpositivized rating from performance history * @param {Number[]} [history] performance history with ascending order * @returns {Number} unpositivized rating */ function calcAlgRatingFromHistory(history) { const n = history.length; let pow = 1; let numerator = 0.0; let denominator = 0.0; for (let i = n - 1; i >= 0; i--) { pow *= 0.9; numerator += Math.pow(2, history[i] / 800.0) * pow; denominator += pow; } return Math.log2(numerator / denominator) * 800.0 - f(n); } /** * calculate unpositivized rating from last state * @param {Number} [last] last unpositivized rating * @param {Number} [perf] performance * @param {Number} [ratedMatches] count of participated rated contest * @returns {number} estimated unpositivized rating */ function calcAlgRatingFromLast(last, perf, ratedMatches) { if (ratedMatches === 0) return perf - 1200; last += f(ratedMatches); const weight = 9 - 9 * 0.9 ** ratedMatches; const numerator = weight * 2 ** (last / 800.0) + 2 ** (perf / 800.0); const denominator = 1 + weight; return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1); } /** * calculate the performance required to reach a target rate * @param {Number} [targetRating] targeted unpositivized rating * @param {Number[]} [history] performance history with ascending order * @returns {number} performance */ function calcRequiredPerformance(targetRating, history) { let valid = 10000.0; let invalid = -10000.0; for (let i = 0; i < 100; ++i) { const mid = (invalid + valid) / 2; const rating = Math.round(calcAlgRatingFromHistory(history.concat([mid]))); if (targetRating <= rating) valid = mid; else invalid = mid; } return valid; } /** * Gets the weight used in the heuristic rating calculation * based on its start and end dates * @param {Date} startAt - The start date of the contest. * @param {Date} endAt - The end date of the contest. * @returns {number} The weight of the contest. */ function getWeight(startAt, endAt) { const isShortContest = endAt.getTime() - startAt.getTime() < 24 * 60 * 60 * 1000; if (endAt < new Date("2025-01-01T00:00:00+09:00")) { return 1; } return isShortContest ? 0.5 : 1; } /** * calculate unpositivized rating from performance history * @param {RatingMaterial[]} [history] performance histories * @returns {Number} unpositivized rating */ function calcHeuristicRatingFromHistory(history) { const S = 724.4744301; const R = 0.8271973364; const qs = []; for (const material of history) { const adjustedPerformance = material.Performance + 150 - 100 * material.DaysFromLatestContest / 365; for (let i = 1; i <= 100; i++) { qs.push({ q: adjustedPerformance - S * Math.log(i), weight: material.Weight }); } } qs.sort((a, b) => b.q - a.q); let r = 0.0; let s = 0.0; for (const { q, weight } of qs) { s += weight; r += q * (R ** (s - weight) - R ** s); } return r; } /** * (-inf, inf) -> (0, inf) * @param {Number} [rating] unpositivized rating * @returns {number} positivized rating */ function positivizeRating(rating) { if (rating >= 400.0) { return rating; } return 400.0 * Math.exp((rating - 400.0) / 400.0); } /** * (0, inf) -> (-inf, inf) * @param {Number} [rating] positivized rating * @returns {number} unpositivized rating */ function unpositivizeRating(rating) { if (rating >= 400.0) { return rating; } return 400.0 + 400.0 * Math.log(rating / 400.0); } const colorNames = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"]; function getColor(rating) { const colorIndex = rating > 0 ? Math.min(Math.floor(rating / 400) + 1, 8) : 0; return colorNames[colorIndex]; } const PATH_PREFIX = "/contests/"; function getContestScreenName() { const location = document.location.pathname; if (!location.startsWith(PATH_PREFIX)) { throw Error("not on the contest page"); } return location.substring(PATH_PREFIX.length).split("/")[0]; } function hasOwnProperty(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } class StandingsLoadingView { loaded; element; hooks; constructor(element) { this.loaded = false; this.element = element; this.hooks = []; this.initHandler(); } onLoad(hook) { this.hooks.push(hook); } initHandler() { new MutationObserver(() => { if (!this.loaded) { if (document.getElementById("standings-tbody") === null) return; this.loaded = true; this.hooks.forEach(f => f()); } }).observe(this.element, { attributes: true }); } static Get() { const loadingElem = document.querySelector("#vue-standings .loading-show"); if (loadingElem === null) { throw new Error("loadingElem not found"); } return new StandingsLoadingView(loadingElem); } } function toSignedString (n) { return `${n >= 0 ? "+" : "-"}${Math.abs(n)}`; } function addStyle(styleSheet) { const styleElem = document.createElement("style"); styleElem.textContent = styleSheet; document.getElementsByTagName("head")[0].append(styleElem); } function getSpan(innerElements, classList) { const span = document.createElement("span"); span.append(...innerElements); span.classList.add(...classList); return span; } function getRatingSpan(rate) { return getSpan([rate.toString()], ["bold", "user-" + getColor(rate)]); } var style = "/* Tooltip container */\n.my-tooltip {\n position: relative;\n display: inline-block;\n}\n\n/* Tooltip text */\n.my-tooltip .my-tooltiptext {\n visibility: hidden;\n width: 120px;\n background-color: black;\n color: #fff;\n text-align: center;\n padding: 5px 0;\n border-radius: 6px;\n /* Position the tooltip text - see examples below! */\n position: absolute;\n top: 50%;\n right: 100%;\n z-index: 1;\n}\n\n/* Show the tooltip text when you mouse over the tooltip container */\n.my-tooltip:hover .my-tooltiptext {\n visibility: visible;\n}"; addStyle(style); function getFadedSpan(innerElements) { return getSpan(innerElements, ["grey"]); } function getRatedRatingElem(r###lt) { const elem = document.createElement("div"); elem.append(getRatingSpan(r###lt.oldRating), " → ", getRatingSpan(r###lt.newRating), " ", getFadedSpan([`(${toSignedString(r###lt.newRating - r###lt.oldRating)})`])); return elem; } function getUnratedRatingElem(r###lt) { const elem = document.createElement("div"); elem.append(getRatingSpan(r###lt.oldRating), " ", getFadedSpan(["(unrated)"])); return elem; } function getDefferedRatingElem(r###lt) { const elem = document.createElement("div"); elem.append(getRatingSpan(r###lt.oldRating), " → ", getSpan(["???"], ["bold"]), document.createElement("br"), getFadedSpan([`(${getTranslation("standings_click_to_compute_label")})`])); async function listener() { elem.removeEventListener("click", listener); elem.replaceChildren(getFadedSpan(["loading..."])); let newRating; try { newRating = await r###lt.newRatingCalculator(); } catch (e) { elem.append(getSpan(["error on load"], []), document.createElement("br"), getSpan(["(hover to see details)"], ["grey", "small"]), getSpan([e.toString()], ["my-tooltiptext"])); elem.classList.add("my-tooltip"); return; } const newElem = getRatedRatingElem({ type: "rated", performance: r###lt.performance, oldRating: r###lt.oldRating, newRating: newRating }); elem.replaceChildren(newElem); } elem.addEventListener("click", listener); return elem; } function getPerfOnlyRatingElem(r###lt) { const elem = document.createElement("div"); elem.append(getFadedSpan([`(${getTranslation("standings_not_provided_label")})`])); return elem; } function getErrorRatingElem(r###lt) { const elem = document.createElement("div"); elem.append(getSpan(["error on load"], []), document.createElement("br"), getSpan(["(hover to see details)"], ["grey", "small"]), getSpan([r###lt.message], ["my-tooltiptext"])); elem.classList.add("my-tooltip"); return elem; } function getRatingElem(r###lt) { if (r###lt.type == "rated") return getRatedRatingElem(r###lt); if (r###lt.type == "unrated") return getUnratedRatingElem(r###lt); if (r###lt.type == "deffered") return getDefferedRatingElem(r###lt); if (r###lt.type == "perfonly") return getPerfOnlyRatingElem(); if (r###lt.type == "error") return getErrorRatingElem(r###lt); throw new Error("unreachable"); } function getPerfElem(r###lt) { if (r###lt.type == "error") return getSpan(["-"], []); return getRatingSpan(r###lt.performance); } const headerHtml = `<th class="ac-predictor-standings-elem" style="width:84px;min-width:84px;">${getTranslation("standings_performance_column_label")}</th><th class="ac-predictor-standings-elem" style="width:168px;min-width:168px;">${getTranslation("standings_rate_change_column_label")}</th>`; function modifyHeader(header) { header.insertAdjacentHTML("beforeend", headerHtml); } function isFooter(row) { return row.firstElementChild?.classList.contains("colspan"); } async function modifyStandingsRow(row, r###lts) { let userScreenName = row.querySelector(".standings-username .username span")?.textContent ?? null; // TODO: この辺のロジックがここにあるの嫌だね…… if (userScreenName !== null && row.querySelector(".standings-username .username img[src='//img.atcoder.jp/assets/icon/ghost.svg']")) { userScreenName = `ghost:${userScreenName}`; } if (userScreenName !== null && row.classList.contains("info") && 3 <= row.children.length && row.children[2].textContent == "-") { // 延長線順位表用 userScreenName = `extended:${userScreenName}`; } const perfCell = document.createElement("td"); perfCell.classList.add("ac-predictor-standings-elem", "standings-r###lt"); const ratingCell = document.createElement("td"); ratingCell.classList.add("ac-predictor-standings-elem", "standings-r###lt"); if (userScreenName === null) { perfCell.append("-"); ratingCell.append("-"); } else { const r###lt = await r###lts(userScreenName); perfCell.append(getPerfElem(r###lt)); ratingCell.append(getRatingElem(r###lt)); } row.insertAdjacentElement("beforeend", perfCell); row.insertAdjacentElement("beforeend", ratingCell); } function modifyFooter(footer) { footer.insertAdjacentHTML("beforeend", '<td class="ac-predictor-standings-elem" colspan="2">-</td>'); } class StandingsTableView { element; provider; refreshHooks = []; constructor(element, r###ltDataProvider) { this.element = element; this.provider = r###ltDataProvider; this.initHandler(); } onRefreshed(hook) { this.refreshHooks.push(hook); } update() { this.removeOldElement(); const header = this.element.querySelector("thead tr"); if (!header) console.warn("header element not found", this.element); else modifyHeader(header); this.element.querySelectorAll("tbody tr").forEach((row) => { if (isFooter(row)) modifyFooter(row); else modifyStandingsRow(row, this.provider); }); } removeOldElement() { this.element.querySelectorAll(".ac-predictor-standings-elem").forEach((elem) => elem.remove()); } initHandler() { new MutationObserver(() => this.update()).observe(this.element.tBodies[0], { childList: true, }); const statsRow = this.element.querySelector(".standings-statistics"); if (statsRow === null) { throw new Error("statsRow not found"); } const acElems = statsRow.querySelectorAll(".standings-ac"); const refreshObserver = new MutationObserver((records) => { if (isDebugMode()) console.log("fire refreshHooks", records); this.refreshHooks.forEach(f => f()); }); acElems.forEach(elem => refreshObserver.observe(elem, { childList: true })); } static Get(r###ltDataProvider) { const tableElem = document.querySelector(".table-responsive table"); return new StandingsTableView(tableElem, r###ltDataProvider); } } class ExtendedStandingsPageController { contestDetails; performanceProvider; standingsTableView; async register() { const loading = StandingsLoadingView.Get(); loading.onLoad(() => this.initialize()); } async initialize() { const contestScreenName = getContestScreenName(); const contestDetailsList = await getContestDetails(); const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName); if (contestDetails === undefined) { throw new Error("contest details not found"); } this.contestDetails = contestDetails; this.standingsTableView = StandingsTableView.Get(async (userScreenName) => { if (!this.performanceProvider) return { "type": "error", "message": "performanceProvider missing" }; if (!this.performanceProvider.availableFor(userScreenName)) return { "type": "error", "message": `performance not available for ${userScreenName}` }; const originalPerformance = this.performanceProvider.getPerformance(userScreenName); const positivizedPerformance = Math.round(positivizeRating(originalPerformance)); return { type: "perfonly", performance: positivizedPerformance }; }); this.standingsTableView.onRefreshed(async () => { await this.updateData(); this.standingsTableView.update(); }); await this.updateData(); this.standingsTableView.update(); } async updateData() { if (!this.contestDetails) throw new Error("contestDetails missing"); const extendedStandings = await getExtendedStandings(this.contestDetails.contestScreenName); const aperfsObj = await getAPerfs(this.contestDetails.contestScreenName); const defaultAPerf = this.contestDetails.defaultAPerf; const normalizedRanks = normalizeRank(extendedStandings.toRanks(true, this.contestDetails.contestType)); const aperfsList = extendedStandings.toRatedUsers(this.contestDetails.contestType).map(userScreenName => hasOwnProperty(aperfsObj, userScreenName) ? aperfsObj[userScreenName] : defaultAPerf); const basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap); const ranks = extendedStandings.toRanks(); this.performanceProvider = new InterpolatePerformanceProvider(ranks, basePerformanceProvider); } } class HistoriesWrapper { data; constructor(data) { this.data = data; } toRatingMaterials(latestContestDate, contestDurationSecondProvider) { const toUtcDate = (date) => Math.floor(date.getTime() / (24 * 60 * 60 * 1000)); const r###lts = []; for (const history of this.data) { if (!history.IsRated) continue; const endTime = new Date(history.EndTime); const startTime = new Date(endTime.getTime() - contestDurationSecondProvider(history.ContestScreenName) * 1000); r###lts.push({ Performance: history.Performance, Weight: getWeight(startTime, endTime), DaysFromLatestContest: toUtcDate(latestContestDate) - toUtcDate(endTime), }); } return r###lts; } } const HISTORY_CACHE_DURATION = 60 * 60 * 1000; const cache$3 = new Cache(HISTORY_CACHE_DURATION); async function getHistory(userScreenName, contestType = "algorithm") { const key = `${userScreenName}:${contestType}`; if (!cache$3.has(key)) { const r###lt = await fetch(`https://atcoder.jp/users/${userScreenName}/history/json?contestType=${contestType}`); if (!r###lt.ok) { throw new Error(`Failed to fetch history: ${r###lt.status}`); } cache$3.set(key, await r###lt.json()); } return new HistoriesWrapper(cache$3.get(key)); } // @ts-nocheck var dom$1 = "<div id=\"estimator-alert\"></div>\n<div class=\"row\">\n\t<div class=\"input-group\">\n\t\t<span class=\"input-group-addon\" id=\"estimator-input-desc\"></span>\n\t\t<input type=\"number\" class=\"form-control\" id=\"estimator-input\">\n\t</div>\n</div>\n<div class=\"row\">\n\t<div class=\"input-group\">\n\t\t<span class=\"input-group-addon\" id=\"estimator-res-desc\"></span>\n\t\t<input class=\"form-control\" id=\"estimator-res\" disabled=\"disabled\">\n\t\t<span class=\"input-group-btn\">\n\t\t\t<button class=\"btn btn-default\" id=\"estimator-toggle\">入替</button>\n\t\t</span>\n\t</div>\n</div>\n<div class=\"row\" style=\"margin: 10px 0px;\">\n\t<a class=\"btn btn-default col-xs-offset-8 col-xs-4\" rel=\"nofollow\" onclick=\"window.open(encodeURI(decodeURI(this.href)),'twwindow','width=550, height=450, personalbar=0, toolbar=0, scrollbars=1'); return false;\" id=\"estimator-tweet\">ツイート</a>\n</div>"; class EstimatorModel { inputDesc; r###ltDesc; perfHistory; constructor(inputValue, perfHistory) { this.inputDesc = ""; this.r###ltDesc = ""; this.perfHistory = perfHistory; this.updateInput(inputValue); } inputValue; r###ltValue; updateInput(value) { this.inputValue = value; this.r###ltValue = this.calcR###lt(value); } toggle() { return null; } calcR###lt(input) { return input; } } class CalcRatingModel extends EstimatorModel { constructor(inputValue, perfHistory) { super(inputValue, perfHistory); this.inputDesc = "パフォーマンス"; this.r###ltDesc = "到達レーティング"; } // @ts-ignore toggle() { return new CalcPerfModel(this.r###ltValue, this.perfHistory); } calcR###lt(input) { return positivizeRating(calcAlgRatingFromHistory(this.perfHistory.concat([input]))); } } class CalcPerfModel extends EstimatorModel { constructor(inputValue, perfHistory) { super(inputValue, perfHistory); this.inputDesc = "目標レーティング"; this.r###ltDesc = "必要パフォーマンス"; } // @ts-ignore toggle() { return new CalcRatingModel(this.r###ltValue, this.perfHistory); } calcR###lt(input) { return calcRequiredPerformance(unpositivizeRating(input), this.perfHistory); } } function GetEmbedTweetLink(content, url) { return `https://twitter.com/share?text=${encodeURI(content)}&url=${encodeURI(url)}`; } function getLS(key) { const val = localStorage.getItem(key); return (val ? JSON.parse(val) : val); } function setLS(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch (error) { console.log(error); } } const models = [CalcPerfModel, CalcRatingModel]; function GetModelFromStateCode(state, value, history) { let model = models.find((model) => model.name === state); if (!model) model = CalcPerfModel; return new model(value, history); } function getPerformanceHistories(history) { const onlyRated = history.filter((x) => x.IsRated); onlyRated.sort((a, b) => { return new Date(a.EndTime).getTime() - new Date(b.EndTime).getTime(); }); return onlyRated.map((x) => x.Performance); } function roundValue(value, numDigits) { return Math.round(value * Math.pow(10, numDigits)) / Math.pow(10, numDigits); } class EstimatorElement { id; title; document; constructor() { this.id = "estimator"; this.title = "Estimator"; this.document = dom$1; } async afterOpen() { const estimatorInputSelector = document.getElementById("estimator-input"); const estimatorR###ltSelector = document.getElementById("estimator-res"); let model = GetModelFromStateCode(getLS("sidemenu_estimator_state"), getLS("sidemenu_estimator_value"), getPerformanceHistories((await getHistory(userScreenName)).data)); updateView(); document.getElementById("estimator-toggle").addEventListener("click", () => { model = model.toggle(); updateLocalStorage(); updateView(); }); estimatorInputSelector.addEventListener("keyup", () => { updateModel(); updateLocalStorage(); updateView(); }); /** modelをinputの値に応じて更新 */ function updateModel() { const inputNumber = estimatorInputSelector.valueAsNumber; if (!isFinite(inputNumber)) return; model.updateInput(inputNumber); } /** modelの状態をLSに保存 */ function updateLocalStorage() { setLS("sidemenu_estimator_value", model.inputValue); setLS("sidemenu_estimator_state", model.constructor.name); } /** modelを元にviewを更新 */ function updateView() { const roundedInput = roundValue(model.inputValue, 2); const roundedR###lt = roundValue(model.r###ltValue, 2); document.getElementById("estimator-input-desc").innerText = model.inputDesc; document.getElementById("estimator-res-desc").innerText = model.r###ltDesc; estimatorInputSelector.value = String(roundedInput); estimatorR###ltSelector.value = String(roundedR###lt); const tweetStr = `AtCoderのハンドルネーム: ${userScreenName}\n${model.inputDesc}: ${roundedInput}\n${model.r###ltDesc}: ${roundedR###lt}\n`; document.getElementById("estimator-tweet").href = GetEmbedTweetLink(tweetStr, "https://greasyfork.org/ja/scripts/369954-ac-predictor"); } } ; GetHTML() { return `<div class="menu-wrapper"> <div class="menu-header"> <h4 class="sidemenu-txt">${this.title}<span class="glyphicon glyphicon-menu-up" style="float: right"></span></h4> </div> <div class="menu-box"><div class="menu-content" id="${this.id}">${this.document}</div></div> </div>`; } } const estimator = new EstimatorElement(); var sidemenuHtml = "<style>\n #menu-wrap {\n display: block;\n position: fixed;\n top: 0;\n z-index: 20;\n width: 400px;\n right: -350px;\n transition: all 150ms 0ms ease;\n margin-top: 50px;\n }\n\n #sidemenu {\n background: #000;\n opacity: 0.85;\n }\n #sidemenu-key {\n border-radius: 5px 0px 0px 5px;\n background: #000;\n opacity: 0.85;\n color: #FFF;\n padding: 30px 0;\n cursor: pointer;\n margin-top: 100px;\n text-align: center;\n }\n\n #sidemenu {\n display: inline-block;\n width: 350px;\n float: right;\n }\n\n #sidemenu-key {\n display: inline-block;\n width: 50px;\n float: right;\n }\n\n .sidemenu-active {\n transform: translateX(-350px);\n }\n\n .sidemenu-txt {\n color: #DDD;\n }\n\n .menu-wrapper {\n border-bottom: 1px solid #FFF;\n }\n\n .menu-header {\n margin: 10px 20px 10px 20px;\n user-select: none;\n }\n\n .menu-box {\n overflow: hidden;\n transition: all 300ms 0s ease;\n }\n .menu-box-collapse {\n height: 0px !important;\n }\n .menu-box-collapse .menu-content {\n transform: translateY(-100%);\n }\n .menu-content {\n padding: 10px 20px 10px 20px;\n transition: all 300ms 0s ease;\n }\n .cnvtb-fixed {\n z-index: 19;\n }\n</style>\n<div id=\"menu-wrap\">\n <div id=\"sidemenu\" class=\"container\"></div>\n <div id=\"sidemenu-key\" class=\"glyphicon glyphicon-menu-left\"></div>\n</div>"; class SideMenu { pendingElements; constructor() { this.pendingElements = []; this.Generate(); } Generate() { document.getElementById("main-div").insertAdjacentHTML("afterbegin", sidemenuHtml); resizeSidemenuHeight(); const key = document.getElementById("sidemenu-key"); const wrap = document.getElementById("menu-wrap"); key.addEventListener("click", () => { this.pendingElements.forEach((elem) => { elem.afterOpen(); }); this.pendingElements.length = 0; key.classList.toggle("glyphicon-menu-left"); key.classList.toggle("glyphicon-menu-right"); wrap.classList.toggle("sidemenu-active"); }); window.addEventListener("onresize", resizeSidemenuHeight); document.getElementById("sidemenu").addEventListener("click", (event) => { const target = event.target; const header = target.closest(".menu-header"); if (!header) return; const box = target.closest(".menu-wrapper").querySelector(".menu-box"); box.classList.toggle("menu-box-collapse"); const arrow = target.querySelector(".glyphicon"); arrow.classList.toggle("glyphicon-menu-down"); arrow.classList.toggle("glyphicon-menu-up"); }); function resizeSidemenuHeight() { document.getElementById("sidemenu").style.height = `${window.innerHeight}px`; } } addElement(element) { const sidemenu = document.getElementById("sidemenu"); sidemenu.insertAdjacentHTML("afterbegin", element.GetHTML()); const content = sidemenu.querySelector(".menu-content"); content.parentElement.style.height = `${content.offsetHeight}px`; // element.afterAppend(); this.pendingElements.push(element); } } function add() { const sidemenu = new SideMenu(); const elements = [estimator]; for (let i = elements.length - 1; i >= 0; i--) { sidemenu.addElement(elements[i]); } } class R###ltsWrapper { data; constructor(data) { this.data = data; } toPerformanceMaps() { const res = new Map(); for (const r###lt of this.data) { if (!r###lt.IsRated) continue; res.set(r###lt.UserScreenName, r###lt.Performance); } return res; } toIsRatedMaps() { const res = new Map(); for (const r###lt of this.data) { res.set(r###lt.UserScreenName, r###lt.IsRated); } return res; } toOldRatingMaps() { const res = new Map(); for (const r###lt of this.data) { res.set(r###lt.UserScreenName, r###lt.OldRating); } return res; } toNewRatingMaps() { const res = new Map(); for (const r###lt of this.data) { res.set(r###lt.UserScreenName, r###lt.NewRating); } return res; } } const R###LTS_CACHE_DURATION = 10 * 1000; const cache$2 = new Cache(R###LTS_CACHE_DURATION); async function getR###lts(contestScreenName) { if (!cache$2.has(contestScreenName)) { const r###lt = await fetch(`https://atcoder.jp/contests/${contestScreenName}/r###lts/json`); if (!r###lt.ok) { throw new Error(`Failed to fetch r###lts: ${r###lt.status}`); } cache$2.set(contestScreenName, await r###lt.json()); } return new R###ltsWrapper(cache$2.get(contestScreenName)); } addHandler((content, path) => { const match = path.match(/^\/contests\/([^/]*)\/r###lts\/json$/); if (!match) return; const contestScreenName = match[1]; cache$2.set(contestScreenName, JSON.parse(content)); }); let StandingsWrapper$1 = class StandingsWrapper { data; constructor(data) { this.data = data; } toRanks(onlyRated = false, contestType = "algorithm") { const res = new Map(); for (const data of this.data.StandingsData) { if (onlyRated && !this.isRated(data, contestType)) continue; res.set(data.UserScreenName, data.Rank); } return res; } toRatedUsers(contestType) { const res = []; for (const data of this.data.StandingsData) { if (this.isRated(data, contestType)) { res.push(data.UserScreenName); } } return res; } toIsRatedMaps(contestType) { const res = new Map(); for (const data of this.data.StandingsData) { res.set(data.UserScreenName, this.isRated(data, contestType)); } return res; } toOldRatingMaps(unpositivize = false) { const res = new Map(); for (const data of this.data.StandingsData) { const rating = this.data.Fixed ? data.OldRating : data.Rating; res.set(data.UserScreenName, unpositivize ? unpositivizeRating(rating) : rating); } return res; } toCompetitionMaps() { const res = new Map(); for (const data of this.data.StandingsData) { res.set(data.UserScreenName, data.Competitions); } return res; } toScores() { const res = new Map(); for (const data of this.data.StandingsData) { res.set(data.UserScreenName, { score: data.TotalR###lt.Score, penalty: data.TotalR###lt.Elapsed }); } return res; } isRated(data, contestType = "algorithm") { if (contestType === "algorithm") { return data.IsRated; } if (contestType === "heuristic") { return data.IsRated && data.TotalR###lt.Count !== 0; } throw new Error("unreachable"); } }; const STANDINGS_CACHE_DURATION$1 = 10 * 1000; const cache$1 = new Cache(STANDINGS_CACHE_DURATION$1); async function getStandings(contestScreenName) { if (!cache$1.has(contestScreenName)) { const r###lt = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/json`); if (!r###lt.ok) { throw new Error(`Failed to fetch standings: ${r###lt.status}`); } cache$1.set(contestScreenName, await r###lt.json()); } return new StandingsWrapper$1(cache$1.get(contestScreenName)); } addHandler((content, path) => { const match = path.match(/^\/contests\/([^/]*)\/standings\/json$/); if (!match) return; const contestScreenName = match[1]; cache$1.set(contestScreenName, JSON.parse(content)); }); class FixedPerformanceProvider { r###lt; constructor(r###lt) { this.r###lt = r###lt; } availableFor(userScreenName) { return this.r###lt.has(userScreenName); } getPerformance(userScreenName) { if (!this.availableFor(userScreenName)) { throw new Error(`User ${userScreenName} not found`); } return this.r###lt.get(userScreenName); } getPerformances() { return this.r###lt; } } class IncrementalAlgRatingProvider { unpositivizedRatingMap; competitionsMap; constructor(unpositivizedRatingMap, competitionsMap) { this.unpositivizedRatingMap = unpositivizedRatingMap; this.competitionsMap = competitionsMap; } availableFor(userScreenName) { return this.unpositivizedRatingMap.has(userScreenName); } async getRating(userScreenName, newPerformance) { if (!this.availableFor(userScreenName)) { throw new Error(`rating not available for ${userScreenName}`); } const rating = this.unpositivizedRatingMap.get(userScreenName); const competitions = this.competitionsMap.get(userScreenName); return Math.round(positivizeRating(calcAlgRatingFromLast(rating, newPerformance, competitions))); } } class ConstRatingProvider { ratings; constructor(ratings) { this.ratings = ratings; } availableFor(userScreenName) { return this.ratings.has(userScreenName); } async getRating(userScreenName, newPerformance) { if (!this.availableFor(userScreenName)) { throw new Error(`rating not available for ${userScreenName}`); } return this.ratings.get(userScreenName); } } class FromHistoryHeuristicRatingProvider { newWeight; performancesProvider; constructor(newWeight, performancesProvider) { this.newWeight = newWeight; this.performancesProvider = performancesProvider; } availableFor(userScreenName) { return true; } async getRating(userScreenName, newPerformance) { const performances = await this.performancesProvider(userScreenName); performances.push({ Performance: newPerformance, Weight: this.newWeight, DaysFromLatestContest: 0, }); return Math.round(positivizeRating(calcHeuristicRatingFromHistory(performances))); } } class StandingsPageController { contestDetails; contestDetailsMap = new Map(); performanceProvider; ratingProvider; oldRatings = new Map(); isRatedMaps = new Map(); standingsTableView; async register() { const loading = StandingsLoadingView.Get(); loading.onLoad(() => this.initialize()); } async initialize() { const contestScreenName = getContestScreenName(); const contestDetailsList = await getContestDetails(); const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName); if (contestDetails === undefined) { throw new Error("contest details not found"); } this.contestDetails = contestDetails; this.contestDetailsMap = new Map(contestDetailsList.map(details => [details.contestScreenName, details])); if (this.contestDetails.beforeContest(new Date())) return; if (getConfig("hideDuringContest") && this.contestDetails.duringContest(new Date())) return; const standings = await getStandings(this.contestDetails.contestScreenName); if (getConfig("hideUntilFixed") && !standings.data.Fixed) return; this.standingsTableView = StandingsTableView.Get(async (userScreenName) => { if (!this.ratingProvider) return { "type": "error", "message": "ratingProvider missing" }; if (!this.performanceProvider) return { "type": "error", "message": "performanceProvider missing" }; if (!this.isRatedMaps) return { "type": "error", "message": "isRatedMapping missing" }; if (!this.oldRatings) return { "type": "error", "message": "oldRatings missing" }; if (!this.oldRatings.has(userScreenName)) return { "type": "error", "message": `oldRating not found for ${userScreenName}` }; const oldRating = this.oldRatings.get(userScreenName); if (!this.performanceProvider.availableFor(userScreenName)) return { "type": "error", "message": `performance not available for ${userScreenName}` }; const originalPerformance = this.performanceProvider.getPerformance(userScreenName); const positivizedPerformance = Math.round(positivizeRating(originalPerformance)); if (this.isRatedMaps.get(userScreenName)) { if (!this.ratingProvider.provider.availableFor(userScreenName)) return { "type": "error", "message": `rating not available for ${userScreenName}` }; if (this.ratingProvider.lazy) { const newRatingCalculator = () => this.ratingProvider.provider.getRating(userScreenName, originalPerformance); return { type: "deffered", oldRating, performance: positivizedPerformance, newRatingCalculator }; } else { const newRating = await this.ratingProvider.provider.getRating(userScreenName, originalPerformance); return { type: "rated", oldRating, performance: positivizedPerformance, newRating }; } } else { return { type: "unrated", oldRating, performance: positivizedPerformance }; } }); this.standingsTableView.onRefreshed(async () => { await this.updateData(); this.standingsTableView.update(); }); await this.updateData(); this.standingsTableView.update(); } async updateData() { if (!this.contestDetails) throw new Error("contestDetails missing"); if (isDebugMode()) console.log("data updating..."); const standings = await getStandings(this.contestDetails.contestScreenName); let basePerformanceProvider = undefined; if (standings.data.Fixed && getConfig("useR###lts")) { try { const r###lts = await getR###lts(this.contestDetails.contestScreenName); if (r###lts.data.length === 0) { throw new Error("r###lts missing"); } basePerformanceProvider = new FixedPerformanceProvider(r###lts.toPerformanceMaps()); this.isRatedMaps = r###lts.toIsRatedMaps(); this.oldRatings = r###lts.toOldRatingMaps(); this.ratingProvider = { provider: new ConstRatingProvider(r###lts.toNewRatingMaps()), lazy: false }; } catch (e) { console.warn("getR###lts failed", e); } } if (basePerformanceProvider === undefined) { const aperfsDict = await getAPerfs(this.contestDetails.contestScreenName); const defaultAPerf = this.contestDetails.defaultAPerf; const normalizedRanks = normalizeRank(standings.toRanks(true, this.contestDetails.contestType)); const aperfsList = standings.toRatedUsers(this.contestDetails.contestType).map(user => hasOwnProperty(aperfsDict, user) ? aperfsDict[user] : defaultAPerf); basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap); this.isRatedMaps = standings.toIsRatedMaps(this.contestDetails.contestType); this.oldRatings = standings.toOldRatingMaps(); if (this.contestDetails.contestType == "algorithm") { this.ratingProvider = { provider: new IncrementalAlgRatingProvider(standings.toOldRatingMaps(true), standings.toCompetitionMaps()), lazy: false }; } else { const startAt = this.contestDetails.startTime; const endAt = this.contestDetails.endTime; this.ratingProvider = { provider: new FromHistoryHeuristicRatingProvider(getWeight(startAt, endAt), async (userScreenName) => { const histories = await getHistory(userScreenName, "heuristic"); histories.data = histories.data.filter(x => new Date(x.EndTime) < endAt); return histories.toRatingMaterials(endAt, x => { const details = this.contestDetailsMap.get(x.split(".")[0]); if (!details) { console.warn(`contest details not found for ${x}`); return 0; } return details.duration; }); }), lazy: true }; } } this.performanceProvider = new InterpolatePerformanceProvider(standings.toRanks(), basePerformanceProvider); if (isDebugMode()) console.log("data updated"); } } class StandingsWrapper { data; constructor(data) { this.data = data; } toRanks(onlyRated = false, contestType = "algorithm") { const res = new Map(); for (const data of this.data.StandingsData) { if (onlyRated && !this.isRated(data, contestType)) continue; const userScreenName = data.Additional["standings.virtualElapsed"] === -2 ? `ghost:${data.UserScreenName}` : data.UserScreenName; res.set(userScreenName, data.Rank); } return res; } toRatedUsers(contestType) { const res = []; for (const data of this.data.StandingsData) { if (this.isRated(data, contestType)) { res.push(data.UserScreenName); } } return res; } toScores() { const res = new Map(); for (const data of this.data.StandingsData) { const userScreenName = data.Additional["standings.virtualElapsed"] === -2 ? `ghost:${data.UserScreenName}` : data.UserScreenName; res.set(userScreenName, { score: data.TotalR###lt.Score, penalty: data.TotalR###lt.Elapsed }); } return res; } isRated(data, contestType) { if (contestType === "algorithm") { return data.IsRated && data.Additional["standings.virtualElapsed"] === -2; } else { return data.IsRated && data.Additional["standings.virtualElapsed"] === -2 && data.TotalR###lt.Count !== 0; } } } function createCacheKey(contestScreenName, showGhost) { return `${contestScreenName}:${showGhost}`; } const STANDINGS_CACHE_DURATION = 10 * 1000; const cache = new Cache(STANDINGS_CACHE_DURATION); async function getVirtualStandings(contestScreenName, showGhost) { const cacheKey = createCacheKey(contestScreenName, showGhost); if (!cache.has(cacheKey)) { const r###lt = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/virtual/json${showGhost ? "?showGhost=true" : ""}`); if (!r###lt.ok) { throw new Error(`Failed to fetch standings: ${r###lt.status}`); } cache.set(cacheKey, await r###lt.json()); } return new StandingsWrapper(cache.get(cacheKey)); } addHandler((content, path) => { const match = path.match(/^\/contests\/([^/]*)\/standings\/virtual\/json(\?showGhost=true)?$/); if (!match) return; const contestScreenName = match[1]; const showGhost = match[2] != ""; cache.set(createCacheKey(contestScreenName, showGhost), JSON.parse(content)); }); function isVirtualStandingsPage() { return /^\/contests\/[^/]*\/standings\/virtual\/?$/.test(document.location.pathname); } function duringVirtualParticipation() { if (!isVirtualStandingsPage()) { throw new Error("not available in this page"); } const timerText = document.getElementById("virtual-timer")?.textContent ?? ""; if (timerText && !timerText.includes("終了") && !timerText.includes("over")) return true; else return false; } function forgeCombinedRanks(a, b) { const res = new Map(); const merged = [...a.entries(), ...b.entries()].sort((a, b) => a[1].score !== b[1].score ? b[1].score - a[1].score : a[1].penalty - b[1].penalty); let rank = 0; let prevScore = NaN; let prevPenalty = NaN; for (const [userScreenName, { score, penalty }] of merged) { if (score !== prevScore || penalty !== prevPenalty) { rank++; prevScore = score; prevPenalty = penalty; } res.set(userScreenName, rank); } return res; } function remapKey(map, mappingFunction) { const newMap = new Map(); for (const [key, val] of map) { newMap.set(mappingFunction(key), val); } return newMap; } class VirtualStandingsPageController { contestDetails; performanceProvider; standingsTableView; async register() { const loading = StandingsLoadingView.Get(); loading.onLoad(() => this.initialize()); } async initialize() { const contestScreenName = getContestScreenName(); const contestDetailsList = await getContestDetails(); const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName); if (contestDetails === undefined) { throw new Error("contest details not found"); } this.contestDetails = contestDetails; this.standingsTableView = StandingsTableView.Get(async (userScreenName) => { if (!this.performanceProvider) return { "type": "error", "message": "performanceProvider missing" }; if (!this.performanceProvider.availableFor(userScreenName)) return { "type": "error", "message": `performance not available for ${userScreenName}` }; const originalPerformance = this.performanceProvider.getPerformance(userScreenName); const positivizedPerformance = Math.round(positivizeRating(originalPerformance)); return { type: "perfonly", performance: positivizedPerformance }; }); this.standingsTableView.onRefreshed(async () => { await this.updateData(); this.standingsTableView.update(); }); await this.updateData(); this.standingsTableView.update(); } async updateData() { if (!this.contestDetails) throw new Error("contestDetails missing"); const virtualStandings = await getVirtualStandings(this.contestDetails.contestScreenName, true); const r###lts = await getR###lts(this.contestDetails.contestScreenName); let ranks; let basePerformanceProvider; if ((!duringVirtualParticipation() || getConfig("useFinalR###ltOnVirtual")) && getConfig("useR###lts")) { const standings = await getStandings(this.contestDetails.contestScreenName); const referencePerformanceMap = remapKey(r###lts.toPerformanceMaps(), userScreenName => `reference:${userScreenName}`); basePerformanceProvider = new FixedPerformanceProvider(referencePerformanceMap); ranks = forgeCombinedRanks(remapKey(standings.toScores(), userScreenName => `reference:${userScreenName}`), virtualStandings.toScores()); } else { const aperfsObj = await getAPerfs(this.contestDetails.contestScreenName); const defaultAPerf = this.contestDetails.defaultAPerf; const normalizedRanks = normalizeRank(virtualStandings.toRanks(true, this.contestDetails.contestType)); const aperfsList = virtualStandings.toRatedUsers(this.contestDetails.contestType).map(userScreenName => hasOwnProperty(aperfsObj, userScreenName) ? aperfsObj[userScreenName] : defaultAPerf); basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap); ranks = virtualStandings.toRanks(); } this.performanceProvider = new InterpolatePerformanceProvider(ranks, basePerformanceProvider); } } function isExtendedStandingsPage() { return /^\/contests\/[^/]*\/standings\/extended\/?$/.test(document.location.pathname); } function isStandingsPage() { return /^\/contests\/[^/]*\/standings\/?$/.test(document.location.pathname); } { const controller = new ConfigController(); controller.register(); add(); } if (isStandingsPage()) { const controller = new StandingsPageController(); controller.register(); } if (isVirtualStandingsPage()) { const controller = new VirtualStandingsPageController(); controller.register(); } if (isExtendedStandingsPage()) { const controller = new ExtendedStandingsPageController(); controller.register(); }