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

+55
View File
@@ -0,0 +1,55 @@
'use strict';
// 保存してある回数から、画面に出す割合を作る。テーブルの種類ごとと、すべてを合わせた合計(total)
const { emptyBucket, MODES } = require('./store');
const pct = (a, b) => (b > 0 ? Math.round((a / b) * 1000) / 10 : null);
const sum = (xs) => xs.reduce((a, b) => a + b, 0);
function merge(buckets) {
const out = emptyBucket();
for (const b of buckets) {
for (const [k, v] of Object.entries(b)) {
if (Array.isArray(v)) v.forEach((x, i) => (out[k][i] += x));
else out[k] += v;
}
}
out.netBB = Math.round(out.netBB * 100) / 100;
return out;
}
function view(b) {
return {
hands: b.hands,
won: b.won,
winRate: pct(b.won, b.hands),
vpip: pct(b.vpip, b.preflop),
pfr: pct(b.pfr, b.preflop),
ats: pct(b.ats, b.atsOpp),
atsOpp: b.atsOpp,
threeBet: pct(b.threeBet, b.threeBetOpp),
threeBetOpp: b.threeBetOpp,
sawFlop: b.sawFlop,
showdowns: b.showdown,
wtsd: pct(b.showdown, b.sawFlop),
wsd: pct(b.showdownWon, b.showdown),
wwsf: pct(b.wonSawFlop, b.sawFlop),
// ストリートごとの降りた割合(そのストリートで判断したハンドのうち)
fold: b.folds.map((f, i) => pct(f, b.seen[i])),
// アグレッション頻度: 全部の行動(フォールド・チェック・コール・ベット・レイズ)のうちベット・レイズ
afq: pct(sum(b.aggressive), sum(b.actions)),
afqStreets: b.aggressive.map((a, i) => pct(a, b.actions[i])),
netBB: b.netBB,
bb100: b.hands > 0 ? Math.round((b.netBB / b.hands) * 1000) / 10 : null,
hours: Math.round((b.ms / 3600000) * 10) / 10,
};
}
function summarize(data) {
const modes = {};
for (const m of MODES) modes[m] = view(data.modes[m] || emptyBucket());
modes.total = view(merge(MODES.map((m) => data.modes[m]).filter(Boolean)));
return { since: data.firstAt, modes };
}
module.exports = { summarize };