ac-predictor

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

  1. // ==UserScript==
  2. // @name ac-predictor
  3. // @namespace http://ac-predictor.azurewebsites.net/
  4. // @version 2.0.11
  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. element;
  713. pendingHooks;
  714. constructor(element) {
  715. this.element = element;
  716. this.pendingHooks = [];
  717. new MutationObserver(() => this.resolveHooksIfPossible()).observe(this.element, { attributes: true });
  718. }
  719. onLoad(hook) {
  720. if (this.isStandingsLoaded()) {
  721. hook();
  722. }
  723. else {
  724. this.pendingHooks.push(hook);
  725. }
  726. }
  727. resolveHooksIfPossible() {
  728. if (this.pendingHooks.length === 0)
  729. return;
  730. if (!this.isStandingsLoaded())
  731. return;
  732. const hooks = this.pendingHooks;
  733. this.pendingHooks = [];
  734. hooks.forEach(f => f());
  735. }
  736. ;
  737. isStandingsLoaded() {
  738. return this.element.style.display === "none";
  739. }
  740. static Get() {
  741. const loadingElem = document.querySelector("#vue-standings .loading-show");
  742. if (loadingElem === null) {
  743. throw new Error("loadingElem not found");
  744. }
  745. return new StandingsLoadingView(loadingElem);
  746. }
  747. }
  748.  
  749. function toSignedString (n) {
  750. return `${n >= 0 ? "+" : "-"}${Math.abs(n)}`;
  751. }
  752.  
  753. function addStyle(styleSheet) {
  754. const styleElem = document.createElement("style");
  755. styleElem.textContent = styleSheet;
  756. document.getElementsByTagName("head")[0].append(styleElem);
  757. }
  758.  
  759. function getSpan(innerElements, classList) {
  760. const span = document.createElement("span");
  761. span.append(...innerElements);
  762. span.classList.add(...classList);
  763. return span;
  764. }
  765.  
  766. function getRatingSpan(rate) {
  767. return getSpan([rate.toString()], ["bold", "user-" + getColor(rate)]);
  768. }
  769.  
  770. 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}";
  771.  
  772. addStyle(style);
  773. function getFadedSpan(innerElements) {
  774. return getSpan(innerElements, ["grey"]);
  775. }
  776. function getRatedRatingElem(result) {
  777. const elem = document.createElement("div");
  778. elem.append(getRatingSpan(result.oldRating), " → ", getRatingSpan(result.newRating), " ", getFadedSpan([`(${toSignedString(result.newRating - result.oldRating)})`]));
  779. return elem;
  780. }
  781. function getUnratedRatingElem(result) {
  782. const elem = document.createElement("div");
  783. elem.append(getRatingSpan(result.oldRating), " ", getFadedSpan(["(unrated)"]));
  784. return elem;
  785. }
  786. function getDefferedRatingElem(result) {
  787. const elem = document.createElement("div");
  788. elem.append(getRatingSpan(result.oldRating), " → ", getSpan(["???"], ["bold"]), document.createElement("br"), getFadedSpan([`(${getTranslation("standings_click_to_compute_label")})`]));
  789. async function listener() {
  790. elem.removeEventListener("click", listener);
  791. elem.replaceChildren(getFadedSpan(["loading..."]));
  792. let newRating;
  793. try {
  794. newRating = await result.newRatingCalculator();
  795. }
  796. catch (e) {
  797. elem.append(getSpan(["error on load"], []), document.createElement("br"), getSpan(["(hover to see details)"], ["grey", "small"]), getSpan([e.toString()], ["my-tooltiptext"]));
  798. elem.classList.add("my-tooltip");
  799. return;
  800. }
  801. const newElem = getRatedRatingElem({ type: "rated", performance: result.performance, oldRating: result.oldRating, newRating: newRating });
  802. elem.replaceChildren(newElem);
  803. }
  804. elem.addEventListener("click", listener);
  805. return elem;
  806. }
  807. function getPerfOnlyRatingElem(result) {
  808. const elem = document.createElement("div");
  809. elem.append(getFadedSpan([`(${getTranslation("standings_not_provided_label")})`]));
  810. return elem;
  811. }
  812. function getErrorRatingElem(result) {
  813. const elem = document.createElement("div");
  814. elem.append(getSpan(["error on load"], []), document.createElement("br"), getSpan(["(hover to see details)"], ["grey", "small"]), getSpan([result.message], ["my-tooltiptext"]));
  815. elem.classList.add("my-tooltip");
  816. return elem;
  817. }
  818. function getRatingElem(result) {
  819. if (result.type == "rated")
  820. return getRatedRatingElem(result);
  821. if (result.type == "unrated")
  822. return getUnratedRatingElem(result);
  823. if (result.type == "deffered")
  824. return getDefferedRatingElem(result);
  825. if (result.type == "perfonly")
  826. return getPerfOnlyRatingElem();
  827. if (result.type == "error")
  828. return getErrorRatingElem(result);
  829. throw new Error("unreachable");
  830. }
  831. function getPerfElem(result) {
  832. if (result.type == "error")
  833. return getSpan(["-"], []);
  834. return getRatingSpan(result.performance);
  835. }
  836. 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>`;
  837. function modifyHeader(header) {
  838. header.insertAdjacentHTML("beforeend", headerHtml);
  839. }
  840. function isFooter(row) {
  841. return row.firstElementChild?.classList.contains("colspan");
  842. }
  843. async function modifyStandingsRow(row, results) {
  844. const rankText = row.children[0].textContent;
  845. const usernameSpan = row.querySelector(".standings-username .username span");
  846. let userScreenName = usernameSpan?.textContent ?? null;
  847. // unratedかつ順位が未表示ならば参加者でない、というヒューリスティック(お気に入り順位表でのエラー解消用)
  848. if (usernameSpan?.className === "user-unrated" && rankText === "-") {
  849. userScreenName = null;
  850. }
  851. // TODO: この辺のロジックがここにあるの嫌だね……
  852. if (userScreenName !== null && row.querySelector(".standings-username .username img[src='//img.atcoder.jp/assets/icon/ghost.svg']")) {
  853. userScreenName = `ghost:${userScreenName}`;
  854. }
  855. if (userScreenName !== null && row.classList.contains("info") && 3 <= row.children.length && row.children[2].textContent == "-") {
  856. // 延長線順位表用
  857. userScreenName = `extended:${userScreenName}`;
  858. }
  859. const perfCell = document.createElement("td");
  860. perfCell.classList.add("ac-predictor-standings-elem", "standings-result");
  861. const ratingCell = document.createElement("td");
  862. ratingCell.classList.add("ac-predictor-standings-elem", "standings-result");
  863. if (userScreenName === null) {
  864. perfCell.append("-");
  865. ratingCell.append("-");
  866. }
  867. else {
  868. const result = await results(userScreenName);
  869. perfCell.append(getPerfElem(result));
  870. ratingCell.append(getRatingElem(result));
  871. }
  872. row.insertAdjacentElement("beforeend", perfCell);
  873. row.insertAdjacentElement("beforeend", ratingCell);
  874. }
  875. function modifyFooter(footer) {
  876. footer.insertAdjacentHTML("beforeend", '<td class="ac-predictor-standings-elem" colspan="2">-</td>');
  877. }
  878. class StandingsTableView {
  879. element;
  880. provider;
  881. refreshHooks = [];
  882. constructor(element, resultDataProvider) {
  883. this.element = element;
  884. this.provider = resultDataProvider;
  885. this.initHandler();
  886. }
  887. onRefreshed(hook) {
  888. this.refreshHooks.push(hook);
  889. }
  890. update() {
  891. this.removeOldElement();
  892. const header = this.element.querySelector("thead tr");
  893. if (!header)
  894. console.warn("header element not found", this.element);
  895. else
  896. modifyHeader(header);
  897. this.element.querySelectorAll("tbody tr").forEach((row) => {
  898. if (isFooter(row))
  899. modifyFooter(row);
  900. else
  901. modifyStandingsRow(row, this.provider);
  902. });
  903. }
  904. removeOldElement() {
  905. this.element.querySelectorAll(".ac-predictor-standings-elem").forEach((elem) => elem.remove());
  906. }
  907. initHandler() {
  908. new MutationObserver(() => this.update()).observe(this.element.tBodies[0], {
  909. childList: true,
  910. });
  911. const statsRow = this.element.querySelector(".standings-statistics");
  912. if (statsRow === null) {
  913. throw new Error("statsRow not found");
  914. }
  915. const acElems = statsRow.querySelectorAll(".standings-ac");
  916. const refreshObserver = new MutationObserver((records) => {
  917. if (isDebugMode())
  918. console.log("fire refreshHooks", records);
  919. this.refreshHooks.forEach(f => f());
  920. });
  921. acElems.forEach(elem => refreshObserver.observe(elem, { childList: true }));
  922. }
  923. static Get(resultDataProvider) {
  924. const tableElem = document.querySelector(".table-responsive table");
  925. return new StandingsTableView(tableElem, resultDataProvider);
  926. }
  927. }
  928.  
  929. class ExtendedStandingsPageController {
  930. contestDetails;
  931. performanceProvider;
  932. standingsTableView;
  933. async register() {
  934. const loading = StandingsLoadingView.Get();
  935. loading.onLoad(() => this.initialize());
  936. }
  937. async initialize() {
  938. const contestScreenName = getContestScreenName();
  939. const contestDetailsList = await getContestDetails();
  940. const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
  941. if (contestDetails === undefined) {
  942. throw new Error("contest details not found");
  943. }
  944. this.contestDetails = contestDetails;
  945. this.standingsTableView = StandingsTableView.Get(async (userScreenName) => {
  946. if (!this.performanceProvider)
  947. return { "type": "error", "message": "performanceProvider missing" };
  948. if (!this.performanceProvider.availableFor(userScreenName))
  949. return { "type": "error", "message": `performance not available for ${userScreenName}` };
  950. const originalPerformance = this.performanceProvider.getPerformance(userScreenName);
  951. const positivizedPerformance = Math.round(positivizeRating(originalPerformance));
  952. return { type: "perfonly", performance: positivizedPerformance };
  953. });
  954. this.standingsTableView.onRefreshed(async () => {
  955. await this.updateData();
  956. this.standingsTableView.update();
  957. });
  958. await this.updateData();
  959. this.standingsTableView.update();
  960. }
  961. async updateData() {
  962. if (!this.contestDetails)
  963. throw new Error("contestDetails missing");
  964. const extendedStandings = await getExtendedStandings(this.contestDetails.contestScreenName);
  965. const aperfsObj = await getAPerfs(this.contestDetails.contestScreenName);
  966. const defaultAPerf = this.contestDetails.defaultAPerf;
  967. const normalizedRanks = normalizeRank(extendedStandings.toRanks(true, this.contestDetails.contestType));
  968. const aperfsList = extendedStandings.toRatedUsers(this.contestDetails.contestType).map(userScreenName => hasOwnProperty(aperfsObj, userScreenName) ? aperfsObj[userScreenName] : defaultAPerf);
  969. const basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap);
  970. const ranks = extendedStandings.toRanks();
  971. this.performanceProvider = new InterpolatePerformanceProvider(ranks, basePerformanceProvider);
  972. }
  973. }
  974.  
  975. class HistoriesWrapper {
  976. data;
  977. constructor(data) {
  978. this.data = data;
  979. }
  980. toRatingMaterials(latestContestDate, contestDurationSecondProvider) {
  981. const toUtcDate = (date) => Math.floor(date.getTime() / (24 * 60 * 60 * 1000));
  982. const results = [];
  983. for (const history of this.data) {
  984. if (!history.IsRated)
  985. continue;
  986. const endTime = new Date(history.EndTime);
  987. const startTime = new Date(endTime.getTime() - contestDurationSecondProvider(history.ContestScreenName) * 1000);
  988. results.push({
  989. Performance: history.Performance,
  990. Weight: getWeight(startTime, endTime),
  991. DaysFromLatestContest: toUtcDate(latestContestDate) - toUtcDate(endTime),
  992. });
  993. }
  994. return results;
  995. }
  996. }
  997. const HISTORY_CACHE_DURATION = 60 * 60 * 1000;
  998. const cache$3 = new Cache(HISTORY_CACHE_DURATION);
  999. async function getHistory(userScreenName, contestType = "algorithm") {
  1000. const key = `${userScreenName}:${contestType}`;
  1001. if (!cache$3.has(key)) {
  1002. const result = await fetch(`https://atcoder.jp/users/${userScreenName}/history/json?contestType=${contestType}`);
  1003. if (!result.ok) {
  1004. throw new Error(`Failed to fetch history: ${result.status}`);
  1005. }
  1006. cache$3.set(key, await result.json());
  1007. }
  1008. return new HistoriesWrapper(cache$3.get(key));
  1009. }
  1010.  
  1011. // @ts-nocheck
  1012. 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>";
  1013. class EstimatorModel {
  1014. inputDesc;
  1015. resultDesc;
  1016. perfHistory;
  1017. constructor(inputValue, perfHistory) {
  1018. this.inputDesc = "";
  1019. this.resultDesc = "";
  1020. this.perfHistory = perfHistory;
  1021. this.updateInput(inputValue);
  1022. }
  1023. inputValue;
  1024. resultValue;
  1025. updateInput(value) {
  1026. this.inputValue = value;
  1027. this.resultValue = this.calcResult(value);
  1028. }
  1029. toggle() {
  1030. return null;
  1031. }
  1032. calcResult(input) {
  1033. return input;
  1034. }
  1035. }
  1036. class CalcRatingModel extends EstimatorModel {
  1037. constructor(inputValue, perfHistory) {
  1038. super(inputValue, perfHistory);
  1039. this.inputDesc = "パフォーマンス";
  1040. this.resultDesc = "到達レーティング";
  1041. }
  1042. // @ts-ignore
  1043. toggle() {
  1044. return new CalcPerfModel(this.resultValue, this.perfHistory);
  1045. }
  1046. calcResult(input) {
  1047. return positivizeRating(calcAlgRatingFromHistory(this.perfHistory.concat([input])));
  1048. }
  1049. }
  1050. class CalcPerfModel extends EstimatorModel {
  1051. constructor(inputValue, perfHistory) {
  1052. super(inputValue, perfHistory);
  1053. this.inputDesc = "目標レーティング";
  1054. this.resultDesc = "必要パフォーマンス";
  1055. }
  1056. // @ts-ignore
  1057. toggle() {
  1058. return new CalcRatingModel(this.resultValue, this.perfHistory);
  1059. }
  1060. calcResult(input) {
  1061. return calcRequiredPerformance(unpositivizeRating(input), this.perfHistory);
  1062. }
  1063. }
  1064. function GetEmbedTweetLink(content, url) {
  1065. return `https://twitter.com/share?text=${encodeURI(content)}&url=${encodeURI(url)}`;
  1066. }
  1067. function getLS(key) {
  1068. const val = localStorage.getItem(key);
  1069. return (val ? JSON.parse(val) : val);
  1070. }
  1071. function setLS(key, val) {
  1072. try {
  1073. localStorage.setItem(key, JSON.stringify(val));
  1074. }
  1075. catch (error) {
  1076. console.log(error);
  1077. }
  1078. }
  1079. const models = [CalcPerfModel, CalcRatingModel];
  1080. function GetModelFromStateCode(state, value, history) {
  1081. let model = models.find((model) => model.name === state);
  1082. if (!model)
  1083. model = CalcPerfModel;
  1084. return new model(value, history);
  1085. }
  1086. function getPerformanceHistories(history) {
  1087. const onlyRated = history.filter((x) => x.IsRated);
  1088. onlyRated.sort((a, b) => {
  1089. return new Date(a.EndTime).getTime() - new Date(b.EndTime).getTime();
  1090. });
  1091. return onlyRated.map((x) => x.Performance);
  1092. }
  1093. function roundValue(value, numDigits) {
  1094. return Math.round(value * Math.pow(10, numDigits)) / Math.pow(10, numDigits);
  1095. }
  1096. class EstimatorElement {
  1097. id;
  1098. title;
  1099. document;
  1100. constructor() {
  1101. this.id = "estimator";
  1102. this.title = "Estimator";
  1103. this.document = dom$1;
  1104. }
  1105. async afterOpen() {
  1106. const estimatorInputSelector = document.getElementById("estimator-input");
  1107. const estimatorResultSelector = document.getElementById("estimator-res");
  1108. let model = GetModelFromStateCode(getLS("sidemenu_estimator_state"), getLS("sidemenu_estimator_value"), getPerformanceHistories((await getHistory(userScreenName)).data));
  1109. updateView();
  1110. document.getElementById("estimator-toggle").addEventListener("click", () => {
  1111. model = model.toggle();
  1112. updateLocalStorage();
  1113. updateView();
  1114. });
  1115. estimatorInputSelector.addEventListener("keyup", () => {
  1116. updateModel();
  1117. updateLocalStorage();
  1118. updateView();
  1119. });
  1120. /** modelをinputの値に応じて更新 */
  1121. function updateModel() {
  1122. const inputNumber = estimatorInputSelector.valueAsNumber;
  1123. if (!isFinite(inputNumber))
  1124. return;
  1125. model.updateInput(inputNumber);
  1126. }
  1127. /** modelの状態をLSに保存 */
  1128. function updateLocalStorage() {
  1129. setLS("sidemenu_estimator_value", model.inputValue);
  1130. setLS("sidemenu_estimator_state", model.constructor.name);
  1131. }
  1132. /** modelを元にviewを更新 */
  1133. function updateView() {
  1134. const roundedInput = roundValue(model.inputValue, 2);
  1135. const roundedResult = roundValue(model.resultValue, 2);
  1136. document.getElementById("estimator-input-desc").innerText = model.inputDesc;
  1137. document.getElementById("estimator-res-desc").innerText = model.resultDesc;
  1138. estimatorInputSelector.value = String(roundedInput);
  1139. estimatorResultSelector.value = String(roundedResult);
  1140. const tweetStr = `AtCoderのハンドルネーム: ${userScreenName}\n${model.inputDesc}: ${roundedInput}\n${model.resultDesc}: ${roundedResult}\n`;
  1141. document.getElementById("estimator-tweet").href = GetEmbedTweetLink(tweetStr, "https://greasyfork.org/ja/scripts/369954-ac-predictor");
  1142. }
  1143. }
  1144. ;
  1145. GetHTML() {
  1146. return `<div class="menu-wrapper">
  1147. <div class="menu-header">
  1148. <h4 class="sidemenu-txt">${this.title}<span class="glyphicon glyphicon-menu-up" style="float: right"></span></h4>
  1149. </div>
  1150. <div class="menu-box"><div class="menu-content" id="${this.id}">${this.document}</div></div>
  1151. </div>`;
  1152. }
  1153. }
  1154. const estimator = new EstimatorElement();
  1155. var sidemenuHtml = "<style>\n #menu-wrap {\n pointer-events: none;\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 pointer-events: auto;\n background: #000;\n opacity: 0.85;\n }\n #sidemenu-key {\n pointer-events: auto;\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>";
  1156. class SideMenu {
  1157. pendingElements;
  1158. constructor() {
  1159. this.pendingElements = [];
  1160. this.Generate();
  1161. }
  1162. Generate() {
  1163. document.getElementById("main-div").insertAdjacentHTML("afterbegin", sidemenuHtml);
  1164. resizeSidemenuHeight();
  1165. const key = document.getElementById("sidemenu-key");
  1166. const wrap = document.getElementById("menu-wrap");
  1167. key.addEventListener("click", () => {
  1168. this.pendingElements.forEach((elem) => {
  1169. elem.afterOpen();
  1170. });
  1171. this.pendingElements.length = 0;
  1172. key.classList.toggle("glyphicon-menu-left");
  1173. key.classList.toggle("glyphicon-menu-right");
  1174. wrap.classList.toggle("sidemenu-active");
  1175. });
  1176. window.addEventListener("onresize", resizeSidemenuHeight);
  1177. document.getElementById("sidemenu").addEventListener("click", (event) => {
  1178. const target = event.target;
  1179. const header = target.closest(".menu-header");
  1180. if (!header)
  1181. return;
  1182. const box = target.closest(".menu-wrapper").querySelector(".menu-box");
  1183. box.classList.toggle("menu-box-collapse");
  1184. const arrow = target.querySelector(".glyphicon");
  1185. arrow.classList.toggle("glyphicon-menu-down");
  1186. arrow.classList.toggle("glyphicon-menu-up");
  1187. });
  1188. function resizeSidemenuHeight() {
  1189. document.getElementById("sidemenu").style.height = `${window.innerHeight}px`;
  1190. }
  1191. }
  1192. addElement(element) {
  1193. const sidemenu = document.getElementById("sidemenu");
  1194. sidemenu.insertAdjacentHTML("afterbegin", element.GetHTML());
  1195. const content = sidemenu.querySelector(".menu-content");
  1196. content.parentElement.style.height = `${content.offsetHeight}px`;
  1197. // element.afterAppend();
  1198. this.pendingElements.push(element);
  1199. }
  1200. }
  1201. function add() {
  1202. const sidemenu = new SideMenu();
  1203. const elements = [estimator];
  1204. for (let i = elements.length - 1; i >= 0; i--) {
  1205. sidemenu.addElement(elements[i]);
  1206. }
  1207. }
  1208.  
  1209. class ResultsWrapper {
  1210. data;
  1211. constructor(data) {
  1212. this.data = data;
  1213. }
  1214. toPerformanceMaps() {
  1215. const res = new Map();
  1216. for (const result of this.data) {
  1217. if (!result.IsRated)
  1218. continue;
  1219. res.set(result.UserScreenName, result.Performance);
  1220. }
  1221. return res;
  1222. }
  1223. toIsRatedMaps() {
  1224. const res = new Map();
  1225. for (const result of this.data) {
  1226. res.set(result.UserScreenName, result.IsRated);
  1227. }
  1228. return res;
  1229. }
  1230. toOldRatingMaps() {
  1231. const res = new Map();
  1232. for (const result of this.data) {
  1233. res.set(result.UserScreenName, result.OldRating);
  1234. }
  1235. return res;
  1236. }
  1237. toNewRatingMaps() {
  1238. const res = new Map();
  1239. for (const result of this.data) {
  1240. res.set(result.UserScreenName, result.NewRating);
  1241. }
  1242. return res;
  1243. }
  1244. }
  1245. const RESULTS_CACHE_DURATION = 10 * 1000;
  1246. const cache$2 = new Cache(RESULTS_CACHE_DURATION);
  1247. async function getResults(contestScreenName) {
  1248. if (!cache$2.has(contestScreenName)) {
  1249. const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/results/json`);
  1250. if (!result.ok) {
  1251. throw new Error(`Failed to fetch results: ${result.status}`);
  1252. }
  1253. cache$2.set(contestScreenName, await result.json());
  1254. }
  1255. return new ResultsWrapper(cache$2.get(contestScreenName));
  1256. }
  1257. addHandler((content, path) => {
  1258. const match = path.match(/^\/contests\/([^/]*)\/results\/json$/);
  1259. if (!match)
  1260. return;
  1261. const contestScreenName = match[1];
  1262. cache$2.set(contestScreenName, JSON.parse(content));
  1263. });
  1264.  
  1265. let StandingsWrapper$1 = class StandingsWrapper {
  1266. data;
  1267. constructor(data) {
  1268. this.data = data;
  1269. }
  1270. toRanks(onlyRated = false, contestType = "algorithm") {
  1271. const res = new Map();
  1272. for (const data of this.data.StandingsData) {
  1273. if (onlyRated && !this.isRated(data, contestType))
  1274. continue;
  1275. res.set(data.UserScreenName, data.Rank);
  1276. }
  1277. return res;
  1278. }
  1279. toRatedUsers(contestType) {
  1280. const res = [];
  1281. for (const data of this.data.StandingsData) {
  1282. if (this.isRated(data, contestType)) {
  1283. res.push(data.UserScreenName);
  1284. }
  1285. }
  1286. return res;
  1287. }
  1288. toIsRatedMaps(contestType) {
  1289. const res = new Map();
  1290. for (const data of this.data.StandingsData) {
  1291. res.set(data.UserScreenName, this.isRated(data, contestType));
  1292. }
  1293. return res;
  1294. }
  1295. toOldRatingMaps(unpositivize = false) {
  1296. const res = new Map();
  1297. for (const data of this.data.StandingsData) {
  1298. const rating = this.data.Fixed ? data.OldRating : data.Rating;
  1299. res.set(data.UserScreenName, unpositivize ? unpositivizeRating(rating) : rating);
  1300. }
  1301. return res;
  1302. }
  1303. toCompetitionMaps() {
  1304. const res = new Map();
  1305. for (const data of this.data.StandingsData) {
  1306. res.set(data.UserScreenName, data.Competitions);
  1307. }
  1308. return res;
  1309. }
  1310. toScores() {
  1311. const res = new Map();
  1312. for (const data of this.data.StandingsData) {
  1313. res.set(data.UserScreenName, { score: data.TotalResult.Score, penalty: data.TotalResult.Elapsed });
  1314. }
  1315. return res;
  1316. }
  1317. isRated(data, contestType = "algorithm") {
  1318. if (contestType === "algorithm") {
  1319. return data.IsRated;
  1320. }
  1321. if (contestType === "heuristic") {
  1322. return data.IsRated && data.TotalResult.Count !== 0;
  1323. }
  1324. throw new Error("unreachable");
  1325. }
  1326. };
  1327. const STANDINGS_CACHE_DURATION$1 = 10 * 1000;
  1328. const cache$1 = new Cache(STANDINGS_CACHE_DURATION$1);
  1329. async function getStandings(contestScreenName) {
  1330. if (!cache$1.has(contestScreenName)) {
  1331. const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/json`);
  1332. if (!result.ok) {
  1333. throw new Error(`Failed to fetch standings: ${result.status}`);
  1334. }
  1335. cache$1.set(contestScreenName, await result.json());
  1336. }
  1337. return new StandingsWrapper$1(cache$1.get(contestScreenName));
  1338. }
  1339. addHandler((content, path) => {
  1340. const match = path.match(/^\/contests\/([^/]*)\/standings\/json$/);
  1341. if (!match)
  1342. return;
  1343. const contestScreenName = match[1];
  1344. cache$1.set(contestScreenName, JSON.parse(content));
  1345. });
  1346.  
  1347. class FixedPerformanceProvider {
  1348. result;
  1349. constructor(result) {
  1350. this.result = result;
  1351. }
  1352. availableFor(userScreenName) {
  1353. return this.result.has(userScreenName);
  1354. }
  1355. getPerformance(userScreenName) {
  1356. if (!this.availableFor(userScreenName)) {
  1357. throw new Error(`User ${userScreenName} not found`);
  1358. }
  1359. return this.result.get(userScreenName);
  1360. }
  1361. getPerformances() {
  1362. return this.result;
  1363. }
  1364. }
  1365.  
  1366. class IncrementalAlgRatingProvider {
  1367. unpositivizedRatingMap;
  1368. competitionsMap;
  1369. constructor(unpositivizedRatingMap, competitionsMap) {
  1370. this.unpositivizedRatingMap = unpositivizedRatingMap;
  1371. this.competitionsMap = competitionsMap;
  1372. }
  1373. availableFor(userScreenName) {
  1374. return this.unpositivizedRatingMap.has(userScreenName);
  1375. }
  1376. async getRating(userScreenName, newPerformance) {
  1377. if (!this.availableFor(userScreenName)) {
  1378. throw new Error(`rating not available for ${userScreenName}`);
  1379. }
  1380. const rating = this.unpositivizedRatingMap.get(userScreenName);
  1381. const competitions = this.competitionsMap.get(userScreenName);
  1382. return Math.round(positivizeRating(calcAlgRatingFromLast(rating, newPerformance, competitions)));
  1383. }
  1384. }
  1385.  
  1386. class ConstRatingProvider {
  1387. ratings;
  1388. constructor(ratings) {
  1389. this.ratings = ratings;
  1390. }
  1391. availableFor(userScreenName) {
  1392. return this.ratings.has(userScreenName);
  1393. }
  1394. async getRating(userScreenName, newPerformance) {
  1395. if (!this.availableFor(userScreenName)) {
  1396. throw new Error(`rating not available for ${userScreenName}`);
  1397. }
  1398. return this.ratings.get(userScreenName);
  1399. }
  1400. }
  1401.  
  1402. class FromHistoryHeuristicRatingProvider {
  1403. newWeight;
  1404. performancesProvider;
  1405. constructor(newWeight, performancesProvider) {
  1406. this.newWeight = newWeight;
  1407. this.performancesProvider = performancesProvider;
  1408. }
  1409. availableFor(userScreenName) {
  1410. return true;
  1411. }
  1412. async getRating(userScreenName, newPerformance) {
  1413. const performances = await this.performancesProvider(userScreenName);
  1414. performances.push({
  1415. Performance: newPerformance,
  1416. Weight: this.newWeight,
  1417. DaysFromLatestContest: 0,
  1418. });
  1419. return Math.round(positivizeRating(calcHeuristicRatingFromHistory(performances)));
  1420. }
  1421. }
  1422.  
  1423. class StandingsPageController {
  1424. contestDetails;
  1425. contestDetailsMap = new Map();
  1426. performanceProvider;
  1427. ratingProvider;
  1428. oldRatings = new Map();
  1429. isRatedMaps = new Map();
  1430. standingsTableView;
  1431. async register() {
  1432. const loading = StandingsLoadingView.Get();
  1433. loading.onLoad(() => this.initialize());
  1434. }
  1435. async initialize() {
  1436. const contestScreenName = getContestScreenName();
  1437. const contestDetailsList = await getContestDetails();
  1438. const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
  1439. if (contestDetails === undefined) {
  1440. throw new Error("contest details not found");
  1441. }
  1442. this.contestDetails = contestDetails;
  1443. this.contestDetailsMap = new Map(contestDetailsList.map(details => [details.contestScreenName, details]));
  1444. if (this.contestDetails.beforeContest(new Date()))
  1445. return;
  1446. if (getConfig("hideDuringContest") && this.contestDetails.duringContest(new Date()))
  1447. return;
  1448. const standings = await getStandings(this.contestDetails.contestScreenName);
  1449. if (getConfig("hideUntilFixed") && !standings.data.Fixed)
  1450. return;
  1451. this.standingsTableView = StandingsTableView.Get(async (userScreenName) => {
  1452. if (!this.ratingProvider)
  1453. return { "type": "error", "message": "ratingProvider missing" };
  1454. if (!this.performanceProvider)
  1455. return { "type": "error", "message": "performanceProvider missing" };
  1456. if (!this.isRatedMaps)
  1457. return { "type": "error", "message": "isRatedMapping missing" };
  1458. if (!this.oldRatings)
  1459. return { "type": "error", "message": "oldRatings missing" };
  1460. if (!this.oldRatings.has(userScreenName))
  1461. return { "type": "error", "message": `oldRating not found for ${userScreenName}` };
  1462. const oldRating = this.oldRatings.get(userScreenName);
  1463. if (!this.performanceProvider.availableFor(userScreenName))
  1464. return { "type": "error", "message": `performance not available for ${userScreenName}` };
  1465. const originalPerformance = this.performanceProvider.getPerformance(userScreenName);
  1466. const positivizedPerformance = Math.round(positivizeRating(originalPerformance));
  1467. if (this.isRatedMaps.get(userScreenName)) {
  1468. if (!this.ratingProvider.provider.availableFor(userScreenName))
  1469. return { "type": "error", "message": `rating not available for ${userScreenName}` };
  1470. if (this.ratingProvider.lazy) {
  1471. const newRatingCalculator = () => this.ratingProvider.provider.getRating(userScreenName, originalPerformance);
  1472. return { type: "deffered", oldRating, performance: positivizedPerformance, newRatingCalculator };
  1473. }
  1474. else {
  1475. const newRating = await this.ratingProvider.provider.getRating(userScreenName, originalPerformance);
  1476. return { type: "rated", oldRating, performance: positivizedPerformance, newRating };
  1477. }
  1478. }
  1479. else {
  1480. return { type: "unrated", oldRating, performance: positivizedPerformance };
  1481. }
  1482. });
  1483. this.standingsTableView.onRefreshed(async () => {
  1484. await this.updateData();
  1485. this.standingsTableView.update();
  1486. });
  1487. await this.updateData();
  1488. this.standingsTableView.update();
  1489. }
  1490. async updateData() {
  1491. if (!this.contestDetails)
  1492. throw new Error("contestDetails missing");
  1493. if (isDebugMode())
  1494. console.log("data updating...");
  1495. const standings = await getStandings(this.contestDetails.contestScreenName);
  1496. let basePerformanceProvider = undefined;
  1497. if (standings.data.Fixed && getConfig("useResults")) {
  1498. try {
  1499. const results = await getResults(this.contestDetails.contestScreenName);
  1500. if (results.data.length === 0) {
  1501. throw new Error("results missing");
  1502. }
  1503. basePerformanceProvider = new FixedPerformanceProvider(results.toPerformanceMaps());
  1504. this.isRatedMaps = results.toIsRatedMaps();
  1505. this.oldRatings = results.toOldRatingMaps();
  1506. this.ratingProvider = { provider: new ConstRatingProvider(results.toNewRatingMaps()), lazy: false };
  1507. }
  1508. catch (e) {
  1509. console.warn("getResults failed", e);
  1510. }
  1511. }
  1512. if (basePerformanceProvider === undefined) {
  1513. const aperfsDict = await getAPerfs(this.contestDetails.contestScreenName);
  1514. const defaultAPerf = this.contestDetails.defaultAPerf;
  1515. const normalizedRanks = normalizeRank(standings.toRanks(true, this.contestDetails.contestType));
  1516. const aperfsList = standings.toRatedUsers(this.contestDetails.contestType).map(user => hasOwnProperty(aperfsDict, user) ? aperfsDict[user] : defaultAPerf);
  1517. basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap);
  1518. this.isRatedMaps = standings.toIsRatedMaps(this.contestDetails.contestType);
  1519. this.oldRatings = standings.toOldRatingMaps();
  1520. if (this.contestDetails.contestType == "algorithm") {
  1521. this.ratingProvider = { provider: new IncrementalAlgRatingProvider(standings.toOldRatingMaps(true), standings.toCompetitionMaps()), lazy: false };
  1522. }
  1523. else {
  1524. const startAt = this.contestDetails.startTime;
  1525. const endAt = this.contestDetails.endTime;
  1526. this.ratingProvider = {
  1527. provider: new FromHistoryHeuristicRatingProvider(getWeight(startAt, endAt), async (userScreenName) => {
  1528. const histories = await getHistory(userScreenName, "heuristic");
  1529. histories.data = histories.data.filter(x => new Date(x.EndTime) < endAt);
  1530. return histories.toRatingMaterials(endAt, x => {
  1531. const details = this.contestDetailsMap.get(x.split(".")[0]);
  1532. if (!details) {
  1533. console.warn(`contest details not found for ${x}`);
  1534. return 0;
  1535. }
  1536. return details.duration;
  1537. });
  1538. }),
  1539. lazy: true
  1540. };
  1541. }
  1542. }
  1543. this.performanceProvider = new InterpolatePerformanceProvider(standings.toRanks(), basePerformanceProvider);
  1544. if (isDebugMode())
  1545. console.log("data updated");
  1546. }
  1547. }
  1548.  
  1549. class StandingsWrapper {
  1550. data;
  1551. constructor(data) {
  1552. this.data = data;
  1553. }
  1554. toRanks(onlyRated = false, contestType = "algorithm") {
  1555. const res = new Map();
  1556. for (const data of this.data.StandingsData) {
  1557. if (onlyRated && !this.isRated(data, contestType))
  1558. continue;
  1559. const userScreenName = data.Additional["standings.virtualElapsed"] === -2 ? `ghost:${data.UserScreenName}` : data.UserScreenName;
  1560. res.set(userScreenName, data.Rank);
  1561. }
  1562. return res;
  1563. }
  1564. toRatedUsers(contestType) {
  1565. const res = [];
  1566. for (const data of this.data.StandingsData) {
  1567. if (this.isRated(data, contestType)) {
  1568. res.push(data.UserScreenName);
  1569. }
  1570. }
  1571. return res;
  1572. }
  1573. toScores() {
  1574. const res = new Map();
  1575. for (const data of this.data.StandingsData) {
  1576. const userScreenName = data.Additional["standings.virtualElapsed"] === -2 ? `ghost:${data.UserScreenName}` : data.UserScreenName;
  1577. res.set(userScreenName, { score: data.TotalResult.Score, penalty: data.TotalResult.Elapsed });
  1578. }
  1579. return res;
  1580. }
  1581. isRated(data, contestType) {
  1582. if (contestType === "algorithm") {
  1583. return data.IsRated && data.Additional["standings.virtualElapsed"] === -2;
  1584. }
  1585. else {
  1586. return data.IsRated && data.Additional["standings.virtualElapsed"] === -2 && data.TotalResult.Count !== 0;
  1587. }
  1588. }
  1589. }
  1590. function createCacheKey(contestScreenName, showGhost) {
  1591. return `${contestScreenName}:${showGhost}`;
  1592. }
  1593. const STANDINGS_CACHE_DURATION = 10 * 1000;
  1594. const cache = new Cache(STANDINGS_CACHE_DURATION);
  1595. async function getVirtualStandings(contestScreenName, showGhost) {
  1596. const cacheKey = createCacheKey(contestScreenName, showGhost);
  1597. if (!cache.has(cacheKey)) {
  1598. const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/virtual/json${showGhost ? "?showGhost=true" : ""}`);
  1599. if (!result.ok) {
  1600. throw new Error(`Failed to fetch standings: ${result.status}`);
  1601. }
  1602. cache.set(cacheKey, await result.json());
  1603. }
  1604. return new StandingsWrapper(cache.get(cacheKey));
  1605. }
  1606. addHandler((content, path) => {
  1607. const match = path.match(/^\/contests\/([^/]*)\/standings\/virtual\/json(\?showGhost=true)?$/);
  1608. if (!match)
  1609. return;
  1610. const contestScreenName = match[1];
  1611. const showGhost = match[2] != "";
  1612. cache.set(createCacheKey(contestScreenName, showGhost), JSON.parse(content));
  1613. });
  1614.  
  1615. function isVirtualStandingsPage() {
  1616. return /^\/contests\/[^/]*\/standings\/virtual\/?$/.test(document.location.pathname);
  1617. }
  1618.  
  1619. function duringVirtualParticipation() {
  1620. if (!isVirtualStandingsPage()) {
  1621. throw new Error("not available in this page");
  1622. }
  1623. const timerText = document.getElementById("virtual-timer")?.textContent ?? "";
  1624. if (timerText && !timerText.includes("終了") && !timerText.includes("over"))
  1625. return true;
  1626. else
  1627. return false;
  1628. }
  1629.  
  1630. function forgeCombinedRanks(a, b) {
  1631. const res = new Map();
  1632. 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);
  1633. let rank = 0;
  1634. let prevScore = NaN;
  1635. let prevPenalty = NaN;
  1636. for (const [userScreenName, { score, penalty }] of merged) {
  1637. if (score !== prevScore || penalty !== prevPenalty) {
  1638. rank++;
  1639. prevScore = score;
  1640. prevPenalty = penalty;
  1641. }
  1642. res.set(userScreenName, rank);
  1643. }
  1644. return res;
  1645. }
  1646. function remapKey(map, mappingFunction) {
  1647. const newMap = new Map();
  1648. for (const [key, val] of map) {
  1649. newMap.set(mappingFunction(key), val);
  1650. }
  1651. return newMap;
  1652. }
  1653. class VirtualStandingsPageController {
  1654. contestDetails;
  1655. performanceProvider;
  1656. standingsTableView;
  1657. async register() {
  1658. const loading = StandingsLoadingView.Get();
  1659. loading.onLoad(() => this.initialize());
  1660. }
  1661. async initialize() {
  1662. const contestScreenName = getContestScreenName();
  1663. const contestDetailsList = await getContestDetails();
  1664. const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
  1665. if (contestDetails === undefined) {
  1666. throw new Error("contest details not found");
  1667. }
  1668. this.contestDetails = contestDetails;
  1669. this.standingsTableView = StandingsTableView.Get(async (userScreenName) => {
  1670. if (!this.performanceProvider)
  1671. return { "type": "error", "message": "performanceProvider missing" };
  1672. if (!this.performanceProvider.availableFor(userScreenName))
  1673. return { "type": "error", "message": `performance not available for ${userScreenName}` };
  1674. const originalPerformance = this.performanceProvider.getPerformance(userScreenName);
  1675. const positivizedPerformance = Math.round(positivizeRating(originalPerformance));
  1676. return { type: "perfonly", performance: positivizedPerformance };
  1677. });
  1678. this.standingsTableView.onRefreshed(async () => {
  1679. await this.updateData();
  1680. this.standingsTableView.update();
  1681. });
  1682. await this.updateData();
  1683. this.standingsTableView.update();
  1684. }
  1685. async updateData() {
  1686. if (!this.contestDetails)
  1687. throw new Error("contestDetails missing");
  1688. const virtualStandings = await getVirtualStandings(this.contestDetails.contestScreenName, true);
  1689. const results = await getResults(this.contestDetails.contestScreenName);
  1690. let ranks;
  1691. let basePerformanceProvider;
  1692. if ((!duringVirtualParticipation() || getConfig("useFinalResultOnVirtual")) && getConfig("useResults")) {
  1693. const standings = await getStandings(this.contestDetails.contestScreenName);
  1694. const referencePerformanceMap = remapKey(results.toPerformanceMaps(), userScreenName => `reference:${userScreenName}`);
  1695. basePerformanceProvider = new FixedPerformanceProvider(referencePerformanceMap);
  1696. ranks = forgeCombinedRanks(remapKey(standings.toScores(), userScreenName => `reference:${userScreenName}`), virtualStandings.toScores());
  1697. }
  1698. else {
  1699. const aperfsObj = await getAPerfs(this.contestDetails.contestScreenName);
  1700. const defaultAPerf = this.contestDetails.defaultAPerf;
  1701. const normalizedRanks = normalizeRank(virtualStandings.toRanks(true, this.contestDetails.contestType));
  1702. const aperfsList = virtualStandings.toRatedUsers(this.contestDetails.contestType).map(userScreenName => hasOwnProperty(aperfsObj, userScreenName) ? aperfsObj[userScreenName] : defaultAPerf);
  1703. basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, this.contestDetails.performanceCap);
  1704. ranks = virtualStandings.toRanks();
  1705. }
  1706. this.performanceProvider = new InterpolatePerformanceProvider(ranks, basePerformanceProvider);
  1707. }
  1708. }
  1709.  
  1710. function isExtendedStandingsPage() {
  1711. return /^\/contests\/[^/]*\/standings\/extended\/?$/.test(document.location.pathname);
  1712. }
  1713.  
  1714. function isStandingsPage() {
  1715. return /^\/contests\/[^/]*\/standings\/?$/.test(document.location.pathname);
  1716. }
  1717.  
  1718. {
  1719. const controller = new ConfigController();
  1720. controller.register();
  1721. add();
  1722. }
  1723. if (isStandingsPage()) {
  1724. const controller = new StandingsPageController();
  1725. controller.register();
  1726. }
  1727. if (isVirtualStandingsPage()) {
  1728. const controller = new VirtualStandingsPageController();
  1729. controller.register();
  1730. }
  1731. if (isExtendedStandingsPage()) {
  1732. const controller = new ExtendedStandingsPageController();
  1733. controller.register();
  1734. }