atcoder-standings-difficulty-analyzer

順位表の得点情報を集計し,推定 difficulty やその推移を表示します.

当前为 2022-05-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name atcoder-standings-difficulty-analyzer
  3. // @namespace iilj
  4. // @version 2022.5.1
  5. // @description 順位表の得点情報を集計し,推定 difficulty やその推移を表示します.
  6. // @author iilj
  7. // @license MIT
  8. // @supportURL https://github.com/iilj/atcoder-standings-difficulty-analyzer/issues
  9. // @match https://atcoder.jp/*standings*
  10. // @exclude https://atcoder.jp/*standings/json
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/plotly.js/1.33.1/plotly.min.js
  12. // @resource loaders.min.css https://cdnjs.cloudflare.com/ajax/libs/loaders.css/0.1.2/loaders.min.css
  13. // @grant GM_getResourceText
  14. // @grant GM_addStyle
  15. // ==/UserScript==
  16. var css = "#acssa-contents .table.acssa-table {\n width: 100%;\n table-layout: fixed;\n margin-bottom: 1.5rem;\n}\n#acssa-contents .table.acssa-table .acssa-thead {\n font-weight: bold;\n}\n#acssa-contents .table.acssa-table > tbody > tr > td.success.acssa-task-success.acssa-task-success-suppress {\n background-color: transparent;\n}\n#acssa-contents #acssa-tab-wrapper {\n display: none;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-chart-tab {\n margin-bottom: 0.5rem;\n display: inline-block;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-chart-tab a {\n cursor: pointer;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-chart-tab a span.glyphicon {\n margin-right: 0.5rem;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab {\n margin-bottom: 0.5rem;\n display: inline-block;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab li a {\n color: black;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab li a:hover {\n background-color: transparent;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab li a label {\n cursor: pointer;\n margin: 0;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab li a label input {\n cursor: pointer;\n margin: 0;\n}\n#acssa-contents #acssa-tab-wrapper #acssa-checkbox-tab #acssa-checkbox-toggle-log-plot-parent {\n display: none;\n}\n#acssa-contents .acssa-loader-wrapper {\n background-color: #337ab7;\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 1rem;\n margin-bottom: 1.5rem;\n border-radius: 3px;\n}\n#acssa-contents .acssa-chart-wrapper {\n display: none;\n}\n#acssa-contents .acssa-chart-wrapper.acssa-chart-wrapper-active {\n display: block;\n}";
  17.  
  18. var teamalert = "<div class=\"alert alert-warning\">\n チーム戦順位表が提供されています.個人単位の順位表ページでは,difficulty 推定値が不正確になります.\n</div>";
  19.  
  20. const arrayLowerBound = (arr, n) => {
  21. let first = 0, last = arr.length - 1, middle;
  22. while (first <= last) {
  23. middle = 0 | ((first + last) / 2);
  24. if (arr[middle] < n)
  25. first = middle + 1;
  26. else
  27. last = middle - 1;
  28. }
  29. return first;
  30. };
  31. const getColor = (rating) => {
  32. if (rating < 400)
  33. return '#808080';
  34. // gray
  35. else if (rating < 800)
  36. return '#804000';
  37. // brown
  38. else if (rating < 1200)
  39. return '#008000';
  40. // green
  41. else if (rating < 1600)
  42. return '#00C0C0';
  43. // cyan
  44. else if (rating < 2000)
  45. return '#0000FF';
  46. // blue
  47. else if (rating < 2400)
  48. return '#C0C000';
  49. // yellow
  50. else if (rating < 2800)
  51. return '#FF8000';
  52. // orange
  53. else if (rating == 9999)
  54. return '#000000';
  55. return '#FF0000'; // red
  56. };
  57. const formatTimespan = (sec) => {
  58. let sign;
  59. if (sec >= 0) {
  60. sign = '';
  61. }
  62. else {
  63. sign = '-';
  64. sec *= -1;
  65. }
  66. return `${sign}${Math.floor(sec / 60)}:${`0${sec % 60}`.slice(-2)}`;
  67. };
  68. /** 現在のページから,コンテストの開始から終了までの秒数を抽出する */
  69. const getContestDurationSec = () => {
  70. if (contestScreenName.startsWith('past')) {
  71. return 300 * 60;
  72. }
  73. // toDate.diff(fromDate) でミリ秒が返ってくる
  74. return endTime.diff(startTime) / 1000;
  75. };
  76. const getCenterOfInnerRatingFromRange = (contestRatedRange) => {
  77. if (contestScreenName.startsWith('abc')) {
  78. return 800;
  79. }
  80. if (contestScreenName.startsWith('arc')) {
  81. const contestNumber = Number(contestScreenName.substring(3, 6));
  82. return contestNumber >= 104 ? 1000 : 1600;
  83. }
  84. if (contestScreenName.startsWith('agc')) {
  85. const contestNumber = Number(contestScreenName.substring(3, 6));
  86. return contestNumber >= 34 ? 1200 : 1600;
  87. }
  88. if (contestRatedRange[1] === 1999) {
  89. return 800;
  90. }
  91. else if (contestRatedRange[1] === 2799) {
  92. return 1000;
  93. }
  94. else if (contestRatedRange[1] === Infinity) {
  95. return 1200;
  96. }
  97. return 800;
  98. };
  99. // ContestRatedRange
  100. /*
  101. function getContestInformationAsync(contestScreenName) {
  102. return __awaiter(this, void 0, void 0, function* () {
  103. const html = yield fetchTextDataAsync(`https://atcoder.jp/contests/${contestScreenName}`);
  104. const topPageDom = new DOMParser().parseFromString(html, "text/html");
  105. const dataParagraph = topPageDom.getElementsByClassName("small")[0];
  106. const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(":")[1].trim());
  107. return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]));
  108. });
  109. }
  110. */
  111. function parseRangeString(s) {
  112. s = s.trim();
  113. if (s === '-')
  114. return [0, -1];
  115. if (s === 'All')
  116. return [0, Infinity];
  117. if (!/[-~]/.test(s))
  118. return [0, -1];
  119. const res = s.split(/[-~]/).map((x) => parseInt(x.trim()));
  120. if (res.length !== 2) {
  121. throw new Error('res is not [number, number]');
  122. }
  123. if (isNaN(res[0]))
  124. res[0] = 0;
  125. if (isNaN(res[1]))
  126. res[1] = Infinity;
  127. return res;
  128. }
  129. const getContestRatedRangeAsync = async (contestScreenName) => {
  130. const html = await fetch(`https://atcoder.jp/contests/${contestScreenName}`);
  131. const topPageDom = new DOMParser().parseFromString(await html.text(), 'text/html');
  132. const dataParagraph = topPageDom.getElementsByClassName('small')[0];
  133. const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(':')[1].trim());
  134. // console.log("data", data);
  135. return parseRangeString(data[1]);
  136. // return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2]));
  137. };
  138. const rangeLen = (len) => Array.from({ length: len }, (v, k) => k);
  139.  
  140. const BASE_URL = 'https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings';
  141. const fetchJson = async (url) => {
  142. const res = await fetch(url);
  143. if (!res.ok) {
  144. throw new Error(res.statusText);
  145. }
  146. const obj = (await res.json());
  147. return obj;
  148. };
  149. const fetchContestAcRatioModel = async (contestScreenName, contestDurationMinutes) => {
  150. // https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings/abc_100m.json
  151. let modelLocation = undefined;
  152. if (/^agc(\d{3,})$/.exec(contestScreenName)) {
  153. if ([110, 120, 130, 140, 150, 160, 180, 200, 210, 240, 270, 300].includes(contestDurationMinutes)) {
  154. modelLocation = `${BASE_URL}/agc_${contestDurationMinutes}m.json`;
  155. }
  156. }
  157. else if (/^arc(\d{3,})$/.exec(contestScreenName)) {
  158. if ([100, 120, 150].includes(contestDurationMinutes)) {
  159. modelLocation = `${BASE_URL}/arc_${contestDurationMinutes}m.json`;
  160. }
  161. }
  162. else if (/^abc(\d{3,})$/.exec(contestScreenName)) {
  163. if ([100, 120].includes(contestDurationMinutes)) {
  164. modelLocation = `${BASE_URL}/abc_${contestDurationMinutes}m.json`;
  165. }
  166. }
  167. if (modelLocation !== undefined) {
  168. return await fetchJson(modelLocation);
  169. }
  170. return undefined;
  171. };
  172. const fetchInnerRatingsFromPredictor = async (contestScreenName) => {
  173. const url = `https://data.ac-predictor.com/aperfs/${contestScreenName}.json`;
  174. try {
  175. return await fetchJson(url);
  176. }
  177. catch (e) {
  178. return {};
  179. }
  180. };
  181.  
  182. class RatingConverter {
  183. }
  184. /** 表示用の低レート帯補正レート → 低レート帯補正前のレート */
  185. RatingConverter.toRealRating = (correctedRating) => {
  186. if (correctedRating >= 400)
  187. return correctedRating;
  188. else
  189. return 400 * (1 - Math.log(400 / correctedRating));
  190. };
  191. /** 低レート帯補正前のレート → 内部レート推定値 */
  192. RatingConverter.toInnerRating = (realRating, comp) => {
  193. return (realRating +
  194. (1200 * (Math.sqrt(1 - Math.pow(0.81, comp)) / (1 - Math.pow(0.9, comp)) - 1)) / (Math.sqrt(19) - 1));
  195. };
  196. /** 低レート帯補正前のレート → 表示用の低レート帯補正レート */
  197. RatingConverter.toCorrectedRating = (realRating) => {
  198. if (realRating >= 400)
  199. return realRating;
  200. else
  201. return Math.floor(400 / Math.exp((400 - realRating) / 400));
  202. };
  203.  
  204. class DifficultyCalculator {
  205. constructor(sortedInnerRatings) {
  206. this.innerRatings = sortedInnerRatings;
  207. this.prepared = new Map();
  208. this.memo = new Map();
  209. }
  210. perf2ExpectedAcceptedCount(m) {
  211. let expectedAcceptedCount;
  212. if (this.prepared.has(m)) {
  213. expectedAcceptedCount = this.prepared.get(m);
  214. }
  215. else {
  216. expectedAcceptedCount = this.innerRatings.reduce((prev_expected_accepts, innerRating) => (prev_expected_accepts += 1 / (1 + Math.pow(6, (m - innerRating) / 400))), 0);
  217. this.prepared.set(m, expectedAcceptedCount);
  218. }
  219. return expectedAcceptedCount;
  220. }
  221. perf2Ranking(x) {
  222. return this.perf2ExpectedAcceptedCount(x) + 0.5;
  223. }
  224. rank2InnerPerf(rank) {
  225. let upper = 9999;
  226. let lower = -9999;
  227. while (upper - lower > 0.1) {
  228. const mid = (upper + lower) / 2;
  229. if (rank > this.perf2Ranking(mid))
  230. upper = mid;
  231. else
  232. lower = mid;
  233. }
  234. return Math.round((upper + lower) / 2);
  235. }
  236. /** Difficulty 推定値を算出する */
  237. binarySearchCorrectedDifficulty(acceptedCount) {
  238. if (this.memo.has(acceptedCount)) {
  239. return this.memo.get(acceptedCount);
  240. }
  241. let lb = -10000;
  242. let ub = 10000;
  243. while (ub - lb > 1) {
  244. const m = Math.floor((ub + lb) / 2);
  245. const expectedAcceptedCount = this.perf2ExpectedAcceptedCount(m);
  246. if (expectedAcceptedCount < acceptedCount)
  247. ub = m;
  248. else
  249. lb = m;
  250. }
  251. const difficulty = lb;
  252. const correctedDifficulty = RatingConverter.toCorrectedRating(difficulty);
  253. this.memo.set(acceptedCount, correctedDifficulty);
  254. return correctedDifficulty;
  255. }
  256. }
  257.  
  258. var html$1 = "<div id=\"acssa-loader\" class=\"loader acssa-loader-wrapper\">\n <div class=\"loader-inner ball-pulse\">\n <div></div>\n <div></div>\n <div></div>\n </div>\n</div>\n<div id=\"acssa-chart-block\">\n <div class=\"acssa-chart-wrapper acssa-chart-wrapper-active\" id=\"acssa-mydiv-difficulty-wrapper\">\n <div id=\"acssa-mydiv-difficulty\" style=\"width:100%;\"></div>\n </div>\n <div class=\"acssa-chart-wrapper\" id=\"acssa-mydiv-accepted-count-wrapper\">\n <div id=\"acssa-mydiv-accepted-count\" style=\"width:100%;\"></div>\n </div>\n <div class=\"acssa-chart-wrapper\" id=\"acssa-mydiv-accepted-time-wrapper\">\n <div id=\"acssa-mydiv-accepted-time\" style=\"width:100%;\"></div>\n </div>\n</div>";
  259.  
  260. const LOADER_ID = 'acssa-loader';
  261. const plotlyDifficultyChartId = 'acssa-mydiv-difficulty';
  262. const plotlyAcceptedCountChartId = 'acssa-mydiv-accepted-count';
  263. const plotlyLastAcceptedTimeChartId = 'acssa-mydiv-accepted-time';
  264. const yourMarker = {
  265. size: 10,
  266. symbol: 'cross',
  267. color: 'red',
  268. line: {
  269. color: 'white',
  270. width: 1,
  271. },
  272. };
  273. const config = { autosize: true };
  274. // 背景用設定
  275. const alpha = 0.3;
  276. const colors = [
  277. [0, 400, `rgba(128,128,128,${alpha})`],
  278. [400, 800, `rgba(128,0,0,${alpha})`],
  279. [800, 1200, `rgba(0,128,0,${alpha})`],
  280. [1200, 1600, `rgba(0,255,255,${alpha})`],
  281. [1600, 2000, `rgba(0,0,255,${alpha})`],
  282. [2000, 2400, `rgba(255,255,0,${alpha})`],
  283. [2400, 2800, `rgba(255,165,0,${alpha})`],
  284. [2800, 10000, `rgba(255,0,0,${alpha})`],
  285. ];
  286. class Charts {
  287. constructor(parent, tasks, scoreLastAcceptedTimeMap, taskAcceptedCounts, taskAcceptedElapsedTimes, yourTaskAcceptedElapsedTimes, yourScore, yourLastAcceptedTime, participants, dcForDifficulty, dcForPerformance, tabs) {
  288. this.tasks = tasks;
  289. this.scoreLastAcceptedTimeMap = scoreLastAcceptedTimeMap;
  290. this.taskAcceptedCounts = taskAcceptedCounts;
  291. this.taskAcceptedElapsedTimes = taskAcceptedElapsedTimes;
  292. this.yourTaskAcceptedElapsedTimes = yourTaskAcceptedElapsedTimes;
  293. this.yourScore = yourScore;
  294. this.yourLastAcceptedTime = yourLastAcceptedTime;
  295. this.participants = participants;
  296. this.dcForDifficulty = dcForDifficulty;
  297. this.dcForPerformance = dcForPerformance;
  298. this.tabs = tabs;
  299. parent.insertAdjacentHTML('beforeend', html$1);
  300. this.duration = getContestDurationSec();
  301. this.xtick = 60 * 10 * Math.max(1, Math.ceil(this.duration / (60 * 10 * 20))); // 10 分を最小単位にする
  302. }
  303. async plotAsync() {
  304. // 以降の計算は時間がかかる
  305. this.taskAcceptedElapsedTimes.forEach((ar) => {
  306. ar.sort((a, b) => a - b);
  307. });
  308. // 時系列データの準備
  309. const [difficultyChartData, acceptedCountChartData] = await this.getTimeSeriesChartData();
  310. // 得点と提出時間データの準備
  311. const [lastAcceptedTimeChartData, maxAcceptedTime] = this.getLastAcceptedTimeChartData();
  312. // 軸フォーマットをカスタムする
  313. this.overrideAxisFormat();
  314. // Difficulty Chart 描画
  315. await this.plotDifficultyChartData(difficultyChartData);
  316. // Accepted Count Chart 描画
  317. await this.plotAcceptedCountChartData(acceptedCountChartData);
  318. // LastAcceptedTime Chart 描画
  319. await this.plotLastAcceptedTimeChartData(lastAcceptedTimeChartData, maxAcceptedTime);
  320. }
  321. /** 時系列データの準備 */
  322. async getTimeSeriesChartData() {
  323. /** Difficulty Chart のデータ */
  324. const difficultyChartData = [];
  325. /** AC Count Chart のデータ */
  326. const acceptedCountChartData = [];
  327. const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  328. for (let j = 0; j < this.tasks.length; ++j) {
  329. //
  330. const interval = Math.ceil(this.taskAcceptedCounts[j] / 140);
  331. const [taskAcceptedElapsedTimesForChart, taskAcceptedCountsForChart] = this.taskAcceptedElapsedTimes[j].reduce(([ar, arr], tm, idx) => {
  332. const tmpInterval = Math.max(1, Math.min(Math.ceil(idx / 10), interval));
  333. if (idx % tmpInterval == 0 || idx == this.taskAcceptedCounts[j] - 1) {
  334. ar.push(tm);
  335. arr.push(idx + 1);
  336. }
  337. return [ar, arr];
  338. }, [[], []]);
  339. const correctedDifficulties = [];
  340. let counter = 0;
  341. for (const taskAcceptedCountForChart of taskAcceptedCountsForChart) {
  342. correctedDifficulties.push(this.dcForDifficulty.binarySearchCorrectedDifficulty(taskAcceptedCountForChart));
  343. counter += 1;
  344. // 20回に1回setTimeout(0)でeventループに処理を移す
  345. if (counter % 20 == 0) {
  346. await sleep(0);
  347. }
  348. }
  349. difficultyChartData.push({
  350. x: taskAcceptedElapsedTimesForChart,
  351. y: correctedDifficulties,
  352. type: 'scatter',
  353. name: `${this.tasks[j].Assignment}`,
  354. });
  355. acceptedCountChartData.push({
  356. x: taskAcceptedElapsedTimesForChart,
  357. y: taskAcceptedCountsForChart,
  358. type: 'scatter',
  359. name: `${this.tasks[j].Assignment}`,
  360. });
  361. }
  362. // 現在のユーザのデータを追加
  363. if (this.yourScore !== -1) {
  364. const yourAcceptedTimes = [];
  365. const yourAcceptedDifficulties = [];
  366. const yourAcceptedCounts = [];
  367. for (let j = 0; j < this.tasks.length; ++j) {
  368. if (this.yourTaskAcceptedElapsedTimes[j] !== -1) {
  369. yourAcceptedTimes.push(this.yourTaskAcceptedElapsedTimes[j]);
  370. const yourAcceptedCount = arrayLowerBound(this.taskAcceptedElapsedTimes[j], this.yourTaskAcceptedElapsedTimes[j]) + 1;
  371. yourAcceptedCounts.push(yourAcceptedCount);
  372. yourAcceptedDifficulties.push(this.dcForDifficulty.binarySearchCorrectedDifficulty(yourAcceptedCount));
  373. }
  374. }
  375. this.tabs.yourDifficultyChartData = {
  376. x: yourAcceptedTimes,
  377. y: yourAcceptedDifficulties,
  378. mode: 'markers',
  379. type: 'scatter',
  380. name: `${userScreenName}`,
  381. marker: yourMarker,
  382. };
  383. this.tabs.yourAcceptedCountChartData = {
  384. x: yourAcceptedTimes,
  385. y: yourAcceptedCounts,
  386. mode: 'markers',
  387. type: 'scatter',
  388. name: `${userScreenName}`,
  389. marker: yourMarker,
  390. };
  391. difficultyChartData.push(this.tabs.yourDifficultyChartData);
  392. acceptedCountChartData.push(this.tabs.yourAcceptedCountChartData);
  393. }
  394. return [difficultyChartData, acceptedCountChartData];
  395. }
  396. /** 得点と提出時間データの準備 */
  397. getLastAcceptedTimeChartData() {
  398. const lastAcceptedTimeChartData = [];
  399. const scores = [...this.scoreLastAcceptedTimeMap.keys()];
  400. scores.sort((a, b) => b - a);
  401. let acc = 0;
  402. let maxAcceptedTime = 0;
  403. scores.forEach((score) => {
  404. const lastAcceptedTimes = this.scoreLastAcceptedTimeMap.get(score);
  405. lastAcceptedTimes.sort((a, b) => a - b);
  406. const interval = Math.ceil(lastAcceptedTimes.length / 100);
  407. const lastAcceptedTimesForChart = lastAcceptedTimes.reduce((ar, tm, idx) => {
  408. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1)
  409. ar.push(tm);
  410. return ar;
  411. }, []);
  412. const lastAcceptedTimesRanks = lastAcceptedTimes.reduce((ar, tm, idx) => {
  413. if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1)
  414. ar.push(acc + idx + 1);
  415. return ar;
  416. }, []);
  417. lastAcceptedTimeChartData.push({
  418. x: lastAcceptedTimesRanks,
  419. y: lastAcceptedTimesForChart,
  420. type: 'scatter',
  421. name: `${score}`,
  422. });
  423. if (score === this.yourScore) {
  424. const lastAcceptedTimesRank = arrayLowerBound(lastAcceptedTimes, this.yourLastAcceptedTime);
  425. this.tabs.yourLastAcceptedTimeChartData = {
  426. x: [acc + lastAcceptedTimesRank + 1],
  427. y: [this.yourLastAcceptedTime],
  428. mode: 'markers',
  429. type: 'scatter',
  430. name: `${userScreenName}`,
  431. marker: yourMarker,
  432. };
  433. this.tabs.yourLastAcceptedTimeChartDataIndex = lastAcceptedTimeChartData.length + 0;
  434. lastAcceptedTimeChartData.push(this.tabs.yourLastAcceptedTimeChartData);
  435. }
  436. acc += lastAcceptedTimes.length;
  437. if (lastAcceptedTimes[lastAcceptedTimes.length - 1] > maxAcceptedTime) {
  438. maxAcceptedTime = lastAcceptedTimes[lastAcceptedTimes.length - 1];
  439. }
  440. });
  441. return [lastAcceptedTimeChartData, maxAcceptedTime];
  442. }
  443. /**
  444. * 軸フォーマットをカスタムする
  445. * Support specifying a function for tickformat · Issue #1464 · plotly/plotly.js
  446. * https://github.com/plotly/plotly.js/issues/1464#issuecomment-498050894
  447. */
  448. overrideAxisFormat() {
  449. const org_locale = Plotly.d3.locale;
  450. Plotly.d3.locale = (locale) => {
  451. const result = org_locale(locale);
  452. // eslint-disable-next-line @typescript-eslint/unbound-method
  453. const org_number_format = result.numberFormat;
  454. result.numberFormat = (format) => {
  455. if (format != 'TIME') {
  456. return org_number_format(format);
  457. }
  458. return (x) => formatTimespan(x).toString();
  459. };
  460. return result;
  461. };
  462. }
  463. /** Difficulty Chart 描画 */
  464. async plotDifficultyChartData(difficultyChartData) {
  465. const maxAcceptedCount = this.taskAcceptedCounts.reduce((a, b) => Math.max(a, b));
  466. const yMax = RatingConverter.toCorrectedRating(this.dcForDifficulty.binarySearchCorrectedDifficulty(1));
  467. const yMin = RatingConverter.toCorrectedRating(this.dcForDifficulty.binarySearchCorrectedDifficulty(Math.max(2, maxAcceptedCount)));
  468. // 描画
  469. const layout = {
  470. title: 'Difficulty',
  471. xaxis: {
  472. dtick: this.xtick,
  473. tickformat: 'TIME',
  474. range: [0, this.duration],
  475. // title: { text: 'Elapsed' }
  476. },
  477. yaxis: {
  478. dtick: 400,
  479. tickformat: 'd',
  480. range: [
  481. Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  482. Math.max(0, Math.ceil((yMax + 100) / 400) * 400),
  483. ],
  484. // title: { text: 'Difficulty' }
  485. },
  486. shapes: colors.map((c) => {
  487. return {
  488. type: 'rect',
  489. layer: 'below',
  490. xref: 'x',
  491. yref: 'y',
  492. x0: 0,
  493. x1: this.duration,
  494. y0: c[0],
  495. y1: c[1],
  496. line: { width: 0 },
  497. fillcolor: c[2],
  498. };
  499. }),
  500. margin: {
  501. b: 60,
  502. t: 30,
  503. },
  504. };
  505. await Plotly.newPlot(plotlyDifficultyChartId, difficultyChartData, layout, config);
  506. window.addEventListener('resize', () => {
  507. if (this.tabs.activeTab == 0)
  508. void Plotly.relayout(plotlyDifficultyChartId, {
  509. width: document.getElementById(plotlyDifficultyChartId).clientWidth,
  510. });
  511. });
  512. }
  513. /** Accepted Count Chart 描画 */
  514. async plotAcceptedCountChartData(acceptedCountChartData) {
  515. this.tabs.acceptedCountYMax = this.participants;
  516. const rectSpans = colors.reduce((ar, cur) => {
  517. const bottom = this.dcForDifficulty.perf2ExpectedAcceptedCount(cur[1]);
  518. if (bottom > this.tabs.acceptedCountYMax)
  519. return ar;
  520. const top = cur[0] == 0 ? this.tabs.acceptedCountYMax : this.dcForDifficulty.perf2ExpectedAcceptedCount(cur[0]);
  521. if (top < 0.5)
  522. return ar;
  523. ar.push([Math.max(0.5, bottom), Math.min(this.tabs.acceptedCountYMax, top), cur[2]]);
  524. return ar;
  525. }, []);
  526. // 描画
  527. const layout = {
  528. title: 'Accepted Count',
  529. xaxis: {
  530. dtick: this.xtick,
  531. tickformat: 'TIME',
  532. range: [0, this.duration],
  533. // title: { text: 'Elapsed' }
  534. },
  535. yaxis: {
  536. // type: 'log',
  537. // dtick: 100,
  538. tickformat: 'd',
  539. range: [0, this.tabs.acceptedCountYMax],
  540. // range: [
  541. // Math.log10(0.5),
  542. // Math.log10(acceptedCountYMax)
  543. // ],
  544. // title: { text: 'Difficulty' }
  545. },
  546. shapes: rectSpans.map((span) => {
  547. return {
  548. type: 'rect',
  549. layer: 'below',
  550. xref: 'x',
  551. yref: 'y',
  552. x0: 0,
  553. x1: this.duration,
  554. y0: span[0],
  555. y1: span[1],
  556. line: { width: 0 },
  557. fillcolor: span[2],
  558. };
  559. }),
  560. margin: {
  561. b: 60,
  562. t: 30,
  563. },
  564. };
  565. await Plotly.newPlot(plotlyAcceptedCountChartId, acceptedCountChartData, layout, config);
  566. window.addEventListener('resize', () => {
  567. if (this.tabs.activeTab == 1)
  568. void Plotly.relayout(plotlyAcceptedCountChartId, {
  569. width: document.getElementById(plotlyAcceptedCountChartId).clientWidth,
  570. });
  571. });
  572. }
  573. /** LastAcceptedTime Chart 描画 */
  574. async plotLastAcceptedTimeChartData(lastAcceptedTimeChartData, maxAcceptedTime) {
  575. const xMax = this.participants;
  576. const yMax = Math.ceil((maxAcceptedTime + this.xtick / 2) / this.xtick) * this.xtick;
  577. const rectSpans = colors.reduce((ar, cur) => {
  578. const right = cur[0] == 0 ? xMax : this.dcForPerformance.perf2Ranking(cur[0]);
  579. if (right < 1)
  580. return ar;
  581. const left = this.dcForPerformance.perf2Ranking(cur[1]);
  582. if (left > xMax)
  583. return ar;
  584. ar.push([Math.max(0, left), Math.min(xMax, right), cur[2]]);
  585. return ar;
  586. }, []);
  587. // console.log(colors);
  588. // console.log(rectSpans);
  589. const layout = {
  590. title: 'LastAcceptedTime v.s. Rank',
  591. xaxis: {
  592. // dtick: 100,
  593. tickformat: 'd',
  594. range: [0, xMax],
  595. // title: { text: 'Elapsed' }
  596. },
  597. yaxis: {
  598. dtick: this.xtick,
  599. tickformat: 'TIME',
  600. range: [0, yMax],
  601. // range: [
  602. // Math.max(0, Math.floor((yMin - 100) / 400) * 400),
  603. // Math.max(0, Math.ceil((yMax + 100) / 400) * 400)
  604. // ],
  605. // title: { text: 'Difficulty' }
  606. },
  607. shapes: rectSpans.map((span) => {
  608. return {
  609. type: 'rect',
  610. layer: 'below',
  611. xref: 'x',
  612. yref: 'y',
  613. x0: span[0],
  614. x1: span[1],
  615. y0: 0,
  616. y1: yMax,
  617. line: { width: 0 },
  618. fillcolor: span[2],
  619. };
  620. }),
  621. margin: {
  622. b: 60,
  623. t: 30,
  624. },
  625. };
  626. await Plotly.newPlot(plotlyLastAcceptedTimeChartId, lastAcceptedTimeChartData, layout, config);
  627. window.addEventListener('resize', () => {
  628. if (this.tabs.activeTab == 2)
  629. void Plotly.relayout(plotlyLastAcceptedTimeChartId, {
  630. width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth,
  631. });
  632. });
  633. }
  634. hideLoader() {
  635. document.getElementById(LOADER_ID).style.display = 'none';
  636. }
  637. }
  638.  
  639. /** レートを表す難易度円(◒)の HTML 文字列を生成 */
  640. const generateDifficultyCircle = (rating, isSmall = true) => {
  641. const size = isSmall ? 12 : 36;
  642. const borderWidth = isSmall ? 1 : 3;
  643. const style = `display:inline-block;border-radius:50%;border-style:solid;border-width:${borderWidth}px;` +
  644. `margin-right:5px;vertical-align:initial;height:${size}px;width:${size}px;`;
  645. if (rating < 3200) {
  646. // 色と円がどのぐらい満ちているかを計算
  647. const color = getColor(rating);
  648. const percentFull = ((rating % 400) / 400) * 100;
  649. // ◒を生成
  650. return (`
  651. <span style='${style}border-color:${color};background:` +
  652. `linear-gradient(to top, ${color} 0%, ${color} ${percentFull}%, ` +
  653. `rgba(0, 0, 0, 0) ${percentFull}%, rgba(0, 0, 0, 0) 100%); '>
  654. </span>`);
  655. }
  656. // 金銀銅は例外処理
  657. else if (rating < 3600) {
  658. return (`<span style="${style}border-color: rgb(150, 92, 44);` +
  659. 'background: linear-gradient(to right, rgb(150, 92, 44), rgb(255, 218, 189), rgb(150, 92, 44));"></span>');
  660. }
  661. else if (rating < 4000) {
  662. return (`<span style="${style}border-color: rgb(128, 128, 128);` +
  663. 'background: linear-gradient(to right, rgb(128, 128, 128), white, rgb(128, 128, 128));"></span>');
  664. }
  665. else {
  666. return (`<span style="${style}border-color: rgb(255, 215, 0);` +
  667. 'background: linear-gradient(to right, rgb(255, 215, 0), white, rgb(255, 215, 0));"></span>');
  668. }
  669. };
  670.  
  671. const COL_PER_ROW = 20;
  672. class DifficyltyTable {
  673. constructor(parent, tasks, isEstimationEnabled, dc, taskAcceptedCounts, yourTaskAcceptedElapsedTimes, acCountPredicted) {
  674. // insert
  675. parent.insertAdjacentHTML('beforeend', `
  676. <p><span class="h2">Difficulty</span></p>
  677. <div id="acssa-table-wrapper">
  678. ${rangeLen(Math.ceil(tasks.length / COL_PER_ROW))
  679. .map((tableIdx) => `
  680. <table id="acssa-table-${tableIdx}" class="table table-bordered table-hover th-center td-center td-middle acssa-table">
  681. <tbody>
  682. <tr id="acssa-thead-${tableIdx}" class="acssa-thead"></tr>
  683. </tbody>
  684. <tbody>
  685. <tr id="acssa-tbody-${tableIdx}" class="acssa-tbody"></tr>
  686. ${isEstimationEnabled
  687. ? `<tr id="acssa-tbody-predicted-${tableIdx}" class="acssa-tbody"></tr>`
  688. : ''}
  689. </tbody>
  690. </table>
  691. `)
  692. .join('')}
  693. </div>
  694. `);
  695. if (isEstimationEnabled) {
  696. for (let tableIdx = 0; tableIdx < Math.ceil(tasks.length / COL_PER_ROW); ++tableIdx) {
  697. document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', `<th></th>`);
  698. document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Current</td>`);
  699. document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Predicted</td>`);
  700. }
  701. }
  702. // build
  703. for (let j = 0; j < tasks.length; ++j) {
  704. const tableIdx = Math.floor(j / COL_PER_ROW);
  705. const correctedDifficulty = dc.binarySearchCorrectedDifficulty(taskAcceptedCounts[j]);
  706. const tdClass = yourTaskAcceptedElapsedTimes[j] === -1 ? '' : 'class="success acssa-task-success"';
  707. document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', `
  708. <td ${tdClass}>
  709. ${tasks[j].Assignment}
  710. </td>
  711. `);
  712. const id = `td-assa-difficulty-${j}`;
  713. document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', `
  714. <td ${tdClass} id="${id}" style="color:${getColor(correctedDifficulty)};">
  715. ${correctedDifficulty === 9999 ? '-' : correctedDifficulty}</td>
  716. `);
  717. if (correctedDifficulty !== 9999) {
  718. document.getElementById(id).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedDifficulty));
  719. }
  720. if (isEstimationEnabled) {
  721. const correctedPredictedDifficulty = dc.binarySearchCorrectedDifficulty(acCountPredicted[j]);
  722. const idPredicted = `td-assa-difficulty-predicted-${j}`;
  723. document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', `
  724. <td ${tdClass} id="${idPredicted}" style="color:${getColor(correctedPredictedDifficulty)};">
  725. ${correctedPredictedDifficulty === 9999 ? '-' : correctedPredictedDifficulty}</td>
  726. `);
  727. if (correctedPredictedDifficulty !== 9999) {
  728. document.getElementById(idPredicted).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedPredictedDifficulty));
  729. }
  730. }
  731. }
  732. }
  733. }
  734.  
  735. var html = "<p><span class=\"h2\">Chart</span></p>\n<div id=\"acssa-tab-wrapper\">\n <ul class=\"nav nav-pills small\" id=\"acssa-chart-tab\">\n <li class=\"active\">\n <a class=\"acssa-chart-tab-button\"><span class=\"glyphicon glyphicon-stats\" aria-hidden=\"true\"></span>Difficulty</a>\n </li>\n <li>\n <a class=\"acssa-chart-tab-button\"><span class=\"glyphicon glyphicon-stats\" aria-hidden=\"true\"></span>AC\n Count</a>\n </li>\n <li>\n <a class=\"acssa-chart-tab-button\"><span class=\"glyphicon glyphicon-stats\" aria-hidden=\"true\"></span>LastAcceptedTime</a>\n </li>\n </ul>\n <ul class=\"nav nav-pills\" id=\"acssa-checkbox-tab\">\n <li id=\"acssa-checkbox-toggle-your-result-visibility-parent\">\n <a><label><input type=\"checkbox\" id=\"acssa-checkbox-toggle-your-result-visibility\" checked=\"checked\"> Plot your\n result</label></a>\n </li>\n <li id=\"acssa-checkbox-toggle-log-plot-parent\">\n <a><label><input type=\"checkbox\" id=\"acssa-checkbox-toggle-log-plot\">Log plot</label></a>\n </li>\n <li>\n <a><label><input type=\"checkbox\" id=\"acssa-checkbox-toggle-onload-plot\">Onload plot</label></a>\n </li>\n </ul>\n</div>";
  736.  
  737. const TABS_WRAPPER_ID = 'acssa-tab-wrapper';
  738. const CHART_TAB_ID = 'acssa-chart-tab';
  739. const CHART_TAB_BUTTON_CLASS = 'acssa-chart-tab-button';
  740. const CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY = 'acssa-checkbox-toggle-your-result-visibility';
  741. const PARENT_CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY = `${CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY}-parent`;
  742. const CHECKBOX_TOGGLE_LOG_PLOT = 'acssa-checkbox-toggle-log-plot';
  743. const CHECKBOX_TOGGLE_ONLOAD_PLOT = 'acssa-checkbox-toggle-onload-plot';
  744. const CONFIG_CNLOAD_PLOT_KEY = 'acssa-config-onload-plot';
  745. const PARENT_CHECKBOX_TOGGLE_LOG_PLOT = `${CHECKBOX_TOGGLE_LOG_PLOT}-parent`;
  746. class Tabs {
  747. constructor(parent, yourScore, participants) {
  748. var _a;
  749. this.yourScore = yourScore;
  750. this.participants = participants;
  751. // insert
  752. parent.insertAdjacentHTML('beforeend', html);
  753. this.showYourResultCheckbox = document.getElementById(CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY);
  754. this.logPlotCheckbox = document.getElementById(CHECKBOX_TOGGLE_LOG_PLOT);
  755. this.logPlotCheckboxParent = document.getElementById(PARENT_CHECKBOX_TOGGLE_LOG_PLOT);
  756. this.onloadPlotCheckbox = document.getElementById(CHECKBOX_TOGGLE_ONLOAD_PLOT);
  757. this.onloadPlot = JSON.parse((_a = localStorage.getItem(CONFIG_CNLOAD_PLOT_KEY)) !== null && _a !== void 0 ? _a : 'true');
  758. this.onloadPlotCheckbox.checked = this.onloadPlot;
  759. // チェックボックス操作時のイベントを登録する */
  760. this.showYourResultCheckbox.addEventListener('change', () => {
  761. if (this.showYourResultCheckbox.checked) {
  762. document.querySelectorAll('.acssa-task-success.acssa-task-success-suppress').forEach((elm) => {
  763. elm.classList.remove('acssa-task-success-suppress');
  764. });
  765. }
  766. else {
  767. document.querySelectorAll('.acssa-task-success').forEach((elm) => {
  768. elm.classList.add('acssa-task-success-suppress');
  769. });
  770. }
  771. });
  772. this.showYourResultCheckbox.addEventListener('change', () => {
  773. void this.onShowYourResultCheckboxChangedAsync();
  774. });
  775. this.logPlotCheckbox.addEventListener('change', () => {
  776. void this.onLogPlotCheckboxChangedAsync();
  777. });
  778. this.onloadPlotCheckbox.addEventListener('change', () => {
  779. this.onloadPlot = this.onloadPlotCheckbox.checked;
  780. localStorage.setItem(CONFIG_CNLOAD_PLOT_KEY, JSON.stringify(this.onloadPlot));
  781. });
  782. this.activeTab = 0;
  783. this.showYourResult = [true, true, true];
  784. this.acceptedCountYMax = -1;
  785. this.useLogPlot = [false, false, false];
  786. this.yourDifficultyChartData = null;
  787. this.yourAcceptedCountChartData = null;
  788. this.yourLastAcceptedTimeChartData = null;
  789. this.yourLastAcceptedTimeChartDataIndex = -1;
  790. document
  791. .querySelectorAll(`.${CHART_TAB_BUTTON_CLASS}`)
  792. .forEach((btn, key) => {
  793. btn.addEventListener('click', () => void this.onTabButtonClicked(btn, key));
  794. });
  795. if (this.yourScore == -1) {
  796. // disable checkbox
  797. this.showYourResultCheckbox.checked = false;
  798. this.showYourResultCheckbox.disabled = true;
  799. const checkboxParent = this.showYourResultCheckbox.parentElement;
  800. checkboxParent.style.cursor = 'default';
  801. checkboxParent.style.textDecoration = 'line-through';
  802. }
  803. }
  804. async onShowYourResultCheckboxChangedAsync() {
  805. this.showYourResult[this.activeTab] = this.showYourResultCheckbox.checked;
  806. if (this.showYourResultCheckbox.checked) {
  807. // show
  808. switch (this.activeTab) {
  809. case 0:
  810. if (this.yourScore > 0 && this.yourDifficultyChartData !== null)
  811. await Plotly.addTraces(plotlyDifficultyChartId, this.yourDifficultyChartData);
  812. break;
  813. case 1:
  814. if (this.yourScore > 0 && this.yourAcceptedCountChartData !== null)
  815. await Plotly.addTraces(plotlyAcceptedCountChartId, this.yourAcceptedCountChartData);
  816. break;
  817. case 2:
  818. if (this.yourLastAcceptedTimeChartData !== null && this.yourLastAcceptedTimeChartDataIndex != -1) {
  819. await Plotly.addTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartData, this.yourLastAcceptedTimeChartDataIndex);
  820. }
  821. break;
  822. }
  823. }
  824. else {
  825. // hide
  826. switch (this.activeTab) {
  827. case 0:
  828. if (this.yourScore > 0)
  829. await Plotly.deleteTraces(plotlyDifficultyChartId, -1);
  830. break;
  831. case 1:
  832. if (this.yourScore > 0)
  833. await Plotly.deleteTraces(plotlyAcceptedCountChartId, -1);
  834. break;
  835. case 2:
  836. if (this.yourLastAcceptedTimeChartDataIndex != -1) {
  837. await Plotly.deleteTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartDataIndex);
  838. }
  839. break;
  840. }
  841. }
  842. } // end async onShowYourResultCheckboxChangedAsync()
  843. async onLogPlotCheckboxChangedAsync() {
  844. if (this.acceptedCountYMax == -1)
  845. return;
  846. this.useLogPlot[this.activeTab] = this.logPlotCheckbox.checked;
  847. if (this.activeTab == 1) {
  848. if (this.logPlotCheckbox.checked) {
  849. // log plot
  850. const layout = {
  851. yaxis: {
  852. type: 'log',
  853. range: [Math.log10(0.5), Math.log10(this.acceptedCountYMax)],
  854. },
  855. };
  856. await Plotly.relayout(plotlyAcceptedCountChartId, layout);
  857. }
  858. else {
  859. // linear plot
  860. const layout = {
  861. yaxis: {
  862. type: 'linear',
  863. range: [0, this.acceptedCountYMax],
  864. },
  865. };
  866. await Plotly.relayout(plotlyAcceptedCountChartId, layout);
  867. }
  868. }
  869. else if (this.activeTab == 2) {
  870. if (this.logPlotCheckbox.checked) {
  871. // log plot
  872. const layout = {
  873. xaxis: {
  874. type: 'log',
  875. range: [Math.log10(0.5), Math.log10(this.participants)],
  876. },
  877. };
  878. await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
  879. }
  880. else {
  881. // linear plot
  882. const layout = {
  883. xaxis: {
  884. type: 'linear',
  885. range: [0, this.participants],
  886. },
  887. };
  888. await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout);
  889. }
  890. }
  891. } // end async onLogPlotCheckboxChangedAsync
  892. async onTabButtonClicked(btn, key) {
  893. // check whether active or not
  894. const buttonParent = btn.parentElement;
  895. if (buttonParent.className == 'active')
  896. return;
  897. // modify visibility
  898. this.activeTab = key;
  899. document.querySelector(`#${CHART_TAB_ID} li.active`).classList.remove('active');
  900. document.querySelector(`#${CHART_TAB_ID} li:nth-child(${key + 1})`).classList.add('active');
  901. document.querySelector('#acssa-chart-block div.acssa-chart-wrapper-active').classList.remove('acssa-chart-wrapper-active');
  902. document.querySelector(`#acssa-chart-block div.acssa-chart-wrapper:nth-child(${key + 1})`).classList.add('acssa-chart-wrapper-active');
  903. // resize charts
  904. switch (key) {
  905. case 0:
  906. await Plotly.relayout(plotlyDifficultyChartId, {
  907. width: document.getElementById(plotlyDifficultyChartId).clientWidth,
  908. });
  909. this.logPlotCheckboxParent.style.display = 'none';
  910. break;
  911. case 1:
  912. await Plotly.relayout(plotlyAcceptedCountChartId, {
  913. width: document.getElementById(plotlyAcceptedCountChartId).clientWidth,
  914. });
  915. this.logPlotCheckboxParent.style.display = 'block';
  916. break;
  917. case 2:
  918. await Plotly.relayout(plotlyLastAcceptedTimeChartId, {
  919. width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth,
  920. });
  921. this.logPlotCheckboxParent.style.display = 'block';
  922. break;
  923. }
  924. if (this.showYourResult[this.activeTab] !== this.showYourResultCheckbox.checked) {
  925. await this.onShowYourResultCheckboxChangedAsync();
  926. }
  927. if (this.activeTab !== 0 && this.useLogPlot[this.activeTab] !== this.logPlotCheckbox.checked) {
  928. await this.onLogPlotCheckboxChangedAsync();
  929. }
  930. }
  931. showTabsControl() {
  932. document.getElementById(TABS_WRAPPER_ID).style.display = 'block';
  933. if (!this.onloadPlot) {
  934. document.getElementById(CHART_TAB_ID).style.display = 'none';
  935. document.getElementById(PARENT_CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY).style.display =
  936. 'none';
  937. }
  938. }
  939. }
  940.  
  941. const finf = bigf(400);
  942. function bigf(n) {
  943. let pow1 = 1;
  944. let pow2 = 1;
  945. let numerator = 0;
  946. let denominator = 0;
  947. for (let i = 0; i < n; ++i) {
  948. pow1 *= 0.81;
  949. pow2 *= 0.9;
  950. numerator += pow1;
  951. denominator += pow2;
  952. }
  953. return Math.sqrt(numerator) / denominator;
  954. }
  955. function f(n) {
  956. return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0;
  957. }
  958. /**
  959. * calculate unpositivized rating from last state
  960. * @param {Number} [last] last unpositivized rating
  961. * @param {Number} [perf] performance
  962. * @param {Number} [ratedMatches] count of participated rated contest
  963. * @returns {number} estimated unpositivized rating
  964. */
  965. function calcRatingFromLast(last, perf, ratedMatches) {
  966. if (ratedMatches === 0)
  967. return perf - 1200;
  968. last += f(ratedMatches);
  969. const weight = 9 - 9 * Math.pow(0.9, ratedMatches);
  970. const numerator = weight * Math.pow(2, last / 800.0) + Math.pow(2, perf / 800.0);
  971. const denominator = 1 + weight;
  972. return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1);
  973. }
  974. // class Random {
  975. // x: number
  976. // y: number
  977. // z: number
  978. // w: number
  979. // constructor(seed = 88675123) {
  980. // this.x = 123456789;
  981. // this.y = 362436069;
  982. // this.z = 521288629;
  983. // this.w = seed;
  984. // }
  985. // // XorShift
  986. // next(): number {
  987. // let t;
  988. // t = this.x ^ (this.x << 11);
  989. // this.x = this.y; this.y = this.z; this.z = this.w;
  990. // return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8));
  991. // }
  992. // // min以上max以下の乱数を生成する
  993. // nextInt(min: number, max: number): number {
  994. // const r = Math.abs(this.next());
  995. // return min + (r % (max + 1 - min));
  996. // }
  997. // };
  998. class PerformanceTable {
  999. constructor(parent, tasks, isEstimationEnabled, yourStandingsEntry, taskAcceptedCounts, acCountPredicted, standingsData, innerRatingsFromPredictor, dcForPerformance, centerOfInnerRating, useRating) {
  1000. this.centerOfInnerRating = centerOfInnerRating;
  1001. if (yourStandingsEntry === undefined)
  1002. return;
  1003. // コンテスト終了時点での順位表を予測する
  1004. const len = acCountPredicted.length;
  1005. const rems = [];
  1006. for (let i = 0; i < len; ++i) {
  1007. rems.push(Math.ceil(acCountPredicted[i] - taskAcceptedCounts[i])); //
  1008. }
  1009. const scores = []; // (現レート,スコア合計,時間,問題ごとのスコア,rated)
  1010. const highestScores = tasks.map(() => 0);
  1011. let rowPtr = undefined;
  1012. // const ratedInnerRatings: Rating[] = [];
  1013. const ratedUserRanks = [];
  1014. // console.log(standingsData);
  1015. const threthold = moment('2021-12-03T21:00:00+09:00');
  1016. const isAfterABC230 = startTime >= threthold;
  1017. // OldRating が全員 0 なら,強制的に Rating を使用する(コンテスト終了後,レート更新前)
  1018. standingsData.forEach((standingsEntry) => {
  1019. const userScores = [];
  1020. let penalty = 0;
  1021. for (let j = 0; j < tasks.length; ++j) {
  1022. const taskResultEntry = standingsEntry.TaskResults[tasks[j].TaskScreenName];
  1023. if (!taskResultEntry) {
  1024. // 未提出
  1025. userScores.push(0);
  1026. }
  1027. else {
  1028. userScores.push(taskResultEntry.Score / 100);
  1029. highestScores[j] = Math.max(highestScores[j], taskResultEntry.Score / 100);
  1030. penalty += taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty;
  1031. }
  1032. }
  1033. // const isRated = standingsEntry.IsRated && standingsEntry.TotalResult.Count > 0;
  1034. const isRated = standingsEntry.IsRated && (isAfterABC230 || standingsEntry.TotalResult.Count > 0);
  1035. if (!isRated) {
  1036. if (standingsEntry.TotalResult.Score === 0 && penalty === 0 && standingsEntry.TotalResult.Count == 0) {
  1037. return; // NoSub を飛ばす
  1038. }
  1039. }
  1040. standingsEntry.Rating;
  1041. // const innerRating: Rating = isTeamOrBeginner
  1042. // ? correctedRating
  1043. // : standingsEntry.UserScreenName in innerRatingsFromPredictor
  1044. // ? innerRatingsFromPredictor[standingsEntry.UserScreenName]
  1045. // : RatingConverter.toInnerRating(
  1046. // Math.max(RatingConverter.toRealRating(correctedRating), 1),
  1047. // standingsEntry.Competitions
  1048. // );
  1049. const innerRating = standingsEntry.UserScreenName in innerRatingsFromPredictor
  1050. ? innerRatingsFromPredictor[standingsEntry.UserScreenName]
  1051. : this.centerOfInnerRating;
  1052. if (isRated) {
  1053. // ratedInnerRatings.push(innerRating);
  1054. ratedUserRanks.push(standingsEntry.EntireRank);
  1055. // if (innerRating || true) {
  1056. const row = [
  1057. innerRating,
  1058. standingsEntry.TotalResult.Score / 100,
  1059. standingsEntry.TotalResult.Elapsed + 300 * standingsEntry.TotalResult.Penalty,
  1060. userScores,
  1061. isRated,
  1062. ];
  1063. scores.push(row);
  1064. if ((standingsEntry.UserScreenName == userScreenName)) {
  1065. rowPtr = row;
  1066. }
  1067. // }
  1068. }
  1069. });
  1070. const sameRatedRankCount = ratedUserRanks.reduce((prev, cur) => {
  1071. if (cur == yourStandingsEntry.EntireRank)
  1072. prev++;
  1073. return prev;
  1074. }, 0);
  1075. const ratedRank = ratedUserRanks.reduce((prev, cur) => {
  1076. if (cur < yourStandingsEntry.EntireRank)
  1077. prev += 1;
  1078. return prev;
  1079. }, (1 + sameRatedRankCount) / 2);
  1080. // レート順でソート
  1081. scores.sort((a, b) => {
  1082. const [innerRatingA, scoreA, timeElapsedA] = a;
  1083. const [innerRatingB, scoreB, timeElapsedB] = b;
  1084. if (innerRatingA != innerRatingB) {
  1085. return innerRatingB - innerRatingA; // 降順(レートが高い順)
  1086. }
  1087. if (scoreA != scoreB) {
  1088. return scoreB - scoreA; // 降順(順位が高い順)
  1089. }
  1090. return timeElapsedA - timeElapsedB; // 昇順(順位が高い順)
  1091. });
  1092. // const random = new Random(0);
  1093. // スコア変化をシミュレート
  1094. // (現レート,スコア合計,時間,問題ごとのスコア,rated)
  1095. scores.forEach((score) => {
  1096. const [, , , scoresA] = score;
  1097. // 自分は飛ばす
  1098. if (score == rowPtr)
  1099. return;
  1100. for (let j = 0; j < tasks.length; ++j) {
  1101. // if (random.nextInt(0, 9) <= 2) continue;
  1102. // まだ満点ではなく,かつ正解者を増やせるなら
  1103. if (scoresA[j] < highestScores[j] && rems[j] > 0) {
  1104. const dif = highestScores[j] - scoresA[j];
  1105. score[1] += dif;
  1106. score[2] += 1000000000 * 60 * 30; // とりあえず30分で解くと仮定する
  1107. scoresA[j] = highestScores[j];
  1108. rems[j]--;
  1109. }
  1110. if (rems[j] == 0)
  1111. break;
  1112. }
  1113. });
  1114. // 順位でソート
  1115. scores.sort((a, b) => {
  1116. const [innerRatingA, scoreA, timeElapsedA, ,] = a;
  1117. const [innerRatingB, scoreB, timeElapsedB, ,] = b;
  1118. if (scoreA != scoreB) {
  1119. return scoreB - scoreA; // 降順(順位が高い順)
  1120. }
  1121. if (timeElapsedA != timeElapsedB) {
  1122. return timeElapsedA - timeElapsedB; // 昇順(順位が高い順)
  1123. }
  1124. return innerRatingB - innerRatingA; // 降順(レートが高い順)
  1125. });
  1126. // 順位を求める
  1127. let estimatedRank = -1;
  1128. let rank = 0;
  1129. let sameCnt = 0;
  1130. for (let i = 0; i < scores.length; ++i) {
  1131. if (estimatedRank == -1) {
  1132. if (scores[i][4] === true) {
  1133. rank++;
  1134. }
  1135. if (scores[i] === rowPtr) {
  1136. if (rank === 0)
  1137. rank = 1;
  1138. estimatedRank = rank;
  1139. // break;
  1140. }
  1141. }
  1142. else {
  1143. if (rowPtr === undefined)
  1144. break;
  1145. if (scores[i][1] === rowPtr[1] && scores[i][2] === rowPtr[2]) {
  1146. sameCnt++;
  1147. }
  1148. else {
  1149. break;
  1150. }
  1151. }
  1152. } //1246
  1153. estimatedRank += sameCnt / 2;
  1154. // const dc = new DifficultyCalculator(ratedInnerRatings);
  1155. // insert
  1156. parent.insertAdjacentHTML('beforeend', `
  1157. <p><span class="h2">Performance</span></p>
  1158. <div id="acssa-perf-table-wrapper">
  1159. <table id="acssa-perf-table" class="table table-bordered table-hover th-center td-center td-middle acssa-table">
  1160. <tbody>
  1161. <tr class="acssa-thead">
  1162. ${isEstimationEnabled ? '<td></td>' : ''}
  1163. <td id="acssa-thead-perf" class="acssa-thead">perf</td>
  1164. <td id="acssa-thead-perf" class="acssa-thead">レート変化</td>
  1165. </tr>
  1166. </tbody>
  1167. <tbody>
  1168. <tr id="acssa-perf-tbody" class="acssa-tbody"></tr>
  1169. ${isEstimationEnabled
  1170. ? `
  1171. <tr id="acssa-perf-tbody-predicted" class="acssa-tbody"></tr>
  1172. `
  1173. : ''}
  1174. </tbody>
  1175. </table>
  1176. </div>
  1177. `);
  1178. if (isEstimationEnabled) {
  1179. document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `<th>Current</td>`);
  1180. document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `<th>Predicted</td>`);
  1181. }
  1182. // build
  1183. const id = `td-assa-perf-current`;
  1184. // TODO: ちゃんと判定する
  1185. // const perf = Math.min(2400, dc.rank2InnerPerf(ratedRank));
  1186. const perf = RatingConverter.toCorrectedRating(dcForPerformance.rank2InnerPerf(ratedRank));
  1187. //
  1188. document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `
  1189. <td id="${id}" style="color:${getColor(perf)};">
  1190. ${perf === 9999 ? '-' : perf}</td>
  1191. `);
  1192. if (perf !== 9999) {
  1193. document.getElementById(id).insertAdjacentHTML('afterbegin', generateDifficultyCircle(perf));
  1194. const oldRating = useRating ? yourStandingsEntry.Rating : yourStandingsEntry.OldRating;
  1195. // const oldRating = yourStandingsEntry.Rating;
  1196. const nextRating = Math.round(RatingConverter.toCorrectedRating(calcRatingFromLast(RatingConverter.toRealRating(oldRating), perf, yourStandingsEntry.Competitions)));
  1197. const sign = nextRating > oldRating ? '+' : nextRating < oldRating ? '-' : '±';
  1198. document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `
  1199. <td>
  1200. <span style="font-weight:bold;color:${getColor(oldRating)}">${oldRating}</span>
  1201. <span style="font-weight:bold;color:${getColor(nextRating)}">${nextRating}</span>
  1202. <span style="color:gray">(${sign}${Math.abs(nextRating - oldRating)})</span>
  1203. </td>
  1204. `);
  1205. }
  1206. if (isEstimationEnabled) {
  1207. if (estimatedRank != -1) {
  1208. const perfEstimated = RatingConverter.toCorrectedRating(dcForPerformance.rank2InnerPerf(estimatedRank));
  1209. const id2 = `td-assa-perf-predicted`;
  1210. document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `
  1211. <td id="${id2}" style="color:${getColor(perfEstimated)};">
  1212. ${perfEstimated === 9999 ? '-' : perfEstimated}</td>
  1213. `);
  1214. if (perfEstimated !== 9999) {
  1215. document.getElementById(id2).insertAdjacentHTML('afterbegin', generateDifficultyCircle(perfEstimated));
  1216. const oldRating = useRating ? yourStandingsEntry.Rating : yourStandingsEntry.OldRating;
  1217. // const oldRating = yourStandingsEntry.Rating;
  1218. const nextRating = Math.round(RatingConverter.toCorrectedRating(calcRatingFromLast(RatingConverter.toRealRating(oldRating), perfEstimated, yourStandingsEntry.Competitions)));
  1219. const sign = nextRating > oldRating ? '+' : nextRating < oldRating ? '-' : '±';
  1220. document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `
  1221. <td>
  1222. <span style="font-weight:bold;color:${getColor(oldRating)}">${oldRating}</span>
  1223. <span style="font-weight:bold;color:${getColor(nextRating)}">${nextRating}</span>
  1224. <span style="color:gray">(${sign}${Math.abs(nextRating - oldRating)})</span>
  1225. </td>
  1226. `);
  1227. }
  1228. }
  1229. else {
  1230. document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', '<td>?</td>');
  1231. }
  1232. }
  1233. }
  1234. }
  1235.  
  1236. const NS2SEC = 1000000000;
  1237. const CONTENT_DIV_ID = 'acssa-contents';
  1238. class Parent {
  1239. constructor(acRatioModel, centerOfInnerRating) {
  1240. const loaderStyles = GM_getResourceText('loaders.min.css');
  1241. GM_addStyle(loaderStyles + '\n' + css);
  1242. // this.centerOfInnerRating = getCenterOfInnerRating(contestScreenName);
  1243. this.centerOfInnerRating = centerOfInnerRating;
  1244. this.acRatioModel = acRatioModel;
  1245. this.working = false;
  1246. this.oldStandingsData = null;
  1247. this.hasTeamStandings = this.searchTeamStandingsPage();
  1248. this.yourStandingsEntry = undefined;
  1249. }
  1250. searchTeamStandingsPage() {
  1251. const teamStandingsLink = document.querySelector(`a[href*="/contests/${contestScreenName}/standings/team"]`);
  1252. return teamStandingsLink !== null;
  1253. }
  1254. async onStandingsChanged(standings) {
  1255. if (!standings)
  1256. return;
  1257. if (this.working)
  1258. return;
  1259. this.tasks = standings.TaskInfo;
  1260. const standingsData = standings.StandingsData; // vueStandings.filteredStandings;
  1261. if (this.oldStandingsData === standingsData)
  1262. return;
  1263. if (this.tasks.length === 0)
  1264. return;
  1265. this.oldStandingsData = standingsData;
  1266. this.working = true;
  1267. this.removeOldContents();
  1268. const currentTime = moment();
  1269. this.elapsedMinutes = Math.floor(currentTime.diff(startTime) / 60 / 1000);
  1270. this.isDuringContest = startTime <= currentTime && currentTime < endTime;
  1271. this.isEstimationEnabled = this.isDuringContest && this.elapsedMinutes >= 1 && this.tasks.length < 10;
  1272. const useRating = this.isDuringContest || this.areOldRatingsAllZero(standingsData);
  1273. this.innerRatingsFromPredictor = await fetchInnerRatingsFromPredictor(contestScreenName);
  1274. this.scanStandingsData(standingsData);
  1275. this.predictAcCountSeries();
  1276. const standingsElement = document.getElementById('vue-standings');
  1277. const acssaContentDiv = document.createElement('div');
  1278. acssaContentDiv.id = CONTENT_DIV_ID;
  1279. standingsElement.insertAdjacentElement('afterbegin', acssaContentDiv);
  1280. if (this.hasTeamStandings) {
  1281. if (!location.href.includes('/standings/team')) {
  1282. // チーム戦順位表へ誘導
  1283. acssaContentDiv.insertAdjacentHTML('afterbegin', teamalert);
  1284. }
  1285. }
  1286. // difficulty
  1287. new DifficyltyTable(acssaContentDiv, this.tasks, this.isEstimationEnabled, this.dcForDifficulty, this.taskAcceptedCounts, this.yourTaskAcceptedElapsedTimes, this.acCountPredicted);
  1288. new PerformanceTable(acssaContentDiv, this.tasks, this.isEstimationEnabled, this.yourStandingsEntry, this.taskAcceptedCounts, this.acCountPredicted, standingsData, this.innerRatingsFromPredictor, this.dcForPerformance, this.centerOfInnerRating, useRating);
  1289. // console.log(this.yourStandingsEntry);
  1290. // console.log(this.yourStandingsEntry?.EntireRank);
  1291. // console.log(this.dc.rank2InnerPerf((this.yourStandingsEntry?.EntireRank ?? 10000) - 0));
  1292. // tabs
  1293. const tabs = new Tabs(acssaContentDiv, this.yourScore, this.participants);
  1294. const charts = new Charts(acssaContentDiv, this.tasks, this.scoreLastAcceptedTimeMap, this.taskAcceptedCounts, this.taskAcceptedElapsedTimes, this.yourTaskAcceptedElapsedTimes, this.yourScore, this.yourLastAcceptedTime, this.participants, this.dcForDifficulty, this.dcForPerformance, tabs);
  1295. if (tabs.onloadPlot) {
  1296. // 順位表のその他の描画を優先するために,プロットは後回しにする
  1297. void charts.plotAsync().then(() => {
  1298. charts.hideLoader();
  1299. tabs.showTabsControl();
  1300. this.working = false;
  1301. });
  1302. }
  1303. else {
  1304. charts.hideLoader();
  1305. tabs.showTabsControl();
  1306. }
  1307. }
  1308. removeOldContents() {
  1309. const oldContents = document.getElementById(CONTENT_DIV_ID);
  1310. if (oldContents) {
  1311. // oldContents.parentNode.removeChild(oldContents);
  1312. oldContents.remove();
  1313. }
  1314. }
  1315. scanStandingsData(standingsData) {
  1316. // init
  1317. this.scoreLastAcceptedTimeMap = new Map();
  1318. this.taskAcceptedCounts = rangeLen(this.tasks.length).fill(0);
  1319. this.taskAcceptedElapsedTimes = rangeLen(this.tasks.length).map(() => []);
  1320. this.innerRatings = [];
  1321. this.ratedInnerRatings = [];
  1322. this.yourTaskAcceptedElapsedTimes = rangeLen(this.tasks.length).fill(-1);
  1323. this.yourScore = -1;
  1324. this.yourLastAcceptedTime = -1;
  1325. this.participants = 0;
  1326. this.yourStandingsEntry = undefined;
  1327. // scan
  1328. const threthold = moment('2021-12-03T21:00:00+09:00');
  1329. const isAfterABC230 = startTime >= threthold;
  1330. for (let i = 0; i < standingsData.length; ++i) {
  1331. const standingsEntry = standingsData[i];
  1332. const isRated = standingsEntry.IsRated && (isAfterABC230 || standingsEntry.TotalResult.Count > 0);
  1333. // const innerRating: Rating = isTeamOrBeginner
  1334. // ? correctedRating
  1335. // : standingsEntry.UserScreenName in this.innerRatingsFromPredictor
  1336. // ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName]
  1337. // : RatingConverter.toInnerRating(
  1338. // Math.max(RatingConverter.toRealRating(correctedRating), 1),
  1339. // standingsEntry.Competitions
  1340. // );
  1341. const innerRating = standingsEntry.UserScreenName in this.innerRatingsFromPredictor
  1342. ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName]
  1343. : this.centerOfInnerRating;
  1344. if (isRated) {
  1345. this.ratedInnerRatings.push(innerRating);
  1346. }
  1347. if (!standingsEntry.TaskResults)
  1348. continue; // 参加登録していない
  1349. if (standingsEntry.UserIsDeleted)
  1350. continue; // アカウント削除
  1351. // let correctedRating = this.isDuringContest ? standingsEntry.Rating : standingsEntry.OldRating;
  1352. standingsEntry.Rating;
  1353. // これは飛ばしちゃダメ(提出しても 0 AC だと Penalty == 0 なので)
  1354. // if (standingsEntry.TotalResult.Score == 0 && standingsEntry.TotalResult.Penalty == 0) continue;
  1355. let score = 0;
  1356. let penalty = 0;
  1357. for (let j = 0; j < this.tasks.length; ++j) {
  1358. const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
  1359. if (!taskResultEntry)
  1360. continue; // 未提出
  1361. score += taskResultEntry.Score;
  1362. penalty += taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty;
  1363. }
  1364. if (score === 0 && penalty === 0 && standingsEntry.TotalResult.Count == 0)
  1365. continue; // NoSub を飛ばす
  1366. this.participants++;
  1367. // console.log(i + 1, score, penalty);
  1368. score /= 100;
  1369. if (this.scoreLastAcceptedTimeMap.has(score)) {
  1370. this.scoreLastAcceptedTimeMap.get(score).push(standingsEntry.TotalResult.Elapsed / NS2SEC);
  1371. }
  1372. else {
  1373. this.scoreLastAcceptedTimeMap.set(score, [standingsEntry.TotalResult.Elapsed / NS2SEC]);
  1374. }
  1375. // console.log(this.isDuringContest, standingsEntry.Rating, standingsEntry.OldRating, innerRating);
  1376. // if (standingsEntry.IsRated && innerRating) {
  1377. // if (innerRating) {
  1378. // this.innerRatings.push(innerRating);
  1379. // } else {
  1380. // console.log(i, innerRating, correctedRating, standingsEntry.Competitions, standingsEntry, this.innerRatingsFromPredictor[standingsEntry.UserScreenName]);
  1381. // continue;
  1382. // }
  1383. this.innerRatings.push(innerRating);
  1384. for (let j = 0; j < this.tasks.length; ++j) {
  1385. const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
  1386. const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1;
  1387. if (isAccepted) {
  1388. ++this.taskAcceptedCounts[j];
  1389. this.taskAcceptedElapsedTimes[j].push(taskResultEntry.Elapsed / NS2SEC);
  1390. }
  1391. }
  1392. if ((standingsEntry.UserScreenName == userScreenName)) {
  1393. this.yourScore = score;
  1394. this.yourLastAcceptedTime = standingsEntry.TotalResult.Elapsed / NS2SEC;
  1395. this.yourStandingsEntry = standingsEntry;
  1396. for (let j = 0; j < this.tasks.length; ++j) {
  1397. const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName];
  1398. const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1;
  1399. if (isAccepted) {
  1400. this.yourTaskAcceptedElapsedTimes[j] = taskResultEntry.Elapsed / NS2SEC;
  1401. }
  1402. }
  1403. }
  1404. } // end for
  1405. this.innerRatings.sort((a, b) => a - b);
  1406. this.dcForDifficulty = new DifficultyCalculator(this.innerRatings);
  1407. this.dcForPerformance = new DifficultyCalculator(this.ratedInnerRatings);
  1408. } // end async scanStandingsData
  1409. predictAcCountSeries() {
  1410. if (!this.isEstimationEnabled) {
  1411. this.acCountPredicted = [];
  1412. return;
  1413. }
  1414. // 時間ごとの AC 数推移を計算する
  1415. const taskAcceptedCountImos = rangeLen(this.tasks.length).map(() => rangeLen(this.elapsedMinutes).map(() => 0));
  1416. this.taskAcceptedElapsedTimes.forEach((ar, index) => {
  1417. ar.forEach((seconds) => {
  1418. const minutes = Math.floor(seconds / 60);
  1419. if (minutes >= this.elapsedMinutes)
  1420. return;
  1421. taskAcceptedCountImos[index][minutes] += 1;
  1422. });
  1423. });
  1424. const taskAcceptedRatio = rangeLen(this.tasks.length).map(() => []);
  1425. taskAcceptedCountImos.forEach((ar, index) => {
  1426. let cum = 0;
  1427. ar.forEach((imos) => {
  1428. cum += imos;
  1429. taskAcceptedRatio[index].push(cum / this.participants);
  1430. });
  1431. });
  1432. // 差の自乗和が最小になるシーケンスを探す
  1433. this.acCountPredicted = taskAcceptedRatio.map((ar) => {
  1434. if (this.acRatioModel === undefined)
  1435. return 0;
  1436. if (ar[this.elapsedMinutes - 1] === 0)
  1437. return 0;
  1438. let minerror = 1.0 * this.elapsedMinutes;
  1439. // let argmin = '';
  1440. let last_ratio = 0;
  1441. Object.keys(this.acRatioModel).forEach((key) => {
  1442. if (this.acRatioModel === undefined)
  1443. return;
  1444. const ar2 = this.acRatioModel[key];
  1445. let error = 0;
  1446. for (let i = 0; i < this.elapsedMinutes; ++i) {
  1447. error += Math.pow(ar[i] - ar2[i], 2);
  1448. }
  1449. if (error < minerror) {
  1450. minerror = error;
  1451. // argmin = key;
  1452. if (ar2[this.elapsedMinutes - 1] > 0) {
  1453. last_ratio = ar2[ar2.length - 1] * (ar[this.elapsedMinutes - 1] / ar2[this.elapsedMinutes - 1]);
  1454. }
  1455. else {
  1456. last_ratio = ar2[ar2.length - 1];
  1457. }
  1458. }
  1459. });
  1460. // console.log(argmin, minerror, last_ratio);
  1461. if (last_ratio > 1)
  1462. last_ratio = 1;
  1463. return this.participants * last_ratio;
  1464. });
  1465. } // end predictAcCountSeries();
  1466. areOldRatingsAllZero(standingsData) {
  1467. return standingsData.every((standingsEntry) => standingsEntry.OldRating == 0);
  1468. }
  1469. }
  1470. Parent.init = async () => {
  1471. const contestRatedRange = await getContestRatedRangeAsync(contestScreenName);
  1472. const centerOfInnerRating = getCenterOfInnerRatingFromRange(contestRatedRange);
  1473. const curr = moment();
  1474. if (startTime <= curr && curr < endTime) {
  1475. const contestDurationMinutes = endTime.diff(startTime) / 1000 / 60;
  1476. return new Parent(await fetchContestAcRatioModel(contestScreenName, contestDurationMinutes), centerOfInnerRating);
  1477. }
  1478. else {
  1479. return new Parent(undefined, centerOfInnerRating);
  1480. }
  1481. };
  1482.  
  1483. void (async () => {
  1484. const parent = await Parent.init();
  1485. vueStandings.$watch('standings', (standings) => {
  1486. void parent.onStandingsChanged(standings);
  1487. }, { deep: true, immediate: true });
  1488. })();