Files
agomizi1cmandClaude Opus 5.5 3ddb0996e0 Switch hand ranges to Poker Skill's 6-max charts with 3bet/call frames
- Replace the Yokosawa ranges with the 6-max opening ranges (UTG, HJ, CO,
  SB, BTN). Colors keep the old order: navy (strong UTG), red UTG, yellow
  HJ, green CO, blue SB, white BTN. SB is narrower than BTN, so blue sits
  one tier above white; the purple-frame category is gone.
- Show the "BTN vs CO open" chart on the range page as frames: light blue
  for 3bet, yellow for call.
- Grade BTN facing an unlimped CO open by those frames in reviews; other
  spots keep the color-tier rule (BB defends down to green).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 15:21:39 +09:00

166 lines
9.2 KiB
JavaScript
Raw Permalink 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';
// プリフロップの判定: 配られた手が登録してあるハンドレンジ(6-max のオープンレンジ)の何色で、そのとおりに行動できたか。
// 自分より前に誰もコール・レイズしていない: 後ろの人数(自分を含めず、まだ行動していない人)に対応する色
// 以上ならレイズ(オープン)、それより弱ければフォールド
// 自分より前にコール・レイズした人がいる: その人のレンジを色で推定し(その人の後ろの人数に対応する色)、
// 自分の手がその色の一つ上ならコール、二つ上ならレイズ、同じかそれより弱ければフォールド
// 自分のレイズにリレイズされた(3bet・4betされた): 相手の推定レンジと同じ色でもコール、二つ上ならレイズ
// BTN で CO のオープン(ほかに参加者なし)に直面: レンジ表の枠のとおり。水色枠は 3bet、黄枠はコール、
// 枠なしはフォールド
// BB で 4BB までのオープン(リレイズなし)に直面: 安く参加できるので広く守る。緑(CO)以上でコール
// (CO のオープンなら青、BTN・SB なら白以上。相手の色の一つ下まで)、三つ上ならレイズ
// 2人目以降に参加した人のレンジも同じ決まりで、コール・レイズできる一番弱い色と推定する
// (ふつうはコールなら直前までで一番強いレンジの一つ上、レイズなら二つ上)。
const HandRange = require('../../admin/range-data');
const TOP_TIER = 6; // 紺
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 でオープンに直面。相手の色の一つ下(緑まで)でコール、三つ上でレイズ(白より下は参加しない)
// 'vsCO': BTN で CO のオープンに直面。判定はレンジ表の枠で行い、ここでの段階は推定レンジにだけ使う('normal' と同じ)
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 if (situation.rule === 'vsCO') {
rec = situation.facing === '3bet' ? 'raise' : situation.facing === 'call' ? 'call' : '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 key = HandRange.handKey(hand.players.get(hero).cards);
const tier = HandRange.tierOf(key);
const order = hand.preflopOrder;
const acted = new Set();
const estimated = new Map(); // 席 → 推定レンジの一番弱い色の段階
let maxTier = null;
let maxSeat = null;
let raises = 0;
let entrants = 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 (entrants === 1 && raises === 1 && positionOf(hand, e.seat) === 'BTN' && positionOf(hand, maxSeat) === 'CO') return 'vsCO';
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, ...(rule === 'vsCO' && { facing: HandRange.facingOf(key) }) };
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 === 'call' || kind === 'raise') entrants++;
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);
if (situation.rule === 'vsCO') {
const action = HandRange.facingAction(situation.facing);
return `BTNで ${who}(CO)のオープンに直面 → レンジ表の枠(水色枠は3bet、黄枠はコール)。この手は${action ? `${action.name}で${action.label}` : '枠なしでフォールド'}`;
}
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 };