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:
1 parent
02c8e76237
commit
d92953a270
9 files changed
+410
-382
No files matched your search
+6
-80
@@ -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 };
|
||||
Reference in new issue
Block a user