Add player stats, in-table ranking, host seating and review return

- Stats page (/stats): play style radar (TAG/LAG/rock/fish), VPIP, PFR,
  ATS, 3bet, WTSD, W$SD, WWSF, per-street fold rate, aggression
  frequency, hand win rate and net in BB, per table type, each with a
  bands-and-explanation sheet. Hands are counted per human player when
  they finish and stored as totals under data/stats/, keyed by a hash
  of the player ID (sent in a POST body, never in the URL).
- Ranking sheet at the table: everyone seated, ordered by stack or by
  net (stack minus buy-ins), updating live.
- The host can reorder seats by dragging (with a mini table preview and
  shuffle). Takes effect immediately between hands, otherwise at the
  start of the next hand; the dealer button stays with the same player.
- The full-session review opens in the same tab and its back button
  returns to the table it was opened from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-22 16:04:33 +09:00
1 parent 49203f202c
commit dd39c043c4
18 files changed
+2699 -18

No files matched your search

+22 -10
View File
@@ -40,14 +40,26 @@
return `<span class="pcard suit-${c[1]} ${extra}"><b>${rank}</b><i>${SUIT_SYMBOL[c[1]]}</i></span>`;
}
// ---------- 画面の切り替え(URL: ?s=対局ID&h=ハンド番号) ----------
// ---------- 画面の切り替え(URL: ?s=対局ID&h=ハンド番号&t=テーブルID) ----------
function show(view) {
for (const v of ['list', 'session', 'replay']) $(`view-${v}`).hidden = v !== view;
window.scrollTo(0, 0);
}
// テーブルのレビューから開いたときのテーブルID。どの画面に移っても引き継ぎ、対局の画面からテーブルへ戻れるようにする
function fromTable() {
const t = new URLSearchParams(location.search).get('t');
return t && /^[0-9a-f]{8}$/.test(t) ? t : null;
}
function setBack(href, label) {
$('back').href = href;
$('back-label').textContent = label;
}
function go(params, push = true) {
const q = new URLSearchParams(params).toString();
const table = fromTable();
const q = new URLSearchParams(table ? { ...params, t: table } : params).toString();
const url = location.pathname + (q ? `?${q}` : '');
if (push) history.pushState(null, '', url);
else history.replaceState(null, '', url);
@@ -58,10 +70,11 @@
const p = new URLSearchParams(location.search);
const id = p.get('s');
const hand = Number(p.get('h'));
const table = fromTable();
if (!id) {
$('title').textContent = 'CPU戦のレビュー';
$('back').href = '/';
$('back-label').textContent = 'ロビー';
if (table) setBack(`/table/${table}`, 'テーブル');
else setBack('/', 'ロビー');
show('list');
return loadList();
}
@@ -77,22 +90,21 @@
if (hand) {
const h = session.hands.find((x) => x.number === hand);
if (h) {
$('back').href = `?s=${encodeURIComponent(id)}`;
$('back-label').textContent = '対局';
setBack(`?s=${encodeURIComponent(id)}`, '対局');
return openReplay(h);
}
}
$('back').href = '/review';
$('back-label').textContent = '一覧';
if (table) setBack(`/table/${table}`, 'テーブル');
else setBack('/review', '一覧');
show('session');
renderSession();
}
window.addEventListener('popstate', route);
// 戻るボタンは履歴を積まずに一つ上へ
// 戻るボタンは履歴を積まずに一つ上へ(ロビー・テーブルへはそのままページを移る)
$('back').addEventListener('click', (e) => {
const href = $('back').getAttribute('href');
if (href === '/') return;
if (href === '/' || href.startsWith('/table/')) return;
e.preventDefault();
const p = new URLSearchParams(location.search);
go(p.get('h') ? { s: p.get('s') } : {});