Files
agomizi1cmandClaude Opus 5.5 dd39c043c4 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>
2026-10-22 16:04:33 +09:00

91 lines
3.4 KiB
JavaScript

'use strict';
// 1ハンドから、1人のプレイヤーの統計の材料(数えるだけ。割合は summary.js で出す)を取り出す。
// 離席・退席などで自動で行った行動は本人の判断として数えない。キャッシュオンリー(場札なし)でも同じように数える
const { positionOf, ACTION_KIND } = require('../review/preflop');
const STREETS = ['PREFLOP', 'FLOP', 'TURN', 'RIVER'];
// スチール(ATS)を試みられる位置: 全員が降りて回ってきた CO・BTN・SB
const STEAL_POSITIONS = new Set(['CO', 'BTN', 'SB', 'BTN/SB']);
function handStats(hand, seat) {
const p = hand.players.get(seat);
const mine = hand.log.filter((e) => e.seat === seat && !e.auto);
const pre = mine.filter((e) => e.street === 'PREFLOP');
const kind = (e) => ACTION_KIND[e.type];
// フロップを見た: プリフロップで降りておらず、ハンドがフロップまで進んだ(プリフロップのオールインで
// 残りをめくったときも含む)
const foldedPre = hand.log.some((e) => e.seat === seat && e.street === 'PREFLOP' && e.type === 'FOLD');
const reachedFlop = hand.showdown || hand.board.length >= 3 || hand.log.some((e) => e.street !== 'PREFLOP');
const sawFlop = !foldedPre && reachedFlop;
const showdown = Boolean(hand.showdown && !p.folded);
const won = p.won > 0;
// スチールと 3bet の機会(プリフロップを順に見る)
const position = positionOf(hand, seat);
let entered = false; // 自分より前に誰かがコール・レイズした
let raises = 0;
let first = true;
let atsOpp = false;
let ats = false;
let threeBetOpp = false;
let threeBet = false;
for (const e of hand.log) {
if (e.street !== 'PREFLOP') continue;
const k = kind(e);
if (e.seat === seat && !e.auto) {
if (first && !entered && STEAL_POSITIONS.has(position)) {
atsOpp = true;
ats = k === 'raise';
}
first = false;
// 3bet の機会: プリフロップでレイズがちょうど1回あった後の判断
if (raises === 1 && !threeBetOpp) {
threeBetOpp = true;
threeBet = k === 'raise';
}
}
if (k === 'call' || k === 'raise') entered = true;
if (k === 'raise') raises++;
}
// ストリートごと: 判断した(そのストリートに来た)・降りた・全部の行動のうちベット・レイズ
const seen = [0, 0, 0, 0];
const folds = [0, 0, 0, 0];
const actions = [0, 0, 0, 0];
const aggressive = [0, 0, 0, 0];
for (const e of mine) {
const i = STREETS.indexOf(e.street);
if (i < 0) continue;
seen[i] = 1;
actions[i]++;
if (kind(e) === 'raise') aggressive[i]++;
if (e.type === 'FOLD') folds[i] = 1;
}
const start = hand.startStacks.get(seat);
const end = hand.endStacks ? hand.endStacks.get(seat) : start;
return {
preflop: pre.length > 0, // VPIP・PFR の分母(BB で全員が降りてきたハンドなど、判断の無いハンドは除く)
vpip: pre.some((e) => kind(e) === 'call' || kind(e) === 'raise'),
pfr: pre.some((e) => kind(e) === 'raise'),
atsOpp,
ats,
threeBetOpp,
threeBet,
sawFlop,
showdown,
showdownWon: showdown && won,
wonSawFlop: sawFlop && won,
won,
seen,
folds,
actions,
aggressive,
netBB: hand.bb > 0 ? (end - start) / hand.bb : 0,
};
}
module.exports = { handStats, STREETS };