您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
順位表の得点情報を集計し,推定 difficulty やその推移を表示します.
当前为
// ==UserScript== // @name atcoder-standings-difficulty-analyzer // @namespace iilj // @version 2022.5.1 // @description 順位表の得点情報を集計し,推定 difficulty やその推移を表示します. // @author iilj // @license MIT // @supportURL https://github.com/iilj/atcoder-standings-difficulty-analyzer/issues // @match https://atcoder.jp/*standings* // @exclude https://atcoder.jp/*standings/json // @require https://cdnjs.cloudflare.com/ajax/libs/plotly.js/1.33.1/plotly.min.js // @resource loaders.min.css https://cdnjs.cloudflare.com/ajax/libs/loaders.css/0.1.2/loaders.min.css // @grant GM_getResourceText // @grant GM_addStyle // ==/UserScript== 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}"; var teamalert = "<div class=\"alert alert-warning\">\n チーム戦順位表が提供されています.個人単位の順位表ページでは,difficulty 推定値が不正確になります.\n</div>"; const arrayLowerBound = (arr, n) => { let first = 0, last = arr.length - 1, middle; while (first <= last) { middle = 0 | ((first + last) / 2); if (arr[middle] < n) first = middle + 1; else last = middle - 1; } return first; }; const getColor = (rating) => { if (rating < 400) return '#808080'; // gray else if (rating < 800) return '#804000'; // brown else if (rating < 1200) return '#008000'; // green else if (rating < 1600) return '#00C0C0'; // cyan else if (rating < 2000) return '#0000FF'; // blue else if (rating < 2400) return '#C0C000'; // yellow else if (rating < 2800) return '#FF8000'; // orange else if (rating == 9999) return '#000000'; return '#FF0000'; // red }; const formatTimespan = (sec) => { let sign; if (sec >= 0) { sign = ''; } else { sign = '-'; sec *= -1; } return `${sign}${Math.floor(sec / 60)}:${`0${sec % 60}`.slice(-2)}`; }; /** 現在のページから,コンテストの開始から終了までの秒数を抽出する */ const getContestDurationSec = () => { if (contestScreenName.startsWith('past')) { return 300 * 60; } // toDate.diff(fromDate) でミリ秒が返ってくる return endTime.diff(startTime) / 1000; }; const getCenterOfInnerRatingFromRange = (contestRatedRange) => { if (contestScreenName.startsWith('abc')) { return 800; } if (contestScreenName.startsWith('arc')) { const contestNumber = Number(contestScreenName.substring(3, 6)); return contestNumber >= 104 ? 1000 : 1600; } if (contestScreenName.startsWith('agc')) { const contestNumber = Number(contestScreenName.substring(3, 6)); return contestNumber >= 34 ? 1200 : 1600; } if (contestRatedRange[1] === 1999) { return 800; } else if (contestRatedRange[1] === 2799) { return 1000; } else if (contestRatedRange[1] === Infinity) { return 1200; } return 800; }; // ContestRatedRange /* function getContestInformationAsync(contestScreenName) { return __awaiter(this, void 0, void 0, function* () { const html = yield fetchTextDataAsync(`https://atcoder.jp/contests/${contestScreenName}`); const topPageDom = new DOMParser().parseFromString(html, "text/html"); const dataParagraph = topPageDom.getElementsByClassName("small")[0]; const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(":")[1].trim()); return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2])); }); } */ function parseRangeString(s) { s = s.trim(); if (s === '-') return [0, -1]; if (s === 'All') return [0, Infinity]; if (!/[-~]/.test(s)) return [0, -1]; const res = s.split(/[-~]/).map((x) => parseInt(x.trim())); if (res.length !== 2) { throw new Error('res is not [number, number]'); } if (isNaN(res[0])) res[0] = 0; if (isNaN(res[1])) res[1] = Infinity; return res; } const getContestRatedRangeAsync = async (contestScreenName) => { const html = await fetch(`https://atcoder.jp/contests/${contestScreenName}`); const topPageDom = new DOMParser().parseFromString(await html.text(), 'text/html'); const dataParagraph = topPageDom.getElementsByClassName('small')[0]; const data = Array.from(dataParagraph.children).map((x) => x.innerHTML.split(':')[1].trim()); // console.log("data", data); return parseRangeString(data[1]); // return new ContestInformation(parseRangeString(data[0]), parseRangeString(data[1]), parseDurationString(data[2])); }; const rangeLen = (len) => Array.from({ length: len }, (v, k) => k); const BASE_URL = 'https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings'; const fetchJson = async (url) => { const res = await fetch(url); if (!res.ok) { throw new Error(res.statusText); } const obj = (await res.json()); return obj; }; const fetchContestAcRatioModel = async (contestScreenName, contestDurationMinutes) => { // https://raw.githubusercontent.com/iilj/atcoder-standings-difficulty-analyzer/main/json/standings/abc_100m.json let modelLocation = undefined; if (/^agc(\d{3,})$/.exec(contestScreenName)) { if ([110, 120, 130, 140, 150, 160, 180, 200, 210, 240, 270, 300].includes(contestDurationMinutes)) { modelLocation = `${BASE_URL}/agc_${contestDurationMinutes}m.json`; } } else if (/^arc(\d{3,})$/.exec(contestScreenName)) { if ([100, 120, 150].includes(contestDurationMinutes)) { modelLocation = `${BASE_URL}/arc_${contestDurationMinutes}m.json`; } } else if (/^abc(\d{3,})$/.exec(contestScreenName)) { if ([100, 120].includes(contestDurationMinutes)) { modelLocation = `${BASE_URL}/abc_${contestDurationMinutes}m.json`; } } if (modelLocation !== undefined) { return await fetchJson(modelLocation); } return undefined; }; const fetchInnerRatingsFromPredictor = async (contestScreenName) => { const url = `https://data.ac-predictor.com/aperfs/${contestScreenName}.json`; try { return await fetchJson(url); } catch (e) { return {}; } }; class RatingConverter { } /** 表示用の低レート帯補正レート → 低レート帯補正前のレート */ RatingConverter.toRealRating = (correctedRating) => { if (correctedRating >= 400) return correctedRating; else return 400 * (1 - Math.log(400 / correctedRating)); }; /** 低レート帯補正前のレート → 内部レート推定値 */ RatingConverter.toInnerRating = (realRating, comp) => { return (realRating + (1200 * (Math.sqrt(1 - Math.pow(0.81, comp)) / (1 - Math.pow(0.9, comp)) - 1)) / (Math.sqrt(19) - 1)); }; /** 低レート帯補正前のレート → 表示用の低レート帯補正レート */ RatingConverter.toCorrectedRating = (realRating) => { if (realRating >= 400) return realRating; else return Math.floor(400 / Math.exp((400 - realRating) / 400)); }; class DifficultyCalculator { constructor(sortedInnerRatings) { this.innerRatings = sortedInnerRatings; this.prepared = new Map(); this.memo = new Map(); } perf2ExpectedAcceptedCount(m) { let expectedAcceptedCount; if (this.prepared.has(m)) { expectedAcceptedCount = this.prepared.get(m); } else { expectedAcceptedCount = this.innerRatings.reduce((prev_expected_accepts, innerRating) => (prev_expected_accepts += 1 / (1 + Math.pow(6, (m - innerRating) / 400))), 0); this.prepared.set(m, expectedAcceptedCount); } return expectedAcceptedCount; } perf2Ranking(x) { return this.perf2ExpectedAcceptedCount(x) + 0.5; } rank2InnerPerf(rank) { let upper = 9999; let lower = -9999; while (upper - lower > 0.1) { const mid = (upper + lower) / 2; if (rank > this.perf2Ranking(mid)) upper = mid; else lower = mid; } return Math.round((upper + lower) / 2); } /** Difficulty 推定値を算出する */ binarySearchCorrectedDifficulty(acceptedCount) { if (this.memo.has(acceptedCount)) { return this.memo.get(acceptedCount); } let lb = -10000; let ub = 10000; while (ub - lb > 1) { const m = Math.floor((ub + lb) / 2); const expectedAcceptedCount = this.perf2ExpectedAcceptedCount(m); if (expectedAcceptedCount < acceptedCount) ub = m; else lb = m; } const difficulty = lb; const correctedDifficulty = RatingConverter.toCorrectedRating(difficulty); this.memo.set(acceptedCount, correctedDifficulty); return correctedDifficulty; } } 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>"; const LOADER_ID = 'acssa-loader'; const plotlyDifficultyChartId = 'acssa-mydiv-difficulty'; const plotlyAcceptedCountChartId = 'acssa-mydiv-accepted-count'; const plotlyLastAcceptedTimeChartId = 'acssa-mydiv-accepted-time'; const yourMarker = { size: 10, symbol: 'cross', color: 'red', line: { color: 'white', width: 1, }, }; const config = { autosize: true }; // 背景用設定 const alpha = 0.3; const colors = [ [0, 400, `rgba(128,128,128,${alpha})`], [400, 800, `rgba(128,0,0,${alpha})`], [800, 1200, `rgba(0,128,0,${alpha})`], [1200, 1600, `rgba(0,255,255,${alpha})`], [1600, 2000, `rgba(0,0,255,${alpha})`], [2000, 2400, `rgba(255,255,0,${alpha})`], [2400, 2800, `rgba(255,165,0,${alpha})`], [2800, 10000, `rgba(255,0,0,${alpha})`], ]; class Charts { constructor(parent, tasks, scoreLastAcceptedTimeMap, taskAcceptedCounts, taskAcceptedElapsedTimes, yourTaskAcceptedElapsedTimes, yourScore, yourLastAcceptedTime, participants, dcForDifficulty, dcForPerformance, tabs) { this.tasks = tasks; this.scoreLastAcceptedTimeMap = scoreLastAcceptedTimeMap; this.taskAcceptedCounts = taskAcceptedCounts; this.taskAcceptedElapsedTimes = taskAcceptedElapsedTimes; this.yourTaskAcceptedElapsedTimes = yourTaskAcceptedElapsedTimes; this.yourScore = yourScore; this.yourLastAcceptedTime = yourLastAcceptedTime; this.participants = participants; this.dcForDifficulty = dcForDifficulty; this.dcForPerformance = dcForPerformance; this.tabs = tabs; parent.insertAdjacentHTML('beforeend', html$1); this.duration = getContestDurationSec(); this.xtick = 60 * 10 * Math.max(1, Math.ceil(this.duration / (60 * 10 * 20))); // 10 分を最小単位にする } async plotAsync() { // 以降の計算は時間がかかる this.taskAcceptedElapsedTimes.forEach((ar) => { ar.sort((a, b) => a - b); }); // 時系列データの準備 const [difficultyChartData, acceptedCountChartData] = await this.getTimeSeriesChartData(); // 得点と提出時間データの準備 const [lastAcceptedTimeChartData, maxAcceptedTime] = this.getLastAcceptedTimeChartData(); // 軸フォーマットをカスタムする this.overrideAxisFormat(); // Difficulty Chart 描画 await this.plotDifficultyChartData(difficultyChartData); // Accepted Count Chart 描画 await this.plotAcceptedCountChartData(acceptedCountChartData); // LastAcceptedTime Chart 描画 await this.plotLastAcceptedTimeChartData(lastAcceptedTimeChartData, maxAcceptedTime); } /** 時系列データの準備 */ async getTimeSeriesChartData() { /** Difficulty Chart のデータ */ const difficultyChartData = []; /** AC Count Chart のデータ */ const acceptedCountChartData = []; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); for (let j = 0; j < this.tasks.length; ++j) { // const interval = Math.ceil(this.taskAcceptedCounts[j] / 140); const [taskAcceptedElapsedTimesForChart, taskAcceptedCountsForChart] = this.taskAcceptedElapsedTimes[j].reduce(([ar, arr], tm, idx) => { const tmpInterval = Math.max(1, Math.min(Math.ceil(idx / 10), interval)); if (idx % tmpInterval == 0 || idx == this.taskAcceptedCounts[j] - 1) { ar.push(tm); arr.push(idx + 1); } return [ar, arr]; }, [[], []]); const correctedDifficulties = []; let counter = 0; for (const taskAcceptedCountForChart of taskAcceptedCountsForChart) { correctedDifficulties.push(this.dcForDifficulty.binarySearchCorrectedDifficulty(taskAcceptedCountForChart)); counter += 1; // 20回に1回setTimeout(0)でeventループに処理を移す if (counter % 20 == 0) { await sleep(0); } } difficultyChartData.push({ x: taskAcceptedElapsedTimesForChart, y: correctedDifficulties, type: 'scatter', name: `${this.tasks[j].Assignment}`, }); acceptedCountChartData.push({ x: taskAcceptedElapsedTimesForChart, y: taskAcceptedCountsForChart, type: 'scatter', name: `${this.tasks[j].Assignment}`, }); } // 現在のユーザのデータを追加 if (this.yourScore !== -1) { const yourAcceptedTimes = []; const yourAcceptedDifficulties = []; const yourAcceptedCounts = []; for (let j = 0; j < this.tasks.length; ++j) { if (this.yourTaskAcceptedElapsedTimes[j] !== -1) { yourAcceptedTimes.push(this.yourTaskAcceptedElapsedTimes[j]); const yourAcceptedCount = arrayLowerBound(this.taskAcceptedElapsedTimes[j], this.yourTaskAcceptedElapsedTimes[j]) + 1; yourAcceptedCounts.push(yourAcceptedCount); yourAcceptedDifficulties.push(this.dcForDifficulty.binarySearchCorrectedDifficulty(yourAcceptedCount)); } } this.tabs.yourDifficultyChartData = { x: yourAcceptedTimes, y: yourAcceptedDifficulties, mode: 'markers', type: 'scatter', name: `${userScreenName}`, marker: yourMarker, }; this.tabs.yourAcceptedCountChartData = { x: yourAcceptedTimes, y: yourAcceptedCounts, mode: 'markers', type: 'scatter', name: `${userScreenName}`, marker: yourMarker, }; difficultyChartData.push(this.tabs.yourDifficultyChartData); acceptedCountChartData.push(this.tabs.yourAcceptedCountChartData); } return [difficultyChartData, acceptedCountChartData]; } /** 得点と提出時間データの準備 */ getLastAcceptedTimeChartData() { const lastAcceptedTimeChartData = []; const scores = [...this.scoreLastAcceptedTimeMap.keys()]; scores.sort((a, b) => b - a); let acc = 0; let maxAcceptedTime = 0; scores.forEach((score) => { const lastAcceptedTimes = this.scoreLastAcceptedTimeMap.get(score); lastAcceptedTimes.sort((a, b) => a - b); const interval = Math.ceil(lastAcceptedTimes.length / 100); const lastAcceptedTimesForChart = lastAcceptedTimes.reduce((ar, tm, idx) => { if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1) ar.push(tm); return ar; }, []); const lastAcceptedTimesRanks = lastAcceptedTimes.reduce((ar, tm, idx) => { if (idx % interval == 0 || idx == lastAcceptedTimes.length - 1) ar.push(acc + idx + 1); return ar; }, []); lastAcceptedTimeChartData.push({ x: lastAcceptedTimesRanks, y: lastAcceptedTimesForChart, type: 'scatter', name: `${score}`, }); if (score === this.yourScore) { const lastAcceptedTimesRank = arrayLowerBound(lastAcceptedTimes, this.yourLastAcceptedTime); this.tabs.yourLastAcceptedTimeChartData = { x: [acc + lastAcceptedTimesRank + 1], y: [this.yourLastAcceptedTime], mode: 'markers', type: 'scatter', name: `${userScreenName}`, marker: yourMarker, }; this.tabs.yourLastAcceptedTimeChartDataIndex = lastAcceptedTimeChartData.length + 0; lastAcceptedTimeChartData.push(this.tabs.yourLastAcceptedTimeChartData); } acc += lastAcceptedTimes.length; if (lastAcceptedTimes[lastAcceptedTimes.length - 1] > maxAcceptedTime) { maxAcceptedTime = lastAcceptedTimes[lastAcceptedTimes.length - 1]; } }); return [lastAcceptedTimeChartData, maxAcceptedTime]; } /** * 軸フォーマットをカスタムする * Support specifying a function for tickformat · Issue #1464 · plotly/plotly.js * https://github.com/plotly/plotly.js/issues/1464#issuecomment-498050894 */ overrideAxisFormat() { const org_locale = Plotly.d3.locale; Plotly.d3.locale = (locale) => { const result = org_locale(locale); // eslint-disable-next-line @typescript-eslint/unbound-method const org_number_format = result.numberFormat; result.numberFormat = (format) => { if (format != 'TIME') { return org_number_format(format); } return (x) => formatTimespan(x).toString(); }; return result; }; } /** Difficulty Chart 描画 */ async plotDifficultyChartData(difficultyChartData) { const maxAcceptedCount = this.taskAcceptedCounts.reduce((a, b) => Math.max(a, b)); const yMax = RatingConverter.toCorrectedRating(this.dcForDifficulty.binarySearchCorrectedDifficulty(1)); const yMin = RatingConverter.toCorrectedRating(this.dcForDifficulty.binarySearchCorrectedDifficulty(Math.max(2, maxAcceptedCount))); // 描画 const layout = { title: 'Difficulty', xaxis: { dtick: this.xtick, tickformat: 'TIME', range: [0, this.duration], // title: { text: 'Elapsed' } }, yaxis: { dtick: 400, tickformat: 'd', range: [ Math.max(0, Math.floor((yMin - 100) / 400) * 400), Math.max(0, Math.ceil((yMax + 100) / 400) * 400), ], // title: { text: 'Difficulty' } }, shapes: colors.map((c) => { return { type: 'rect', layer: 'below', xref: 'x', yref: 'y', x0: 0, x1: this.duration, y0: c[0], y1: c[1], line: { width: 0 }, fillcolor: c[2], }; }), margin: { b: 60, t: 30, }, }; await Plotly.newPlot(plotlyDifficultyChartId, difficultyChartData, layout, config); window.addEventListener('resize', () => { if (this.tabs.activeTab == 0) void Plotly.relayout(plotlyDifficultyChartId, { width: document.getElementById(plotlyDifficultyChartId).clientWidth, }); }); } /** Accepted Count Chart 描画 */ async plotAcceptedCountChartData(acceptedCountChartData) { this.tabs.acceptedCountYMax = this.participants; const rectSpans = colors.reduce((ar, cur) => { const bottom = this.dcForDifficulty.perf2ExpectedAcceptedCount(cur[1]); if (bottom > this.tabs.acceptedCountYMax) return ar; const top = cur[0] == 0 ? this.tabs.acceptedCountYMax : this.dcForDifficulty.perf2ExpectedAcceptedCount(cur[0]); if (top < 0.5) return ar; ar.push([Math.max(0.5, bottom), Math.min(this.tabs.acceptedCountYMax, top), cur[2]]); return ar; }, []); // 描画 const layout = { title: 'Accepted Count', xaxis: { dtick: this.xtick, tickformat: 'TIME', range: [0, this.duration], // title: { text: 'Elapsed' } }, yaxis: { // type: 'log', // dtick: 100, tickformat: 'd', range: [0, this.tabs.acceptedCountYMax], // range: [ // Math.log10(0.5), // Math.log10(acceptedCountYMax) // ], // title: { text: 'Difficulty' } }, shapes: rectSpans.map((span) => { return { type: 'rect', layer: 'below', xref: 'x', yref: 'y', x0: 0, x1: this.duration, y0: span[0], y1: span[1], line: { width: 0 }, fillcolor: span[2], }; }), margin: { b: 60, t: 30, }, }; await Plotly.newPlot(plotlyAcceptedCountChartId, acceptedCountChartData, layout, config); window.addEventListener('resize', () => { if (this.tabs.activeTab == 1) void Plotly.relayout(plotlyAcceptedCountChartId, { width: document.getElementById(plotlyAcceptedCountChartId).clientWidth, }); }); } /** LastAcceptedTime Chart 描画 */ async plotLastAcceptedTimeChartData(lastAcceptedTimeChartData, maxAcceptedTime) { const xMax = this.participants; const yMax = Math.ceil((maxAcceptedTime + this.xtick / 2) / this.xtick) * this.xtick; const rectSpans = colors.reduce((ar, cur) => { const right = cur[0] == 0 ? xMax : this.dcForPerformance.perf2Ranking(cur[0]); if (right < 1) return ar; const left = this.dcForPerformance.perf2Ranking(cur[1]); if (left > xMax) return ar; ar.push([Math.max(0, left), Math.min(xMax, right), cur[2]]); return ar; }, []); // console.log(colors); // console.log(rectSpans); const layout = { title: 'LastAcceptedTime v.s. Rank', xaxis: { // dtick: 100, tickformat: 'd', range: [0, xMax], // title: { text: 'Elapsed' } }, yaxis: { dtick: this.xtick, tickformat: 'TIME', range: [0, yMax], // range: [ // Math.max(0, Math.floor((yMin - 100) / 400) * 400), // Math.max(0, Math.ceil((yMax + 100) / 400) * 400) // ], // title: { text: 'Difficulty' } }, shapes: rectSpans.map((span) => { return { type: 'rect', layer: 'below', xref: 'x', yref: 'y', x0: span[0], x1: span[1], y0: 0, y1: yMax, line: { width: 0 }, fillcolor: span[2], }; }), margin: { b: 60, t: 30, }, }; await Plotly.newPlot(plotlyLastAcceptedTimeChartId, lastAcceptedTimeChartData, layout, config); window.addEventListener('resize', () => { if (this.tabs.activeTab == 2) void Plotly.relayout(plotlyLastAcceptedTimeChartId, { width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth, }); }); } hideLoader() { document.getElementById(LOADER_ID).style.display = 'none'; } } /** レートを表す難易度円(◒)の HTML 文字列を生成 */ const generateDifficultyCircle = (rating, isSmall = true) => { const size = isSmall ? 12 : 36; const borderWidth = isSmall ? 1 : 3; const style = `display:inline-block;border-radius:50%;border-style:solid;border-width:${borderWidth}px;` + `margin-right:5px;vertical-align:initial;height:${size}px;width:${size}px;`; if (rating < 3200) { // 色と円がどのぐらい満ちているかを計算 const color = getColor(rating); const percentFull = ((rating % 400) / 400) * 100; // ◒を生成 return (` <span style='${style}border-color:${color};background:` + `linear-gradient(to top, ${color} 0%, ${color} ${percentFull}%, ` + `rgba(0, 0, 0, 0) ${percentFull}%, rgba(0, 0, 0, 0) 100%); '> </span>`); } // 金銀銅は例外処理 else if (rating < 3600) { return (`<span style="${style}border-color: rgb(150, 92, 44);` + 'background: linear-gradient(to right, rgb(150, 92, 44), rgb(255, 218, 189), rgb(150, 92, 44));"></span>'); } else if (rating < 4000) { return (`<span style="${style}border-color: rgb(128, 128, 128);` + 'background: linear-gradient(to right, rgb(128, 128, 128), white, rgb(128, 128, 128));"></span>'); } else { return (`<span style="${style}border-color: rgb(255, 215, 0);` + 'background: linear-gradient(to right, rgb(255, 215, 0), white, rgb(255, 215, 0));"></span>'); } }; const COL_PER_ROW = 20; class DifficyltyTable { constructor(parent, tasks, isEstimationEnabled, dc, taskAcceptedCounts, yourTaskAcceptedElapsedTimes, acCountPredicted) { // insert parent.insertAdjacentHTML('beforeend', ` <p><span class="h2">Difficulty</span></p> <div id="acssa-table-wrapper"> ${rangeLen(Math.ceil(tasks.length / COL_PER_ROW)) .map((tableIdx) => ` <table id="acssa-table-${tableIdx}" class="table table-bordered table-hover th-center td-center td-middle acssa-table"> <tbody> <tr id="acssa-thead-${tableIdx}" class="acssa-thead"></tr> </tbody> <tbody> <tr id="acssa-tbody-${tableIdx}" class="acssa-tbody"></tr> ${isEstimationEnabled ? `<tr id="acssa-tbody-predicted-${tableIdx}" class="acssa-tbody"></tr>` : ''} </tbody> </table> `) .join('')} </div> `); if (isEstimationEnabled) { for (let tableIdx = 0; tableIdx < Math.ceil(tasks.length / COL_PER_ROW); ++tableIdx) { document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', `<th></th>`); document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Current</td>`); document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', `<th>Predicted</td>`); } } // build for (let j = 0; j < tasks.length; ++j) { const tableIdx = Math.floor(j / COL_PER_ROW); const correctedDifficulty = dc.binarySearchCorrectedDifficulty(taskAcceptedCounts[j]); const tdClass = yourTaskAcceptedElapsedTimes[j] === -1 ? '' : 'class="success acssa-task-success"'; document.getElementById(`acssa-thead-${tableIdx}`).insertAdjacentHTML('beforeend', ` <td ${tdClass}> ${tasks[j].Assignment} </td> `); const id = `td-assa-difficulty-${j}`; document.getElementById(`acssa-tbody-${tableIdx}`).insertAdjacentHTML('beforeend', ` <td ${tdClass} id="${id}" style="color:${getColor(correctedDifficulty)};"> ${correctedDifficulty === 9999 ? '-' : correctedDifficulty}</td> `); if (correctedDifficulty !== 9999) { document.getElementById(id).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedDifficulty)); } if (isEstimationEnabled) { const correctedPredictedDifficulty = dc.binarySearchCorrectedDifficulty(acCountPredicted[j]); const idPredicted = `td-assa-difficulty-predicted-${j}`; document.getElementById(`acssa-tbody-predicted-${tableIdx}`).insertAdjacentHTML('beforeend', ` <td ${tdClass} id="${idPredicted}" style="color:${getColor(correctedPredictedDifficulty)};"> ${correctedPredictedDifficulty === 9999 ? '-' : correctedPredictedDifficulty}</td> `); if (correctedPredictedDifficulty !== 9999) { document.getElementById(idPredicted).insertAdjacentHTML('afterbegin', generateDifficultyCircle(correctedPredictedDifficulty)); } } } } } 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>"; const TABS_WRAPPER_ID = 'acssa-tab-wrapper'; const CHART_TAB_ID = 'acssa-chart-tab'; const CHART_TAB_BUTTON_CLASS = 'acssa-chart-tab-button'; const CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY = 'acssa-checkbox-toggle-your-result-visibility'; const PARENT_CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY = `${CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY}-parent`; const CHECKBOX_TOGGLE_LOG_PLOT = 'acssa-checkbox-toggle-log-plot'; const CHECKBOX_TOGGLE_ONLOAD_PLOT = 'acssa-checkbox-toggle-onload-plot'; const CONFIG_CNLOAD_PLOT_KEY = 'acssa-config-onload-plot'; const PARENT_CHECKBOX_TOGGLE_LOG_PLOT = `${CHECKBOX_TOGGLE_LOG_PLOT}-parent`; class Tabs { constructor(parent, yourScore, participants) { var _a; this.yourScore = yourScore; this.participants = participants; // insert parent.insertAdjacentHTML('beforeend', html); this.showYourResultCheckbox = document.getElementById(CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY); this.logPlotCheckbox = document.getElementById(CHECKBOX_TOGGLE_LOG_PLOT); this.logPlotCheckboxParent = document.getElementById(PARENT_CHECKBOX_TOGGLE_LOG_PLOT); this.onloadPlotCheckbox = document.getElementById(CHECKBOX_TOGGLE_ONLOAD_PLOT); this.onloadPlot = JSON.parse((_a = localStorage.getItem(CONFIG_CNLOAD_PLOT_KEY)) !== null && _a !== void 0 ? _a : 'true'); this.onloadPlotCheckbox.checked = this.onloadPlot; // チェックボックス操作時のイベントを登録する */ this.showYourResultCheckbox.addEventListener('change', () => { if (this.showYourResultCheckbox.checked) { document.querySelectorAll('.acssa-task-success.acssa-task-success-suppress').forEach((elm) => { elm.classList.remove('acssa-task-success-suppress'); }); } else { document.querySelectorAll('.acssa-task-success').forEach((elm) => { elm.classList.add('acssa-task-success-suppress'); }); } }); this.showYourResultCheckbox.addEventListener('change', () => { void this.onShowYourResultCheckboxChangedAsync(); }); this.logPlotCheckbox.addEventListener('change', () => { void this.onLogPlotCheckboxChangedAsync(); }); this.onloadPlotCheckbox.addEventListener('change', () => { this.onloadPlot = this.onloadPlotCheckbox.checked; localStorage.setItem(CONFIG_CNLOAD_PLOT_KEY, JSON.stringify(this.onloadPlot)); }); this.activeTab = 0; this.showYourResult = [true, true, true]; this.acceptedCountYMax = -1; this.useLogPlot = [false, false, false]; this.yourDifficultyChartData = null; this.yourAcceptedCountChartData = null; this.yourLastAcceptedTimeChartData = null; this.yourLastAcceptedTimeChartDataIndex = -1; document .querySelectorAll(`.${CHART_TAB_BUTTON_CLASS}`) .forEach((btn, key) => { btn.addEventListener('click', () => void this.onTabButtonClicked(btn, key)); }); if (this.yourScore == -1) { // disable checkbox this.showYourResultCheckbox.checked = false; this.showYourResultCheckbox.disabled = true; const checkboxParent = this.showYourResultCheckbox.parentElement; checkboxParent.style.cursor = 'default'; checkboxParent.style.textDecoration = 'line-through'; } } async onShowYourResultCheckboxChangedAsync() { this.showYourResult[this.activeTab] = this.showYourResultCheckbox.checked; if (this.showYourResultCheckbox.checked) { // show switch (this.activeTab) { case 0: if (this.yourScore > 0 && this.yourDifficultyChartData !== null) await Plotly.addTraces(plotlyDifficultyChartId, this.yourDifficultyChartData); break; case 1: if (this.yourScore > 0 && this.yourAcceptedCountChartData !== null) await Plotly.addTraces(plotlyAcceptedCountChartId, this.yourAcceptedCountChartData); break; case 2: if (this.yourLastAcceptedTimeChartData !== null && this.yourLastAcceptedTimeChartDataIndex != -1) { await Plotly.addTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartData, this.yourLastAcceptedTimeChartDataIndex); } break; } } else { // hide switch (this.activeTab) { case 0: if (this.yourScore > 0) await Plotly.deleteTraces(plotlyDifficultyChartId, -1); break; case 1: if (this.yourScore > 0) await Plotly.deleteTraces(plotlyAcceptedCountChartId, -1); break; case 2: if (this.yourLastAcceptedTimeChartDataIndex != -1) { await Plotly.deleteTraces(plotlyLastAcceptedTimeChartId, this.yourLastAcceptedTimeChartDataIndex); } break; } } } // end async onShowYourResultCheckboxChangedAsync() async onLogPlotCheckboxChangedAsync() { if (this.acceptedCountYMax == -1) return; this.useLogPlot[this.activeTab] = this.logPlotCheckbox.checked; if (this.activeTab == 1) { if (this.logPlotCheckbox.checked) { // log plot const layout = { yaxis: { type: 'log', range: [Math.log10(0.5), Math.log10(this.acceptedCountYMax)], }, }; await Plotly.relayout(plotlyAcceptedCountChartId, layout); } else { // linear plot const layout = { yaxis: { type: 'linear', range: [0, this.acceptedCountYMax], }, }; await Plotly.relayout(plotlyAcceptedCountChartId, layout); } } else if (this.activeTab == 2) { if (this.logPlotCheckbox.checked) { // log plot const layout = { xaxis: { type: 'log', range: [Math.log10(0.5), Math.log10(this.participants)], }, }; await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout); } else { // linear plot const layout = { xaxis: { type: 'linear', range: [0, this.participants], }, }; await Plotly.relayout(plotlyLastAcceptedTimeChartId, layout); } } } // end async onLogPlotCheckboxChangedAsync async onTabButtonClicked(btn, key) { // check whether active or not const buttonParent = btn.parentElement; if (buttonParent.className == 'active') return; // modify visibility this.activeTab = key; document.querySelector(`#${CHART_TAB_ID} li.active`).classList.remove('active'); document.querySelector(`#${CHART_TAB_ID} li:nth-child(${key + 1})`).classList.add('active'); document.querySelector('#acssa-chart-block div.acssa-chart-wrapper-active').classList.remove('acssa-chart-wrapper-active'); document.querySelector(`#acssa-chart-block div.acssa-chart-wrapper:nth-child(${key + 1})`).classList.add('acssa-chart-wrapper-active'); // resize charts switch (key) { case 0: await Plotly.relayout(plotlyDifficultyChartId, { width: document.getElementById(plotlyDifficultyChartId).clientWidth, }); this.logPlotCheckboxParent.style.display = 'none'; break; case 1: await Plotly.relayout(plotlyAcceptedCountChartId, { width: document.getElementById(plotlyAcceptedCountChartId).clientWidth, }); this.logPlotCheckboxParent.style.display = 'block'; break; case 2: await Plotly.relayout(plotlyLastAcceptedTimeChartId, { width: document.getElementById(plotlyLastAcceptedTimeChartId).clientWidth, }); this.logPlotCheckboxParent.style.display = 'block'; break; } if (this.showYourResult[this.activeTab] !== this.showYourResultCheckbox.checked) { await this.onShowYourResultCheckboxChangedAsync(); } if (this.activeTab !== 0 && this.useLogPlot[this.activeTab] !== this.logPlotCheckbox.checked) { await this.onLogPlotCheckboxChangedAsync(); } } showTabsControl() { document.getElementById(TABS_WRAPPER_ID).style.display = 'block'; if (!this.onloadPlot) { document.getElementById(CHART_TAB_ID).style.display = 'none'; document.getElementById(PARENT_CHECKBOX_TOGGLE_YOUR_RESULT_VISIBILITY).style.display = 'none'; } } } const finf = bigf(400); function bigf(n) { let pow1 = 1; let pow2 = 1; let numerator = 0; let denominator = 0; for (let i = 0; i < n; ++i) { pow1 *= 0.81; pow2 *= 0.9; numerator += pow1; denominator += pow2; } return Math.sqrt(numerator) / denominator; } function f(n) { return ((bigf(n) - finf) / (bigf(1) - finf)) * 1200.0; } /** * calculate unpositivized rating from last state * @param {Number} [last] last unpositivized rating * @param {Number} [perf] performance * @param {Number} [ratedMatches] count of participated rated contest * @returns {number} estimated unpositivized rating */ function calcRatingFromLast(last, perf, ratedMatches) { if (ratedMatches === 0) return perf - 1200; last += f(ratedMatches); const weight = 9 - 9 * Math.pow(0.9, ratedMatches); const numerator = weight * Math.pow(2, last / 800.0) + Math.pow(2, perf / 800.0); const denominator = 1 + weight; return Math.log2(numerator / denominator) * 800.0 - f(ratedMatches + 1); } // class Random { // x: number // y: number // z: number // w: number // constructor(seed = 88675123) { // this.x = 123456789; // this.y = 362436069; // this.z = 521288629; // this.w = seed; // } // // XorShift // next(): number { // let t; // t = this.x ^ (this.x << 11); // this.x = this.y; this.y = this.z; this.z = this.w; // return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8)); // } // // min以上max以下の乱数を生成する // nextInt(min: number, max: number): number { // const r = Math.abs(this.next()); // return min + (r % (max + 1 - min)); // } // }; class PerformanceTable { constructor(parent, tasks, isEstimationEnabled, yourStandingsEntry, taskAcceptedCounts, acCountPredicted, standingsData, innerRatingsFromPredictor, dcForPerformance, centerOfInnerRating, useRating) { this.centerOfInnerRating = centerOfInnerRating; if (yourStandingsEntry === undefined) return; // コンテスト終了時点での順位表を予測する const len = acCountPredicted.length; const rems = []; for (let i = 0; i < len; ++i) { rems.push(Math.ceil(acCountPredicted[i] - taskAcceptedCounts[i])); // } const scores = []; // (現レート,スコア合計,時間,問題ごとのスコア,rated) const highestScores = tasks.map(() => 0); let rowPtr = undefined; // const ratedInnerRatings: Rating[] = []; const ratedUserRanks = []; // console.log(standingsData); const threthold = moment('2021-12-03T21:00:00+09:00'); const isAfterABC230 = startTime >= threthold; // OldRating が全員 0 なら,強制的に Rating を使用する(コンテスト終了後,レート更新前) standingsData.forEach((standingsEntry) => { const userScores = []; let penalty = 0; for (let j = 0; j < tasks.length; ++j) { const taskResultEntry = standingsEntry.TaskResults[tasks[j].TaskScreenName]; if (!taskResultEntry) { // 未提出 userScores.push(0); } else { userScores.push(taskResultEntry.Score / 100); highestScores[j] = Math.max(highestScores[j], taskResultEntry.Score / 100); penalty += taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty; } } // const isRated = standingsEntry.IsRated && standingsEntry.TotalResult.Count > 0; const isRated = standingsEntry.IsRated && (isAfterABC230 || standingsEntry.TotalResult.Count > 0); if (!isRated) { if (standingsEntry.TotalResult.Score === 0 && penalty === 0 && standingsEntry.TotalResult.Count == 0) { return; // NoSub を飛ばす } } standingsEntry.Rating; // const innerRating: Rating = isTeamOrBeginner // ? correctedRating // : standingsEntry.UserScreenName in innerRatingsFromPredictor // ? innerRatingsFromPredictor[standingsEntry.UserScreenName] // : RatingConverter.toInnerRating( // Math.max(RatingConverter.toRealRating(correctedRating), 1), // standingsEntry.Competitions // ); const innerRating = standingsEntry.UserScreenName in innerRatingsFromPredictor ? innerRatingsFromPredictor[standingsEntry.UserScreenName] : this.centerOfInnerRating; if (isRated) { // ratedInnerRatings.push(innerRating); ratedUserRanks.push(standingsEntry.EntireRank); // if (innerRating || true) { const row = [ innerRating, standingsEntry.TotalResult.Score / 100, standingsEntry.TotalResult.Elapsed + 300 * standingsEntry.TotalResult.Penalty, userScores, isRated, ]; scores.push(row); if ((standingsEntry.UserScreenName == userScreenName)) { rowPtr = row; } // } } }); const sameRatedRankCount = ratedUserRanks.reduce((prev, cur) => { if (cur == yourStandingsEntry.EntireRank) prev++; return prev; }, 0); const ratedRank = ratedUserRanks.reduce((prev, cur) => { if (cur < yourStandingsEntry.EntireRank) prev += 1; return prev; }, (1 + sameRatedRankCount) / 2); // レート順でソート scores.sort((a, b) => { const [innerRatingA, scoreA, timeElapsedA] = a; const [innerRatingB, scoreB, timeElapsedB] = b; if (innerRatingA != innerRatingB) { return innerRatingB - innerRatingA; // 降順(レートが高い順) } if (scoreA != scoreB) { return scoreB - scoreA; // 降順(順位が高い順) } return timeElapsedA - timeElapsedB; // 昇順(順位が高い順) }); // const random = new Random(0); // スコア変化をシミュレート // (現レート,スコア合計,時間,問題ごとのスコア,rated) scores.forEach((score) => { const [, , , scoresA] = score; // 自分は飛ばす if (score == rowPtr) return; for (let j = 0; j < tasks.length; ++j) { // if (random.nextInt(0, 9) <= 2) continue; // まだ満点ではなく,かつ正解者を増やせるなら if (scoresA[j] < highestScores[j] && rems[j] > 0) { const dif = highestScores[j] - scoresA[j]; score[1] += dif; score[2] += 1000000000 * 60 * 30; // とりあえず30分で解くと仮定する scoresA[j] = highestScores[j]; rems[j]--; } if (rems[j] == 0) break; } }); // 順位でソート scores.sort((a, b) => { const [innerRatingA, scoreA, timeElapsedA, ,] = a; const [innerRatingB, scoreB, timeElapsedB, ,] = b; if (scoreA != scoreB) { return scoreB - scoreA; // 降順(順位が高い順) } if (timeElapsedA != timeElapsedB) { return timeElapsedA - timeElapsedB; // 昇順(順位が高い順) } return innerRatingB - innerRatingA; // 降順(レートが高い順) }); // 順位を求める let estimatedRank = -1; let rank = 0; let sameCnt = 0; for (let i = 0; i < scores.length; ++i) { if (estimatedRank == -1) { if (scores[i][4] === true) { rank++; } if (scores[i] === rowPtr) { if (rank === 0) rank = 1; estimatedRank = rank; // break; } } else { if (rowPtr === undefined) break; if (scores[i][1] === rowPtr[1] && scores[i][2] === rowPtr[2]) { sameCnt++; } else { break; } } } //1246 estimatedRank += sameCnt / 2; // const dc = new DifficultyCalculator(ratedInnerRatings); // insert parent.insertAdjacentHTML('beforeend', ` <p><span class="h2">Performance</span></p> <div id="acssa-perf-table-wrapper"> <table id="acssa-perf-table" class="table table-bordered table-hover th-center td-center td-middle acssa-table"> <tbody> <tr class="acssa-thead"> ${isEstimationEnabled ? '<td></td>' : ''} <td id="acssa-thead-perf" class="acssa-thead">perf</td> <td id="acssa-thead-perf" class="acssa-thead">レート変化</td> </tr> </tbody> <tbody> <tr id="acssa-perf-tbody" class="acssa-tbody"></tr> ${isEstimationEnabled ? ` <tr id="acssa-perf-tbody-predicted" class="acssa-tbody"></tr> ` : ''} </tbody> </table> </div> `); if (isEstimationEnabled) { document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', `<th>Current</td>`); document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', `<th>Predicted</td>`); } // build const id = `td-assa-perf-current`; // TODO: ちゃんと判定する // const perf = Math.min(2400, dc.rank2InnerPerf(ratedRank)); const perf = RatingConverter.toCorrectedRating(dcForPerformance.rank2InnerPerf(ratedRank)); // document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', ` <td id="${id}" style="color:${getColor(perf)};"> ${perf === 9999 ? '-' : perf}</td> `); if (perf !== 9999) { document.getElementById(id).insertAdjacentHTML('afterbegin', generateDifficultyCircle(perf)); const oldRating = useRating ? yourStandingsEntry.Rating : yourStandingsEntry.OldRating; // const oldRating = yourStandingsEntry.Rating; const nextRating = Math.round(RatingConverter.toCorrectedRating(calcRatingFromLast(RatingConverter.toRealRating(oldRating), perf, yourStandingsEntry.Competitions))); const sign = nextRating > oldRating ? '+' : nextRating < oldRating ? '-' : '±'; document.getElementById(`acssa-perf-tbody`).insertAdjacentHTML('beforeend', ` <td> <span style="font-weight:bold;color:${getColor(oldRating)}">${oldRating}</span> → <span style="font-weight:bold;color:${getColor(nextRating)}">${nextRating}</span> <span style="color:gray">(${sign}${Math.abs(nextRating - oldRating)})</span> </td> `); } if (isEstimationEnabled) { if (estimatedRank != -1) { const perfEstimated = RatingConverter.toCorrectedRating(dcForPerformance.rank2InnerPerf(estimatedRank)); const id2 = `td-assa-perf-predicted`; document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', ` <td id="${id2}" style="color:${getColor(perfEstimated)};"> ${perfEstimated === 9999 ? '-' : perfEstimated}</td> `); if (perfEstimated !== 9999) { document.getElementById(id2).insertAdjacentHTML('afterbegin', generateDifficultyCircle(perfEstimated)); const oldRating = useRating ? yourStandingsEntry.Rating : yourStandingsEntry.OldRating; // const oldRating = yourStandingsEntry.Rating; const nextRating = Math.round(RatingConverter.toCorrectedRating(calcRatingFromLast(RatingConverter.toRealRating(oldRating), perfEstimated, yourStandingsEntry.Competitions))); const sign = nextRating > oldRating ? '+' : nextRating < oldRating ? '-' : '±'; document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', ` <td> <span style="font-weight:bold;color:${getColor(oldRating)}">${oldRating}</span> → <span style="font-weight:bold;color:${getColor(nextRating)}">${nextRating}</span> <span style="color:gray">(${sign}${Math.abs(nextRating - oldRating)})</span> </td> `); } } else { document.getElementById(`acssa-perf-tbody-predicted`).insertAdjacentHTML('beforeend', '<td>?</td>'); } } } } const NS2SEC = 1000000000; const CONTENT_DIV_ID = 'acssa-contents'; class Parent { constructor(acRatioModel, centerOfInnerRating) { const loaderStyles = GM_getResourceText('loaders.min.css'); GM_addStyle(loaderStyles + '\n' + css); // this.centerOfInnerRating = getCenterOfInnerRating(contestScreenName); this.centerOfInnerRating = centerOfInnerRating; this.acRatioModel = acRatioModel; this.working = false; this.oldStandingsData = null; this.hasTeamStandings = this.searchTeamStandingsPage(); this.yourStandingsEntry = undefined; } searchTeamStandingsPage() { const teamStandingsLink = document.querySelector(`a[href*="/contests/${contestScreenName}/standings/team"]`); return teamStandingsLink !== null; } async onStandingsChanged(standings) { if (!standings) return; if (this.working) return; this.tasks = standings.TaskInfo; const standingsData = standings.StandingsData; // vueStandings.filteredStandings; if (this.oldStandingsData === standingsData) return; if (this.tasks.length === 0) return; this.oldStandingsData = standingsData; this.working = true; this.removeOldContents(); const currentTime = moment(); this.elapsedMinutes = Math.floor(currentTime.diff(startTime) / 60 / 1000); this.isDuringContest = startTime <= currentTime && currentTime < endTime; this.isEstimationEnabled = this.isDuringContest && this.elapsedMinutes >= 1 && this.tasks.length < 10; const useRating = this.isDuringContest || this.areOldRatingsAllZero(standingsData); this.innerRatingsFromPredictor = await fetchInnerRatingsFromPredictor(contestScreenName); this.scanStandingsData(standingsData); this.predictAcCountSeries(); const standingsElement = document.getElementById('vue-standings'); const acssaContentDiv = document.createElement('div'); acssaContentDiv.id = CONTENT_DIV_ID; standingsElement.insertAdjacentElement('afterbegin', acssaContentDiv); if (this.hasTeamStandings) { if (!location.href.includes('/standings/team')) { // チーム戦順位表へ誘導 acssaContentDiv.insertAdjacentHTML('afterbegin', teamalert); } } // difficulty new DifficyltyTable(acssaContentDiv, this.tasks, this.isEstimationEnabled, this.dcForDifficulty, this.taskAcceptedCounts, this.yourTaskAcceptedElapsedTimes, this.acCountPredicted); new PerformanceTable(acssaContentDiv, this.tasks, this.isEstimationEnabled, this.yourStandingsEntry, this.taskAcceptedCounts, this.acCountPredicted, standingsData, this.innerRatingsFromPredictor, this.dcForPerformance, this.centerOfInnerRating, useRating); // console.log(this.yourStandingsEntry); // console.log(this.yourStandingsEntry?.EntireRank); // console.log(this.dc.rank2InnerPerf((this.yourStandingsEntry?.EntireRank ?? 10000) - 0)); // tabs const tabs = new Tabs(acssaContentDiv, this.yourScore, this.participants); const charts = new Charts(acssaContentDiv, this.tasks, this.scoreLastAcceptedTimeMap, this.taskAcceptedCounts, this.taskAcceptedElapsedTimes, this.yourTaskAcceptedElapsedTimes, this.yourScore, this.yourLastAcceptedTime, this.participants, this.dcForDifficulty, this.dcForPerformance, tabs); if (tabs.onloadPlot) { // 順位表のその他の描画を優先するために,プロットは後回しにする void charts.plotAsync().then(() => { charts.hideLoader(); tabs.showTabsControl(); this.working = false; }); } else { charts.hideLoader(); tabs.showTabsControl(); } } removeOldContents() { const oldContents = document.getElementById(CONTENT_DIV_ID); if (oldContents) { // oldContents.parentNode.removeChild(oldContents); oldContents.remove(); } } scanStandingsData(standingsData) { // init this.scoreLastAcceptedTimeMap = new Map(); this.taskAcceptedCounts = rangeLen(this.tasks.length).fill(0); this.taskAcceptedElapsedTimes = rangeLen(this.tasks.length).map(() => []); this.innerRatings = []; this.ratedInnerRatings = []; this.yourTaskAcceptedElapsedTimes = rangeLen(this.tasks.length).fill(-1); this.yourScore = -1; this.yourLastAcceptedTime = -1; this.participants = 0; this.yourStandingsEntry = undefined; // scan const threthold = moment('2021-12-03T21:00:00+09:00'); const isAfterABC230 = startTime >= threthold; for (let i = 0; i < standingsData.length; ++i) { const standingsEntry = standingsData[i]; const isRated = standingsEntry.IsRated && (isAfterABC230 || standingsEntry.TotalResult.Count > 0); // const innerRating: Rating = isTeamOrBeginner // ? correctedRating // : standingsEntry.UserScreenName in this.innerRatingsFromPredictor // ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName] // : RatingConverter.toInnerRating( // Math.max(RatingConverter.toRealRating(correctedRating), 1), // standingsEntry.Competitions // ); const innerRating = standingsEntry.UserScreenName in this.innerRatingsFromPredictor ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName] : this.centerOfInnerRating; if (isRated) { this.ratedInnerRatings.push(innerRating); } if (!standingsEntry.TaskResults) continue; // 参加登録していない if (standingsEntry.UserIsDeleted) continue; // アカウント削除 // let correctedRating = this.isDuringContest ? standingsEntry.Rating : standingsEntry.OldRating; standingsEntry.Rating; // これは飛ばしちゃダメ(提出しても 0 AC だと Penalty == 0 なので) // if (standingsEntry.TotalResult.Score == 0 && standingsEntry.TotalResult.Penalty == 0) continue; let score = 0; let penalty = 0; for (let j = 0; j < this.tasks.length; ++j) { const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName]; if (!taskResultEntry) continue; // 未提出 score += taskResultEntry.Score; penalty += taskResultEntry.Score === 0 ? taskResultEntry.Failure : taskResultEntry.Penalty; } if (score === 0 && penalty === 0 && standingsEntry.TotalResult.Count == 0) continue; // NoSub を飛ばす this.participants++; // console.log(i + 1, score, penalty); score /= 100; if (this.scoreLastAcceptedTimeMap.has(score)) { this.scoreLastAcceptedTimeMap.get(score).push(standingsEntry.TotalResult.Elapsed / NS2SEC); } else { this.scoreLastAcceptedTimeMap.set(score, [standingsEntry.TotalResult.Elapsed / NS2SEC]); } // console.log(this.isDuringContest, standingsEntry.Rating, standingsEntry.OldRating, innerRating); // if (standingsEntry.IsRated && innerRating) { // if (innerRating) { // this.innerRatings.push(innerRating); // } else { // console.log(i, innerRating, correctedRating, standingsEntry.Competitions, standingsEntry, this.innerRatingsFromPredictor[standingsEntry.UserScreenName]); // continue; // } this.innerRatings.push(innerRating); for (let j = 0; j < this.tasks.length; ++j) { const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName]; const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1; if (isAccepted) { ++this.taskAcceptedCounts[j]; this.taskAcceptedElapsedTimes[j].push(taskResultEntry.Elapsed / NS2SEC); } } if ((standingsEntry.UserScreenName == userScreenName)) { this.yourScore = score; this.yourLastAcceptedTime = standingsEntry.TotalResult.Elapsed / NS2SEC; this.yourStandingsEntry = standingsEntry; for (let j = 0; j < this.tasks.length; ++j) { const taskResultEntry = standingsEntry.TaskResults[this.tasks[j].TaskScreenName]; const isAccepted = (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Score) > 0 && (taskResultEntry === null || taskResultEntry === void 0 ? void 0 : taskResultEntry.Status) == 1; if (isAccepted) { this.yourTaskAcceptedElapsedTimes[j] = taskResultEntry.Elapsed / NS2SEC; } } } } // end for this.innerRatings.sort((a, b) => a - b); this.dcForDifficulty = new DifficultyCalculator(this.innerRatings); this.dcForPerformance = new DifficultyCalculator(this.ratedInnerRatings); } // end async scanStandingsData predictAcCountSeries() { if (!this.isEstimationEnabled) { this.acCountPredicted = []; return; } // 時間ごとの AC 数推移を計算する const taskAcceptedCountImos = rangeLen(this.tasks.length).map(() => rangeLen(this.elapsedMinutes).map(() => 0)); this.taskAcceptedElapsedTimes.forEach((ar, index) => { ar.forEach((seconds) => { const minutes = Math.floor(seconds / 60); if (minutes >= this.elapsedMinutes) return; taskAcceptedCountImos[index][minutes] += 1; }); }); const taskAcceptedRatio = rangeLen(this.tasks.length).map(() => []); taskAcceptedCountImos.forEach((ar, index) => { let cum = 0; ar.forEach((imos) => { cum += imos; taskAcceptedRatio[index].push(cum / this.participants); }); }); // 差の自乗和が最小になるシーケンスを探す this.acCountPredicted = taskAcceptedRatio.map((ar) => { if (this.acRatioModel === undefined) return 0; if (ar[this.elapsedMinutes - 1] === 0) return 0; let minerror = 1.0 * this.elapsedMinutes; // let argmin = ''; let last_ratio = 0; Object.keys(this.acRatioModel).forEach((key) => { if (this.acRatioModel === undefined) return; const ar2 = this.acRatioModel[key]; let error = 0; for (let i = 0; i < this.elapsedMinutes; ++i) { error += Math.pow(ar[i] - ar2[i], 2); } if (error < minerror) { minerror = error; // argmin = key; if (ar2[this.elapsedMinutes - 1] > 0) { last_ratio = ar2[ar2.length - 1] * (ar[this.elapsedMinutes - 1] / ar2[this.elapsedMinutes - 1]); } else { last_ratio = ar2[ar2.length - 1]; } } }); // console.log(argmin, minerror, last_ratio); if (last_ratio > 1) last_ratio = 1; return this.participants * last_ratio; }); } // end predictAcCountSeries(); areOldRatingsAllZero(standingsData) { return standingsData.every((standingsEntry) => standingsEntry.OldRating == 0); } } Parent.init = async () => { const contestRatedRange = await getContestRatedRangeAsync(contestScreenName); const centerOfInnerRating = getCenterOfInnerRatingFromRange(contestRatedRange); const curr = moment(); if (startTime <= curr && curr < endTime) { const contestDurationMinutes = endTime.diff(startTime) / 1000 / 60; return new Parent(await fetchContestAcRatioModel(contestScreenName, contestDurationMinutes), centerOfInnerRating); } else { return new Parent(undefined, centerOfInnerRating); } }; void (async () => { const parent = await Parent.init(); vueStandings.$watch('standings', (standings) => { void parent.onStandingsChanged(standings); }, { deep: true, immediate: true }); })();