ac-history-perf-estimator-with-AJL

ac-history-perf-estimatorをAJLスコアに対応させました

  1. // ==UserScript==
  2. // @name ac-history-perf-estimator-with-AJL
  3. // @namespace http://ac-history-perf-filler.example.com
  4. // @version 1.0.3
  5. // @description ac-history-perf-estimatorをAJLスコアに対応させました
  6. // @match https://atcoder.jp/users/*/history*
  7. // @grant none
  8. // @license MIT
  9. // ==/UserScript==
  10.  
  11.  
  12.  
  13. // should not be here
  14. function isDebugMode() {
  15. return isDebug;
  16. }
  17.  
  18. const colorNames = ["unrated", "gray", "brown", "green", "cyan", "blue", "yellow", "orange", "red"];
  19. async function getColor(rating) {
  20. const colorIndex = rating > 0 ? Math.min(Math.floor(rating / 400) + 1, 8) : 0;
  21. return colorNames[colorIndex];
  22. }
  23.  
  24. async function getAPerfs(contestScreenName) {
  25. const result = await fetch(`https://data.ac-predictor.com/aperfs/${contestScreenName}.json`);
  26. if (!result.ok) {
  27. throw new Error(`Failed to fetch aperfs: ${result.status}`);
  28. }
  29. return await result.json();
  30. }
  31.  
  32. // [start, end]
  33. class Range {
  34. start;
  35. end;
  36. constructor(start, end) {
  37. this.start = start;
  38. this.end = end;
  39. }
  40. contains(val) {
  41. return this.start <= val && val <= this.end;
  42. }
  43. hasValue() {
  44. return this.start <= this.end;
  45. }
  46. }
  47.  
  48. class ContestDetails {
  49. contestName;
  50. contestScreenName;
  51. contestType;
  52. startTime;
  53. duration;
  54. ratedrange;
  55. constructor(contestName, contestScreenName, contestType, startTime, duration, ratedRange) {
  56. this.contestName = contestName;
  57. this.contestScreenName = contestScreenName;
  58. this.contestType = contestType;
  59. this.startTime = startTime;
  60. this.duration = duration;
  61. this.ratedrange = ratedRange;
  62. }
  63. get endTime() {
  64. return new Date(this.startTime.getTime() + this.duration * 1000);
  65. }
  66. get defaultAPerf() {
  67. if (this.contestType == "heuristic")
  68. return 1000;
  69. if (!this.ratedrange.hasValue()) {
  70. throw new Error("unrated contest");
  71. }
  72. // value is not relevant as it is never used
  73. if (!this.ratedrange.contains(0))
  74. return 800;
  75. if (this.ratedrange.end == 1199)
  76. return 800;
  77. if (this.ratedrange.end == 1999)
  78. return 800;
  79. const DEFAULT_CHANGED_AT = new Date("2019-05-25"); // maybe wrong
  80. if (this.ratedrange.end == 2799) {
  81. if (this.startTime < DEFAULT_CHANGED_AT)
  82. return 1600;
  83. else
  84. return 1000;
  85. }
  86. if (4000 <= this.ratedrange.end) {
  87. if (this.startTime < DEFAULT_CHANGED_AT)
  88. return 1600;
  89. else
  90. return 1200;
  91. }
  92. throw new Error("unknown contest type");
  93. }
  94. get performanceCap() {
  95. if (this.contestType == "heuristic")
  96. return Infinity;
  97. if (!this.ratedrange.hasValue()) {
  98. throw new Error("unrated contest");
  99. }
  100. if (4000 <= this.ratedrange.end)
  101. return Infinity;
  102. return this.ratedrange.end + 1 + 400;
  103. }
  104. beforeContest(dateTime) {
  105. return dateTime < this.startTime;
  106. }
  107. duringContest(dateTime) {
  108. return this.startTime < dateTime && dateTime < this.endTime;
  109. }
  110. isOver(dateTime) {
  111. return this.endTime < dateTime;
  112. }
  113. }
  114.  
  115. async function getContestDetails() {
  116. const result = await fetch(`https://data.ac-predictor.com/contest-details.json`);
  117. if (!result.ok) {
  118. throw new Error(`Failed to fetch contest details: ${result.status}`);
  119. }
  120. const parsed = await result.json();
  121. const res = [];
  122. for (const elem of parsed) {
  123. if (typeof elem !== "object")
  124. throw new Error("invalid object returned");
  125. if (typeof elem.contestName !== "string")
  126. throw new Error("invalid object returned");
  127. const contestName = elem.contestName;
  128. if (typeof elem.contestScreenName !== "string")
  129. throw new Error("invalid object returned");
  130. const contestScreenName = elem.contestScreenName;
  131. if (elem.contestType !== "algorithm" && elem.contestType !== "heuristic")
  132. throw new Error("invalid object returned");
  133. const contestType = elem.contestType;
  134. if (typeof elem.startTime !== "number")
  135. throw new Error("invalid object returned");
  136. const startTime = new Date(elem.startTime * 1000);
  137. if (typeof elem.duration !== "number")
  138. throw new Error("invalid object returned");
  139. const duration = elem.duration;
  140. if (typeof elem.ratedrange !== "object" || typeof elem.ratedrange[0] !== "number" || typeof elem.ratedrange[1] !== "number")
  141. throw new Error("invalid object returned");
  142. const ratedRange = new Range(elem.ratedrange[0], elem.ratedrange[1]);
  143. res.push(new ContestDetails(contestName, contestScreenName, contestType, startTime, duration, ratedRange));
  144. }
  145. return res;
  146. }
  147.  
  148. class Cache {
  149. cacheDuration;
  150. cacheExpires = new Map();
  151. cacheData = new Map();
  152. constructor(cacheDuration) {
  153. this.cacheDuration = cacheDuration;
  154. }
  155. has(key) {
  156. return this.cacheExpires.has(key) || Date.now() <= this.cacheExpires.get(key);
  157. }
  158. set(key, content) {
  159. const expire = Date.now() + this.cacheDuration;
  160. this.cacheExpires.set(key, expire);
  161. this.cacheData.set(key, content);
  162. }
  163. get(key) {
  164. if (!this.has(key)) {
  165. throw new Error(`invalid key: ${key}`);
  166. }
  167. return this.cacheData.get(key);
  168. }
  169. }
  170.  
  171.  
  172.  
  173. class EloPerformanceProvider {
  174. ranks;
  175. ratings;
  176. cap;
  177. rankMemo = new Map();
  178. constructor(ranks, ratings, cap) {
  179. this.ranks = ranks;
  180. this.ratings = ratings;
  181. this.cap = cap;
  182. }
  183. availableFor(userScreenName) {
  184. return this.ranks.has(userScreenName);
  185. }
  186. getPerformance(userScreenName) {
  187. if (!this.availableFor(userScreenName)) {
  188. throw new Error(`User ${userScreenName} not found`);
  189. }
  190. const rank = this.ranks.get(userScreenName);
  191. return this.getPerformanceForRank(rank);
  192. }
  193. getPerformances() {
  194. const performances = new Map();
  195. for (const userScreenName of this.ranks.keys()) {
  196. performances.set(userScreenName, this.getPerformance(userScreenName));
  197. }
  198. return performances;
  199. }
  200. getPerformanceForRank(rank) {
  201. let upper = 6144;
  202. let lower = -2048;
  203. while (upper - lower > 0.5) {
  204. const mid = (upper + lower) / 2;
  205. if (rank > this.getRankForPerformance(mid))
  206. upper = mid;
  207. else
  208. lower = mid;
  209. }
  210. return Math.min(this.cap, Math.round((upper + lower) / 2));
  211. }
  212. getRankForPerformance(performance) {
  213. if (this.rankMemo.has(performance))
  214. return this.rankMemo.get(performance);
  215. const res = this.ratings.reduce((val, APerf) => val + 1.0 / (1.0 + Math.pow(6.0, (performance - APerf) / 400.0)), 0.5);
  216. this.rankMemo.set(performance, res);
  217. return res;
  218. }
  219. }
  220.  
  221. function getRankToUsers(ranks) {
  222. const rankToUsers = new Map();
  223. for (const [userScreenName, rank] of ranks) {
  224. if (!rankToUsers.has(rank))
  225. rankToUsers.set(rank, []);
  226. rankToUsers.get(rank).push(userScreenName);
  227. }
  228. return rankToUsers;
  229. }
  230. function getMaxRank(ranks) {
  231. return Math.max(...ranks.values());
  232. }
  233. class InterpolatePerformanceProvider {
  234. ranks;
  235. maxRank;
  236. rankToUsers;
  237. baseProvider;
  238. constructor(ranks, baseProvider) {
  239. this.ranks = ranks;
  240. this.maxRank = getMaxRank(ranks);
  241. this.rankToUsers = getRankToUsers(ranks);
  242. this.baseProvider = baseProvider;
  243. }
  244. availableFor(userScreenName) {
  245. return this.ranks.has(userScreenName);
  246. }
  247. getPerformance(userScreenName) {
  248. if (!this.availableFor(userScreenName)) {
  249. throw new Error(`User ${userScreenName} not found`);
  250. }
  251. if (this.performanceCache.has(userScreenName))
  252. return this.performanceCache.get(userScreenName);
  253. let rank = this.ranks.get(userScreenName);
  254. while (rank <= this.maxRank) {
  255. const perf = this.getPerformanceIfAvailable(rank);
  256. if (perf !== null) {
  257. return perf;
  258. }
  259. rank++;
  260. }
  261. this.performanceCache.set(userScreenName, -Infinity);
  262. return -Infinity;
  263. }
  264. performanceCache = new Map();
  265. getPerformances() {
  266. let currentPerformance = -Infinity;
  267. const res = new Map();
  268. for (let rank = this.maxRank; rank >= 0; rank--) {
  269. const users = this.rankToUsers.get(rank);
  270. if (users === undefined)
  271. continue;
  272. const perf = this.getPerformanceIfAvailable(rank);
  273. if (perf !== null)
  274. currentPerformance = perf;
  275. for (const userScreenName of users) {
  276. res.set(userScreenName, currentPerformance);
  277. }
  278. }
  279. this.performanceCache = res;
  280. return res;
  281. }
  282. cacheForRank = new Map();
  283. getPerformanceIfAvailable(rank) {
  284. if (!this.rankToUsers.has(rank))
  285. return null;
  286. if (this.cacheForRank.has(rank))
  287. return this.cacheForRank.get(rank);
  288. for (const userScreenName of this.rankToUsers.get(rank)) {
  289. if (!this.baseProvider.availableFor(userScreenName))
  290. continue;
  291. const perf = this.baseProvider.getPerformance(userScreenName);
  292. this.cacheForRank.set(rank, perf);
  293. return perf;
  294. }
  295. return null;
  296. }
  297. }
  298.  
  299. function normalizeRank(ranks) {
  300. const rankValues = [...new Set(ranks.values()).values()];
  301. const rankToUsers = new Map();
  302. for (const [userScreenName, rank] of ranks) {
  303. if (!rankToUsers.has(rank))
  304. rankToUsers.set(rank, []);
  305. rankToUsers.get(rank).push(userScreenName);
  306. }
  307. rankValues.sort((a, b) => a - b);
  308. const res = new Map();
  309. let currentRank = 1;
  310. for (const rank of rankValues) {
  311. const users = rankToUsers.get(rank);
  312. const averageRank = currentRank + (users.length - 1) / 2;
  313. for (const userScreenName of users) {
  314. res.set(userScreenName, averageRank);
  315. }
  316. currentRank += users.length;
  317. }
  318. return res;
  319. }
  320.  
  321. //Copyright © 2017 koba-e964.
  322. //from : https://github.com/koba-e964/atcoder-rating-estimator
  323. const finf = bigf(400);
  324. function bigf(n) {
  325. let pow1 = 1;
  326. let pow2 = 1;
  327. let numerator = 0;
  328. let denominator = 0;
  329. for (let i = 0; i < n; ++i) {
  330. pow1 *= 0.81;
  331. pow2 *= 0.9;
  332. numerator += pow1;
  333. denominator += pow2;
  334. }
  335. return Math.sqrt(numerator) / denominator;
  336. }
  337. function f(n) {
  338. return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0;
  339. }
  340. /**
  341. * calculate unpositivized rating from performance history
  342. * @param {Number[]} [history] performance history with ascending order
  343. * @returns {Number} unpositivized rating
  344. */
  345. function calcAlgRatingFromHistory(history) {
  346. const n = history.length;
  347. let pow = 1;
  348. let numerator = 0.0;
  349. let denominator = 0.0;
  350. for (let i = n - 1; i >= 0; i--) {
  351. pow *= 0.9;
  352. numerator += Math.pow(2, history[i] / 800.0) * pow;
  353. denominator += pow;
  354. }
  355. return Math.log2(numerator / denominator) * 800.0 - f(n);
  356. }
  357. /**
  358. * calculate unpositivized rating from last state
  359. * @param {Number} [last] last unpositivized rating
  360. * @param {Number} [perf] performance
  361. * @param {Number} [ratedMatches] count of participated rated contest
  362. * @returns {number} estimated unpositivized rating
  363. */
  364. function calcAlgRatingFromLast(last, perf, ratedMatches) {
  365. if (ratedMatches === 0)
  366. return perf - 1200;
  367. last += f(ratedMatches);
  368. const weight = 9 - 9 * 0.9 ** ratedMatches;
  369. const numerator = weight * 2 ** (last / 800.0) + 2 ** (perf / 800.0);
  370. const denominator = 1 + weight;
  371. return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
  372. }
  373. /**
  374. * calculate the performance required to reach a target rate
  375. * @param {Number} [targetRating] targeted unpositivized rating
  376. * @param {Number[]} [history] performance history with ascending order
  377. * @returns {number} performance
  378. */
  379. function calcRequiredPerformance(targetRating, history) {
  380. let valid = 10000.0;
  381. let invalid = -10000.0;
  382. for (let i = 0; i < 100; ++i) {
  383. const mid = (invalid + valid) / 2;
  384. const rating = Math.round(calcAlgRatingFromHistory(history.concat([mid])));
  385. if (targetRating <= rating)
  386. valid = mid;
  387. else
  388. invalid = mid;
  389. }
  390. return valid;
  391. }
  392. /**
  393. * Gets the weight used in the heuristic rating calculation
  394. * based on its start and end dates
  395. * @param {Date} startAt - The start date of the contest.
  396. * @param {Date} endAt - The end date of the contest.
  397. * @returns {number} The weight of the contest.
  398. */
  399. function getWeight(startAt, endAt) {
  400. const isShortContest = endAt.getTime() - startAt.getTime() < 24 * 60 * 60 * 1000;
  401. if (endAt < new Date("2025-01-01T00:00:00+09:00")) {
  402. return 1;
  403. }
  404. return isShortContest ? 0.5 : 1;
  405. }
  406. /**
  407. * calculate unpositivized rating from performance history
  408. * @param {RatingMaterial[]} [history] performance histories
  409. * @returns {Number} unpositivized rating
  410. */
  411. function calcHeuristicRatingFromHistory(history) {
  412. const S = 724.4744301;
  413. const R = 0.8271973364;
  414. const qs = [];
  415. for (const material of history) {
  416. const adjustedPerformance = material.Performance + 150 - 100 * material.DaysFromLatestContest / 365;
  417. for (let i = 1; i <= 100; i++) {
  418. qs.push({ q: adjustedPerformance - S * Math.log(i), weight: material.Weight });
  419. }
  420. }
  421. qs.sort((a, b) => b.q - a.q);
  422. let r = 0.0;
  423. let s = 0.0;
  424. for (const { q, weight } of qs) {
  425. s += weight;
  426. r += q * (R ** (s - weight) - R ** s);
  427. }
  428. return r;
  429. }
  430. /**
  431. * (-inf, inf) -> (0, inf)
  432. * @param {Number} [rating] unpositivized rating
  433. * @returns {number} positivized rating
  434. */
  435. function positivizeRating(rating) {
  436. if (rating >= 400.0) {
  437. return rating;
  438. }
  439. return 400.0 * Math.exp((rating - 400.0) / 400.0);
  440. }
  441. /**
  442. * (0, inf) -> (-inf, inf)
  443. * @param {Number} [rating] positivized rating
  444. * @returns {number} unpositivized rating
  445. */
  446. function unpositivizeRating(rating) {
  447. if (rating >= 400.0) {
  448. return rating;
  449. }
  450. return 400.0 + 400.0 * Math.log(rating / 400.0);
  451. }
  452.  
  453. function hasOwnProperty(obj, key) {
  454. return Object.prototype.hasOwnProperty.call(obj, key);
  455. }
  456.  
  457.  
  458.  
  459. let StandingsWrapper$1 = class StandingsWrapper {
  460. data;
  461. constructor(data) {
  462. this.data = data;
  463. }
  464. toRanks(onlyRated = false, contestType = "algorithm") {
  465. const res = new Map();
  466. for (const data of this.data.StandingsData) {
  467. if (onlyRated && !this.isRated(data, contestType))
  468. continue;
  469. res.set(data.UserScreenName, data.Rank);
  470. }
  471. return res;
  472. }
  473. toRatedUsers(contestType) {
  474. const res = [];
  475. for (const data of this.data.StandingsData) {
  476. if (this.isRated(data, contestType)) {
  477. res.push(data.UserScreenName);
  478. }
  479. }
  480. return res;
  481. }
  482. toScore(user) {
  483. for (const data of this.data.StandingsData) {
  484. if (data.UserScreenName == user) return data.TotalResult.Score
  485. }
  486. }
  487. isRated(data, contestType = "algorithm") {
  488. if (contestType === "algorithm") {
  489. return data.IsRated;
  490. }
  491. if (contestType === "heuristic") {
  492. return data.IsRated && data.TotalResult.Count !== 0;
  493. }
  494. throw new Error("unreachable");
  495. }
  496. };
  497. const STANDINGS_CACHE_DURATION$1 = 10 * 1000;
  498. const cache$1 = new Cache(STANDINGS_CACHE_DURATION$1);
  499. async function getStandings(contestScreenName) {
  500. if (!cache$1.has(contestScreenName)) {
  501. const result = await fetch(`https://atcoder.jp/contests/${contestScreenName}/standings/json`);
  502. if (!result.ok) {
  503. throw new Error(`Failed to fetch standings: ${result.status}`);
  504. }
  505. cache$1.set(contestScreenName, await result.json());
  506. }
  507. return new StandingsWrapper$1(cache$1.get(contestScreenName));
  508. }
  509. async function loadPerformances() {
  510. 'use strict';
  511. const pathParts = location.pathname.split('/');
  512. const user = pathParts[2];
  513.  
  514. // コンテスト詳細一覧を取得
  515. const contestDetailsList = await getContestDetails();
  516.  
  517. // 各行に処理を並列で適用
  518. const rowPromises = Array.from(document.querySelectorAll('#history tbody tr')).map(async (row) => {
  519. const perfCell = row.children[3];
  520. if (perfCell && perfCell.textContent.trim() === '-') {
  521. const link = row.children[1].querySelector('a');
  522. if (!link) return;
  523.  
  524. const parts = link.pathname.split('/');
  525. const contestScreenName = parts[2];
  526. const contestDetails = contestDetailsList.find(details => details.contestScreenName == contestScreenName);
  527. if (!contestDetails) return;
  528.  
  529. const aperfsDict = await getAPerfs(contestDetails.contestScreenName);
  530. const defaultAPerf = contestDetails.defaultAPerf;
  531. const standings = await getStandings(contestDetails.contestScreenName);
  532. const score = standings.toScore(user);
  533. const normalizedRanks = normalizeRank(standings.toRanks(true, contestDetails.contestType));
  534. const aperfsList = standings.toRatedUsers(contestDetails.contestType).map(user => hasOwnProperty(aperfsDict, user) ? aperfsDict[user] : defaultAPerf);
  535. const basePerformanceProvider = new EloPerformanceProvider(normalizedRanks, aperfsList, contestDetails.performanceCap);
  536. const performanceProvider = new InterpolatePerformanceProvider(standings.toRanks(), basePerformanceProvider);
  537. const perfRaw = score == 0 ? 0 : parseInt(positivizeRating(performanceProvider.getPerformance(user)));
  538.  
  539. const span = document.createElement("span");
  540. span.textContent = perfRaw.toString();
  541. span.style.color = await getColor(perfRaw);
  542. span.style.opacity = "0.6";
  543. perfCell.innerHTML = "";
  544. perfCell.appendChild(span);
  545. }
  546. });
  547.  
  548. await Promise.all(rowPromises); // 全ての行の処理を待つ
  549. }
  550.  
  551. (async () => {
  552. await loadPerformances(); // 完了まで待つ
  553.  
  554. console.debug("Loading AJL Scores");
  555.  
  556. const table = Array.from(document.querySelector("#history").rows);
  557. const tableTitleElem = document.createElement("th");
  558. tableTitleElem.style["text-align"] = "center";
  559. tableTitleElem.textContent = "AJL";
  560. tableTitleElem.classList.add("sorting", "ajl");
  561. tableTitleElem.addEventListener("click", () => {
  562. document.querySelector("th:nth-child(4)").click();
  563. });
  564. table[0].insertBefore(tableTitleElem, table[0].childNodes[5]);
  565.  
  566. const scores = [];
  567. table.slice(1).forEach((element) => {
  568. const perf = Number(element.childNodes[7].textContent);
  569. const ajlScore = perf == 0 ? 0 : Math.round(Math.pow(2, perf / 400) * 1000);
  570. const ajlScoreElem = document.createElement("td");
  571. ajlScoreElem.textContent = ajlScore;
  572. if (!isNaN(ajlScore)) {
  573. scores.push(ajlScore);
  574. }
  575. const ratingElem = element.childNodes[11];
  576. element.insertBefore(ajlScoreElem, ratingElem);
  577. });
  578.  
  579. const labelEle = document.createElement("label");
  580. labelEle.textContent = "AJL Calculator (Please enter the latest number of doses you would like.) ->";
  581. labelEle.htmlFor = "ajl-cal";
  582. const numEle = document.createElement("input");
  583. numEle.type = "number";
  584. numEle.id = "ajl-cal";
  585. numEle.value = table.slice(1).length;
  586.  
  587. const titleEle = document.querySelector("div.col-sm-12:has(h2) #user-nav-tabs");
  588. titleEle.parentNode.insertBefore(numEle, titleEle);
  589. numEle.parentNode.insertBefore(labelEle, numEle);
  590.  
  591. const ansEle = document.createElement("p");
  592. titleEle.parentNode.insertBefore(ansEle, titleEle);
  593.  
  594. function getScore(count) {
  595. const src = scores.slice(0, count).sort((a, b) => b - a);
  596. let sum = 0, cnt = 0;
  597. for (let i = 0; i < src.length && cnt < 10; i++) {
  598. if (isNaN(src[i])) continue;
  599. sum += src[i];
  600. cnt++;
  601. }
  602. ansEle.textContent = "AJL Score: " + sum;
  603. }
  604.  
  605. getScore(numEle.value);
  606. numEle.addEventListener("change", () => getScore(numEle.value));
  607. })();
  608.