Add admin-only CPU tables with per-hand reviews
A new table type, "review" (レビュー付きCPU戦), can only be created by admins. Only the creator and CPUs sit there: friends cannot join, and the table is hidden from non-admin lobbies. After every hand a review opens, and the next hand waits until the creator presses "次のハンドへ". - Preflop: shows which color of the registered hand range the hole cards are and whether the action followed it. With no entrant before you, open with the color for the number of players still to act; facing an entrant, estimate their range from their position, call one color above it and raise two above. Purple ranks just below white (SB only). - Flop onward: equity vs ranges estimated from those colors (narrowed by later bets/calls) gives a rough EV per option. The score is the EV lost against the best option, relative to the pot. It is a guide, not GTO. - Also: pot-odds call check, all-in equity and luck-adjusted P&L, VPIP/PFR/3bet/Cbet/WTSD/W$SD/AF, per-color and per-position compliance, habits to watch, worst-5 hands, a P&L chart and a step-by-step replay on the admin-only /review page. Hand now logs every action for the review. The range data moves to admin/range-data.js, shared by the range page and the server. Reviews are saved under data/reviews/ (newest 50 sessions). Admin scripts and CSS are no longer cached for a day, so updates show up immediately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3116b1da9f
commit
3f2515a9fa
25 files changed
+2262
-60
No files matched your search
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
// プリフロップの判定: 配られた手が登録してあるハンドレンジの何色で、そのとおりに行動できたか。
|
||||
// 自分より前に誰もコール・レイズしていない: 後ろの人数(自分を含めず、まだ行動していない人)に対応する色
|
||||
// 以上ならレイズ(オープン)、それより弱ければフォールド
|
||||
// 自分より前にコール・レイズした人がいる: その人のレンジを色で推定し(その人の後ろの人数に対応する色)、
|
||||
// 自分の手がその色の一つ上ならコール、二つ上ならレイズ、それより弱ければフォールド
|
||||
// 2人目以降に参加した人のレンジは、コールなら直前までで一番強いレンジの一つ上、レイズなら二つ上と推定する
|
||||
// (同じ決まりで打つ人がコール・レイズできる一番弱い色)。
|
||||
const HandRange = require('../../admin/range-data');
|
||||
|
||||
const TOP_TIER = 7; // 紺
|
||||
|
||||
const ACTION_KIND = { FOLD: 'fold', CHECK: 'check', CALL: 'call', BET: 'raise', RAISE: 'raise' };
|
||||
const ACTION_LABEL = { fold: 'フォールド', check: 'チェック', call: 'コール', raise: 'レイズ' };
|
||||
|
||||
const VERDICTS = {
|
||||
good: { mark: '◎', score: 100, label: 'レンジどおり' },
|
||||
passive: { mark: '△', score: 50, label: 'レイズできる手でコール(チェック)した' },
|
||||
aggressive: { mark: '△', score: 40, label: 'コールまでの手でレイズした' },
|
||||
loose: { mark: '✕', score: 0, label: 'レンジ外の手で参加した' },
|
||||
tight: { mark: '✕', score: 0, label: '参加できる手で降りた' },
|
||||
needless: { mark: '✕', score: 0, label: 'チェックできるのに降りた' },
|
||||
};
|
||||
|
||||
// 9人のテーブルの呼び方。UTG から CO までは人数に合わせて後ろから当てはめる
|
||||
const MIDDLE_POSITIONS = ['UTG', 'UTG+1', 'UTG+2', 'LJ', 'HJ', 'CO'];
|
||||
|
||||
function positionOf(hand, seat) {
|
||||
const n = hand.order.length;
|
||||
if (seat === hand.button) return n === 2 ? 'BTN/SB' : 'BTN';
|
||||
if (seat === hand.sbSeat) return 'SB';
|
||||
if (seat === hand.bbSeat) return 'BB';
|
||||
const middle = hand.preflopOrder.slice(0, n - 3);
|
||||
const names = MIDDLE_POSITIONS.slice(-middle.length);
|
||||
return names[middle.indexOf(seat)] || '?';
|
||||
}
|
||||
|
||||
const tierName = (tier) => (tier > TOP_TIER ? 'なし' : HandRange.categoryOfTier(Math.max(0, tier)).name);
|
||||
|
||||
// 推奨: 'raise' / 'call' / 'fold'(チェックできるときの 'fold' と 'call' は 'check')
|
||||
function recommend(tier, situation, entry) {
|
||||
let rec;
|
||||
if (situation.kind === 'open') {
|
||||
rec = situation.behind === 0 ? 'check' : tier >= situation.required ? 'raise' : 'fold';
|
||||
} else {
|
||||
const ref = situation.refTier;
|
||||
if (tier >= ref + 2) rec = 'raise';
|
||||
else if (tier >= ref + 1 || (tier === TOP_TIER && ref >= TOP_TIER)) rec = 'call';
|
||||
else rec = 'fold';
|
||||
}
|
||||
if (rec === 'raise' && !entry.canRaise) rec = 'call';
|
||||
if ((rec === 'call' || rec === 'fold') && entry.canCheck) rec = 'check';
|
||||
return rec;
|
||||
}
|
||||
|
||||
function verdictOf(rec, actual) {
|
||||
if (rec === actual) return 'good';
|
||||
if (actual === 'fold') return rec === 'check' ? 'needless' : 'tight';
|
||||
if (rec === 'raise') return 'passive'; // コール・チェックした
|
||||
if (rec === 'call') return 'aggressive'; // レイズした
|
||||
return 'loose'; // 降りる(チェックする)べき手で参加した
|
||||
}
|
||||
|
||||
// プリフロップの各行動を順に見て、hero の判断ごとに判定する。あわせて各席の推定レンジ(色の段階)を返す
|
||||
function judgePreflop(hand, hero) {
|
||||
const tier = HandRange.tierOf(HandRange.handKey(hand.players.get(hero).cards));
|
||||
const order = hand.preflopOrder;
|
||||
const acted = new Set();
|
||||
const estimated = new Map(); // 席 → 推定レンジの一番弱い色の段階
|
||||
let maxTier = null;
|
||||
let maxSeat = null;
|
||||
const judgments = [];
|
||||
|
||||
// まだ行動していない後ろの人数(自分を含めない)
|
||||
const behindOf = (seat) => order.slice(order.indexOf(seat) + 1).filter((s) => !acted.has(s)).length;
|
||||
|
||||
hand.log.forEach((e, index) => {
|
||||
if (e.street !== 'PREFLOP') return;
|
||||
const kind = ACTION_KIND[e.type];
|
||||
if (e.seat === hero && !e.auto) {
|
||||
const situation =
|
||||
maxTier == null
|
||||
? { kind: 'open', behind: behindOf(hero), required: HandRange.requiredTier(behindOf(hero)) }
|
||||
: { kind: 'facing', refTier: maxTier, refSeat: maxSeat };
|
||||
const rec = recommend(tier, situation, e);
|
||||
const verdict = verdictOf(rec, kind);
|
||||
judgments.push({ index, situation, recommended: rec, actual: kind, verdict, ...VERDICTS[verdict] });
|
||||
}
|
||||
if (kind === 'call' || kind === 'raise') {
|
||||
const est =
|
||||
maxTier == null
|
||||
? HandRange.requiredTier(behindOf(e.seat))
|
||||
: Math.min(TOP_TIER, maxTier + (kind === 'raise' ? 2 : 1));
|
||||
estimated.set(e.seat, Math.max(estimated.get(e.seat) || 0, est));
|
||||
if (maxTier == null || est >= maxTier) {
|
||||
maxTier = est;
|
||||
maxSeat = e.seat;
|
||||
}
|
||||
}
|
||||
acted.add(e.seat);
|
||||
});
|
||||
return { tier, judgments, estimated };
|
||||
}
|
||||
|
||||
// 状況の説明(画面表示用)
|
||||
function describeSituation(situation, nameOf) {
|
||||
if (situation.kind === 'open') {
|
||||
if (situation.behind === 0) return '全員が降りてBBに回ってきた';
|
||||
return `前に参加者なし・後ろに${situation.behind}人 → ${tierName(situation.required)}以上でオープン`;
|
||||
}
|
||||
const ref = situation.refTier;
|
||||
return `${nameOf(situation.refSeat)} の推定レンジ ${tierName(ref)} → ${tierName(ref + 1)}以上でコール、${tierName(ref + 2)}以上でレイズ`;
|
||||
}
|
||||
|
||||
module.exports = { judgePreflop, positionOf, describeSituation, tierName, ACTION_KIND, ACTION_LABEL, VERDICTS };
|
||||
Reference in new issue
Block a user