Use @pokertools/engine and evaluator for poker rules and hand ranking

Replace the hand-rolled betting rules, side pots, showdown and hand
evaluator with the published @pokertools/engine and @pokertools/evaluator
libraries (MIT, pinned to 2.1.0).

- server/hand.js now wraps a PokerEngine per hand: it maps seats,
  mirrors stacks, staggers the all-in runout, and pays host-chosen
  winners on cash-only tables.
- server/cards.js and server/ai.js use the library evaluator.
- Out-of-turn folds go through the engine's stand; all-in hands stay
  live when the player leaves; uncalled bets return to a leaver.
- The engine refuses some check-able folds that would leave a side pot
  without eligible players; those now return a message (or check for
  a leaving player) instead of crashing.
- BB must be greater than SB (engine requirement); Node >= 24.
- Tests rig cards via engine snapshots and cover the new edge cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-17 17:25:15 +09:00
1 parent 02c8e76237
commit d92953a270
9 files changed
+410 -382

No files matched your search

+6 -80
View File
@@ -5,85 +5,13 @@
// ポットオッズ・ポジション・その CPU の性格(攻撃性・ブラフ頻度)から行動を決める。
// 相手の手札は見ない(自分の手札と場札、公開されているベット状況だけを使う)。
const RANKS = '23456789TJQKA';
const SUITS = 'shdc';
// "As" → 0〜51(rank * 4 + suit)
const toInt = (c) => RANKS.indexOf(c[0]) * 4 + SUITS.indexOf(c[1]);
// 13ビットのランクの集合から最も高いストレートの上端(0〜12)。無ければ -1
function straightHigh(mask) {
for (let h = 12; h >= 4; h--) {
const need = 0b11111 << (h - 4);
if ((mask & need) === need) return h;
}
// A-2-3-4-5
if ((mask & 0b1000000001111) === 0b1000000001111) return 3;
return -1;
}
// [役, 比較用の数字...] を1つの整数にする(大きいほど強い)
function encode(category, kickers) {
let v = category;
for (let i = 0; i < 5; i++) v = v * 16 + (kickers[i] === undefined ? 0 : kickers[i] + 1);
return v;
}
// 上位から n 個のランク(mask のビット)
function topRanks(mask, n, exclude = []) {
const out = [];
for (let r = 12; r >= 0 && out.length < n; r--) if (mask & (1 << r) && !exclude.includes(r)) out.push(r);
return out;
}
// 5〜7枚(整数)の最強の役の強さ。cards.js の bestHand と同じ順序になる(テストで確認)
function evaluate(cards) {
const counts = new Array(13).fill(0);
const suitCount = [0, 0, 0, 0];
const suitMask = [0, 0, 0, 0];
let mask = 0;
for (const c of cards) {
const r = c >> 2;
const s = c & 3;
counts[r]++;
suitCount[s]++;
suitMask[s] |= 1 << r;
mask |= 1 << r;
}
let flushMask = 0;
for (let s = 0; s < 4; s++) if (suitCount[s] >= 5) flushMask = suitMask[s];
if (flushMask) {
const sf = straightHigh(flushMask);
if (sf >= 0) return encode(8, [sf]);
}
let quad = -1;
const trips = [];
const pairs = [];
for (let r = 12; r >= 0; r--) {
if (counts[r] === 4) quad = r;
else if (counts[r] === 3) trips.push(r);
else if (counts[r] === 2) pairs.push(r);
}
if (quad >= 0) return encode(7, [quad, topRanks(mask, 1, [quad])[0]]);
if (trips.length && (trips.length > 1 || pairs.length)) {
const pair = Math.max(trips[1] === undefined ? -1 : trips[1], pairs[0] === undefined ? -1 : pairs[0]);
return encode(6, [trips[0], pair]);
}
if (flushMask) return encode(5, topRanks(flushMask, 5));
const st = straightHigh(mask);
if (st >= 0) return encode(4, [st]);
if (trips.length) return encode(3, [trips[0], ...topRanks(mask, 2, [trips[0]])]);
if (pairs.length >= 2) return encode(2, [pairs[0], pairs[1], topRanks(mask, 1, [pairs[0], pairs[1]])[0]]);
if (pairs.length) return encode(1, [pairs[0], ...topRanks(mask, 3, [pairs[0]])]);
return encode(0, topRanks(mask, 5));
}
const { evaluate, getCardCode } = require('@pokertools/evaluator');
// hole / board は "As" 形式。opponents 人のランダムな手札を相手にした勝率(引き分けは等分)
// 役の強さは @pokertools/evaluator で求める(スコアは小さいほど強い。カードは 0〜51 の整数)
function equity(hole, board, opponents, iterations, rng = Math.random) {
const mine = hole.map(toInt);
const known = board.map(toInt);
const mine = hole.map(getCardCode);
const known = board.map(getCardCode);
const used = new Set([...mine, ...known]);
const deck = [];
for (let c = 0; c < 52; c++) if (!used.has(c)) deck.push(c);
@@ -101,18 +29,16 @@ function equity(hole, board, opponents, iterations, rng = Math.random) {
}
const fullBoard = known.concat(deck.slice(0, needBoard));
const me = evaluate(mine.concat(fullBoard));
let best = 0;
let ties = 0;
let lost = false;
for (let k = 0; k < opponents; k++) {
const o = needBoard + k * 2;
const v = evaluate([deck[o], deck[o + 1]].concat(fullBoard));
if (v > me) {
if (v < me) {
lost = true;
break;
}
if (v === me) ties++;
if (v > best) best = v;
}
if (!lost) score += 1 / (ties + 1);
}
@@ -210,4 +136,4 @@ function decide(hand, seat, { style = DEFAULT_STYLE, rng = Math.random, iteratio
return { type: 'fold' };
}
module.exports = { evaluate, equity, decide, randomStyle, toInt, straightHigh };
module.exports = { equity, decide, randomStyle };