- // ==UserScript==
- // @name ac-predictor-cn
- // @namespace https://github.com/GoodCoder666/ac-predictor-extension-CN
- // @icon https://atcoder.jp/favicon.ico
- // @version 1.2.16
- // @description AtCoder 预测工具 (由GoodCoder666翻译为简体中文)
- // @author GoodCoder666
- // @license MIT
- // @supportURL https://github.com/GoodCoder666/ac-predictor-extension-CN/issues
- // @match https://atcoder.jp/*
- // @exclude https://atcoder.jp/*/json
- // ==/UserScript==
-
- function __awaiter(thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- }
-
- var dom = "<div id=\"predictor-alert\" class=\"row\"><h5 class=\"sidemenu-txt\">加载中…</h5></div>\n<div id=\"predictor-data\" class=\"row\">\n <div class=\"input-group col-xs-12\">\n <span class=\"input-group-addon\">名次\n <style>\n .predictor-tooltip-icon:hover+.tooltip{\n opacity: .9;\n filter: alpha(opacity=90);\n }\n </style>\n <span class=\"predictor-tooltip-icon glyphicon glyphicon-question-sign\"></span>\n <div class=\"tooltip fade bottom\" style=\"pointer-events:none\">\n <div class=\"tooltip-arrow\" style=\"left: 18%;\"></div>\n <div class=\"tooltip-inner\">Rated 范围内的排名,多人同名次时加上人数。</div>\n </div>\n </span>\n <input class=\"form-control\" id=\"predictor-input-rank\">\n <span class=\"input-group-addon\">位</span>\n </div>\n \n <div class=\"input-group col-xs-12\">\n <span class=\"input-group-addon\">Performance</span>\n <input class=\"form-control\" id=\"predictor-input-perf\">\n </div>\n\n <div class=\"input-group col-xs-12\">\n <span class=\"input-group-addon\">预计 Rating</span>\n <input class=\"form-control\" id=\"predictor-input-rate\">\n </div>\n</div>\n<div class=\"row\">\n <div class=\"btn-group\">\n <button class=\"btn btn-default\" id=\"predictor-current\">现在的名次</button>\n <button type=\"button\" class=\"btn btn-primary\" id=\"predictor-reload\" data-loading-text=\"更新中…\">更新</button>\n <!--<button class=\"btn btn-default\" id=\"predictor-solved\" disabled>当前问题AC后</button>-->\n </div>\n</div>";
-
- class Result {
- constructor(isRated, isSubmitted, userScreenName, place, ratedRank, oldRating, newRating, competitions, performance, innerPerformance) {
- this.IsRated = isRated;
- this.IsSubmitted = isSubmitted;
- this.UserScreenName = userScreenName;
- this.Place = place;
- this.RatedRank = ratedRank;
- this.OldRating = oldRating;
- this.NewRating = newRating;
- this.Competitions = competitions;
- this.Performance = performance;
- this.InnerPerformance = innerPerformance;
- }
- }
-
- function analyzeStandingsData(fixed, standingsData, aPerfs, defaultAPerf, ratedLimit, isHeuristic) {
- function analyze(isUserRated) {
- const contestantAPerf = [];
- const templateResults = {};
- let currentRatedRank = 1;
- let lastRank = 0;
- const tiedUsers = [];
- let ratedInTiedUsers = 0;
- function applyTiedUsers() {
- tiedUsers.forEach((data) => {
- if (isUserRated(data)) {
- contestantAPerf.push(aPerfs[data.UserScreenName] || defaultAPerf);
- ratedInTiedUsers++;
- }
- });
- const ratedRank = currentRatedRank + Math.max(0, ratedInTiedUsers - 1) / 2;
- tiedUsers.forEach((data) => {
- templateResults[data.UserScreenName] = new Result(!isHeuristic /* FIXME: Temporary disabled for the AHC rating system */ && isUserRated(data), !isHeuristic || data.TotalResult.Count !== 0, data.UserScreenName, data.Rank, ratedRank, fixed ? data.OldRating : data.Rating, null, data.Competitions, null, null);
- });
- currentRatedRank += ratedInTiedUsers;
- tiedUsers.length = 0;
- ratedInTiedUsers = 0;
- }
- standingsData.forEach((data) => {
- if (lastRank !== data.Rank)
- applyTiedUsers();
- lastRank = data.Rank;
- tiedUsers.push(data);
- });
- applyTiedUsers();
- return {
- contestantAPerf: contestantAPerf,
- templateResults: templateResults,
- };
- }
- let analyzedData = analyze((data) => data.IsRated && (!isHeuristic || data.TotalResult.Count !== 0));
- let isRated = true;
- if (analyzedData.contestantAPerf.length === 0) {
- analyzedData = analyze((data) => data.OldRating < ratedLimit && (!isHeuristic || data.TotalResult.Count !== 0));
- isRated = false;
- }
- const res = analyzedData;
- res.isRated = isRated;
- return res;
- }
- class Contest {
- constructor(contestScreenName, contestInformation, standings, aPerfs) {
- this.ratedLimit = contestInformation.RatedRange[1] + 1;
- this.perfLimit = this.ratedLimit + 400;
- this.standings = standings;
- this.aPerfs = aPerfs;
- this.rankMemo = {};
- const analyzedData = analyzeStandingsData(standings.Fixed, standings.StandingsData, aPerfs, contestInformation.isHeuristic ? 1000 : ({ 2000: 800, 2800: 1000, Infinity: 1200 }[this.ratedLimit] || 1200), this.ratedLimit, contestInformation.isHeuristic);
- this.contestantAPerf = analyzedData.contestantAPerf;
- this.templateResults = analyzedData.templateResults;
- this.IsRated = analyzedData.isRated;
- }
- getRatedRank(X) {
- if (this.rankMemo[X])
- return this.rankMemo[X];
- return (this.rankMemo[X] = this.contestantAPerf.reduce((val, APerf) => val + 1.0 / (1.0 + Math.pow(6.0, (X - APerf) / 400.0)), 0.5));
- }
- getPerf(ratedRank) {
- return Math.min(this.getInnerPerf(ratedRank), this.perfLimit);
- }
- getInnerPerf(ratedRank) {
- let upper = 6144;
- let lower = -2048;
- while (upper - lower > 0.5) {
- const mid = (upper + lower) / 2;
- if (ratedRank > this.getRatedRank(mid))
- upper = mid;
- else
- lower = mid;
- }
- return Math.round((upper + lower) / 2);
- }
- }
-
- class Results {
- }
-
- //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 calcRatingFromHistory(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 calcRatingFromLast(last, perf, ratedMatches) {
- if (ratedMatches === 0)
- return perf - 1200;
- last += f(ratedMatches);
- const weight = 9 - 9 * Math.pow(0.9, ratedMatches);
- const numerator = weight * Math.pow(2, (last / 800.0)) + Math.pow(2, (perf / 800.0));
- const denominator = 1 + weight;
- return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
- }
- /**
- * (-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);
- }
- /**
- * 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(calcRatingFromHistory(history.concat([mid])));
- if (targetRating <= rating)
- valid = mid;
- else
- invalid = mid;
- }
- return valid;
- }
- 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];
- }
-
- class OnDemandResults extends Results {
- constructor(contest, templateResults) {
- super();
- this.Contest = contest;
- this.TemplateResults = templateResults;
- }
- getUserResult(userScreenName) {
- if (!Object.prototype.hasOwnProperty.call(this.TemplateResults, userScreenName))
- return null;
- const baseResults = this.TemplateResults[userScreenName];
- if (!baseResults)
- return null;
- if (!baseResults.Performance) {
- baseResults.InnerPerformance = this.Contest.getInnerPerf(baseResults.RatedRank);
- baseResults.Performance = Math.min(baseResults.InnerPerformance, this.Contest.perfLimit);
- baseResults.NewRating = Math.round(positivizeRating(calcRatingFromLast(unpositivizeRating(baseResults.OldRating), baseResults.Performance, baseResults.Competitions)));
- }
- return baseResults;
- }
- }
-
- class FixedResults extends Results {
- constructor(results) {
- super();
- this.resultsDic = {};
- results.forEach((result) => {
- this.resultsDic[result.UserScreenName] = result;
- });
- }
- getUserResult(userScreenName) {
- return Object.prototype.hasOwnProperty.call(this.resultsDic, userScreenName)
- ? this.resultsDic[userScreenName]
- : null;
- }
- }
-
- class PredictorModel {
- constructor(model) {
- this.enabled = model.enabled;
- this.contest = model.contest;
- this.history = model.history;
- this.updateInformation(model.information);
- this.updateData(model.rankValue, model.perfValue, model.rateValue);
- }
- setEnable(state) {
- this.enabled = state;
- }
- updateInformation(information) {
- this.information = information;
- }
- updateData(rankValue, perfValue, rateValue) {
- this.rankValue = rankValue;
- this.perfValue = perfValue;
- this.rateValue = rateValue;
- }
- }
-
- class CalcFromRankModel extends PredictorModel {
- updateData(rankValue, perfValue, rateValue) {
- perfValue = this.contest.getPerf(rankValue);
- rateValue = positivizeRating(calcRatingFromHistory(this.history.concat([perfValue])));
- super.updateData(rankValue, perfValue, rateValue);
- }
- }
-
- class CalcFromPerfModel extends PredictorModel {
- updateData(rankValue, perfValue, rateValue) {
- rankValue = this.contest.getRatedRank(perfValue);
- rateValue = positivizeRating(calcRatingFromHistory(this.history.concat([perfValue])));
- super.updateData(rankValue, perfValue, rateValue);
- }
- }
-
- class CalcFromRateModel extends PredictorModel {
- updateData(rankValue, perfValue, rateValue) {
- perfValue = calcRequiredPerformance(unpositivizeRating(rateValue), this.history);
- rankValue = this.contest.getRatedRank(perfValue);
- super.updateData(rankValue, perfValue, rateValue);
- }
- }
-
- function roundValue(value, numDigits) {
- return Math.round(value * Math.pow(10, numDigits)) / Math.pow(10, numDigits);
- }
-
- class ContestInformation {
- constructor(canParticipateRange, ratedRange, penalty, isHeuristic) {
- this.CanParticipateRange = canParticipateRange;
- this.RatedRange = ratedRange;
- this.Penalty = penalty;
- this.isHeuristic = isHeuristic;
- }
- }
- function parseRangeString(s) {
- s = s.trim();
- if (s === "-")
- return [0, -1];
- if (s === "All")
- return [0, Infinity];
- if (!/[-~]/.test(s))
- return [0, -1];
- const res = s.split(/[-~]/).map((x) => parseInt(x.trim()));
- if (isNaN(res[0]))
- res[0] = 0;
- if (isNaN(res[1]))
- res[1] = Infinity;
- return res;
- }
- function parseDurationString(s) {
- if (s === "None" || s === "なし")
- return 0;
- if (!/(\d+[^\d]+)/.test(s))
- return NaN;
- const durationDic = {
- 日: 24 * 60 * 60 * 1000,
- day: 24 * 60 * 60 * 1000,
- days: 24 * 60 * 60 * 1000,
- 時間: 60 * 60 * 1000,
- hour: 60 * 60 * 1000,
- hours: 60 * 60 * 1000,
- 分: 60 * 1000,
- minute: 60 * 1000,
- minutes: 60 * 1000,
- 秒: 1000,
- second: 1000,
- seconds: 1000,
- };
- let res = 0;
- s.match(/(\d+[^\d]+)/g).forEach((x) => {
- var _a;
- const trimmed = x.trim();
- const num = parseInt(/\d+/.exec(trimmed)[0]);
- const unit = /[^\d]+/.exec(trimmed)[0];
- const duration = (_a = durationDic[unit]) !== null && _a !== void 0 ? _a : 0;
- res += num * duration;
- });
- return res;
- }
- function fetchJsonDataAsync(url) {
- return __awaiter(this, void 0, void 0, function* () {
- const response = yield fetch(url);
- if (response.ok)
- return (yield response.json());
- throw new Error(`request to ${url} returns ${response.status}`);
- });
- }
- function fetchTextDataAsync(url) {
- return __awaiter(this, void 0, void 0, function* () {
- const response = yield fetch(url);
- if (response.ok)
- return response.text();
- throw new Error(`request to ${url} returns ${response.status}`);
- });
- }
- function getStandingsDataAsync(contestScreenName) {
- return __awaiter(this, void 0, void 0, function* () {
- return yield fetchJsonDataAsync(`https://atcoder.jp/contests/${contestScreenName}/standings/json`);
- });
- }
-
- function getAPerfsDataAsync(contestScreenName) {
- return __awaiter(this, void 0, void 0, function* () {
- let url = `https://data.ac-predictor.com/aperfs/${contestScreenName}.json`;
- // if (contestScreenName === "arc119") url = `https://raw.githubusercontent.com/key-moon/ac-predictor-data/master/aperfs/${contestScreenName}.json`;
- return yield fetchJsonDataAsync(url);
- });
- }
- function getResultsDataAsync(contestScreenName) {
- return __awaiter(this, void 0, void 0, function* () {
- return yield fetchJsonDataAsync(`https://atcoder.jp/contests/${contestScreenName}/results/json`);
- });
- }
- function getHistoryDataAsync(userScreenName) {
- return __awaiter(this, void 0, void 0, function* () {
- return yield fetchJsonDataAsync(`https://atcoder.jp/users/${userScreenName}/history/json`);
- });
- }
- function getContestInformationAsync(contestScreenName) {
- return __awaiter(this, void 0, void 0, function* () {
- const html = yield fetchTextDataAsync(`https://atcoder.jp/contests/${contestScreenName}`);
- const topPageDom = new DOMParser().parseFromString(html, "text/html");
- const dataParagraph = topPageDom.getElementsByClassName("small")[0];
- const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(":")[1].trim());
- const isAHC = /^ahc\d{3}$/.test(contestScreenName) || html.includes("This contest is rated for AHC rating");
- return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]), isAHC);
- });
- }
- /**
- * ユーザーのPerformance履歴を時間昇順で取得
- */
- 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);
- }
-
- /**
- * サイドメニューに追加される要素のクラス
- */
- class SideMenuElement {
- shouldDisplayed(url) {
- return this.match.test(url);
- }
- /**
- * 要素のHTMLを取得
- */
- 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>`;
- }
- }
-
- function getGlobalVals() {
- const script = [...document.querySelectorAll("head script:not([src])")].map((x) => x.innerHTML).join("\n");
- const res = {};
- script.match(/var [^ ]+ = .+$/gm).forEach((statement) => {
- const match = /var ([^ ]+) = (.+)$/m.exec(statement);
- function safeEval(val) {
- function trim(val) {
- while (val.endsWith(";") || val.endsWith(" "))
- val = val.substr(0, val.length - 1);
- while (val.startsWith(" "))
- val = val.substr(1, val.length - 1);
- return val;
- }
- function isStringToken(val) {
- return 1 < val.length && val.startsWith('"') && val.endsWith('"');
- }
- function evalStringToken(val) {
- if (!isStringToken(val))
- throw new Error();
- return val.substr(1, val.length - 2); // TODO: parse escape
- }
- val = trim(val);
- if (isStringToken(val))
- return evalStringToken(val);
- if (val.startsWith("moment("))
- return new Date(evalStringToken(trim(val.substr(7, val.length - (7 + 1)))));
- return val;
- }
- res[match[1]] = safeEval(match[2]);
- });
- return res;
- }
- const globalVals = getGlobalVals();
- const userScreenName = globalVals["userScreenName"];
- const contestScreenName = globalVals["contestScreenName"];
- const startTime = globalVals["startTime"];
-
- class AllRowUpdater {
- update(table) {
- Array.from(table.rows).forEach((row) => this.rowModifier.modifyRow(row));
- }
- }
-
- class StandingsRowModifier {
- isHeader(row) {
- return row.parentElement.tagName.toLowerCase() == "thead";
- }
- isFooter(row) {
- return row.firstElementChild.hasAttribute("colspan") && row.firstElementChild.getAttribute("colspan") == "3";
- }
- modifyRow(row) {
- if (this.isHeader(row))
- this.modifyHeader(row);
- else if (this.isFooter(row))
- this.modifyFooter(row);
- else
- this.modifyContent(row);
- }
- }
-
- class PerfAndRateChangeAppender extends StandingsRowModifier {
- modifyContent(content) {
- var _a;
- this.removeOldElem(content);
- if (content.firstElementChild.textContent === "-") {
- const longCell = content.getElementsByClassName("standings-result")[0];
- longCell.setAttribute("colspan", (parseInt(longCell.getAttribute("colspan")) + 2).toString());
- return;
- }
- const userScreenName = content.querySelector(".standings-username .username span").textContent;
- const result = (_a = this.results) === null || _a === void 0 ? void 0 : _a.getUserResult(userScreenName);
- const perfElem = (result === null || result === void 0 ? void 0 : result.IsSubmitted) ? this.getRatingSpan(Math.round(positivizeRating(result.Performance)))
- : "-";
- const ratingElem = result
- ? (result === null || result === void 0 ? void 0 : result.IsRated) && (this === null || this === void 0 ? void 0 : this.isRated)
- ? this.getChangedRatingElem(result.OldRating, result.NewRating)
- : this.getUnratedElem(result.OldRating)
- : "-";
- content.insertAdjacentHTML("beforeend", `<td class="standings-result standings-perf">${perfElem}</td>`);
- content.insertAdjacentHTML("beforeend", `<td class="standings-result standings-rate">${ratingElem}</td>`);
- }
- getChangedRatingElem(oldRate, newRate) {
- const oldRateSpan = this.getRatingSpan(oldRate);
- const newRateSpan = this.getRatingSpan(newRate);
- const diff = this.toSignedString(newRate - oldRate);
- return `<span class="bold">${oldRateSpan}</span> → <span class="bold">${newRateSpan}</span> <span class="grey">(${diff})</span>`;
- }
- toSignedString(n) {
- return `${n >= 0 ? "+" : ""}${n}`;
- }
- getUnratedElem(rate) {
- return `<span class="bold">${this.getRatingSpan(rate)}</span> <span class="grey">(unrated)</span>`;
- }
- getRatingSpan(rate) {
- return `<span class="user-${getColor(rate)}">${rate}</span>`;
- }
- modifyFooter(footer) {
- this.removeOldElem(footer);
- footer.insertAdjacentHTML("beforeend", '<td class="standings-result standings-perf standings-rate" colspan="2">-</td>');
- }
- modifyHeader(header) {
- this.removeOldElem(header);
- header.insertAdjacentHTML("beforeend", '<th class="standings-result-th standings-perf" style="width:84px;min-width:84px;">Performance</th><th class="standings-result-th standings-rate" style="width:168px;min-width:168px;">Rating 变化</th>');
- }
- removeOldElem(row) {
- row.querySelectorAll(".standings-perf, .standings-rate").forEach((elem) => elem.remove());
- }
- }
-
- class PredictorElement extends SideMenuElement {
- constructor() {
- super(...arguments);
- this.id = "predictor";
- this.title = "Predictor";
- this.match = /atcoder.jp\/contests\/.+/;
- this.document = dom;
- this.historyData = [];
- this.contestOnUpdated = [];
- this.resultsOnUpdated = [];
- }
- set contest(val) {
- this._contest = val;
- this.contestOnUpdated.forEach((func) => func(val));
- }
- get contest() {
- return this._contest;
- }
- set results(val) {
- this._results = val;
- this.resultsOnUpdated.forEach((func) => func(val));
- }
- get results() {
- return this._results;
- }
- isStandingsPage() {
- return /standings([^/]*)?$/.test(document.location.href);
- }
- afterAppend() {
- const loaded = () => !!document.getElementById("standings-tbody");
- if (!this.isStandingsPage() || loaded()) {
- void this.initialize();
- return;
- }
- const loadingElem = document.getElementById("vue-standings").getElementsByClassName("loading-show")[0];
- new MutationObserver(() => {
- if (loaded())
- void this.initialize();
- }).observe(loadingElem, { attributes: true });
- }
- initialize() {
- var _a;
- return __awaiter(this, void 0, void 0, function* () {
- const firstContestDate = new Date(2016, 6, 16, 21);
- const predictorElements = [
- "predictor-input-rank",
- "predictor-input-perf",
- "predictor-input-rate",
- "predictor-current",
- "predictor-reload",
- ];
- const isStandingsPage = this.isStandingsPage();
- const contestInformation = yield getContestInformationAsync(contestScreenName);
- const rowUpdater = new PerfAndRateChangeAppender();
- this.resultsOnUpdated.push((val) => {
- rowUpdater.results = val;
- });
- this.contestOnUpdated.push((val) => {
- rowUpdater.isRated = val.IsRated;
- });
- const tableUpdater = new AllRowUpdater();
- tableUpdater.rowModifier = rowUpdater;
- const tableElement = (_a = document.getElementById("standings-tbody")) === null || _a === void 0 ? void 0 : _a.parentElement;
- let model = new PredictorModel({
- rankValue: 0,
- perfValue: 0,
- rateValue: 0,
- enabled: false,
- history: this.historyData,
- });
- const updateData = (aperfs, standings) => __awaiter(this, void 0, void 0, function* () {
- this.contest = new Contest(contestScreenName, contestInformation, standings, aperfs);
- model.contest = this.contest;
- if (this.contest.standings.Fixed && this.contest.IsRated) {
- const rawResult = yield getResultsDataAsync(contestScreenName);
- rawResult.sort((a, b) => (a.Place !== b.Place ? a.Place - b.Place : b.OldRating - a.OldRating));
- const sortedStandingsData = Array.from(this.contest.standings.StandingsData);
- if (contestInformation.isHeuristic) sortedStandingsData.filter((x) => x.TotalResult.Count !== 0);
- sortedStandingsData.sort((a, b) => {
- if (a.TotalResult.Count === 0 && b.TotalResult.Count === 0)
- return 0;
- if (a.TotalResult.Count === 0)
- return 1;
- if (b.TotalResult.Count === 0)
- return -1;
- if (a.Rank !== b.Rank)
- return a.Rank - b.Rank;
- if (b.OldRating !== a.OldRating)
- return b.OldRating - a.OldRating;
- if (a.UserIsDeleted)
- return -1;
- if (b.UserIsDeleted)
- return 1;
- return 0;
- });
- let lastPerformance = this.contest.perfLimit;
- let deletedCount = 0;
- this.results = new FixedResults(sortedStandingsData.map((data, index) => {
- let result = rawResult[index - deletedCount];
- if (!result || data.OldRating !== result.OldRating) {
- deletedCount++;
- result = null;
- }
- return new Result(result ? result.IsRated : false, !contestInformation.isHeuristic || data.TotalResult.Count !== 0, data.UserScreenName, data.Rank, -1, data.OldRating, result ? result.NewRating : 0, 0, result && result.IsRated ? (lastPerformance = result.Performance) : lastPerformance, result ? result.InnerPerformance : 0);
- }));
- }
- else {
- this.results = new OnDemandResults(this.contest, this.contest.templateResults);
- }
- });
- if (!shouldEnabledPredictor().verdict) {
- model.updateInformation(shouldEnabledPredictor().message);
- updateView();
- return;
- }
- try {
- let aPerfs;
- let standings;
- try {
- standings = yield getStandingsDataAsync(contestScreenName);
- }
- catch (e) {
- throw new Error("Standings读取失败。");
- }
- try {
- aPerfs = yield getAPerfsDataAsync(contestScreenName);
- }
- catch (e) {
- throw new Error("APerf获取失败。");
- }
- yield updateData(aPerfs, standings);
- model.setEnable(true);
- model.updateInformation(`最后更新时间: ${new Date().toTimeString().split(" ")[0]}`);
- if (isStandingsPage) {
- new MutationObserver(() => {
- tableUpdater.update(tableElement);
- }).observe(tableElement.tBodies[0], {
- childList: true,
- });
- const refreshElem = document.getElementById("refresh");
- if (refreshElem)
- new MutationObserver((mutationRecord) => {
- const disabled = mutationRecord[0].target.classList.contains("disabled");
- if (disabled) {
- void (() => __awaiter(this, void 0, void 0, function* () {
- yield updateStandingsFromAPI();
- updateView();
- }))();
- }
- }).observe(refreshElem, {
- attributes: true,
- attributeFilter: ["class"],
- });
- }
- }
- catch (e) {
- model.updateInformation(e.message);
- model.setEnable(false);
- }
- updateView();
- {
- const reloadButton = document.getElementById("predictor-reload");
- reloadButton.addEventListener("click", () => {
- void (() => __awaiter(this, void 0, void 0, function* () {
- model.updateInformation("");
- reloadButton.disabled = true;
- updateView();
- yield updateStandingsFromAPI();
- reloadButton.disabled = false;
- updateView();
- }))();
- });
- document.getElementById("predictor-current").addEventListener("click", () => {
- const myResult = this.contest.templateResults[userScreenName];
- if (!myResult)
- return;
- model = new CalcFromRankModel(model);
- model.updateData(myResult.RatedRank, model.perfValue, model.rateValue);
- updateView();
- });
- document.getElementById("predictor-input-rank").addEventListener("keyup", () => {
- const inputString = document.getElementById("predictor-input-rank").value;
- const inputNumber = parseInt(inputString);
- if (!isFinite(inputNumber))
- return;
- model = new CalcFromRankModel(model);
- model.updateData(inputNumber, 0, 0);
- updateView();
- });
- document.getElementById("predictor-input-perf").addEventListener("keyup", () => {
- const inputString = document.getElementById("predictor-input-perf").value;
- const inputNumber = parseInt(inputString);
- if (!isFinite(inputNumber))
- return;
- model = new CalcFromPerfModel(model);
- model.updateData(0, inputNumber, 0);
- updateView();
- });
- document.getElementById("predictor-input-rate").addEventListener("keyup", () => {
- const inputString = document.getElementById("predictor-input-rate").value;
- const inputNumber = parseInt(inputString);
- if (!isFinite(inputNumber))
- return;
- model = new CalcFromRateModel(model);
- model.updateData(0, 0, inputNumber);
- updateView();
- });
- }
- function updateStandingsFromAPI() {
- return __awaiter(this, void 0, void 0, function* () {
- try {
- const shouldEnabled = shouldEnabledPredictor();
- if (!shouldEnabled.verdict) {
- model.updateInformation(shouldEnabled.message);
- model.setEnable(false);
- return;
- }
- const standings = yield getStandingsDataAsync(contestScreenName);
- const aperfs = yield getAPerfsDataAsync(contestScreenName);
- yield updateData(aperfs, standings);
- model.updateInformation(`最后更新时间: ${new Date().toTimeString().split(" ")[0]}`);
- model.setEnable(true);
- }
- catch (e) {
- model.updateInformation(e.message);
- model.setEnable(false);
- }
- });
- }
- function shouldEnabledPredictor() {
- if (new Date() < startTime)
- return { verdict: false, message: "比赛暂未开始" };
- if (startTime < firstContestDate)
- return {
- verdict: false,
- message: "这场比赛是在使用现行 Rating 制度之前举行的,无法准确计算 Rating 数据。",
- };
- if (contestInformation.RatedRange[0] > contestInformation.RatedRange[1])
- return {
- verdict: false,
- message: "This contest is unrated.",
- };
- return { verdict: true, message: "" };
- }
- function updateView() {
- const roundedRankValue = isFinite(model.rankValue) ? roundValue(model.rankValue, 2).toString() : "";
- const roundedPerfValue = isFinite(model.perfValue) ? roundValue(model.perfValue, 2).toString() : "";
- const roundedRateValue = isFinite(model.rateValue) ? roundValue(model.rateValue, 2).toString() : "";
- document.getElementById("predictor-input-rank").value = roundedRankValue;
- document.getElementById("predictor-input-perf").value = roundedPerfValue;
- document.getElementById("predictor-input-rate").value = roundedRateValue;
- document.getElementById("predictor-alert").innerHTML = `<h5 class='sidemenu-txt'>${model.information}</h5>`;
- if (model.enabled)
- enabled();
- else
- disabled();
- if (isStandingsPage && shouldEnabledPredictor().verdict) {
- tableUpdater.update(tableElement);
- }
- function enabled() {
- predictorElements.forEach((element) => {
- document.getElementById(element).disabled = false;
- });
- }
- function disabled() {
- predictorElements.forEach((element) => {
- document.getElementById(element).disabled = false;
- });
- }
- }
- });
- }
- afterOpen() {
- return __awaiter(this, void 0, void 0, function* () {
- getPerformanceHistories(yield getHistoryDataAsync(userScreenName)).forEach((elem) => this.historyData.push(elem));
- });
- }
- }
- const predictor = new PredictorElement();
-
- 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\">Tweet</a>\n</div>";
-
- class EstimatorModel {
- constructor(inputValue, perfHistory) {
- this.inputDesc = "";
- this.resultDesc = "";
- this.perfHistory = perfHistory;
- this.updateInput(inputValue);
- }
- updateInput(value) {
- this.inputValue = value;
- this.resultValue = this.calcResult(value);
- }
- toggle() {
- return null;
- }
- calcResult(input) {
- return input;
- }
- }
-
- class CalcRatingModel extends EstimatorModel {
- constructor(inputValue, perfHistory) {
- super(inputValue, perfHistory);
- this.inputDesc = "Performance";
- this.resultDesc = "预计 Rating";
- }
- toggle() {
- return new CalcPerfModel(this.resultValue, this.perfHistory);
- }
- calcResult(input) {
- return positivizeRating(calcRatingFromHistory(this.perfHistory.concat([input])));
- }
- }
-
- class CalcPerfModel extends EstimatorModel {
- constructor(inputValue, perfHistory) {
- super(inputValue, perfHistory);
- this.inputDesc = "目标 Rating";
- this.resultDesc = "所需 Performance";
- }
- toggle() {
- return new CalcRatingModel(this.resultValue, this.perfHistory);
- }
- calcResult(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);
- }
- class EstimatorElement extends SideMenuElement {
- constructor() {
- super(...arguments);
- this.id = "estimator";
- this.title = "Estimator";
- this.document = dom$1;
- this.match = /atcoder.jp/;
- }
- afterAppend() {
- //nothing to do
- }
- // nothing to do
- afterOpen() {
- return __awaiter(this, void 0, void 0, function* () {
- const estimatorInputSelector = document.getElementById("estimator-input");
- const estimatorResultSelector = document.getElementById("estimator-res");
- let model = GetModelFromStateCode(getLS("sidemenu_estimator_state"), getLS("sidemenu_estimator_value"), getPerformanceHistories(yield getHistoryDataAsync(userScreenName)));
- 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 roundedResult = roundValue(model.resultValue, 2);
- document.getElementById("estimator-input-desc").innerText = model.inputDesc;
- document.getElementById("estimator-res-desc").innerText = model.resultDesc;
- estimatorInputSelector.value = String(roundedInput);
- estimatorResultSelector.value = String(roundedResult);
- const tweetStr = `AtCoderのハンドルネーム: ${userScreenName}\n${model.inputDesc}: ${roundedInput}\n${model.resultDesc}: ${roundedResult}\n`;
- document.getElementById("estimator-tweet").href = GetEmbedTweetLink(tweetStr, "https://greasyfork.org/ja/scripts/369954-ac-predictor");
- }
- });
- }
- }
- 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>";
-
- //import "./sidemenu.scss";
- class SideMenu {
- 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) {
- if (!element.shouldDisplayed(document.location.href))
- return;
- 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);
- }
- }
-
- const sidemenu = new SideMenu();
- const elements = [predictor, estimator];
- for (let i = elements.length - 1; i >= 0; i--) {
- sidemenu.addElement(elements[i]);
- }