ac-predictor

コンテスト中にAtCoderのパフォーマンスを予測します

目前为 2025-03-26 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name ac-predictor
  3. // @namespace http://ac-predictor.azurewebsites.net/
  4. // @version 2.0.9
  5. // @description コンテスト中にAtCoderのパフォーマンスを予測します
  6. // @author keymoon
  7. // @license MIT
  8. // @match https://atcoder.jp/*
  9. // @exclude /^https://atcoder\.jp/[^#?]*/json/
  10. // @grant none
  11. // ==/UserScript==
  12. var config_header_text$1 = "ac-predictor 設定";
  13. var config_hideDuringContest_label$1 = "コンテスト中に予測を非表示にする";
  14. var config_hideUntilFixed_label$1 = "パフォーマンスが確定するまで予測を非表示にする";
  15. var config_useFinalResultOnVirtual_label$1 = "バーチャル参加時のパフォーマンス計算に最終結果を用いる";
  16. var config_useFinalResultOnVirtual_description$1 = "チェックを入れると、当時の参加者が既にコンテストを終えているものとしてパフォーマンスを計算します。";
  17. var config_dropdown$1 = "ac-predictor 設定";
  18. var standings_performance_column_label$1 = "perf";
  19. var standings_rate_change_column_label$1 = "レート変化";
  20. var standings_click_to_compute_label$1 = "クリックして計算";
  21. var standings_not_provided_label$1 = "提供不可";
  22. var jaJson = {
  23. config_header_text: config_header_text$1,
  24. config_hideDuringContest_label: config_hideDuringContest_label$1,
  25. config_hideUntilFixed_label: config_hideUntilFixed_label$1,
  26. config_useFinalResultOnVirtual_label: config_useFinalResultOnVirtual_label$1,
  27. config_useFinalResultOnVirtual_description: config_useFinalResultOnVirtual_description$1,
  28. config_dropdown: config_dropdown$1,
  29. standings_performance_column_label: standings_performance_column_label$1,
  30. standings_rate_change_column_label: standings_rate_change_column_label$1,
  31. standings_click_to_compute_label: standings_click_to_compute_label$1,
  32. standings_not_provided_label: standings_not_provided_label$1
  33. };
  34.  
  35. var config_header_text = "ac-predictor settings";
  36. var config_hideDuringContest_label = "hide prediction during contests";
  37. var config_hideUntilFixed_label = "hide prediction until performances are fixed";
  38. var config_useFinalResultOnVirtual_label = "use final result as a performance reference during the virtual participation";
  39. var config_useFinalResultOnVirtual_description = "If enabled, the performance is calculated as if the original participant had already done the contest.";
  40. var config_dropdown = "ac-predictor";
  41. var standings_performance_column_label = "perf";
  42. var standings_rate_change_column_label = "rating delta";
  43. var standings_click_to_compute_label = "click to compute";
  44. var standings_not_provided_label = "not provided";
  45. var enJson = {
  46. config_header_text: config_header_text,
  47. config_hideDuringContest_label: config_hideDuringContest_label,
  48. config_hideUntilFixed_label: config_hideUntilFixed_label,
  49. config_useFinalResultOnVirtual_label: config_useFinalResultOnVirtual_label,
  50. config_useFinalResultOnVirtual_description: config_useFinalResultOnVirtual_description,
  51. config_dropdown: config_dropdown,
  52. standings_performance_column_label: standings_performance_column_label,
  53. standings_rate_change_column_label: standings_rate_change_column_label,
  54. standings_click_to_compute_label: standings_click_to_compute_label,
  55. standings_not_provided_label: standings_not_provided_label
  56. };
  57.  
  58. // should not be here
  59. function getCurrentLanguage() {
  60. const elems = document.querySelectorAll("#navbar-collapse .dropdown > a");
  61. if (elems.length == 0)
  62. return "JA";
  63. for (let i = 0; i < elems.length; i++) {
  64. if (elems[i].textContent?.includes("English"))
  65. return "EN";
  66. if (elems[i].textContent?.includes("日本語"))
  67. return "JA";
  68. }
  69. console.warn("language detection failed. fallback to English");
  70. return "EN";
  71. }
  72. const language = getCurrentLanguage();
  73. const currentJson = { "EN": enJson, "JA": jaJson }[language];
  74. function getTranslation(label) {
  75. return currentJson[label];
  76. }
  77. function substitute(input) {
  78. for (const key in currentJson) {
  79. // @ts-ignore
  80. input = input.replaceAll(`{${key}}`, currentJson[key]);
  81. }
  82. return input;
  83. }
  84.  
  85. const configKey = "ac-predictor-config";
  86. const defaultConfig = {
  87. useResults: true,
  88. hideDuringContest: false,
  89. isDebug: false,
  90. hideUntilFixed: false,
  91. useFinalResultOnVirtual: false
  92. };
  93. function getConfigObj() {
  94. const val = localStorage.getItem(configKey) ?? "{}";
  95. let config;
  96. try {
  97. config = JSON.parse(val);
  98. }
  99. catch {
  100. console.warn("invalid config found", val);
  101. config = {};
  102. }
  103. return { ...defaultConfig, ...config };
  104. }
  105. function storeConfigObj(config) {
  106. localStorage.setItem(configKey, JSON.stringify(config));
  107. }
  108. function getConfig(configKey) {
  109. return getConfigObj()[configKey];
  110. }
  111. function setConfig(key, value) {
  112. const config = getConfigObj();
  113. config[key] = value;
  114. storeConfigObj(config);
  115. }
  116.  
  117. const isDebug = location.hash.includes("ac-predictor-debug") || getConfig("isDebug");
  118. function isDebugMode() {
  119. return isDebug;
  120. }
  121.  
  122. 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>";
  123.  
  124. 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";
  125.  
  126. 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";
  127.  
  128. class ConfigView {
  129. modalElement;
  130. constructor(modalElement) {
  131. this.modalElement = modalElement;
  132. }
  133. addCheckbox(label, val, description, handler) {
  134. const settingsRow = this.getSettingsRow();
  135. const div = document.createElement("div");
  136. div.classList.add("checkbox");
  137. const labelElem = document.createElement("label");
  138. const input = document.createElement("input");
  139. input.type = "checkbox";
  140. input.checked = val;
  141. labelElem.append(input);
  142. labelElem.append(label);
  143. if (description) {
  144. const descriptionDiv = document.createElement("div");
  145. descriptionDiv.append(description);
  146. descriptionDiv.classList.add("small");
  147. descriptionDiv.classList.add("gray");
  148. labelElem.append(descriptionDiv);
  149. }
  150. div.append(labelElem);
  151. settingsRow.append(div);
  152. input.addEventListener("change", () => {
  153. handler(input.checked);
  154. });
  155. }
  156. addHeader(level, content) {
  157. const settingsRow = this.getSettingsRow();
  158. const div = document.createElement(`h${level}`);
  159. div.textContent = content;
  160. settingsRow.append(div);
  161. }
  162. getSettingsRow() {
  163. return this.modalElement.querySelector(".settings-row");
  164. }
  165. static Create() {
  166. document.querySelector("body")?.insertAdjacentHTML("afterbegin", substitute(modalHTML));
  167. document.querySelector(".header-mypage_list li:nth-last-child(1)")?.insertAdjacentHTML("beforebegin", substitute(newDropdownElem));
  168. document.querySelector(".navbar-right .dropdown-menu .divider:nth-last-child(2)")?.insertAdjacentHTML("beforebegin", substitute(legacyDropdownElem));
  169. const element = document.querySelector("#modal-ac-predictor-settings");
  170. if (element === null) {
  171. throw new Error("settings modal not found");
  172. }
  173. return new ConfigView(element);
  174. }
  175. }
  176.  
  177. class ConfigController {
  178. register() {
  179. const configView = ConfigView.Create();
  180. // TODO: 流石に処理をまとめたい
  181. configView.addCheckbox(getTranslation("config_useFinalResultOnVirtual_label"), getConfig("useFinalResultOnVirtual"), getTranslation("config_useFinalResultOnVirtual_description"), val => setConfig("useFinalResultOnVirtual", val));
  182. configView.addCheckbox(getTranslation("config_hideDuringContest_label"), getConfig("hideDuringContest"), null, val => setConfig("hideDuringContest", val));
  183. configView.addCheckbox(getTranslation("config_hideUntilFixed_label"), getConfig("hideUntilFixed"), null, val => setConfig("hideUntilFixed", val));
  184. if (isDebugMode()) {
  185. configView.addCheckbox("[DEBUG] enable debug mode", getConfig("isDebug"), null, val => setConfig("isDebug", val));
  186. configView.addCheckbox("[DEBUG] use results", getConfig("useResults"), null, val => setConfig("useResults", val));
  187. }
  188. }
  189. }
  190.  
  191. async function getAPerfs(contestScreenName) {
  192. const result = await fetch(`https://data.ac-predictor.com/aperfs/${contestScreenName}.json`);
  193. if (!result.ok) {
  194. throw new Error(`Failed to fetch aperfs: ${result.status}`);
  195. }
  196. return await result.json();
  197. }
  198.  
  199. // [start, end]
  200. class Range {
  201. start;
  202. end;
  203. constructor(start, end) {
  204. this.start = start;
  205. this.end = end;
  206. }
  207. contains(val) {
  208. return this.start <= val && val <= this.end;
  209. }
  210. hasValue() {
  211. return this.start <= this.end;
  212. }
  213. }
  214.  
  215. class ContestDetails {
  216. contestName;
  217. contestScreenName;
  218. contestType;
  219. startTime;
  220. duration;
  221. ratedrange;
  222. constructor(contestName, contestScreenName, contestType, startTime, duration, ratedRange) {
  223. this.contestName = contestName;
  224. this.contestScreenName = contestScreenName;
  225. this.contestType = contestType;
  226. this.startTime = startTime;
  227. this.duration = duration;
  228. this.ratedrange = ratedRange;
  229. }
  230. get endTime() {
  231. return new Date(this.startTime.getTime() + this.duration * 1000);
  232. }
  233. get defaultAPerf() {
  234. if (this.contestType == "heuristic")
  235. return 1000;
  236. if (!this.ratedrange.hasValue()) {
  237. throw new Error("unrated contest");
  238. }
  239. // value is not relevant as it is never used
  240. if (!this.ratedrange.contains(0))
  241. return 800;
  242. if (this.ratedrange.end == 1199)
  243. return 800;
  244. if (this.ratedrange.end == 1999)
  245. return 800;
  246. const DEFAULT_CHANGED_AT = new Date("2019-05-25"); // maybe wrong
  247. if (this.ratedrange.end == 2799) {
  248. if (this.startTime < DEFAULT_CHANGED_AT)
  249. return 1600;
  250. else
  251. return 1000;
  252. }
  253. if (4000 <= this.ratedrange.end) {
  254. if (this.startTime < DEFAULT_CHANGED_AT)
  255. return 1600;
  256. else
  257. return 1200;
  258. }
  259. throw new Error("unknown contest type");
  260. }
  261. get performanceCap() {
  262. if (this.contestType == "heuristic")
  263. return Infinity;
  264. if (!this.ratedrange.hasValue()) {
  265. throw new Error("unrated contest");
  266. }
  267. if (4000 <= this.ratedrange.end)
  268. return Infinity;
  269. return this.ratedrange.end + 1 + 400;
  270. }
  271. beforeContest(dateTime) {
  272. return dateTime < this.startTime;
  273. }
  274. duringContest(dateTime) {
  275. return this.startTime < dateTime && dateTime < this.endTime;
  276. }
  277. isOver(dateTime) {
  278. return this.endTime < dateTime;
  279. }
  280. }
  281.  
  282. async function getContestDetails() {
  283. const result = await fetch(`https://data.ac-predictor.com/contest-details.json`);
  284. if (!result.ok) {
  285. throw new Error(`Failed to fetch contest details: ${result.status}`);
  286. }
  287. const parsed = await result.json();
  288. const res = [];
  289. for (const elem of parsed) {
  290. if (typeof elem !== "object")
  291. throw new Error("invalid object returned");
  292. if (typeof elem.contestName !== "string")
  293. throw new Error("invalid object returned");
  294. const contestName = elem.contestName;
  295. if (typeof elem.contestScreenName !== "string")
  296. throw new Error("invalid object returned");
  297. const contestScreenName = elem.contestScreenName;
  298. if (elem.contestType !== "algorithm" && elem.contestType !== "heuristic")
  299. throw new Error("invalid object returned");
  300. const contestType = elem.contestType;
  301. if (typeof elem.startTime !== "number")
  302. throw new Error("invalid object returned");
  303. const startTime = new Date(elem.startTime * 1000);
  304. if (typeof elem.duration !== "number")
  305. throw new Error("invalid object returned");
  306. const duration = elem.duration;
  307. if (typeof elem.ratedrange !== "object" || typeof elem.ratedrange[0] !== "number" || typeof elem.ratedrange[1] !== "number")
  308. throw new Error("invalid object returned");
  309. const ratedRange = new Range(elem.ratedrange[0], elem.ratedrange[1]);
  310. res.push(new ContestDetails(contestName, contestScreenName, contestType, startTime, duration, ratedRange));
  311. }
  312. return res;
  313. }
  314.  
  315. class Cache {
  316. cacheDuration;
  317. cacheExpires = new Map();
  318. cacheData = new Map();
  319. constructor(cacheDuration) {
  320. this.cacheDuration = cacheDuration;
  321. }
  322. has(key) {
  323. return this.cacheExpires.has(key) || Date.now() <= this.cacheExpires.get(key);
  324. }
  325. set(key, content) {
  326. const expire = Date.now() + this.cacheDuration;
  327. this.cacheExpires.set(key, expire);
  328. this.cacheData.set(key, content);
  329. }
  330. get(key) {
  331. if (!this.has(key)) {
  332. throw new Error(`invalid key: ${key}`);
  333. }
  334. return this.cacheData.get(key);
  335. }
  336. }
  337.  
  338. const handlers = [];
  339. function addHandler(handler) {
  340. handlers.push(handler);
  341. }
  342. // absurd hack to steal ajax response data for caching
  343. // @ts-ignore
  344. $(document).on("ajaxComplete", (_, xhr, settings) => {
  345. if (xhr.status == 200) {
  346. for (const handler of handlers) {
  347. handler(xhr.responseText, settings.url);
  348. }
  349. }
  350. });
  351.  
  352. let StandingsWrapper$2 = class StandingsWrapper {
  353. data;
  354. constructor(data) {
  355. this.data = data;
  356. }
  357. toRanks(onlyRated = false, contestType = "algorithm") {
  358. const res = new Map();
  359. for (const data of this.data.StandingsData) {
  360. if (onlyRated && !this.isRated(data, contestType))
  361. continue;
  362. const userScreenName = typeof (data.Additional["standings.extendedContestRank"]) == "undefined" ? `extended:${data.UserScreenName}` : data.UserScreenName;
  363. res.set(userScreenName, data.Rank);
  364. }
  365. return res;
  366. }
  367. toRatedUsers(contestType) {
  368. const res = [];
  369. for (const data of this.data.StandingsData) {
  370. if (this.isRated(data, contestType)) {
  371. res.push(data.UserScreenName);
  372. }
  373. }
  374. return res;
  375. }
  376. toScores() {
  377. const res = new Map();
  378. for (const data of this.data.StandingsData) {
  379. const userScreenName = typeof (data.Additional["standings.extendedContestRank"]) == "undefined" ? `extended:${data.UserScreenName}` : data.UserScreenName;
  380. res.set(userScreenName, { score: data.TotalResult.Score, penalty: data.TotalResult.Elapsed });
  381. }
  382. return res;
  383. }
  384. isRated(data, contestType) {
  385. if (contestType === "algorithm") {
  386. return data.IsRated && typeof (data.Additional["standings.extendedContestRank"]) != "undefined";
  387. }
  388. else {
  389. return data.IsRated && typeof (data.Additional["standings.extendedContestRank"]) != "undefined" && data.TotalResult.Count !== 0;
  390. }
  391. }
  392. };
  393. const STANDINGS_CACHE_DURATION$2 = 10 * 1000;
  394. const cache$4 = new Cache(STANDINGS_CACHE_DURATION$2);
  395. async function getExtendedStandings(contestScreenName) {
  396. if (!cache$4.has(contestScreenName)) {
  397. const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/extended/json`);
  398. if (!result.ok) {
  399. throw new Error(`Failed to fetch extended standings: ${result.status}`);
  400. }
  401. cache$4.set(contestScreenName, await result.json());
  402. }
  403. return new StandingsWrapper$2(cache$4.get(contestScreenName));
  404. }
  405. addHandler((content, path) => {
  406. const match = path.match(/^\/contests\/([^/]*)\/standings\/extended\/json$/);
  407. if (!match)
  408. return;
  409. const contestScreenName = match[1];
  410. cache$4.set(contestScreenName, JSON.parse(content));
  411. });
  412.  
  413. class EloPerformanceProvider {
  414. ranks;
  415. ratings;
  416. cap;
  417. rankMemo = new Map();
  418. constructor(ranks, ratings, cap) {
  419. this.ranks = ranks;
  420. this.ratings = ratings;
  421. this.cap = cap;
  422. }
  423. availableFor(userScreenName) {
  424. return this.ranks.has(userScreenName);
  425. }
  426. getPerformance(userScreenName) {
  427. if (!this.availableFor(userScreenName)) {
  428. throw new Error(`User ${userScreenName} not found`);
  429. }
  430. const rank = this.ranks.get(userScreenName);
  431. return this.getPerformanceForRank(rank);
  432. }
  433. getPerformances() {
  434. const performances = new Map();
  435. for (const userScreenName of this.ranks.keys()) {
  436. performances.set(userScreenName, this.getPerformance(userScreenName));
  437. }
  438. return performances;
  439. }
  440. getPerformanceForRank(rank) {
  441. let upper = 6144;
  442. let lower = -2048;
  443. while (upper - lower > 0.5) {
  444. const mid = (upper + lower) / 2;
  445. if (rank > this.getRankForPerformance(mid))
  446. upper = mid;
  447. else
  448. lower = mid;
  449. }
  450. return Math.min(this.cap, Math.round((upper + lower) / 2));
  451. }
  452. getRankForPerformance(performance) {
  453. if (this.rankMemo.has(performance))
  454. return this.rankMemo.get(performance);
  455. const res = this.ratings.reduce((val, APerf) => val + 1.0 / (1.0 + Math.pow(6.0, (performance - APerf) / 400.0)), 0.5);
  456. this.rankMemo.set(performance, res);
  457. return res;
  458. }
  459. }
  460.  
  461. function getRankToUsers(ranks) {
  462. const rankToUsers = new Map();
  463. for (const [userScreenName, rank] of ranks) {
  464. if (!rankToUsers.has(rank))
  465. rankToUsers.set(rank, []);
  466. rankToUsers.get(rank).push(userScreenName);
  467. }
  468. return rankToUsers;
  469. }
  470. function getMaxRank(ranks) {
  471. return Math.max(...ranks.values());
  472. }
  473. class InterpolatePerformanceProvider {
  474. ranks;
  475. maxRank;
  476. rankToUsers;
  477. baseProvider;
  478. constructor(ranks, baseProvider) {
  479. this.ranks = ranks;
  480. this.maxRank = getMaxRank(ranks);
  481. this.rankToUsers = getRankToUsers(ranks);
  482. this.baseProvider = baseProvider;
  483. }
  484. availableFor(userScreenName) {
  485. return this.ranks.has(userScreenName);
  486. }
  487. getPerformance(userScreenName) {
  488. if (!this.availableFor(userScreenName)) {
  489. throw new Error(`User ${userScreenName} not found`);
  490. }
  491. if (this.performanceCache.has(userScreenName))
  492. return this.performanceCache.get(userScreenName);
  493. let rank = this.ranks.get(userScreenName);
  494. while (rank <= this.maxRank) {
  495. const perf = this.getPerformanceIfAvailable(rank);
  496. if (perf !== null) {
  497. return perf;
  498. }
  499. rank++;
  500. }
  501. this.performanceCache.set(userScreenName, -Infinity);
  502. return -Infinity;
  503. }
  504. performanceCache = new Map();
  505. getPerformances() {
  506. let currentPerformance = -Infinity;
  507. const res = new Map();
  508. for (let rank = this.maxRank; rank >= 0; rank--) {
  509. const users = this.rankToUsers.get(rank);
  510. if (users === undefined)
  511. continue;
  512. const perf = this.getPerformanceIfAvailable(rank);
  513. if (perf !== null)
  514. currentPerformance = perf;
  515. for (const userScreenName of users) {
  516. res.set(userScreenName, currentPerformance);
  517. }
  518. }
  519. this.performanceCache = res;
  520. return res;
  521. }
  522. cacheForRank = new Map();
  523. getPerformanceIfAvailable(rank) {
  524. if (!this.rankToUsers.has(rank))
  525. return null;
  526. if (this.cacheForRank.has(rank))
  527. return this.cacheForRank.get(rank);
  528. for (const userScreenName of this.rankToUsers.get(rank)) {
  529. if (!this.baseProvider.availableFor(userScreenName))
  530. continue;
  531. const perf = this.baseProvider.getPerformance(userScreenName);
  532. this.cacheForRank.set(rank, perf);
  533. return perf;
  534. }
  535. return null;
  536. }
  537. }
  538.  
  539. function normalizeRank(ranks) {
  540. const rankValues = [...new Set(ranks.values()).values()];
  541. const rankToUsers = new Map();
  542. for (const [userScreenName, rank] of ranks) {
  543. if (!rankToUsers.has(rank))
  544. rankToUsers.set(rank, []);
  545. rankToUsers.get(rank).push(userScreenName);
  546. }
  547. rankValues.sort((a, b) => a - b);
  548. const res = new Map();
  549. let currentRank = 1;
  550. for (const rank of rankValues) {
  551. const users = rankToUsers.get(rank);
  552. const averageRank = currentRank + (users.length - 1) / 2;
  553. for (const userScreenName of users) {
  554. res.set(userScreenName, averageRank);
  555. }
  556. currentRank += users.length;
  557. }
  558. return res;
  559. }
  560.  
  561. //Copyright © 2017 koba-e964.
  562. //from : https://github.com/koba-e964/atcoder-rating-estimator
  563. const finf = bigf(400);
  564. function bigf(n) {
  565. let pow1 = 1;
  566. let pow2 = 1;
  567. let numerator = 0;
  568. let denominator = 0;
  569. for (let i = 0; i < n; ++i) {
  570. pow1 *= 0.81;
  571. pow2 *= 0.9;
  572. numerator += pow1;
  573. denominator += pow2;
  574. }
  575. return Math.sqrt(numerator) / denominator;
  576. }
  577. function f(n) {
  578. return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0;
  579. }
  580. /**
  581. * calculate unpositivized rating from performance history
  582. * @param {Number[]} [history] performance history with ascending order
  583. * @returns {Number} unpositivized rating
  584. */
  585. function calcAlgRatingFromHistory(history) {
  586. const n = history.length;
  587. let pow = 1;
  588. let numerator = 0.0;
  589. let denominator = 0.0;
  590. for (let i = n - 1; i >= 0; i--) {
  591. pow *= 0.9;
  592. numerator += Math.pow(2, history[i] / 800.0) * pow;
  593. denominator += pow;
  594. }
  595. return Math.log2(numerator / denominator) * 800.0 - f(n);
  596. }
  597. /**
  598. * calculate unpositivized rating from last state
  599. * @param {Number} [last] last unpositivized rating
  600. * @param {Number} [perf] performance
  601. * @param {Number} [ratedMatches] count of participated rated contest
  602. * @returns {number} estimated unpositivized rating
  603. */
  604. function calcAlgRatingFromLast(last, perf, ratedMatches) {
  605. if (ratedMatches === 0)
  606. return perf - 1200;
  607. last += f(ratedMatches);
  608. const weight = 9 - 9 * 0.9 ** ratedMatches;
  609. const numerator = weight * 2 ** (last / 800.0) + 2 ** (perf / 800.0);
  610. const denominator = 1 + weight;
  611. return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
  612. }
  613. /**
  614. * calculate the performance required to reach a target rate
  615. * @param {Number} [targetRating] targeted unpositivized rating
  616. * @param {Number[]} [history] performance history with ascending order
  617. * @returns {number} performance
  618. */
  619. function calcRequiredPerformance(targetRating, history) {
  620. let valid = 10000.0;
  621. let invalid = -10000.0;
  622. for (let i = 0; i < 100; ++i) {
  623. const mid = (invalid + valid) / 2;
  624. const rating = Math.round(calcAlgRatingFromHistory(history.concat([mid])));
  625. if (targetRating <= rating)
  626. valid = mid;
  627. else
  628. invalid = mid;
  629. }
  630. return valid;
  631. }
  632. /**
  633. * Gets the weight used in the heuristic rating calculation
  634. * based on its start and end dates
  635. * @param {Date} startAt - The start date of the contest.
  636. * @param {Date} endAt - The end date of the contest.
  637. * @returns {number} The weight of the contest.
  638. */
  639. function getWeight(startAt, endAt) {
  640. const isShortContest = endAt.getTime() - startAt.getTime() < 24 * 60 * 60 * 1000;
  641. if (endAt < new Date("2025-01-01T00:00:00+09:00")) {
  642. return 1;
  643. }
  644. return isShortContest ? 0.5 : 1;
  645. }
  646. /**
  647. * calculate unpositivized rating from performance history
  648. * @param {RatingMaterial[]} [history] performance histories
  649. * @returns {Number} unpositivized rating
  650. */
  651. function calcHeuristicRatingFromHistory(history) {
  652. const S = 724.4744301;
  653. const R = 0.8271973364;
  654. const qs = [];
  655. for (const material of history) {
  656. const adjustedPerformance = material.Performance + 150 - 100 * material.DaysFromLatestContest / 365;
  657. for (let i = 1; i <= 100; i++) {
  658. qs.push({ q: adjustedPerformance - S * Math.log(i), weight: material.Weight });
  659. }
  660. }
  661. qs.sort((a, b) => b.q - a.q);
  662. let r = 0.0;
  663. let s = 0.0;
  664. for (const { q, weight } of qs) {
  665. s += weight;
  666. r += q * (R ** (s - weight) - R ** s);
  667. }
  668. return r;
  669. }
  670. /**
  671. * (-inf, inf) -> (0, inf)
  672. * @param {Number} [rating] unpositivized rating
  673. * @returns {number} positivized rating
  674. */
  675. function positivizeRating(rating) {
  676. if (rating >= 400.0) {
  677. return rating;
  678. }
  679. return 400.0 * Math.exp((rating - 400.0) / 400.0);
  680. }
  681. /**
  682. * (0, inf) -> (-inf, inf)
  683. * @param {Number} [rating] positivized rating
  684. * @returns {number} unpositivized rating
  685. */
  686. function unpositivizeRating(rating) {
  687. if (rating >= 400.0) {
  688. return rating;
  689. }
  690. return 400.0 + 400.0 * Math.log(rating / 400.0);
  691. }
  692. const colorNames = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"];
  693. function getColor(rating) {
  694. const colorIndex = rating > 0 ? Math.min(Math.floor(rating / 400) + 1, 8) : 0;
  695. return colorNames[colorIndex];
  696. }
  697.  
  698. const PATH_PREFIX = "/contests/";
  699. function getContestScreenName() {
  700. const location = document.location.pathname;
  701. if (!location.startsWith(PATH_PREFIX)) {
  702. throw Error("not on the contest page");
  703. }
  704. return location.substring(PATH_PREFIX.length).split("/")[0];
  705. }
  706.  
  707. function hasOwnProperty(obj, key) {
  708. return Object.prototype.hasOwnProperty.call(obj, key);
  709. }
  710.  
  711. class StandingsLoadingView {
  712. loaded;
  713. element;
  714. hooks;
  715. constructor(element) {
  716. this.loaded = false;
  717. this.element = element;
  718. this.hooks = [];
  719. this.initHandler();
  720. }
  721. onLoad(hook) {
  722. this.hooks.push(hook);
  723. }
  724. initHandler() {
  725. new MutationObserver(() => {
  726. if (!this.loaded) {
  727. if (document.getElementById("standings-tbody") === null)
  728. return;
  729. this.loaded = true;
  730. this.hooks.forEach(f => f());
  731. }
  732. }).observe(this.element, { attributes: true });
  733. }
  734. static Get() {
  735. const loadingElem = document.querySelector("#vue-standings .loading-show");
  736. if (loadingElem === null) {
  737. throw new Error("loadingElem not found");
  738. }
  739. return new StandingsLoadingView(loadingElem);
  740. }
  741. }
  742.  
  743. function toSignedString (n) {
  744. return `${n >= 0 ? "+" : "-"}${Math.abs(n)}`;
  745. }
  746.  
  747. function addStyle(styleSheet) {
  748. const styleElem = document.createElement("style");
  749. styleElem.textContent = styleSheet;
  750. document.getElementsByTagName("head")[0].append(styleElem);
  751. }
  752.  
  753. function getSpan(innerElements, classList) {
  754. const span = document.createElement("span");
  755. span.append(...innerElements);
  756. span.classList.add(...classList);
  757. return span;
  758. }
  759.  
  760. function getRatingSpan(rate) {
  761. return getSpan([rate.toString()], ["bold", "user-" + getColor(rate)]);
  762. }
  763.  
  764. 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}";
  765.  
  766. addStyle(style);
  767. function getFadedSpan(innerElements) {
  768. return getSpan(innerElements, ["grey"]);
  769. }
  770. function getRatedRatingElem(result) {
  771. const elem = document.createElement("div");
  772. elem.append(getRatingSpan(result.oldRating), " → ", getRatingSpan(result.newRating), " ", getFadedSpan([`(${toSignedString(result.newRating - result.oldRating)})`]));
  773. return elem;
  774. }
  775. function getUnratedRatingElem(result) {
  776. const elem = document.createElement("div");
  777. elem.append(getRatingSpan(result.oldRating), " ", getFadedSpan(["(unrated)"]));
  778. return elem;
  779. }
  780. function getDefferedRatingElem(result) {
  781. const elem = document.createElement("div");
  782. elem.append(getRatingSpan(result.oldRating), " → ", getSpan(["???"], ["bold"]), document.createElement("br"), getFadedSpan([`(${getTranslation("standings_click_to_compute_label")})`]));
  783. async function listener() {
  784. elem.removeEventListener("click", listener);
  785. elem.replaceChildren(getFadedSpan(["loading..."]));
  786. let newRating;
  787. try {
  788. newRating = await result.newRatingCalculator();
  789. }
  790. catch (e) {
  791. elem.append(getSpan(["error on load"], []), document.createElement("br"), getSpan(["(hover to see details)"], ["grey", "small"]), getSpan([e.toString()], ["my-tooltiptext"]));
  792. elem.classList.add("my-tooltip");
  793. return;
  794. }
  795. const newElem = getRatedRatingElem({ type: "rated", performance: result.performance, oldRating: result.oldRating, newRating: newRating });
  796. elem.replaceChildren(newElem);
  797. }
  798. elem.addEventListener("click", listener);
  799. return elem;
  800. }
  801. function getPerfOnlyRatingElem(result) {
  802. const elem = document.createElement("div");
  803. elem.append(getFadedSpan([`(${getTranslation("standings_not_provided_label")})`]));
  804. return elem;
  805. }
  806. function getErrorRatingElem(result) {
  807. const elem = document.createElement("div");
  808. elem.append(getSpan(["error on load"], []), document.createElement("br"), getSpan(["(hover to see details)"], ["grey", "small"]), getSpan([result.message], ["my-tooltiptext"]));
  809. elem.classList.add("my-tooltip");
  810. return elem;
  811. }
  812. function getRatingElem(result) {
  813. if (result.type == "rated")
  814. return getRatedRatingElem(result);
  815. if (result.type == "unrated")
  816. return getUnratedRatingElem(result);
  817. if (result.type == "deffered")
  818. return getDefferedRatingElem(result);
  819. if (result.type == "perfonly")
  820. return getPerfOnlyRatingElem();
  821. if (result.type == "error")
  822. return getErrorRatingElem(result);
  823. throw new Error("unreachable");
  824. }
  825. function getPerfElem(result) {
  826. if (result.type == "error")
  827. return getSpan(["-"], []);
  828. return getRatingSpan(result.performance);
  829. }
  830. 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>`;
  831. function modifyHeader(header) {
  832. header.insertAdjacentHTML("beforeend", headerHtml);
  833. }
  834. function isFooter(row) {
  835. return row.firstElementChild?.classList.contains("colspan");
  836. }
  837. async function modifyStandingsRow(row, results) {
  838. const rankText = row.children[0].textContent;
  839. const usernameSpan = row.querySelector(".standings-username .username span");
  840. let userScreenName = usernameSpan?.textContent ?? null;
  841. // unratedかつ順位が未表示ならば参加者でない、というヒューリスティック(お気に入り順位表でのエラー解消用)
  842. if (usernameSpan?.className === "user-unrated" && rankText === "-") {
  843. userScreenName = null;
  844. }
  845. // TODO: この辺のロジックがここにあるの嫌だね……
  846. if (userScreenName !== null && row.querySelector(".standings-username .username img[src='//img.atcoder.jp/assets/icon/ghost.svg']")) {
  847. userScreenName = `ghost:${userScreenName}`;
  848. }
  849. if (userScreenName !== null && row.classList.contains("info") && 3 <= row.children.length && row.children[2].textContent == "-") {
  850. // 延長線順位表用
  851. userScreenName = `extended:${userScreenName}`;
  852. }
  853. const perfCell = document.createElement("td");
  854. perfCell.classList.add("ac-predictor-standings-elem", "standings-result");
  855. const ratingCell = document.createElement("td");
  856. ratingCell.classList.add("ac-predictor-standings-elem", "standings-result");
  857. if (userScreenName === null) {
  858. perfCell.append("-");
  859. ratingCell.append("-");
  860. }
  861. else {
  862. const result = await results(userScreenName);
  863. perfCell.append(getPerfElem(result));
  864. ratingCell.append(getRatingElem(result));
  865. }
  866. row.insertAdjacentElement("beforeend", perfCell);
  867. row.insertAdjacentElement("beforeend", ratingCell);
  868. }
  869. function modifyFooter(footer) {
  870. footer.insertAdjacentHTML("beforeend", '<td class="ac-predictor-standings-elem" colspan="2">-</td>');
  871. }
  872. class StandingsTableView {
  873. element;
  874. provider;
  875. refreshHooks = [];
  876. constructor(element, resultDataProvider) {
  877. this.element = element;
  878. this.provider = resultDataProvider;
  879. this.initHandler();
  880. }
  881. onRefreshed(hook) {
  882. this.refreshHooks.push(hook);
  883. }
  884. update() {
  885. this.removeOldElement();
  886. const header = this.element.querySelector("thead tr");
  887. if (!header)
  888. console.warn("header element not found", this.element);
  889. else
  890. modifyHeader(header);
  891. this.element.querySelectorAll("tbody tr").forEach((row) => {
  892. if (isFooter(row))
  893. modifyFooter(row);
  894. else
  895. modifyStandingsRow(row, this.provider);
  896. });
  897. }
  898. removeOldElement() {
  899. this.element.querySelectorAll(".ac-predictor-standings-elem").forEach((elem) => elem.remove());
  900. }
  901. initHandler() {
  902. new MutationObserver(() => this.update()).observe(this.element.tBodies[0], {
  903. childList: true,
  904. });
  905. const statsRow = this.element.querySelector(".standings-statistics");
  906. if (statsRow === null) {
  907. throw new Error("statsRow not found");
  908. }
  909. const acElems = statsRow.querySelectorAll(".standings-ac");
  910. const refreshObserver = new MutationObserver((records) => {
  911. if (isDebugMode())
  912. console.log("fire refreshHooks", records);
  913. this.refreshHooks.forEach(f => f());
  914. });
  915. acElems.forEach(elem => refreshObserver.observe(elem, { childList: true }));
  916. }
  917. static Get(resultDataProvider) {
  918. const tableElem = document.querySelector(".table-responsive table");
  919. return new StandingsTableView(tableElem, resultDataProvider);
  920. }
  921. }
  922.  
  923. class ExtendedStandingsPageController {
  924. contestDetails;
  925. performanceProvider;
  926. standingsTableView;
  927. async register() {
  928. const loading = StandingsLoadingView.Get();
  929. loading.onLoad(() => this.initialize());
  930. }
  931. async initialize() {
  932. const contestScreenName = getContestScreenName();
  933. const contestDetailsList = await getContestDetails();
  934. const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
  935. if (contestDetails === undefined) {
  936. throw new Error("contest details not found");
  937. }
  938. this.contestDetails = contestDetails;
  939. this.standingsTableView = StandingsTableView.Get(async (userScreenName) => {
  940. if (!this.performanceProvider)
  941. return { "type": "error", "message": "performanceProvider missing" };
  942. if (!this.performanceProvider.availableFor(userScreenName))
  943. return { "type": "error", "message": `performance not available for ${userScreenName}` };
  944. const originalPerformance = this.performanceProvider.getPerformance(userScreenName);
  945. const positivizedPerformance = Math.round(positivizeRating(originalPerformance));
  946. return { type: "perfonly", performance: positivizedPerformance };
  947. });
  948. this.standingsTableView.onRefreshed(async () => {
  949. await this.updateData();
  950. this.standingsTableView.update();
  951. });
  952. await this.updateData();
  953. this.standingsTableView.update();
  954. }
  955. async updateData() {
  956. if (!this.contestDetails)
  957. throw new Error("contestDetails missing");
  958. const extendedStandings = await getExtendedStandings(this.contestDetails.contestScreenName);
  959. const aperfsObj = await getAPerfs(this.contestDetails.contestScreenName);
  960. const defaultAPerf = this.contestDetails.defaultAPerf;
  961. const normalizedRanks = normalizeRank(extendedStandings.toRanks(true, this.contestDetails.contestType));
  962. const aperfsList = extendedStandings.toRatedUsers(this.contestDetails.contestType).map(userScreenName => hasOwnProperty(aperfsObj, userScreenName) ? aperfsObj[userScreenName] : defaultAPerf);
  963. const basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap);
  964. const ranks = extendedStandings.toRanks();
  965. this.performanceProvider = new InterpolatePerformanceProvider(ranks, basePerformanceProvider);
  966. }
  967. }
  968.  
  969. class HistoriesWrapper {
  970. data;
  971. constructor(data) {
  972. this.data = data;
  973. }
  974. toRatingMaterials(latestContestDate, contestDurationSecondProvider) {
  975. const toUtcDate = (date) => Math.floor(date.getTime() / (24 * 60 * 60 * 1000));
  976. const results = [];
  977. for (const history of this.data) {
  978. if (!history.IsRated)
  979. continue;
  980. const endTime = new Date(history.EndTime);
  981. const startTime = new Date(endTime.getTime() - contestDurationSecondProvider(history.ContestScreenName) * 1000);
  982. results.push({
  983. Performance: history.Performance,
  984. Weight: getWeight(startTime, endTime),
  985. DaysFromLatestContest: toUtcDate(latestContestDate) - toUtcDate(endTime),
  986. });
  987. }
  988. return results;
  989. }
  990. }
  991. const HISTORY_CACHE_DURATION = 60 * 60 * 1000;
  992. const cache$3 = new Cache(HISTORY_CACHE_DURATION);
  993. async function getHistory(userScreenName, contestType = "algorithm") {
  994. const key = `${userScreenName}:${contestType}`;
  995. if (!cache$3.has(key)) {
  996. const result = await fetch(`https://atcoder.jp/users/${userScreenName}/history/json?contestType=${contestType}`);
  997. if (!result.ok) {
  998. throw new Error(`Failed to fetch history: ${result.status}`);
  999. }
  1000. cache$3.set(key, await result.json());
  1001. }
  1002. return new HistoriesWrapper(cache$3.get(key));
  1003. }
  1004.  
  1005. // @ts-nocheck
  1006. 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>";
  1007. class EstimatorModel {
  1008. inputDesc;
  1009. resultDesc;
  1010. perfHistory;
  1011. constructor(inputValue, perfHistory) {
  1012. this.inputDesc = "";
  1013. this.resultDesc = "";
  1014. this.perfHistory = perfHistory;
  1015. this.updateInput(inputValue);
  1016. }
  1017. inputValue;
  1018. resultValue;
  1019. updateInput(value) {
  1020. this.inputValue = value;
  1021. this.resultValue = this.calcResult(value);
  1022. }
  1023. toggle() {
  1024. return null;
  1025. }
  1026. calcResult(input) {
  1027. return input;
  1028. }
  1029. }
  1030. class CalcRatingModel extends EstimatorModel {
  1031. constructor(inputValue, perfHistory) {
  1032. super(inputValue, perfHistory);
  1033. this.inputDesc = "パフォーマンス";
  1034. this.resultDesc = "到達レーティング";
  1035. }
  1036. // @ts-ignore
  1037. toggle() {
  1038. return new CalcPerfModel(this.resultValue, this.perfHistory);
  1039. }
  1040. calcResult(input) {
  1041. return positivizeRating(calcAlgRatingFromHistory(this.perfHistory.concat([input])));
  1042. }
  1043. }
  1044. class CalcPerfModel extends EstimatorModel {
  1045. constructor(inputValue, perfHistory) {
  1046. super(inputValue, perfHistory);
  1047. this.inputDesc = "目標レーティング";
  1048. this.resultDesc = "必要パフォーマンス";
  1049. }
  1050. // @ts-ignore
  1051. toggle() {
  1052. return new CalcRatingModel(this.resultValue, this.perfHistory);
  1053. }
  1054. calcResult(input) {
  1055. return calcRequiredPerformance(unpositivizeRating(input), this.perfHistory);
  1056. }
  1057. }
  1058. function GetEmbedTweetLink(content, url) {
  1059. return `https://twitter.com/share?text=${encodeURI(content)}&url=${encodeURI(url)}`;
  1060. }
  1061. function getLS(key) {
  1062. const val = localStorage.getItem(key);
  1063. return (val ? JSON.parse(val) : val);
  1064. }
  1065. function setLS(key, val) {
  1066. try {
  1067. localStorage.setItem(key, JSON.stringify(val));
  1068. }
  1069. catch (error) {
  1070. console.log(error);
  1071. }
  1072. }
  1073. const models = [CalcPerfModel, CalcRatingModel];
  1074. function GetModelFromStateCode(state, value, history) {
  1075. let model = models.find((model) => model.name === state);
  1076. if (!model)
  1077. model = CalcPerfModel;
  1078. return new model(value, history);
  1079. }
  1080. function getPerformanceHistories(history) {
  1081. const onlyRated = history.filter((x) => x.IsRated);
  1082. onlyRated.sort((a, b) => {
  1083. return new Date(a.EndTime).getTime() - new Date(b.EndTime).getTime();
  1084. });
  1085. return onlyRated.map((x) => x.Performance);
  1086. }
  1087. function roundValue(value, numDigits) {
  1088. return Math.round(value * Math.pow(10, numDigits)) / Math.pow(10, numDigits);
  1089. }
  1090. class EstimatorElement {
  1091. id;
  1092. title;
  1093. document;
  1094. constructor() {
  1095. this.id = "estimator";
  1096. this.title = "Estimator";
  1097. this.document = dom$1;
  1098. }
  1099. async afterOpen() {
  1100. const estimatorInputSelector = document.getElementById("estimator-input");
  1101. const estimatorResultSelector = document.getElementById("estimator-res");
  1102. let model = GetModelFromStateCode(getLS("sidemenu_estimator_state"), getLS("sidemenu_estimator_value"), getPerformanceHistories((await getHistory(userScreenName)).data));
  1103. updateView();
  1104. document.getElementById("estimator-toggle").addEventListener("click", () => {
  1105. model = model.toggle();
  1106. updateLocalStorage();
  1107. updateView();
  1108. });
  1109. estimatorInputSelector.addEventListener("keyup", () => {
  1110. updateModel();
  1111. updateLocalStorage();
  1112. updateView();
  1113. });
  1114. /** modelをinputの値に応じて更新 */
  1115. function updateModel() {
  1116. const inputNumber = estimatorInputSelector.valueAsNumber;
  1117. if (!isFinite(inputNumber))
  1118. return;
  1119. model.updateInput(inputNumber);
  1120. }
  1121. /** modelの状態をLSに保存 */
  1122. function updateLocalStorage() {
  1123. setLS("sidemenu_estimator_value", model.inputValue);
  1124. setLS("sidemenu_estimator_state", model.constructor.name);
  1125. }
  1126. /** modelを元にviewを更新 */
  1127. function updateView() {
  1128. const roundedInput = roundValue(model.inputValue, 2);
  1129. const roundedResult = roundValue(model.resultValue, 2);
  1130. document.getElementById("estimator-input-desc").innerText = model.inputDesc;
  1131. document.getElementById("estimator-res-desc").innerText = model.resultDesc;
  1132. estimatorInputSelector.value = String(roundedInput);
  1133. estimatorResultSelector.value = String(roundedResult);
  1134. const tweetStr = `AtCoderのハンドルネーム: ${userScreenName}\n${model.inputDesc}: ${roundedInput}\n${model.resultDesc}: ${roundedResult}\n`;
  1135. document.getElementById("estimator-tweet").href = GetEmbedTweetLink(tweetStr, "https://greasyfork.org/ja/scripts/369954-ac-predictor");
  1136. }
  1137. }
  1138. ;
  1139. GetHTML() {
  1140. return `<div class="menu-wrapper">
  1141. <div class="menu-header">
  1142. <h4 class="sidemenu-txt">${this.title}<span class="glyphicon glyphicon-menu-up" style="float: right"></span></h4>
  1143. </div>
  1144. <div class="menu-box"><div class="menu-content" id="${this.id}">${this.document}</div></div>
  1145. </div>`;
  1146. }
  1147. }
  1148. const estimator = new EstimatorElement();
  1149. 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>";
  1150. class SideMenu {
  1151. pendingElements;
  1152. constructor() {
  1153. this.pendingElements = [];
  1154. this.Generate();
  1155. }
  1156. Generate() {
  1157. document.getElementById("main-div").insertAdjacentHTML("afterbegin", sidemenuHtml);
  1158. resizeSidemenuHeight();
  1159. const key = document.getElementById("sidemenu-key");
  1160. const wrap = document.getElementById("menu-wrap");
  1161. key.addEventListener("click", () => {
  1162. this.pendingElements.forEach((elem) => {
  1163. elem.afterOpen();
  1164. });
  1165. this.pendingElements.length = 0;
  1166. key.classList.toggle("glyphicon-menu-left");
  1167. key.classList.toggle("glyphicon-menu-right");
  1168. wrap.classList.toggle("sidemenu-active");
  1169. });
  1170. window.addEventListener("onresize", resizeSidemenuHeight);
  1171. document.getElementById("sidemenu").addEventListener("click", (event) => {
  1172. const target = event.target;
  1173. const header = target.closest(".menu-header");
  1174. if (!header)
  1175. return;
  1176. const box = target.closest(".menu-wrapper").querySelector(".menu-box");
  1177. box.classList.toggle("menu-box-collapse");
  1178. const arrow = target.querySelector(".glyphicon");
  1179. arrow.classList.toggle("glyphicon-menu-down");
  1180. arrow.classList.toggle("glyphicon-menu-up");
  1181. });
  1182. function resizeSidemenuHeight() {
  1183. document.getElementById("sidemenu").style.height = `${window.innerHeight}px`;
  1184. }
  1185. }
  1186. addElement(element) {
  1187. const sidemenu = document.getElementById("sidemenu");
  1188. sidemenu.insertAdjacentHTML("afterbegin", element.GetHTML());
  1189. const content = sidemenu.querySelector(".menu-content");
  1190. content.parentElement.style.height = `${content.offsetHeight}px`;
  1191. // element.afterAppend();
  1192. this.pendingElements.push(element);
  1193. }
  1194. }
  1195. function add() {
  1196. const sidemenu = new SideMenu();
  1197. const elements = [estimator];
  1198. for (let i = elements.length - 1; i >= 0; i--) {
  1199. sidemenu.addElement(elements[i]);
  1200. }
  1201. }
  1202.  
  1203. class ResultsWrapper {
  1204. data;
  1205. constructor(data) {
  1206. this.data = data;
  1207. }
  1208. toPerformanceMaps() {
  1209. const res = new Map();
  1210. for (const result of this.data) {
  1211. if (!result.IsRated)
  1212. continue;
  1213. res.set(result.UserScreenName, result.Performance);
  1214. }
  1215. return res;
  1216. }
  1217. toIsRatedMaps() {
  1218. const res = new Map();
  1219. for (const result of this.data) {
  1220. res.set(result.UserScreenName, result.IsRated);
  1221. }
  1222. return res;
  1223. }
  1224. toOldRatingMaps() {
  1225. const res = new Map();
  1226. for (const result of this.data) {
  1227. res.set(result.UserScreenName, result.OldRating);
  1228. }
  1229. return res;
  1230. }
  1231. toNewRatingMaps() {
  1232. const res = new Map();
  1233. for (const result of this.data) {
  1234. res.set(result.UserScreenName, result.NewRating);
  1235. }
  1236. return res;
  1237. }
  1238. }
  1239. const RESULTS_CACHE_DURATION = 10 * 1000;
  1240. const cache$2 = new Cache(RESULTS_CACHE_DURATION);
  1241. async function getResults(contestScreenName) {
  1242. if (!cache$2.has(contestScreenName)) {
  1243. const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/results/json`);
  1244. if (!result.ok) {
  1245. throw new Error(`Failed to fetch results: ${result.status}`);
  1246. }
  1247. cache$2.set(contestScreenName, await result.json());
  1248. }
  1249. return new ResultsWrapper(cache$2.get(contestScreenName));
  1250. }
  1251. addHandler((content, path) => {
  1252. const match = path.match(/^\/contests\/([^/]*)\/results\/json$/);
  1253. if (!match)
  1254. return;
  1255. const contestScreenName = match[1];
  1256. cache$2.set(contestScreenName, JSON.parse(content));
  1257. });
  1258.  
  1259. let StandingsWrapper$1 = class StandingsWrapper {
  1260. data;
  1261. constructor(data) {
  1262. this.data = data;
  1263. }
  1264. toRanks(onlyRated = false, contestType = "algorithm") {
  1265. const res = new Map();
  1266. for (const data of this.data.StandingsData) {
  1267. if (onlyRated && !this.isRated(data, contestType))
  1268. continue;
  1269. res.set(data.UserScreenName, data.Rank);
  1270. }
  1271. return res;
  1272. }
  1273. toRatedUsers(contestType) {
  1274. const res = [];
  1275. for (const data of this.data.StandingsData) {
  1276. if (this.isRated(data, contestType)) {
  1277. res.push(data.UserScreenName);
  1278. }
  1279. }
  1280. return res;
  1281. }
  1282. toIsRatedMaps(contestType) {
  1283. const res = new Map();
  1284. for (const data of this.data.StandingsData) {
  1285. res.set(data.UserScreenName, this.isRated(data, contestType));
  1286. }
  1287. return res;
  1288. }
  1289. toOldRatingMaps(unpositivize = false) {
  1290. const res = new Map();
  1291. for (const data of this.data.StandingsData) {
  1292. const rating = this.data.Fixed ? data.OldRating : data.Rating;
  1293. res.set(data.UserScreenName, unpositivize ? unpositivizeRating(rating) : rating);
  1294. }
  1295. return res;
  1296. }
  1297. toCompetitionMaps() {
  1298. const res = new Map();
  1299. for (const data of this.data.StandingsData) {
  1300. res.set(data.UserScreenName, data.Competitions);
  1301. }
  1302. return res;
  1303. }
  1304. toScores() {
  1305. const res = new Map();
  1306. for (const data of this.data.StandingsData) {
  1307. res.set(data.UserScreenName, { score: data.TotalResult.Score, penalty: data.TotalResult.Elapsed });
  1308. }
  1309. return res;
  1310. }
  1311. isRated(data, contestType = "algorithm") {
  1312. if (contestType === "algorithm") {
  1313. return data.IsRated;
  1314. }
  1315. if (contestType === "heuristic") {
  1316. return data.IsRated && data.TotalResult.Count !== 0;
  1317. }
  1318. throw new Error("unreachable");
  1319. }
  1320. };
  1321. const STANDINGS_CACHE_DURATION$1 = 10 * 1000;
  1322. const cache$1 = new Cache(STANDINGS_CACHE_DURATION$1);
  1323. async function getStandings(contestScreenName) {
  1324. if (!cache$1.has(contestScreenName)) {
  1325. const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/json`);
  1326. if (!result.ok) {
  1327. throw new Error(`Failed to fetch standings: ${result.status}`);
  1328. }
  1329. cache$1.set(contestScreenName, await result.json());
  1330. }
  1331. return new StandingsWrapper$1(cache$1.get(contestScreenName));
  1332. }
  1333. addHandler((content, path) => {
  1334. const match = path.match(/^\/contests\/([^/]*)\/standings\/json$/);
  1335. if (!match)
  1336. return;
  1337. const contestScreenName = match[1];
  1338. cache$1.set(contestScreenName, JSON.parse(content));
  1339. });
  1340.  
  1341. class FixedPerformanceProvider {
  1342. result;
  1343. constructor(result) {
  1344. this.result = result;
  1345. }
  1346. availableFor(userScreenName) {
  1347. return this.result.has(userScreenName);
  1348. }
  1349. getPerformance(userScreenName) {
  1350. if (!this.availableFor(userScreenName)) {
  1351. throw new Error(`User ${userScreenName} not found`);
  1352. }
  1353. return this.result.get(userScreenName);
  1354. }
  1355. getPerformances() {
  1356. return this.result;
  1357. }
  1358. }
  1359.  
  1360. class IncrementalAlgRatingProvider {
  1361. unpositivizedRatingMap;
  1362. competitionsMap;
  1363. constructor(unpositivizedRatingMap, competitionsMap) {
  1364. this.unpositivizedRatingMap = unpositivizedRatingMap;
  1365. this.competitionsMap = competitionsMap;
  1366. }
  1367. availableFor(userScreenName) {
  1368. return this.unpositivizedRatingMap.has(userScreenName);
  1369. }
  1370. async getRating(userScreenName, newPerformance) {
  1371. if (!this.availableFor(userScreenName)) {
  1372. throw new Error(`rating not available for ${userScreenName}`);
  1373. }
  1374. const rating = this.unpositivizedRatingMap.get(userScreenName);
  1375. const competitions = this.competitionsMap.get(userScreenName);
  1376. return Math.round(positivizeRating(calcAlgRatingFromLast(rating, newPerformance, competitions)));
  1377. }
  1378. }
  1379.  
  1380. class ConstRatingProvider {
  1381. ratings;
  1382. constructor(ratings) {
  1383. this.ratings = ratings;
  1384. }
  1385. availableFor(userScreenName) {
  1386. return this.ratings.has(userScreenName);
  1387. }
  1388. async getRating(userScreenName, newPerformance) {
  1389. if (!this.availableFor(userScreenName)) {
  1390. throw new Error(`rating not available for ${userScreenName}`);
  1391. }
  1392. return this.ratings.get(userScreenName);
  1393. }
  1394. }
  1395.  
  1396. class FromHistoryHeuristicRatingProvider {
  1397. newWeight;
  1398. performancesProvider;
  1399. constructor(newWeight, performancesProvider) {
  1400. this.newWeight = newWeight;
  1401. this.performancesProvider = performancesProvider;
  1402. }
  1403. availableFor(userScreenName) {
  1404. return true;
  1405. }
  1406. async getRating(userScreenName, newPerformance) {
  1407. const performances = await this.performancesProvider(userScreenName);
  1408. performances.push({
  1409. Performance: newPerformance,
  1410. Weight: this.newWeight,
  1411. DaysFromLatestContest: 0,
  1412. });
  1413. return Math.round(positivizeRating(calcHeuristicRatingFromHistory(performances)));
  1414. }
  1415. }
  1416.  
  1417. class StandingsPageController {
  1418. contestDetails;
  1419. contestDetailsMap = new Map();
  1420. performanceProvider;
  1421. ratingProvider;
  1422. oldRatings = new Map();
  1423. isRatedMaps = new Map();
  1424. standingsTableView;
  1425. async register() {
  1426. const loading = StandingsLoadingView.Get();
  1427. loading.onLoad(() => this.initialize());
  1428. }
  1429. async initialize() {
  1430. const contestScreenName = getContestScreenName();
  1431. const contestDetailsList = await getContestDetails();
  1432. const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
  1433. if (contestDetails === undefined) {
  1434. throw new Error("contest details not found");
  1435. }
  1436. this.contestDetails = contestDetails;
  1437. this.contestDetailsMap = new Map(contestDetailsList.map(details => [details.contestScreenName, details]));
  1438. if (this.contestDetails.beforeContest(new Date()))
  1439. return;
  1440. if (getConfig("hideDuringContest") && this.contestDetails.duringContest(new Date()))
  1441. return;
  1442. const standings = await getStandings(this.contestDetails.contestScreenName);
  1443. if (getConfig("hideUntilFixed") && !standings.data.Fixed)
  1444. return;
  1445. this.standingsTableView = StandingsTableView.Get(async (userScreenName) => {
  1446. if (!this.ratingProvider)
  1447. return { "type": "error", "message": "ratingProvider missing" };
  1448. if (!this.performanceProvider)
  1449. return { "type": "error", "message": "performanceProvider missing" };
  1450. if (!this.isRatedMaps)
  1451. return { "type": "error", "message": "isRatedMapping missing" };
  1452. if (!this.oldRatings)
  1453. return { "type": "error", "message": "oldRatings missing" };
  1454. if (!this.oldRatings.has(userScreenName))
  1455. return { "type": "error", "message": `oldRating not found for ${userScreenName}` };
  1456. const oldRating = this.oldRatings.get(userScreenName);
  1457. if (!this.performanceProvider.availableFor(userScreenName))
  1458. return { "type": "error", "message": `performance not available for ${userScreenName}` };
  1459. const originalPerformance = this.performanceProvider.getPerformance(userScreenName);
  1460. const positivizedPerformance = Math.round(positivizeRating(originalPerformance));
  1461. if (this.isRatedMaps.get(userScreenName)) {
  1462. if (!this.ratingProvider.provider.availableFor(userScreenName))
  1463. return { "type": "error", "message": `rating not available for ${userScreenName}` };
  1464. if (this.ratingProvider.lazy) {
  1465. const newRatingCalculator = () => this.ratingProvider.provider.getRating(userScreenName, originalPerformance);
  1466. return { type: "deffered", oldRating, performance: positivizedPerformance, newRatingCalculator };
  1467. }
  1468. else {
  1469. const newRating = await this.ratingProvider.provider.getRating(userScreenName, originalPerformance);
  1470. return { type: "rated", oldRating, performance: positivizedPerformance, newRating };
  1471. }
  1472. }
  1473. else {
  1474. return { type: "unrated", oldRating, performance: positivizedPerformance };
  1475. }
  1476. });
  1477. this.standingsTableView.onRefreshed(async () => {
  1478. await this.updateData();
  1479. this.standingsTableView.update();
  1480. });
  1481. await this.updateData();
  1482. this.standingsTableView.update();
  1483. }
  1484. async updateData() {
  1485. if (!this.contestDetails)
  1486. throw new Error("contestDetails missing");
  1487. if (isDebugMode())
  1488. console.log("data updating...");
  1489. const standings = await getStandings(this.contestDetails.contestScreenName);
  1490. let basePerformanceProvider = undefined;
  1491. if (standings.data.Fixed && getConfig("useResults")) {
  1492. try {
  1493. const results = await getResults(this.contestDetails.contestScreenName);
  1494. if (results.data.length === 0) {
  1495. throw new Error("results missing");
  1496. }
  1497. basePerformanceProvider = new FixedPerformanceProvider(results.toPerformanceMaps());
  1498. this.isRatedMaps = results.toIsRatedMaps();
  1499. this.oldRatings = results.toOldRatingMaps();
  1500. this.ratingProvider = { provider: new ConstRatingProvider(results.toNewRatingMaps()), lazy: false };
  1501. }
  1502. catch (e) {
  1503. console.warn("getResults failed", e);
  1504. }
  1505. }
  1506. if (basePerformanceProvider === undefined) {
  1507. const aperfsDict = await getAPerfs(this.contestDetails.contestScreenName);
  1508. const defaultAPerf = this.contestDetails.defaultAPerf;
  1509. const normalizedRanks = normalizeRank(standings.toRanks(true, this.contestDetails.contestType));
  1510. const aperfsList = standings.toRatedUsers(this.contestDetails.contestType).map(user => hasOwnProperty(aperfsDict, user) ? aperfsDict[user] : defaultAPerf);
  1511. basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap);
  1512. this.isRatedMaps = standings.toIsRatedMaps(this.contestDetails.contestType);
  1513. this.oldRatings = standings.toOldRatingMaps();
  1514. if (this.contestDetails.contestType == "algorithm") {
  1515. this.ratingProvider = { provider: new IncrementalAlgRatingProvider(standings.toOldRatingMaps(true), standings.toCompetitionMaps()), lazy: false };
  1516. }
  1517. else {
  1518. const startAt = this.contestDetails.startTime;
  1519. const endAt = this.contestDetails.endTime;
  1520. this.ratingProvider = {
  1521. provider: new FromHistoryHeuristicRatingProvider(getWeight(startAt, endAt), async (userScreenName) => {
  1522. const histories = await getHistory(userScreenName, "heuristic");
  1523. histories.data = histories.data.filter(x => new Date(x.EndTime) < endAt);
  1524. return histories.toRatingMaterials(endAt, x => {
  1525. const details = this.contestDetailsMap.get(x.split(".")[0]);
  1526. if (!details) {
  1527. console.warn(`contest details not found for ${x}`);
  1528. return 0;
  1529. }
  1530. return details.duration;
  1531. });
  1532. }),
  1533. lazy: true
  1534. };
  1535. }
  1536. }
  1537. this.performanceProvider = new InterpolatePerformanceProvider(standings.toRanks(), basePerformanceProvider);
  1538. if (isDebugMode())
  1539. console.log("data updated");
  1540. }
  1541. }
  1542.  
  1543. class StandingsWrapper {
  1544. data;
  1545. constructor(data) {
  1546. this.data = data;
  1547. }
  1548. toRanks(onlyRated = false, contestType = "algorithm") {
  1549. const res = new Map();
  1550. for (const data of this.data.StandingsData) {
  1551. if (onlyRated && !this.isRated(data, contestType))
  1552. continue;
  1553. const userScreenName = data.Additional["standings.virtualElapsed"] === -2 ? `ghost:${data.UserScreenName}` : data.UserScreenName;
  1554. res.set(userScreenName, data.Rank);
  1555. }
  1556. return res;
  1557. }
  1558. toRatedUsers(contestType) {
  1559. const res = [];
  1560. for (const data of this.data.StandingsData) {
  1561. if (this.isRated(data, contestType)) {
  1562. res.push(data.UserScreenName);
  1563. }
  1564. }
  1565. return res;
  1566. }
  1567. toScores() {
  1568. const res = new Map();
  1569. for (const data of this.data.StandingsData) {
  1570. const userScreenName = data.Additional["standings.virtualElapsed"] === -2 ? `ghost:${data.UserScreenName}` : data.UserScreenName;
  1571. res.set(userScreenName, { score: data.TotalResult.Score, penalty: data.TotalResult.Elapsed });
  1572. }
  1573. return res;
  1574. }
  1575. isRated(data, contestType) {
  1576. if (contestType === "algorithm") {
  1577. return data.IsRated && data.Additional["standings.virtualElapsed"] === -2;
  1578. }
  1579. else {
  1580. return data.IsRated && data.Additional["standings.virtualElapsed"] === -2 && data.TotalResult.Count !== 0;
  1581. }
  1582. }
  1583. }
  1584. function createCacheKey(contestScreenName, showGhost) {
  1585. return `${contestScreenName}:${showGhost}`;
  1586. }
  1587. const STANDINGS_CACHE_DURATION = 10 * 1000;
  1588. const cache = new Cache(STANDINGS_CACHE_DURATION);
  1589. async function getVirtualStandings(contestScreenName, showGhost) {
  1590. const cacheKey = createCacheKey(contestScreenName, showGhost);
  1591. if (!cache.has(cacheKey)) {
  1592. const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/virtual/json${showGhost ? "?showGhost=true" : ""}`);
  1593. if (!result.ok) {
  1594. throw new Error(`Failed to fetch standings: ${result.status}`);
  1595. }
  1596. cache.set(cacheKey, await result.json());
  1597. }
  1598. return new StandingsWrapper(cache.get(cacheKey));
  1599. }
  1600. addHandler((content, path) => {
  1601. const match = path.match(/^\/contests\/([^/]*)\/standings\/virtual\/json(\?showGhost=true)?$/);
  1602. if (!match)
  1603. return;
  1604. const contestScreenName = match[1];
  1605. const showGhost = match[2] != "";
  1606. cache.set(createCacheKey(contestScreenName, showGhost), JSON.parse(content));
  1607. });
  1608.  
  1609. function isVirtualStandingsPage() {
  1610. return /^\/contests\/[^/]*\/standings\/virtual\/?$/.test(document.location.pathname);
  1611. }
  1612.  
  1613. function duringVirtualParticipation() {
  1614. if (!isVirtualStandingsPage()) {
  1615. throw new Error("not available in this page");
  1616. }
  1617. const timerText = document.getElementById("virtual-timer")?.textContent ?? "";
  1618. if (timerText && !timerText.includes("終了") && !timerText.includes("over"))
  1619. return true;
  1620. else
  1621. return false;
  1622. }
  1623.  
  1624. function forgeCombinedRanks(a, b) {
  1625. const res = new Map();
  1626. 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);
  1627. let rank = 0;
  1628. let prevScore = NaN;
  1629. let prevPenalty = NaN;
  1630. for (const [userScreenName, { score, penalty }] of merged) {
  1631. if (score !== prevScore || penalty !== prevPenalty) {
  1632. rank++;
  1633. prevScore = score;
  1634. prevPenalty = penalty;
  1635. }
  1636. res.set(userScreenName, rank);
  1637. }
  1638. return res;
  1639. }
  1640. function remapKey(map, mappingFunction) {
  1641. const newMap = new Map();
  1642. for (const [key, val] of map) {
  1643. newMap.set(mappingFunction(key), val);
  1644. }
  1645. return newMap;
  1646. }
  1647. class VirtualStandingsPageController {
  1648. contestDetails;
  1649. performanceProvider;
  1650. standingsTableView;
  1651. async register() {
  1652. const loading = StandingsLoadingView.Get();
  1653. loading.onLoad(() => this.initialize());
  1654. }
  1655. async initialize() {
  1656. const contestScreenName = getContestScreenName();
  1657. const contestDetailsList = await getContestDetails();
  1658. const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
  1659. if (contestDetails === undefined) {
  1660. throw new Error("contest details not found");
  1661. }
  1662. this.contestDetails = contestDetails;
  1663. this.standingsTableView = StandingsTableView.Get(async (userScreenName) => {
  1664. if (!this.performanceProvider)
  1665. return { "type": "error", "message": "performanceProvider missing" };
  1666. if (!this.performanceProvider.availableFor(userScreenName))
  1667. return { "type": "error", "message": `performance not available for ${userScreenName}` };
  1668. const originalPerformance = this.performanceProvider.getPerformance(userScreenName);
  1669. const positivizedPerformance = Math.round(positivizeRating(originalPerformance));
  1670. return { type: "perfonly", performance: positivizedPerformance };
  1671. });
  1672. this.standingsTableView.onRefreshed(async () => {
  1673. await this.updateData();
  1674. this.standingsTableView.update();
  1675. });
  1676. await this.updateData();
  1677. this.standingsTableView.update();
  1678. }
  1679. async updateData() {
  1680. if (!this.contestDetails)
  1681. throw new Error("contestDetails missing");
  1682. const virtualStandings = await getVirtualStandings(this.contestDetails.contestScreenName, true);
  1683. const results = await getResults(this.contestDetails.contestScreenName);
  1684. let ranks;
  1685. let basePerformanceProvider;
  1686. if ((!duringVirtualParticipation() || getConfig("useFinalResultOnVirtual")) && getConfig("useResults")) {
  1687. const standings = await getStandings(this.contestDetails.contestScreenName);
  1688. const referencePerformanceMap = remapKey(results.toPerformanceMaps(), userScreenName => `reference:${userScreenName}`);
  1689. basePerformanceProvider = new FixedPerformanceProvider(referencePerformanceMap);
  1690. ranks = forgeCombinedRanks(remapKey(standings.toScores(), userScreenName => `reference:${userScreenName}`), virtualStandings.toScores());
  1691. }
  1692. else {
  1693. const aperfsObj = await getAPerfs(this.contestDetails.contestScreenName);
  1694. const defaultAPerf = this.contestDetails.defaultAPerf;
  1695. const normalizedRanks = normalizeRank(virtualStandings.toRanks(true, this.contestDetails.contestType));
  1696. const aperfsList = virtualStandings.toRatedUsers(this.contestDetails.contestType).map(userScreenName => hasOwnProperty(aperfsObj, userScreenName) ? aperfsObj[userScreenName] : defaultAPerf);
  1697. basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap);
  1698. ranks = virtualStandings.toRanks();
  1699. }
  1700. this.performanceProvider = new InterpolatePerformanceProvider(ranks, basePerformanceProvider);
  1701. }
  1702. }
  1703.  
  1704. function isExtendedStandingsPage() {
  1705. return /^\/contests\/[^/]*\/standings\/extended\/?$/.test(document.location.pathname);
  1706. }
  1707.  
  1708. function isStandingsPage() {
  1709. return /^\/contests\/[^/]*\/standings\/?$/.test(document.location.pathname);
  1710. }
  1711.  
  1712. {
  1713. const controller = new ConfigController();
  1714. controller.register();
  1715. add();
  1716. }
  1717. if (isStandingsPage()) {
  1718. const controller = new StandingsPageController();
  1719. controller.register();
  1720. }
  1721. if (isVirtualStandingsPage()) {
  1722. const controller = new VirtualStandingsPageController();
  1723. controller.register();
  1724. }
  1725. if (isExtendedStandingsPage()) {
  1726. const controller = new ExtendedStandingsPageController();
  1727. controller.register();
  1728. }