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
+42
-88
@@ -1,96 +1,50 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
// 役の判定は @pokertools/evaluator(@pokertools/engine のショーダウンと同じ判定器)を使う。
|
||||
// ここでは表示用に、最強の5枚と日本語の役名を求める。
|
||||
// カードは "As"(スペードのA), "Td"(ダイヤの10)のような2文字
|
||||
const RANKS = '23456789TJQKA';
|
||||
const SUITS = 'shdc';
|
||||
const { evaluate, getCardCodes, rank, HandRank } = require('@pokertools/evaluator');
|
||||
|
||||
// 暗号論的乱数で Fisher-Yates シャッフル(山札はサーバの外に出さない)
|
||||
function newDeck() {
|
||||
const deck = [];
|
||||
for (const r of RANKS) for (const s of SUITS) deck.push(r + s);
|
||||
for (let i = deck.length - 1; i > 0; i--) {
|
||||
const j = crypto.randomInt(i + 1);
|
||||
[deck[i], deck[j]] = [deck[j], deck[i]];
|
||||
}
|
||||
return deck;
|
||||
}
|
||||
const HAND_NAMES = {
|
||||
[HandRank.HighCard]: 'ハイカード',
|
||||
[HandRank.OnePair]: 'ワンペア',
|
||||
[HandRank.TwoPair]: 'ツーペア',
|
||||
[HandRank.ThreeOfAKind]: 'スリーカード',
|
||||
[HandRank.Straight]: 'ストレート',
|
||||
[HandRank.Flush]: 'フラッシュ',
|
||||
[HandRank.FullHouse]: 'フルハウス',
|
||||
[HandRank.FourOfAKind]: 'フォーカード',
|
||||
[HandRank.StraightFlush]: 'ストレートフラッシュ',
|
||||
};
|
||||
const ROYAL_FLUSH = 1; // 判定器のスコアは小さいほど強く、1 がロイヤルフラッシュ
|
||||
|
||||
const rankValue = (card) => RANKS.indexOf(card[0]) + 2;
|
||||
|
||||
// 5枚の役の強さ。配列を先頭から比較して大きいほうが強い
|
||||
// [役の種類(0〜8), 比較用の数字...]
|
||||
function score5(cards) {
|
||||
const vals = cards.map(rankValue).sort((a, b) => b - a);
|
||||
const flush = cards.every((c) => c[1] === cards[0][1]);
|
||||
|
||||
let straightHigh = 0;
|
||||
if (new Set(vals).size === 5) {
|
||||
if (vals[0] - vals[4] === 4) straightHigh = vals[0];
|
||||
else if (vals[0] === 14 && vals[1] === 5) straightHigh = 5; // A-2-3-4-5
|
||||
}
|
||||
if (straightHigh && flush) return [8, straightHigh];
|
||||
|
||||
// 枚数の多い順、同じ枚数なら数字の大きい順
|
||||
const counts = new Map();
|
||||
for (const v of vals) counts.set(v, (counts.get(v) || 0) + 1);
|
||||
const groups = [...counts].sort((a, b) => b[1] - a[1] || b[0] - a[0]);
|
||||
const byGroup = groups.map((g) => g[0]);
|
||||
|
||||
if (groups[0][1] === 4) return [7, ...byGroup];
|
||||
if (groups[0][1] === 3 && groups[1][1] === 2) return [6, ...byGroup];
|
||||
if (flush) return [5, ...vals];
|
||||
if (straightHigh) return [4, straightHigh];
|
||||
if (groups[0][1] === 3) return [3, ...byGroup];
|
||||
if (groups[0][1] === 2 && groups[1][1] === 2) return [2, ...byGroup];
|
||||
if (groups[0][1] === 2) return [1, ...byGroup];
|
||||
return [0, ...vals];
|
||||
}
|
||||
|
||||
function compareScore(a, b) {
|
||||
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
||||
const d = (a[i] || 0) - (b[i] || 0);
|
||||
if (d) return d;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const HAND_NAMES = [
|
||||
'ハイカード',
|
||||
'ワンペア',
|
||||
'ツーペア',
|
||||
'スリーカード',
|
||||
'ストレート',
|
||||
'フラッシュ',
|
||||
'フルハウス',
|
||||
'フォーカード',
|
||||
'ストレートフラッシュ',
|
||||
];
|
||||
|
||||
function handName(score) {
|
||||
if (score[0] === 8 && score[1] === 14) return 'ロイヤルフラッシュ';
|
||||
return HAND_NAMES[score[0]];
|
||||
}
|
||||
|
||||
// 5〜7枚から最も強い5枚を選ぶ
|
||||
// 5〜7枚の中で最も強い役。score は小さいほど強い
|
||||
function bestHand(cards) {
|
||||
let best = null;
|
||||
const n = cards.length;
|
||||
const pick = (start, chosen) => {
|
||||
if (chosen.length === 5) {
|
||||
const score = score5(chosen);
|
||||
if (!best || compareScore(score, best.score) > 0) best = { score, cards: chosen.slice() };
|
||||
return;
|
||||
}
|
||||
for (let i = start; i <= n - (5 - chosen.length); i++) {
|
||||
chosen.push(cards[i]);
|
||||
pick(i + 1, chosen);
|
||||
chosen.pop();
|
||||
}
|
||||
};
|
||||
pick(0, []);
|
||||
return best && { score: best.score, cards: best.cards, name: handName(best.score) };
|
||||
const codes = getCardCodes(cards);
|
||||
const score = evaluate(codes);
|
||||
const name = score === ROYAL_FLUSH ? 'ロイヤルフラッシュ' : HAND_NAMES[rank(codes)];
|
||||
// 役を作っている5枚(7枚から5枚を選ぶ21通りで、全体と同じ強さになる組)
|
||||
let five = cards.slice(0, 5);
|
||||
if (cards.length > 5) {
|
||||
const n = cards.length;
|
||||
const pick = (start, chosen) => {
|
||||
if (chosen.length === 5) return evaluate(chosen.map((i) => codes[i])) === score ? chosen.slice() : null;
|
||||
for (let i = start; i <= n - (5 - chosen.length); i++) {
|
||||
chosen.push(i);
|
||||
const found = pick(i + 1, chosen);
|
||||
chosen.pop();
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
five = pick(0, []).map((i) => cards[i]);
|
||||
}
|
||||
return { score, cards: five, name };
|
||||
}
|
||||
|
||||
module.exports = { newDeck, score5, compareScore, bestHand, handName };
|
||||
// a が強ければ正、b が強ければ負、同じ強さなら 0
|
||||
function compareScore(a, b) {
|
||||
return b - a;
|
||||
}
|
||||
|
||||
module.exports = { bestHand, compareScore };
|
||||
Reference in new issue
Block a user