ac-predictor-cn

AtCoder 预测工具 (由GoodCoder666翻译为简体中文)

  1. // ==UserScript==
  2. // @name ac-predictor-cn
  3. // @namespace https://github.com/GoodCoder666/ac-predictor-extension-CN
  4. // @icon https://atcoder.jp/favicon.ico
  5. // @version 1.2.16
  6. // @description AtCoder 预测工具 (由GoodCoder666翻译为简体中文)
  7. // @author GoodCoder666
  8. // @license MIT
  9. // @supportURL https://github.com/GoodCoder666/ac-predictor-extension-CN/issues
  10. // @match https://atcoder.jp/*
  11. // @exclude https://atcoder.jp/*/json
  12. // ==/UserScript==
  13.  
  14. function __awaiter(thisArg, _arguments, P, generator) {
  15. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  16. return new (P || (P = Promise))(function (resolve, reject) {
  17. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  18. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  19. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  20. step((generator = generator.apply(thisArg, _arguments || [])).next());
  21. });
  22. }
  23.  
  24. 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>";
  25.  
  26. class Result {
  27. constructor(isRated, isSubmitted, userScreenName, place, ratedRank, oldRating, newRating, competitions, performance, innerPerformance) {
  28. this.IsRated = isRated;
  29. this.IsSubmitted = isSubmitted;
  30. this.UserScreenName = userScreenName;
  31. this.Place = place;
  32. this.RatedRank = ratedRank;
  33. this.OldRating = oldRating;
  34. this.NewRating = newRating;
  35. this.Competitions = competitions;
  36. this.Performance = performance;
  37. this.InnerPerformance = innerPerformance;
  38. }
  39. }
  40.  
  41. function analyzeStandingsData(fixed, standingsData, aPerfs, defaultAPerf, ratedLimit, isHeuristic) {
  42. function analyze(isUserRated) {
  43. const contestantAPerf = [];
  44. const templateResults = {};
  45. let currentRatedRank = 1;
  46. let lastRank = 0;
  47. const tiedUsers = [];
  48. let ratedInTiedUsers = 0;
  49. function applyTiedUsers() {
  50. tiedUsers.forEach((data) => {
  51. if (isUserRated(data)) {
  52. contestantAPerf.push(aPerfs[data.UserScreenName] || defaultAPerf);
  53. ratedInTiedUsers++;
  54. }
  55. });
  56. const ratedRank = currentRatedRank + Math.max(0, ratedInTiedUsers - 1) / 2;
  57. tiedUsers.forEach((data) => {
  58. 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);
  59. });
  60. currentRatedRank += ratedInTiedUsers;
  61. tiedUsers.length = 0;
  62. ratedInTiedUsers = 0;
  63. }
  64. standingsData.forEach((data) => {
  65. if (lastRank !== data.Rank)
  66. applyTiedUsers();
  67. lastRank = data.Rank;
  68. tiedUsers.push(data);
  69. });
  70. applyTiedUsers();
  71. return {
  72. contestantAPerf: contestantAPerf,
  73. templateResults: templateResults,
  74. };
  75. }
  76. let analyzedData = analyze((data) => data.IsRated && (!isHeuristic || data.TotalResult.Count !== 0));
  77. let isRated = true;
  78. if (analyzedData.contestantAPerf.length === 0) {
  79. analyzedData = analyze((data) => data.OldRating < ratedLimit && (!isHeuristic || data.TotalResult.Count !== 0));
  80. isRated = false;
  81. }
  82. const res = analyzedData;
  83. res.isRated = isRated;
  84. return res;
  85. }
  86. class Contest {
  87. constructor(contestScreenName, contestInformation, standings, aPerfs) {
  88. this.ratedLimit = contestInformation.RatedRange[1] + 1;
  89. this.perfLimit = this.ratedLimit + 400;
  90. this.standings = standings;
  91. this.aPerfs = aPerfs;
  92. this.rankMemo = {};
  93. const analyzedData = analyzeStandingsData(standings.Fixed, standings.StandingsData, aPerfs, contestInformation.isHeuristic ? 1000 : ({ 2000: 800, 2800: 1000, Infinity: 1200 }[this.ratedLimit] || 1200), this.ratedLimit, contestInformation.isHeuristic);
  94. this.contestantAPerf = analyzedData.contestantAPerf;
  95. this.templateResults = analyzedData.templateResults;
  96. this.IsRated = analyzedData.isRated;
  97. }
  98. getRatedRank(X) {
  99. if (this.rankMemo[X])
  100. return this.rankMemo[X];
  101. return (this.rankMemo[X] = this.contestantAPerf.reduce((val, APerf) => val + 1.0 / (1.0 + Math.pow(6.0, (X - APerf) / 400.0)), 0.5));
  102. }
  103. getPerf(ratedRank) {
  104. return Math.min(this.getInnerPerf(ratedRank), this.perfLimit);
  105. }
  106. getInnerPerf(ratedRank) {
  107. let upper = 6144;
  108. let lower = -2048;
  109. while (upper - lower > 0.5) {
  110. const mid = (upper + lower) / 2;
  111. if (ratedRank > this.getRatedRank(mid))
  112. upper = mid;
  113. else
  114. lower = mid;
  115. }
  116. return Math.round((upper + lower) / 2);
  117. }
  118. }
  119.  
  120. class Results {
  121. }
  122.  
  123. //Copyright © 2017 koba-e964.
  124. //from : https://github.com/koba-e964/atcoder-rating-estimator
  125. const finf = bigf(400);
  126. function bigf(n) {
  127. let pow1 = 1;
  128. let pow2 = 1;
  129. let numerator = 0;
  130. let denominator = 0;
  131. for (let i = 0; i < n; ++i) {
  132. pow1 *= 0.81;
  133. pow2 *= 0.9;
  134. numerator += pow1;
  135. denominator += pow2;
  136. }
  137. return Math.sqrt(numerator) / denominator;
  138. }
  139. function f(n) {
  140. return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0;
  141. }
  142. /**
  143. * calculate unpositivized rating from performance history
  144. * @param {Number[]} [history] performance history with ascending order
  145. * @returns {Number} unpositivized rating
  146. */
  147. function calcRatingFromHistory(history) {
  148. const n = history.length;
  149. let pow = 1;
  150. let numerator = 0.0;
  151. let denominator = 0.0;
  152. for (let i = n - 1; i >= 0; i--) {
  153. pow *= 0.9;
  154. numerator += Math.pow(2, history[i] / 800.0) * pow;
  155. denominator += pow;
  156. }
  157. return Math.log2(numerator / denominator) * 800.0 - f(n);
  158. }
  159. /**
  160. * calculate unpositivized rating from last state
  161. * @param {Number} [last] last unpositivized rating
  162. * @param {Number} [perf] performance
  163. * @param {Number} [ratedMatches] count of participated rated contest
  164. * @returns {number} estimated unpositivized rating
  165. */
  166. function calcRatingFromLast(last, perf, ratedMatches) {
  167. if (ratedMatches === 0)
  168. return perf - 1200;
  169. last += f(ratedMatches);
  170. const weight = 9 - 9 * Math.pow(0.9, ratedMatches);
  171. const numerator = weight * Math.pow(2, (last / 800.0)) + Math.pow(2, (perf / 800.0));
  172. const denominator = 1 + weight;
  173. return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
  174. }
  175. /**
  176. * (-inf, inf) -> (0, inf)
  177. * @param {Number} [rating] unpositivized rating
  178. * @returns {number} positivized rating
  179. */
  180. function positivizeRating(rating) {
  181. if (rating >= 400.0) {
  182. return rating;
  183. }
  184. return 400.0 * Math.exp((rating - 400.0) / 400.0);
  185. }
  186. /**
  187. * (0, inf) -> (-inf, inf)
  188. * @param {Number} [rating] positivized rating
  189. * @returns {number} unpositivized rating
  190. */
  191. function unpositivizeRating(rating) {
  192. if (rating >= 400.0) {
  193. return rating;
  194. }
  195. return 400.0 + 400.0 * Math.log(rating / 400.0);
  196. }
  197. /**
  198. * calculate the performance required to reach a target rate
  199. * @param {Number} [targetRating] targeted unpositivized rating
  200. * @param {Number[]} [history] performance history with ascending order
  201. * @returns {number} performance
  202. */
  203. function calcRequiredPerformance(targetRating, history) {
  204. let valid = 10000.0;
  205. let invalid = -10000.0;
  206. for (let i = 0; i < 100; ++i) {
  207. const mid = (invalid + valid) / 2;
  208. const rating = Math.round(calcRatingFromHistory(history.concat([mid])));
  209. if (targetRating <= rating)
  210. valid = mid;
  211. else
  212. invalid = mid;
  213. }
  214. return valid;
  215. }
  216. const colorNames = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"];
  217. function getColor(rating) {
  218. const colorIndex = rating > 0 ? Math.min(Math.floor(rating / 400) + 1, 8) : 0;
  219. return colorNames[colorIndex];
  220. }
  221.  
  222. class OnDemandResults extends Results {
  223. constructor(contest, templateResults) {
  224. super();
  225. this.Contest = contest;
  226. this.TemplateResults = templateResults;
  227. }
  228. getUserResult(userScreenName) {
  229. if (!Object.prototype.hasOwnProperty.call(this.TemplateResults, userScreenName))
  230. return null;
  231. const baseResults = this.TemplateResults[userScreenName];
  232. if (!baseResults)
  233. return null;
  234. if (!baseResults.Performance) {
  235. baseResults.InnerPerformance = this.Contest.getInnerPerf(baseResults.RatedRank);
  236. baseResults.Performance = Math.min(baseResults.InnerPerformance, this.Contest.perfLimit);
  237. baseResults.NewRating = Math.round(positivizeRating(calcRatingFromLast(unpositivizeRating(baseResults.OldRating), baseResults.Performance, baseResults.Competitions)));
  238. }
  239. return baseResults;
  240. }
  241. }
  242.  
  243. class FixedResults extends Results {
  244. constructor(results) {
  245. super();
  246. this.resultsDic = {};
  247. results.forEach((result) => {
  248. this.resultsDic[result.UserScreenName] = result;
  249. });
  250. }
  251. getUserResult(userScreenName) {
  252. return Object.prototype.hasOwnProperty.call(this.resultsDic, userScreenName)
  253. ? this.resultsDic[userScreenName]
  254. : null;
  255. }
  256. }
  257.  
  258. class PredictorModel {
  259. constructor(model) {
  260. this.enabled = model.enabled;
  261. this.contest = model.contest;
  262. this.history = model.history;
  263. this.updateInformation(model.information);
  264. this.updateData(model.rankValue, model.perfValue, model.rateValue);
  265. }
  266. setEnable(state) {
  267. this.enabled = state;
  268. }
  269. updateInformation(information) {
  270. this.information = information;
  271. }
  272. updateData(rankValue, perfValue, rateValue) {
  273. this.rankValue = rankValue;
  274. this.perfValue = perfValue;
  275. this.rateValue = rateValue;
  276. }
  277. }
  278.  
  279. class CalcFromRankModel extends PredictorModel {
  280. updateData(rankValue, perfValue, rateValue) {
  281. perfValue = this.contest.getPerf(rankValue);
  282. rateValue = positivizeRating(calcRatingFromHistory(this.history.concat([perfValue])));
  283. super.updateData(rankValue, perfValue, rateValue);
  284. }
  285. }
  286.  
  287. class CalcFromPerfModel extends PredictorModel {
  288. updateData(rankValue, perfValue, rateValue) {
  289. rankValue = this.contest.getRatedRank(perfValue);
  290. rateValue = positivizeRating(calcRatingFromHistory(this.history.concat([perfValue])));
  291. super.updateData(rankValue, perfValue, rateValue);
  292. }
  293. }
  294.  
  295. class CalcFromRateModel extends PredictorModel {
  296. updateData(rankValue, perfValue, rateValue) {
  297. perfValue = calcRequiredPerformance(unpositivizeRating(rateValue), this.history);
  298. rankValue = this.contest.getRatedRank(perfValue);
  299. super.updateData(rankValue, perfValue, rateValue);
  300. }
  301. }
  302.  
  303. function roundValue(value, numDigits) {
  304. return Math.round(value * Math.pow(10, numDigits)) / Math.pow(10, numDigits);
  305. }
  306.  
  307. class ContestInformation {
  308. constructor(canParticipateRange, ratedRange, penalty, isHeuristic) {
  309. this.CanParticipateRange = canParticipateRange;
  310. this.RatedRange = ratedRange;
  311. this.Penalty = penalty;
  312. this.isHeuristic = isHeuristic;
  313. }
  314. }
  315. function parseRangeString(s) {
  316. s = s.trim();
  317. if (s === "-")
  318. return [0, -1];
  319. if (s === "All")
  320. return [0, Infinity];
  321. if (!/[-~]/.test(s))
  322. return [0, -1];
  323. const res = s.split(/[-~]/).map((x) => parseInt(x.trim()));
  324. if (isNaN(res[0]))
  325. res[0] = 0;
  326. if (isNaN(res[1]))
  327. res[1] = Infinity;
  328. return res;
  329. }
  330. function parseDurationString(s) {
  331. if (s === "None" || s === "なし")
  332. return 0;
  333. if (!/(\d+[^\d]+)/.test(s))
  334. return NaN;
  335. const durationDic = {
  336. 日: 24 * 60 * 60 * 1000,
  337. day: 24 * 60 * 60 * 1000,
  338. days: 24 * 60 * 60 * 1000,
  339. 時間: 60 * 60 * 1000,
  340. hour: 60 * 60 * 1000,
  341. hours: 60 * 60 * 1000,
  342. 分: 60 * 1000,
  343. minute: 60 * 1000,
  344. minutes: 60 * 1000,
  345. 秒: 1000,
  346. second: 1000,
  347. seconds: 1000,
  348. };
  349. let res = 0;
  350. s.match(/(\d+[^\d]+)/g).forEach((x) => {
  351. var _a;
  352. const trimmed = x.trim();
  353. const num = parseInt(/\d+/.exec(trimmed)[0]);
  354. const unit = /[^\d]+/.exec(trimmed)[0];
  355. const duration = (_a = durationDic[unit]) !== null && _a !== void 0 ? _a : 0;
  356. res += num * duration;
  357. });
  358. return res;
  359. }
  360. function fetchJsonDataAsync(url) {
  361. return __awaiter(this, void 0, void 0, function* () {
  362. const response = yield fetch(url);
  363. if (response.ok)
  364. return (yield response.json());
  365. throw new Error(`request to ${url} returns ${response.status}`);
  366. });
  367. }
  368. function fetchTextDataAsync(url) {
  369. return __awaiter(this, void 0, void 0, function* () {
  370. const response = yield fetch(url);
  371. if (response.ok)
  372. return response.text();
  373. throw new Error(`request to ${url} returns ${response.status}`);
  374. });
  375. }
  376. function getStandingsDataAsync(contestScreenName) {
  377. return __awaiter(this, void 0, void 0, function* () {
  378. return yield fetchJsonDataAsync(`https://atcoder.jp/contests/${contestScreenName}/standings/json`);
  379. });
  380. }
  381.  
  382. function getAPerfsDataAsync(contestScreenName) {
  383. return __awaiter(this, void 0, void 0, function* () {
  384. let url = `https://data.ac-predictor.com/aperfs/${contestScreenName}.json`;
  385. // if (contestScreenName === "arc119") url = `https://raw.githubusercontent.com/key-moon/ac-predictor-data/master/aperfs/${contestScreenName}.json`;
  386. return yield fetchJsonDataAsync(url);
  387. });
  388. }
  389. function getResultsDataAsync(contestScreenName) {
  390. return __awaiter(this, void 0, void 0, function* () {
  391. return yield fetchJsonDataAsync(`https://atcoder.jp/contests/${contestScreenName}/results/json`);
  392. });
  393. }
  394. function getHistoryDataAsync(userScreenName) {
  395. return __awaiter(this, void 0, void 0, function* () {
  396. return yield fetchJsonDataAsync(`https://atcoder.jp/users/${userScreenName}/history/json`);
  397. });
  398. }
  399. function getContestInformationAsync(contestScreenName) {
  400. return __awaiter(this, void 0, void 0, function* () {
  401. const html = yield fetchTextDataAsync(`https://atcoder.jp/contests/${contestScreenName}`);
  402. const topPageDom = new DOMParser().parseFromString(html, "text/html");
  403. const dataParagraph = topPageDom.getElementsByClassName("small")[0];
  404. const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(":")[1].trim());
  405. const isAHC = /^ahc\d{3}$/.test(contestScreenName) || html.includes("This contest is rated for AHC rating");
  406. return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]), isAHC);
  407. });
  408. }
  409. /**
  410. * ユーザーのPerformance履歴を時間昇順で取得
  411. */
  412. function getPerformanceHistories(history) {
  413. const onlyRated = history.filter((x) => x.IsRated);
  414. onlyRated.sort((a, b) => {
  415. return new Date(a.EndTime).getTime() - new Date(b.EndTime).getTime();
  416. });
  417. return onlyRated.map((x) => x.Performance);
  418. }
  419.  
  420. /**
  421. * サイドメニューに追加される要素のクラス
  422. */
  423. class SideMenuElement {
  424. shouldDisplayed(url) {
  425. return this.match.test(url);
  426. }
  427. /**
  428. * 要素のHTMLを取得
  429. */
  430. GetHTML() {
  431. return `<div class="menu-wrapper">
  432. <div class="menu-header">
  433. <h4 class="sidemenu-txt">${this.title}<span class="glyphicon glyphicon-menu-up" style="float: right"></span></h4>
  434. </div>
  435. <div class="menu-box"><div class="menu-content" id="${this.id}">${this.document}</div></div>
  436. </div>`;
  437. }
  438. }
  439.  
  440. function getGlobalVals() {
  441. const script = [...document.querySelectorAll("head script:not([src])")].map((x) => x.innerHTML).join("\n");
  442. const res = {};
  443. script.match(/var [^ ]+ = .+$/gm).forEach((statement) => {
  444. const match = /var ([^ ]+) = (.+)$/m.exec(statement);
  445. function safeEval(val) {
  446. function trim(val) {
  447. while (val.endsWith(";") || val.endsWith(" "))
  448. val = val.substr(0, val.length - 1);
  449. while (val.startsWith(" "))
  450. val = val.substr(1, val.length - 1);
  451. return val;
  452. }
  453. function isStringToken(val) {
  454. return 1 < val.length && val.startsWith('"') && val.endsWith('"');
  455. }
  456. function evalStringToken(val) {
  457. if (!isStringToken(val))
  458. throw new Error();
  459. return val.substr(1, val.length - 2); // TODO: parse escape
  460. }
  461. val = trim(val);
  462. if (isStringToken(val))
  463. return evalStringToken(val);
  464. if (val.startsWith("moment("))
  465. return new Date(evalStringToken(trim(val.substr(7, val.length - (7 + 1)))));
  466. return val;
  467. }
  468. res[match[1]] = safeEval(match[2]);
  469. });
  470. return res;
  471. }
  472. const globalVals = getGlobalVals();
  473. const userScreenName = globalVals["userScreenName"];
  474. const contestScreenName = globalVals["contestScreenName"];
  475. const startTime = globalVals["startTime"];
  476.  
  477. class AllRowUpdater {
  478. update(table) {
  479. Array.from(table.rows).forEach((row) => this.rowModifier.modifyRow(row));
  480. }
  481. }
  482.  
  483. class StandingsRowModifier {
  484. isHeader(row) {
  485. return row.parentElement.tagName.toLowerCase() == "thead";
  486. }
  487. isFooter(row) {
  488. return row.firstElementChild.hasAttribute("colspan") && row.firstElementChild.getAttribute("colspan") == "3";
  489. }
  490. modifyRow(row) {
  491. if (this.isHeader(row))
  492. this.modifyHeader(row);
  493. else if (this.isFooter(row))
  494. this.modifyFooter(row);
  495. else
  496. this.modifyContent(row);
  497. }
  498. }
  499.  
  500. class PerfAndRateChangeAppender extends StandingsRowModifier {
  501. modifyContent(content) {
  502. var _a;
  503. this.removeOldElem(content);
  504. if (content.firstElementChild.textContent === "-") {
  505. const longCell = content.getElementsByClassName("standings-result")[0];
  506. longCell.setAttribute("colspan", (parseInt(longCell.getAttribute("colspan")) + 2).toString());
  507. return;
  508. }
  509. const userScreenName = content.querySelector(".standings-username .username span").textContent;
  510. const result = (_a = this.results) === null || _a === void 0 ? void 0 : _a.getUserResult(userScreenName);
  511. const perfElem = (result === null || result === void 0 ? void 0 : result.IsSubmitted) ? this.getRatingSpan(Math.round(positivizeRating(result.Performance)))
  512. : "-";
  513. const ratingElem = result
  514. ? (result === null || result === void 0 ? void 0 : result.IsRated) && (this === null || this === void 0 ? void 0 : this.isRated)
  515. ? this.getChangedRatingElem(result.OldRating, result.NewRating)
  516. : this.getUnratedElem(result.OldRating)
  517. : "-";
  518. content.insertAdjacentHTML("beforeend", `<td class="standings-result standings-perf">${perfElem}</td>`);
  519. content.insertAdjacentHTML("beforeend", `<td class="standings-result standings-rate">${ratingElem}</td>`);
  520. }
  521. getChangedRatingElem(oldRate, newRate) {
  522. const oldRateSpan = this.getRatingSpan(oldRate);
  523. const newRateSpan = this.getRatingSpan(newRate);
  524. const diff = this.toSignedString(newRate - oldRate);
  525. return `<span class="bold">${oldRateSpan}</span> → <span class="bold">${newRateSpan}</span> <span class="grey">(${diff})</span>`;
  526. }
  527. toSignedString(n) {
  528. return `${n >= 0 ? "+" : ""}${n}`;
  529. }
  530. getUnratedElem(rate) {
  531. return `<span class="bold">${this.getRatingSpan(rate)}</span> <span class="grey">(unrated)</span>`;
  532. }
  533. getRatingSpan(rate) {
  534. return `<span class="user-${getColor(rate)}">${rate}</span>`;
  535. }
  536. modifyFooter(footer) {
  537. this.removeOldElem(footer);
  538. footer.insertAdjacentHTML("beforeend", '<td class="standings-result standings-perf standings-rate" colspan="2">-</td>');
  539. }
  540. modifyHeader(header) {
  541. this.removeOldElem(header);
  542. 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>');
  543. }
  544. removeOldElem(row) {
  545. row.querySelectorAll(".standings-perf, .standings-rate").forEach((elem) => elem.remove());
  546. }
  547. }
  548.  
  549. class PredictorElement extends SideMenuElement {
  550. constructor() {
  551. super(...arguments);
  552. this.id = "predictor";
  553. this.title = "Predictor";
  554. this.match = /atcoder.jp\/contests\/.+/;
  555. this.document = dom;
  556. this.historyData = [];
  557. this.contestOnUpdated = [];
  558. this.resultsOnUpdated = [];
  559. }
  560. set contest(val) {
  561. this._contest = val;
  562. this.contestOnUpdated.forEach((func) => func(val));
  563. }
  564. get contest() {
  565. return this._contest;
  566. }
  567. set results(val) {
  568. this._results = val;
  569. this.resultsOnUpdated.forEach((func) => func(val));
  570. }
  571. get results() {
  572. return this._results;
  573. }
  574. isStandingsPage() {
  575. return /standings([^/]*)?$/.test(document.location.href);
  576. }
  577. afterAppend() {
  578. const loaded = () => !!document.getElementById("standings-tbody");
  579. if (!this.isStandingsPage() || loaded()) {
  580. void this.initialize();
  581. return;
  582. }
  583. const loadingElem = document.getElementById("vue-standings").getElementsByClassName("loading-show")[0];
  584. new MutationObserver(() => {
  585. if (loaded())
  586. void this.initialize();
  587. }).observe(loadingElem, { attributes: true });
  588. }
  589. initialize() {
  590. var _a;
  591. return __awaiter(this, void 0, void 0, function* () {
  592. const firstContestDate = new Date(2016, 6, 16, 21);
  593. const predictorElements = [
  594. "predictor-input-rank",
  595. "predictor-input-perf",
  596. "predictor-input-rate",
  597. "predictor-current",
  598. "predictor-reload",
  599. ];
  600. const isStandingsPage = this.isStandingsPage();
  601. const contestInformation = yield getContestInformationAsync(contestScreenName);
  602. const rowUpdater = new PerfAndRateChangeAppender();
  603. this.resultsOnUpdated.push((val) => {
  604. rowUpdater.results = val;
  605. });
  606. this.contestOnUpdated.push((val) => {
  607. rowUpdater.isRated = val.IsRated;
  608. });
  609. const tableUpdater = new AllRowUpdater();
  610. tableUpdater.rowModifier = rowUpdater;
  611. const tableElement = (_a = document.getElementById("standings-tbody")) === null || _a === void 0 ? void 0 : _a.parentElement;
  612. let model = new PredictorModel({
  613. rankValue: 0,
  614. perfValue: 0,
  615. rateValue: 0,
  616. enabled: false,
  617. history: this.historyData,
  618. });
  619. const updateData = (aperfs, standings) => __awaiter(this, void 0, void 0, function* () {
  620. this.contest = new Contest(contestScreenName, contestInformation, standings, aperfs);
  621. model.contest = this.contest;
  622. if (this.contest.standings.Fixed && this.contest.IsRated) {
  623. const rawResult = yield getResultsDataAsync(contestScreenName);
  624. rawResult.sort((a, b) => (a.Place !== b.Place ? a.Place - b.Place : b.OldRating - a.OldRating));
  625. const sortedStandingsData = Array.from(this.contest.standings.StandingsData);
  626. if (contestInformation.isHeuristic) sortedStandingsData.filter((x) => x.TotalResult.Count !== 0);
  627. sortedStandingsData.sort((a, b) => {
  628. if (a.TotalResult.Count === 0 && b.TotalResult.Count === 0)
  629. return 0;
  630. if (a.TotalResult.Count === 0)
  631. return 1;
  632. if (b.TotalResult.Count === 0)
  633. return -1;
  634. if (a.Rank !== b.Rank)
  635. return a.Rank - b.Rank;
  636. if (b.OldRating !== a.OldRating)
  637. return b.OldRating - a.OldRating;
  638. if (a.UserIsDeleted)
  639. return -1;
  640. if (b.UserIsDeleted)
  641. return 1;
  642. return 0;
  643. });
  644. let lastPerformance = this.contest.perfLimit;
  645. let deletedCount = 0;
  646. this.results = new FixedResults(sortedStandingsData.map((data, index) => {
  647. let result = rawResult[index - deletedCount];
  648. if (!result || data.OldRating !== result.OldRating) {
  649. deletedCount++;
  650. result = null;
  651. }
  652. 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);
  653. }));
  654. }
  655. else {
  656. this.results = new OnDemandResults(this.contest, this.contest.templateResults);
  657. }
  658. });
  659. if (!shouldEnabledPredictor().verdict) {
  660. model.updateInformation(shouldEnabledPredictor().message);
  661. updateView();
  662. return;
  663. }
  664. try {
  665. let aPerfs;
  666. let standings;
  667. try {
  668. standings = yield getStandingsDataAsync(contestScreenName);
  669. }
  670. catch (e) {
  671. throw new Error("Standings读取失败。");
  672. }
  673. try {
  674. aPerfs = yield getAPerfsDataAsync(contestScreenName);
  675. }
  676. catch (e) {
  677. throw new Error("APerf获取失败。");
  678. }
  679. yield updateData(aPerfs, standings);
  680. model.setEnable(true);
  681. model.updateInformation(`最后更新时间: ${new Date().toTimeString().split(" ")[0]}`);
  682. if (isStandingsPage) {
  683. new MutationObserver(() => {
  684. tableUpdater.update(tableElement);
  685. }).observe(tableElement.tBodies[0], {
  686. childList: true,
  687. });
  688. const refreshElem = document.getElementById("refresh");
  689. if (refreshElem)
  690. new MutationObserver((mutationRecord) => {
  691. const disabled = mutationRecord[0].target.classList.contains("disabled");
  692. if (disabled) {
  693. void (() => __awaiter(this, void 0, void 0, function* () {
  694. yield updateStandingsFromAPI();
  695. updateView();
  696. }))();
  697. }
  698. }).observe(refreshElem, {
  699. attributes: true,
  700. attributeFilter: ["class"],
  701. });
  702. }
  703. }
  704. catch (e) {
  705. model.updateInformation(e.message);
  706. model.setEnable(false);
  707. }
  708. updateView();
  709. {
  710. const reloadButton = document.getElementById("predictor-reload");
  711. reloadButton.addEventListener("click", () => {
  712. void (() => __awaiter(this, void 0, void 0, function* () {
  713. model.updateInformation("");
  714. reloadButton.disabled = true;
  715. updateView();
  716. yield updateStandingsFromAPI();
  717. reloadButton.disabled = false;
  718. updateView();
  719. }))();
  720. });
  721. document.getElementById("predictor-current").addEventListener("click", () => {
  722. const myResult = this.contest.templateResults[userScreenName];
  723. if (!myResult)
  724. return;
  725. model = new CalcFromRankModel(model);
  726. model.updateData(myResult.RatedRank, model.perfValue, model.rateValue);
  727. updateView();
  728. });
  729. document.getElementById("predictor-input-rank").addEventListener("keyup", () => {
  730. const inputString = document.getElementById("predictor-input-rank").value;
  731. const inputNumber = parseInt(inputString);
  732. if (!isFinite(inputNumber))
  733. return;
  734. model = new CalcFromRankModel(model);
  735. model.updateData(inputNumber, 0, 0);
  736. updateView();
  737. });
  738. document.getElementById("predictor-input-perf").addEventListener("keyup", () => {
  739. const inputString = document.getElementById("predictor-input-perf").value;
  740. const inputNumber = parseInt(inputString);
  741. if (!isFinite(inputNumber))
  742. return;
  743. model = new CalcFromPerfModel(model);
  744. model.updateData(0, inputNumber, 0);
  745. updateView();
  746. });
  747. document.getElementById("predictor-input-rate").addEventListener("keyup", () => {
  748. const inputString = document.getElementById("predictor-input-rate").value;
  749. const inputNumber = parseInt(inputString);
  750. if (!isFinite(inputNumber))
  751. return;
  752. model = new CalcFromRateModel(model);
  753. model.updateData(0, 0, inputNumber);
  754. updateView();
  755. });
  756. }
  757. function updateStandingsFromAPI() {
  758. return __awaiter(this, void 0, void 0, function* () {
  759. try {
  760. const shouldEnabled = shouldEnabledPredictor();
  761. if (!shouldEnabled.verdict) {
  762. model.updateInformation(shouldEnabled.message);
  763. model.setEnable(false);
  764. return;
  765. }
  766. const standings = yield getStandingsDataAsync(contestScreenName);
  767. const aperfs = yield getAPerfsDataAsync(contestScreenName);
  768. yield updateData(aperfs, standings);
  769. model.updateInformation(`最后更新时间: ${new Date().toTimeString().split(" ")[0]}`);
  770. model.setEnable(true);
  771. }
  772. catch (e) {
  773. model.updateInformation(e.message);
  774. model.setEnable(false);
  775. }
  776. });
  777. }
  778. function shouldEnabledPredictor() {
  779. if (new Date() < startTime)
  780. return { verdict: false, message: "比赛暂未开始" };
  781. if (startTime < firstContestDate)
  782. return {
  783. verdict: false,
  784. message: "这场比赛是在使用现行 Rating 制度之前举行的,无法准确计算 Rating 数据。",
  785. };
  786. if (contestInformation.RatedRange[0] > contestInformation.RatedRange[1])
  787. return {
  788. verdict: false,
  789. message: "This contest is unrated.",
  790. };
  791. return { verdict: true, message: "" };
  792. }
  793. function updateView() {
  794. const roundedRankValue = isFinite(model.rankValue) ? roundValue(model.rankValue, 2).toString() : "";
  795. const roundedPerfValue = isFinite(model.perfValue) ? roundValue(model.perfValue, 2).toString() : "";
  796. const roundedRateValue = isFinite(model.rateValue) ? roundValue(model.rateValue, 2).toString() : "";
  797. document.getElementById("predictor-input-rank").value = roundedRankValue;
  798. document.getElementById("predictor-input-perf").value = roundedPerfValue;
  799. document.getElementById("predictor-input-rate").value = roundedRateValue;
  800. document.getElementById("predictor-alert").innerHTML = `<h5 class='sidemenu-txt'>${model.information}</h5>`;
  801. if (model.enabled)
  802. enabled();
  803. else
  804. disabled();
  805. if (isStandingsPage && shouldEnabledPredictor().verdict) {
  806. tableUpdater.update(tableElement);
  807. }
  808. function enabled() {
  809. predictorElements.forEach((element) => {
  810. document.getElementById(element).disabled = false;
  811. });
  812. }
  813. function disabled() {
  814. predictorElements.forEach((element) => {
  815. document.getElementById(element).disabled = false;
  816. });
  817. }
  818. }
  819. });
  820. }
  821. afterOpen() {
  822. return __awaiter(this, void 0, void 0, function* () {
  823. getPerformanceHistories(yield getHistoryDataAsync(userScreenName)).forEach((elem) => this.historyData.push(elem));
  824. });
  825. }
  826. }
  827. const predictor = new PredictorElement();
  828.  
  829. 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>";
  830.  
  831. class EstimatorModel {
  832. constructor(inputValue, perfHistory) {
  833. this.inputDesc = "";
  834. this.resultDesc = "";
  835. this.perfHistory = perfHistory;
  836. this.updateInput(inputValue);
  837. }
  838. updateInput(value) {
  839. this.inputValue = value;
  840. this.resultValue = this.calcResult(value);
  841. }
  842. toggle() {
  843. return null;
  844. }
  845. calcResult(input) {
  846. return input;
  847. }
  848. }
  849.  
  850. class CalcRatingModel extends EstimatorModel {
  851. constructor(inputValue, perfHistory) {
  852. super(inputValue, perfHistory);
  853. this.inputDesc = "Performance";
  854. this.resultDesc = "预计 Rating";
  855. }
  856. toggle() {
  857. return new CalcPerfModel(this.resultValue, this.perfHistory);
  858. }
  859. calcResult(input) {
  860. return positivizeRating(calcRatingFromHistory(this.perfHistory.concat([input])));
  861. }
  862. }
  863.  
  864. class CalcPerfModel extends EstimatorModel {
  865. constructor(inputValue, perfHistory) {
  866. super(inputValue, perfHistory);
  867. this.inputDesc = "目标 Rating";
  868. this.resultDesc = "所需 Performance";
  869. }
  870. toggle() {
  871. return new CalcRatingModel(this.resultValue, this.perfHistory);
  872. }
  873. calcResult(input) {
  874. return calcRequiredPerformance(unpositivizeRating(input), this.perfHistory);
  875. }
  876. }
  877.  
  878. function GetEmbedTweetLink(content, url) {
  879. return `https://twitter.com/share?text=${encodeURI(content)}&url=${encodeURI(url)}`;
  880. }
  881.  
  882. function getLS(key) {
  883. const val = localStorage.getItem(key);
  884. return (val ? JSON.parse(val) : val);
  885. }
  886. function setLS(key, val) {
  887. try {
  888. localStorage.setItem(key, JSON.stringify(val));
  889. }
  890. catch (error) {
  891. console.log(error);
  892. }
  893. }
  894. const models = [CalcPerfModel, CalcRatingModel];
  895. function GetModelFromStateCode(state, value, history) {
  896. let model = models.find((model) => model.name === state);
  897. if (!model)
  898. model = CalcPerfModel;
  899. return new model(value, history);
  900. }
  901. class EstimatorElement extends SideMenuElement {
  902. constructor() {
  903. super(...arguments);
  904. this.id = "estimator";
  905. this.title = "Estimator";
  906. this.document = dom$1;
  907. this.match = /atcoder.jp/;
  908. }
  909. afterAppend() {
  910. //nothing to do
  911. }
  912. // nothing to do
  913. afterOpen() {
  914. return __awaiter(this, void 0, void 0, function* () {
  915. const estimatorInputSelector = document.getElementById("estimator-input");
  916. const estimatorResultSelector = document.getElementById("estimator-res");
  917. let model = GetModelFromStateCode(getLS("sidemenu_estimator_state"), getLS("sidemenu_estimator_value"), getPerformanceHistories(yield getHistoryDataAsync(userScreenName)));
  918. updateView();
  919. document.getElementById("estimator-toggle").addEventListener("click", () => {
  920. model = model.toggle();
  921. updateLocalStorage();
  922. updateView();
  923. });
  924. estimatorInputSelector.addEventListener("keyup", () => {
  925. updateModel();
  926. updateLocalStorage();
  927. updateView();
  928. });
  929. /** modelをinputの値に応じて更新 */
  930. function updateModel() {
  931. const inputNumber = estimatorInputSelector.valueAsNumber;
  932. if (!isFinite(inputNumber))
  933. return;
  934. model.updateInput(inputNumber);
  935. }
  936. /** modelの状態をLSに保存 */
  937. function updateLocalStorage() {
  938. setLS("sidemenu_estimator_value", model.inputValue);
  939. setLS("sidemenu_estimator_state", model.constructor.name);
  940. }
  941. /** modelを元にviewを更新 */
  942. function updateView() {
  943. const roundedInput = roundValue(model.inputValue, 2);
  944. const roundedResult = roundValue(model.resultValue, 2);
  945. document.getElementById("estimator-input-desc").innerText = model.inputDesc;
  946. document.getElementById("estimator-res-desc").innerText = model.resultDesc;
  947. estimatorInputSelector.value = String(roundedInput);
  948. estimatorResultSelector.value = String(roundedResult);
  949. const tweetStr = `AtCoderのハンドルネーム: ${userScreenName}\n${model.inputDesc}: ${roundedInput}\n${model.resultDesc}: ${roundedResult}\n`;
  950. document.getElementById("estimator-tweet").href = GetEmbedTweetLink(tweetStr, "https://greasyfork.org/ja/scripts/369954-ac-predictor");
  951. }
  952. });
  953. }
  954. }
  955. const estimator = new EstimatorElement();
  956.  
  957. 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>";
  958.  
  959. //import "./sidemenu.scss";
  960. class SideMenu {
  961. constructor() {
  962. this.pendingElements = [];
  963. this.Generate();
  964. }
  965. Generate() {
  966. document.getElementById("main-div").insertAdjacentHTML("afterbegin", sidemenuHtml);
  967. resizeSidemenuHeight();
  968. const key = document.getElementById("sidemenu-key");
  969. const wrap = document.getElementById("menu-wrap");
  970. key.addEventListener("click", () => {
  971. this.pendingElements.forEach((elem) => {
  972. elem.afterOpen();
  973. });
  974. this.pendingElements.length = 0;
  975. key.classList.toggle("glyphicon-menu-left");
  976. key.classList.toggle("glyphicon-menu-right");
  977. wrap.classList.toggle("sidemenu-active");
  978. });
  979. window.addEventListener("onresize", resizeSidemenuHeight);
  980. document.getElementById("sidemenu").addEventListener("click", (event) => {
  981. const target = event.target;
  982. const header = target.closest(".menu-header");
  983. if (!header)
  984. return;
  985. const box = target.closest(".menu-wrapper").querySelector(".menu-box");
  986. box.classList.toggle("menu-box-collapse");
  987. const arrow = target.querySelector(".glyphicon");
  988. arrow.classList.toggle("glyphicon-menu-down");
  989. arrow.classList.toggle("glyphicon-menu-up");
  990. });
  991. function resizeSidemenuHeight() {
  992. document.getElementById("sidemenu").style.height = `${window.innerHeight}px`;
  993. }
  994. }
  995. addElement(element) {
  996. if (!element.shouldDisplayed(document.location.href))
  997. return;
  998. const sidemenu = document.getElementById("sidemenu");
  999. sidemenu.insertAdjacentHTML("afterbegin", element.GetHTML());
  1000. const content = sidemenu.querySelector(".menu-content");
  1001. content.parentElement.style.height = `${content.offsetHeight}px`;
  1002. element.afterAppend();
  1003. this.pendingElements.push(element);
  1004. }
  1005. }
  1006.  
  1007. const sidemenu = new SideMenu();
  1008. const elements = [predictor, estimator];
  1009. for (let i = elements.length - 1; i >= 0; i--) {
  1010. sidemenu.addElement(elements[i]);
  1011. }