atcoder-standings-difficulty-analyzer

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

当前为 2022-09-01 提交的版本,查看 最新版本

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