OMCStandingsStatistics

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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);
      });
  }
})();