atcoder-standings-difficulty-analyzer

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

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

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