'use strict'; // 1回の対局(レビュー付きCPU戦のテーブル1つ)のハンドのレビューをまとめる // 成績(VPIP・PFR・3bet・Cベット・WTSD・W$SD・AF)、ストリートごとの平均点、 // 色ごとの参加率とレンジどおりに打てた割合、ポジションごとの傾向とクセ、見直したいハンド、収支の推移 const HandRange = require('../../admin/range-data'); const { STREETS, COLOR_LABEL } = require('./analyze'); const pct = (a, b) => (b ? Math.round((a / b) * 1000) / 10 : null); const mean = (xs) => (xs.length ? Math.round(xs.reduce((x, y) => x + y, 0) / xs.length) : null); const POSITION_ORDER = ['UTG', 'UTG+1', 'UTG+2', 'LJ', 'HJ', 'CO', 'BTN', 'BTN/SB', 'SB', 'BB']; const STREET_LABEL = { FLOP: 'フロップ', TURN: 'ターン', RIVER: 'リバー' }; // クセとして数える判定と、その説明 const LEAK_TEXT = { loose: 'レンジ外の手で参加', tight: '参加できる手で降りた', passive: 'レイズできる手でコール', aggressive: 'コールまでの手でレイズ', needless: 'チェックできるのに降りた', }; function summarize(hands) { const n = hands.length; const count = (f) => hands.filter(f).length; const stat = (key) => count((h) => h.stats[key]); const sawFlop = stat('sawFlop'); const showdowns = stat('showdown'); const aggressive = hands.reduce((a, h) => a + h.stats.aggressive, 0); const calls = hands.reduce((a, h) => a + h.stats.calls, 0); const stats = { hands: n, vpip: pct(stat('vpip'), n), pfr: pct(stat('pfr'), n), threeBet: pct(stat('threeBet'), stat('threeBetOpp')), threeBetOpp: stat('threeBetOpp'), cbet: pct(stat('cbet'), stat('cbetOpp')), cbetOpp: stat('cbetOpp'), wtsd: pct(showdowns, sawFlop), wsd: pct(stat('wonShowdown'), showdowns), af: calls ? Math.round((aggressive / calls) * 100) / 100 : aggressive ? null : 0, }; const streets = {}; for (const st of STREETS) streets[st] = mean(hands.map((h) => h.streetScores[st]).filter((x) => x != null)); const total = mean(hands.map((h) => h.handScore).filter((x) => x != null)); // 色ごと: 配られた回数・参加した回数・プリフロップの判断がレンジどおりだった割合 const colors = HandRange.CATEGORIES.map((c) => { const hs = hands.filter((h) => h.hero.color === c.id); const judged = hs.flatMap((h) => h.preflop); return { id: c.id, name: c.name, label: COLOR_LABEL[c.id] || c.label, dealt: hs.length, played: hs.filter((h) => h.stats.vpip).length, judged: judged.length, good: judged.filter((j) => j.verdict === 'good').length, }; }); // ポジションごと const byPos = new Map(); for (const h of hands) { const pos = h.hero.position; if (!byPos.has(pos)) byPos.set(pos, { position: pos, hands: 0, played: 0, judged: 0, good: 0, verdicts: {} }); const p = byPos.get(pos); p.hands++; if (h.stats.vpip) p.played++; for (const j of h.preflop) { p.judged++; if (j.verdict === 'good') p.good++; else p.verdicts[j.verdict] = (p.verdicts[j.verdict] || 0) + 1; } } const positions = [...byPos.values()].sort((a, b) => POSITION_ORDER.indexOf(a.position) - POSITION_ORDER.indexOf(b.position)); // クセ: ポジション × 間違いの種類で多いもの。フロップ以降は失った期待値が大きいストリート × 行動 const leaks = []; for (const p of positions) { for (const [v, k] of Object.entries(p.verdicts)) leaks.push({ text: `${p.position}: ${LEAK_TEXT[v]}`, count: k, of: p.judged }); } const postLeaks = new Map(); for (const h of hands) { for (const d of h.decisions) { if (d.score >= 70) continue; const key = `${d.street}:${d.actual.kind}`; if (!postLeaks.has(key)) postLeaks.set(key, { street: d.street, kind: d.actual.kind, count: 0, lossBB: 0 }); const x = postLeaks.get(key); x.count++; x.lossBB += d.lossBB; } } const KIND = { fold: 'フォールド', check: 'チェック', call: 'コール', raise: 'ベット・レイズ' }; for (const x of postLeaks.values()) { leaks.push({ text: `${STREET_LABEL[x.street]}: ${KIND[x.kind]}で期待値を失った(合計 ${Math.round(x.lossBB * 10) / 10}BB)`, count: x.count }); } leaks.sort((a, b) => b.count - a.count); // 見直したいハンド: 失った期待値(BB)が大きい順、同じならプリフロップの判定が悪い順 const worst = hands .filter((h) => h.lossBB > 0 || (h.handScore != null && h.handScore < 100)) .map((h) => ({ number: h.number, key: h.hero.key, color: h.hero.color, position: h.hero.position, lossBB: h.lossBB, handScore: h.handScore, net: h.hero.net, bb: h.bb, })) .sort((a, b) => b.lossBB - a.lossBB || a.handScore - b.handScore) .slice(0, 5); // 収支の推移(実際の収支と、オールインの期待値で運を除いた収支) let net = 0; let evNet = 0; const graph = hands.map((h) => { net += h.hero.net; evNet += h.hero.evNet; return { number: h.number, net, evNet }; }); return { stats, streets, total, colors, positions, leaks: leaks.slice(0, 6), worst, graph }; } module.exports = { summarize };