Greasy Fork is available in English.

atcoder-standings-difficulty-analyzer

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

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         atcoder-standings-difficulty-analyzer
// @namespace    iilj
// @version      2022.9.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, ratedRank2EntireRank, 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.ratedRank2EntireRank = ratedRank2EntireRank;
        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;
        // Rated 内のランクから,全体のランクへ変換する
        const convRatedRank2EntireRank = (ratedRank) => {
            const intRatedRank = Math.floor(ratedRank);
            if (intRatedRank >= this.ratedRank2EntireRank.length)
                return xMax;
            return this.ratedRank2EntireRank[intRatedRank];
        };
        const yMax = Math.ceil((maxAcceptedTime + this.xtick / 2) / this.xtick) * this.xtick;
        const rectSpans = colors.reduce((ar, cur) => {
            const right = cur[0] == 0 ? xMax : convRatedRank2EntireRank(this.dcForPerformance.perf2Ranking(cur[0]));
            if (right < 1)
                return ar;
            const left = cur[1] === 10000 ? 0 : convRatedRank2EntireRank(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, this.ratedRank2EntireRank, 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.ratedRank2EntireRank = [];
        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);
            if (isRated) {
                const ratedInnerRating = standingsEntry.UserScreenName in this.innerRatingsFromPredictor
                    ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName]
                    : this.centerOfInnerRating;
                this.ratedInnerRatings.push(ratedInnerRating);
                this.ratedRank2EntireRank.push(standingsEntry.EntireRank);
            }
            if (!standingsEntry.TaskResults)
                continue; // 参加登録していない
            if (standingsEntry.UserIsDeleted)
                continue; // アカウント削除
            // let correctedRating = this.isDuringContest ? standingsEntry.Rating : standingsEntry.OldRating;
            let correctedRating = standingsEntry.Rating;
            const isTeamOrBeginner = correctedRating === 0;
            if (isTeamOrBeginner) {
                // continue; // 初参加 or チーム
                correctedRating = this.centerOfInnerRating;
            }
            const innerRating = isTeamOrBeginner
                ? correctedRating
                : standingsEntry.UserScreenName in this.innerRatingsFromPredictor
                    ? this.innerRatingsFromPredictor[standingsEntry.UserScreenName]
                    : RatingConverter.toInnerRating(Math.max(RatingConverter.toRealRating(correctedRating), 1), standingsEntry.Competitions);
            // これは飛ばしちゃダメ(提出しても 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.ratedInnerRatings.sort((a, b) => a - b);
        this.ratedRank2EntireRank.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 });
})();