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>
153 lines
8.0 KiB
JavaScript
153 lines
8.0 KiB
JavaScript
'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 };
|