- 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>
91 lines
3.4 KiB
JavaScript
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 };
|