'use strict'; // レビュー用の勝率計算(@pokertools/evaluator の役判定を使う。役の強さのスコアは小さいほど強い) // rangeEquity : 相手ごとの推定レンジ(手札の組み合わせの一覧)に対する勝率(モンテカルロ法) // showdownShare: 実際の手札どうしで、残りの場札をすべて(多いときはサンプリングで)めくったときの取り分 const { evaluate, getCardCode } = require('@pokertools/evaluator'); const HandRange = require('../../admin/range-data'); const codes = (cards) => cards.map(getCardCode); // 1326通りの手札。段階(色)ごとに使う const COMBOS = []; { const ranks = 'AKQJT98765432'; const suits = 'shdc'; const deck = []; for (const r of ranks) for (const s of suits) deck.push(r + s); for (let i = 0; i < deck.length; i++) { for (let j = i + 1; j < deck.length; j++) { const cards = [deck[i], deck[j]]; COMBOS.push({ c: codes(cards), tier: HandRange.tierOf(HandRange.handKey(cards)) }); } } } // minTier 以上の色の手(0 ならすべて)から、dead のカードを含むものを除く function rangeOf(minTier, dead) { const d = new Set(dead); return COMBOS.filter((x) => x.tier >= minTier && !d.has(x.c[0]) && !d.has(x.c[1])).map((x) => x.c); } // いまの場札での役の強さの上位 fraction だけ残す(ベット・レイズされるほど強い手に絞る) function narrow(range, board, fraction) { if (fraction >= 1 || board.length < 3 || range.length === 0) return range; const scored = range.map((c) => ({ c, v: evaluate(c.concat(board)) })); scored.sort((a, b) => a.v - b.v); const keep = Math.max(1, Math.ceil(scored.length * fraction)); return scored.slice(0, keep).map((x) => x.c); } // hero の手札 vs 相手ごとのレンジ。引き分けは等分。ranges が空なら 1 function rangeEquity(hero, board, ranges, { iterations = 1200, rng = Math.random } = {}) { const live = ranges.filter((r) => r.length > 0); if (live.length === 0) return 1; const fixed = hero.concat(board); let score = 0; let n = 0; const used = new Uint8Array(52); for (let it = 0; it < iterations; it++) { used.fill(0); for (const c of fixed) used[c] = 1; const opp = []; let ok = true; for (const r of live) { let pick = null; for (let tries = 0; tries < 30 && !pick; tries++) { const c = r[Math.floor(rng() * r.length)]; if (!used[c[0]] && !used[c[1]]) pick = c; } if (!pick) { ok = false; break; } used[pick[0]] = used[pick[1]] = 1; opp.push(pick); } if (!ok) continue; const full = board.slice(); while (full.length < 5) { const c = Math.floor(rng() * 52); if (!used[c]) { used[c] = 1; full.push(c); } } const me = evaluate(hero.concat(full)); let ties = 0; let lost = false; for (const o of opp) { const v = evaluate(o.concat(full)); if (v < me) { lost = true; break; } if (v === me) ties++; } if (!lost) score += 1 / (ties + 1); n++; } return n ? score / n : 0.5; } // 残りの場札の組み合わせ。2枚以下ならすべて、3枚以上はサンプリング function* runouts(board, dead, samples, rng) { const need = 5 - board.length; const used = new Set(board.concat(dead)); const deck = []; for (let c = 0; c < 52; c++) if (!used.has(c)) deck.push(c); if (need === 0) { yield board; } else if (need === 1) { for (const a of deck) yield board.concat([a]); } else if (need === 2) { for (let i = 0; i < deck.length; i++) for (let j = i + 1; j < deck.length; j++) yield board.concat([deck[i], deck[j]]); } else { for (let k = 0; k < samples; k++) { for (let i = 0; i < need; i++) { const j = i + Math.floor(rng() * (deck.length - i)); [deck[i], deck[j]] = [deck[j], deck[i]]; } yield board.concat(deck.slice(0, need)); } } } // オールインの時点で、hero がポットごとに受け取る額の期待値と勝率 // hands: 席番号 → 手札("As" 形式)。pots: [{ amount, eligible: [席番号] }] function showdownShare(heroSeat, hands, boardCards, pots, { samples = 4000, rng = Math.random } = {}) { const board = codes(boardCards); const seats = [...hands.keys()]; const hole = new Map(seats.map((s) => [s, codes(hands.get(s))])); const dead = seats.flatMap((s) => hole.get(s)); let ev = 0; let equity = 0; // メインポット(全員が対象)での取り分の割合 let n = 0; for (const full of runouts(board, dead, samples, rng)) { const score = new Map(seats.map((s) => [s, evaluate(hole.get(s).concat(full))])); pots.forEach((pot, i) => { if (!pot.eligible.includes(heroSeat)) return; const best = Math.min(...pot.eligible.map((s) => score.get(s))); if (score.get(heroSeat) !== best) return; const share = 1 / pot.eligible.filter((s) => score.get(s) === best).length; ev += pot.amount * share; if (i === 0) equity += share; }); n++; } return { ev: ev / n, equity: equity / n }; } module.exports = { codes, rangeOf, narrow, rangeEquity, showdownShare };