Files
Poker/server/review/preflop.js
T
agomizi1cmandClaude Opus 5.5 d89ce1dd30 Follow Yokosawa hand range rules for 3-bets and BB defense in reviews
When the hero's own raise is re-raised, a hand in the same color as the
re-raiser's estimated range is now a call (raise still needs two above).
In the BB facing a single open of up to 4BB, the hero now defends wider:
blue or better (white vs CO, purple frame vs BTN, one below the opener)
calls, and three above raises. Opponent range estimates follow the same
rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:31:07 +09:00

153 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// プリフロップの判定: 配られた手が登録してあるハンドレンジ(ヨコサワハンドレンジ)の何色で、そのとおりに行動できたか。
// 自分より前に誰もコール・レイズしていない: 後ろの人数(自分を含めず、まだ行動していない人)に対応する色
// 以上ならレイズ(オープン)、それより弱ければフォールド
// 自分より前にコール・レイズした人がいる: その人のレンジを色で推定し(その人の後ろの人数に対応する色)、
// 自分の手がその色の一つ上ならコール、二つ上ならレイズ、同じかそれより弱ければフォールド
// 自分のレイズにリレイズされた(3bet・4betされた): 相手の推定レンジと同じ色でもコール、二つ上ならレイズ
// BB で 4BB までのオープン(リレイズなし)に直面: 安く参加できるので広く守る。青以上でコール
// (CO のオープンなら白、BTN なら紫枠以上。相手の色の一つ下まで)、三つ上ならレイズ
// 2人目以降に参加した人のレンジも同じ決まりで、コール・レイズできる一番弱い色と推定する
// (ふつうはコールなら直前までで一番強いレンジの一つ上、レイズなら二つ上)。
const HandRange = require('../../admin/range-data');
const TOP_TIER = 7; // 紺
const BB_CALL_TIER = 3; // 青: BB はどこからのオープンにもこの色以上でコールできる
const BB_DEFENSE_MAX_OPEN = 4; // BB で広く守るのはオープンがこの BB 数まで
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);
// 前に参加した人がいるときに、コール・レイズできる一番弱い色の段階。rule は
// 'normal': 一つ上でコール、二つ上でレイズ(紺は紺に対してもコールできる)
// 'reraised': 自分のレイズにリレイズされた。同じ色でコール、二つ上でレイズ
// 'bb': BB でオープンに直面。相手の色の一つ下(青まで)でコール、三つ上でレイズ(紫枠より下は参加しない)
function thresholds(rule, ref) {
if (rule === 'reraised') return { call: Math.min(TOP_TIER, ref), raise: ref + 2 };
if (rule === 'bb') return { call: Math.max(1, Math.min(BB_CALL_TIER, ref - 1)), raise: ref + 3 };
return { call: Math.min(TOP_TIER, ref + 1), raise: ref + 2 };
}
// 推奨: '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 {
rec = tier >= situation.raise ? 'raise' : tier >= situation.call ? 'call' : '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;
let raises = 0;
const raisers = new Set(); // プリフロップでレイズした席
const judgments = [];
// まだ行動していない後ろの人数(自分を含めない)
const behindOf = (seat) => order.slice(order.indexOf(seat) + 1).filter((s) => !acted.has(s)).length;
// 前に参加した人がいるときの決まり(thresholds を参照)
const ruleOf = (e) => {
if (raisers.has(e.seat)) return 'reraised';
if (e.seat === hand.bbSeat && raises === 1 && e.currentBet <= BB_DEFENSE_MAX_OPEN * hand.bb) return 'bb';
return 'normal';
};
hand.log.forEach((e, index) => {
if (e.street !== 'PREFLOP') return;
const kind = ACTION_KIND[e.type];
const rule = maxTier == null ? null : ruleOf(e);
const limits = rule && thresholds(rule, maxTier);
if (e.seat === hero && !e.auto) {
const situation =
maxTier == null
? { kind: 'open', behind: behindOf(hero), required: HandRange.requiredTier(behindOf(hero)) }
: { kind: 'facing', rule, refTier: maxTier, refSeat: maxSeat, ...limits };
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, kind === 'raise' ? limits.raise : limits.call);
estimated.set(e.seat, Math.max(estimated.get(e.seat) || 0, est));
// 一番強いレンジ(同じならレイズした人)を、後の人の判断の基準にする
if (maxTier == null || est > maxTier || (est === maxTier && kind === 'raise')) {
maxTier = est;
maxSeat = e.seat;
}
}
if (kind === 'raise') {
raises++;
raisers.add(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 who = nameOf(situation.refSeat);
const ref = tierName(situation.refTier);
const head =
situation.rule === 'reraised'
? `自分のレイズに ${who} がリレイズ(推定レンジ ${ref})`
: situation.rule === 'bb'
? `BBで ${who} のオープン(推定レンジ ${ref})に直面`
: `${who} の推定レンジ ${ref}`;
const raise = situation.raise > TOP_TIER ? 'レイズはなし' : `${tierName(situation.raise)}以上でレイズ`;
return `${head} → ${tierName(situation.call)}以上でコール、${raise}`;
}
module.exports = { judgePreflop, positionOf, describeSituation, tierName, ACTION_KIND, ACTION_LABEL, VERDICTS };