'use strict'; // 理論型(タイト): poker-utils のプリフロップの強さの順位で参加する手を絞り、 // フロップ以降は「相手がこのくらいの範囲の手を持っている」と仮定したレンジに対する勝率で判断する。 // ブラフはライブラリの alpha(必要なフォールド率)と bluffEV で割に合うときだけ打つ。 const pu = require('poker-utils'); const { situation, CHECK, CALL, FOLD, passive, allIn, raiseToAmount, raisePot, between } = require('./common'); // 169種類の手 → 上位何%か(0 が最強)。preflopStrengthOrder は弱い順 const PERCENTILE = new Map(); { const combos = (h) => (h.length === 2 ? 6 : h[2] === 's' ? 4 : 12); let stronger = 0; for (const h of pu.preflopStrengthOrder.slice().reverse()) { PERCENTILE.set(h, stronger / 1326); stronger += combos(h); } } const handPercentile = (cards) => PERCENTILE.get(pu.handToIsoPre(pu.boardToInts(cards.join('')))); // 上位 width の手のレンジ(5%刻みで作り置き) const ranges = new Map(); function topRange(width) { const key = Math.min(20, Math.max(1, Math.round(width * 20))); if (!ranges.has(key)) ranges.set(key, pu.PokerRange.fromPreflop(pu.PreflopRange.fromPercentiles(0, key / 20))); return ranges.get(key); } const RIVER_SAMPLES = 24; // 相手のレンジに対する勝率(引き分けは半分)。フロップは残り2枚をサンプリングする function rangeEquity(cards, board, vsRange, rng) { const hand = pu.boardToInts(cards.join('')); const known = pu.boardToInts(board.join('')); const boards = []; if (known.length === 3) { const used = new Set([...hand, ...known]); const deck = []; for (let c = 0; c < 52; c++) if (!used.has(c)) deck.push(c); for (let k = 0; k < RIVER_SAMPLES; k++) { const i = Math.floor(rng() * deck.length); let j = Math.floor(rng() * (deck.length - 1)); if (j >= i) j++; boards.push(known.concat([deck[i], deck[j]])); } } else { boards.push(known); } let sum = 0; let n = 0; for (const b of boards) { const r = pu.equityEval({ board: b, hand, vsRange }); for (const [win, tie, lose] of Array.isArray(r[0]) ? r : [r]) { const total = win + tie + lose; if (total > 0) { sum += (win + tie / 2) / total; n++; } } } return n ? sum / n : 0.5; } function randomStyle(rng) { return { width: between(rng, 0.8, 1.2), // 参加する手の広さ aggression: between(rng, 0.9, 1.2), // 大きいほど薄い手でもバリューベットする bluff: between(rng, 0.04, 0.12), // ブラフを考える頻度 foldEstimate: between(rng, 0.35, 0.5), // 相手が降りると見積もる割合 margin: between(rng, 0, 0.05), // コールに必要な勝率の上乗せ }; } function preflop(hand, seat, s, style) { const { o, p, stack, bb, opponents, raises } = s; const pct = handPercentile(p.cards); const stackBb = (stack + p.bet) / bb; // スタックが 12BB 以下ならプッシュかフォールド if (stackBb <= 12) { const pushPct = Math.min(0.6, 2.5 / stackBb) * style.width; if (pct < (raises === 0 ? pushPct : pushPct * 0.5)) return o.canRaise ? allIn(o) : CALL; return passive(o); } // 残りの人数が少ないほど広く参加する const open = (opponents >= 5 ? 0.14 : opponents >= 3 ? 0.2 : opponents === 2 ? 0.3 : 0.45) * style.width; if (raises === 0) { if (pct < open && o.canRaise) return raiseToAmount(hand, seat, Math.max(2.5 * bb, hand.currentBet * 3)); return passive(o); } if (raises === 1) { if (pct < open * 0.2 && o.canRaise) return raiseToAmount(hand, seat, hand.currentBet * 3); if (pct < open * 0.55) return CALL; return passive(o); } if (pct < 0.025) return o.canRaise ? allIn(o) : CALL; if (pct < 0.05) return CALL; return passive(o); } function decide(hand, seat, { style, rng = Math.random } = {}) { const s = situation(hand, seat); if (!s) return null; if (s.preflop) return preflop(hand, seat, s, style); const { o, p, opponents, pot, potOdds, raises } = s; // 相手のレンジ: フロップまで残った手(上位45%)。ベット・レイズされるほど強い手に絞る let width = 0.45 * style.width; if (o.toCall > 0) width *= 1 - 0.5 * Math.min(1, o.toCall / Math.max(1, pot - o.toCall)); if (raises >= 2) width = Math.min(width, 0.12); const eq = rangeEquity(p.cards, hand.board, topRange(width), rng); // 複数の相手全員に勝つ確率(独立とみなす) const eqAll = eq ** Math.max(1, opponents); if (o.canCheck) { if (o.canRaise && eqAll > 0.62 / style.aggression) return raisePot(hand, seat, 0.66); if (o.canRaise && opponents <= 2 && rng() < style.bluff) { // ポットの半分のブラフで必要なフォールド率(alpha)より、相手が降りる見積もりが大きければ打つ const bet = pot * 0.5; if (pu.bluffEV(style.foldEstimate, pu.alpha(bet, pot)) > 0) return raisePot(hand, seat, 0.5); } return CHECK; } if (o.canRaise && eqAll > 0.8 && raises < 3) return raisePot(hand, seat, 0.75); if (eqAll >= potOdds + style.margin) return CALL; return FOLD; } module.exports = { decide, randomStyle, handPercentile, rangeEquity };