OMCStandingsStatistics

onlinemathcontestでの /standings ページに補助情報を追加します

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         OMCStandingsStatistics
// @namespace    none
// @version      1.0.2
// @description  onlinemathcontestでの /standings ページに補助情報を追加します
// @match        https://onlinemathcontest.com/contests/*/standings
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  // ------------------------------------------
  // /standings ページのURLパターン
  // ------------------------------------------
  const standingsPattern = /^https:\/\/onlinemathcontest\.com\/contests\/[^/]+\/standings$/;

  if (standingsPattern.test(location.href)) {
    // コンテスト名をURLから抜き出す
    const match = location.href.match(/contests\/([^/]+)\/standings/);
    if (!match) return;
    const contestName = match[1];
    const apiUrl = `https://onlinemathcontest.com/api/contests/${contestName}/standings?rated=0`;

    fetch(apiUrl)
      .then(res => res.json())
      .then(json => {
        const standingsDiv = document.getElementById('standings');
        if (!standingsDiv) {
          console.warn('#standingsが見つかりません');
          return;
        }
        const tasks = json.tasks;
        const standings = json.standings;
        const isPast = json.isPast;
        const isPointVisible = json.is_point_visible;

        // isPast かつ is_point_visible のときのみテーブルを表示
        if(isPast && isPointVisible){
          // テーブルを作成し、standingsDivの最上位に挿入
          const table = document.createElement('table');
          table.style.width = '100%';
          table.style.borderCollapse = 'collapse';
          table.innerHTML = `
            <thead>
              <tr>
                <th style="border:1px solid #ccc; padding:4px;">問題</th>
                <th style="border:1px solid #ccc; padding:4px;">得点</th>
                <th style="border:1px solid #ccc; padding:4px;">人数</th>
                <th style="border:1px solid #ccc; padding:4px;">正解率</th>
                <th style="border:1px solid #ccc; padding:4px;">平均ペナ</th>
                <th style="border:1px solid #ccc; padding:4px;">ペナ率</th>
                <th style="border:1px solid #ccc; padding:4px; width:200px;">レート</th>
              </tr>
            </thead>
            <tbody></tbody>
          `;
          standingsDiv.insertBefore(table, standingsDiv.firstChild);

          const tbody = table.querySelector('tbody');

          // レート→色 (ただし r=0 で黒)
          const rateColors = [
            { min: 1,    max: 400,    color: '#808080' },
            { min: 400,  max: 800,    color: '#804000' },
            { min: 800,  max: 1200,   color: '#008000' },
            { min: 1200, max: 1600,   color: '#00c0c0' },
            { min: 1600, max: 2000,   color: '#0000ff' },
            { min: 2000, max: 2400,   color: '#c0c000' },
            { min: 2400, max: 2800,   color: '#ff8000' },
            { min: 2800, max: 999999, color: '#ff0000' },
          ];

          tasks.forEach((task, idx) => {
            const label = String.fromCharCode('A'.charCodeAt(0) + idx);
            // タスクへのリンク
            const taskUrl = `https://onlinemathcontest.com/contests/${contestName}/tasks/${task.id}`;

            // 集計用
            let triedCount = 0;
            let solvedCount = 0;
            let penSum = 0;
            let penNonZero = 0;
            // レート分布のカウント
            const rateCounter = Array(rateColors.length).fill(0);
            // レート0 (None含む) のユーザーをカウントするための変数
            let countBlack = 0;

            standings.forEach(userStand => {
              const t = userStand.tasks?.[idx];
              if (!t) return;

              const tim = t.time;         // 整数 or null
              const pen = t.penalty;      // 整数 or null
              // userStand.user?.rate がnull/undefinedの場合は0と同じ扱い
              const r   = userStand.user?.rate ?? 0;

              // 提出ずみかどうか
              const tried = (tim != null) || (pen != null && pen >= 1);
              if (tried) {
                triedCount++;
              }

              // CA(正解)したかどうか
              if (tim != null) {
                solvedCount++;
                const realPen = pen ?? 0;
                penSum += realPen;
                if (realPen >= 1) {
                  penNonZero++;
                }
                // レートが0なら黒にする
                if (r === 0) {
                  countBlack++;
                } else {
                  // レートごとにカウント
                  for (let i = 0; i < rateColors.length; i++){
                    if (r >= rateColors[i].min && r < rateColors[i].max) {
                      rateCounter[i]++;
                      break;
                    }
                  }
                }
              }
            });

            // 表示用の文字列
            let peopleText = '0/0';
            let accPct = '0.00%';
            let avgPen = '0.00';
            let penPct = '0.00%';

            if (triedCount > 0) {
              peopleText = `${solvedCount}/${triedCount}`;
              const accuracy = (solvedCount / triedCount) * 100;
              accPct = accuracy.toFixed(2) + '%';
            }
            if (solvedCount > 0) {
              avgPen = (penSum / solvedCount).toFixed(2);
              const penRate = (penNonZero / solvedCount) * 100;
              penPct = penRate.toFixed(2) + '%';
            }

            const tr = document.createElement('tr');
            // ここで問題ラベルをリンク化
            tr.innerHTML = `
              <td style="border:1px solid #ccc; padding:4px;">
                <a href="${taskUrl}">
                  ${label}
                </a>
              </td>
              <td style="border:1px solid #ccc; padding:4px;">${task.admin_point ?? '-'}</td>
              <td style="border:1px solid #ccc; padding:4px;">${peopleText}</td>
              <td style="border:1px solid #ccc; padding:4px;">${accPct}</td>
              <td style="border:1px solid #ccc; padding:4px;">${avgPen}</td>
              <td style="border:1px solid #ccc; padding:4px;">${penPct}</td>
            `;

            // レート分布の棒グラフ
            const tdRate = document.createElement('td');
            tdRate.style.border = '1px solid #ccc';
            tdRate.style.padding = '4px';
            tdRate.style.width = '200px';

            const barDiv = document.createElement('div');
            barDiv.style.width = '100%';
            barDiv.style.height = '16px';
            barDiv.style.display = 'flex';
            barDiv.style.border = '1px solid #ccc';

            if (solvedCount > 0) {
              // まずレート0 (None) のユーザーを描画
              if (countBlack > 0) {
                const ratio = (countBlack / solvedCount) * 100;
                const segBlack = document.createElement('div');
                segBlack.style.width = ratio + '%';
                segBlack.style.backgroundColor = '#000000'; // 黒
                barDiv.appendChild(segBlack);
              }

              // それ以外の既存レート分布を描画
              for (let i = 0; i < rateCounter.length; i++){
                const c = rateCounter[i];
                if (!c) continue;
                const ratio = (c / solvedCount) * 100;
                const seg = document.createElement('div');
                seg.style.width = ratio + '%';
                seg.style.backgroundColor = rateColors[i].color;
                barDiv.appendChild(seg);
              }
            }

            tdRate.appendChild(barDiv);
            tr.appendChild(tdRate);
            tbody.appendChild(tr);
          });
        }
      })
      .catch(err => {
        console.error('Standings API取得失敗:', err);
      });
  }
})();