Give CPUs distinct personalities built on different poker libraries

Each CPU now thinks with one of four engines, each backed by a different
published library (pinned versions):

- montecarlo (@pokertools/evaluator): the existing balanced Monte Carlo
  equity player, moved to server/bots/montecarlo.js.
- calculator (poker-calculations): the library's decideAction over the
  table state; push/fold below 12BB via its jam breakeven equity.
  Skipped automatically if the native addon cannot load.
- range (poker-utils): tight player; preflop by hand-strength percentile,
  postflop equity vs an estimated opponent range, bluffs only when
  alpha/bluffEV say they pay.
- madehand (pokersolver): loose, sticky player that only looks at the
  made hand and draws.

server/ai.js assigns a mix of engines per table (makeStyles), falls back
to the Monte Carlo engine on errors, and legalizes every decision.
The engine and its parameters stay server-side; players cannot tell
which library a CPU uses. Tests cover every engine and the no-leak rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-17 17:51:15 +09:00
1 parent d92953a270
commit 3116b1da9f
11 files changed
+702 -131

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
'use strict';
// 計算型: poker-calculations(ネイティブ実装)の decideAction に、勝率・ポットオッズ・コールの期待値から決めさせる。
// スタックが少ないときは、同じライブラリのオールインの損益分岐勝率と比べてプッシュかフォールドにする。
const { situation, CALL, FOLD, CHECK, passive, allIn, raiseToAmount, seedFrom, between } = require('./common');
// ネイティブアドオンが読み込めない環境では、この思考は使わない
let pc = null;
try {
pc = require('poker-calculations');
} catch {
pc = null;
}
const available = () => pc !== null;
function randomStyle(rng) {
return {
// 小さいほどベット・レイズが多い(0.4 未満は無謀になり、0.6 を超えるとほとんどレイズしない)
aggressionThreshold: between(rng, 0.4, 0.52),
riskTolerance: between(rng, 0.4, 0.6), // 大きいほど薄い勝率でもコールする
raisePotFraction: between(rng, 0.55, 0.9), // ベット・レイズの大きさ(ポットに対する割合)
pushBias: between(rng, -0.04, 0.04), // プッシュの基準勝率への上乗せ(負ほど広くプッシュ)
};
}
const PHASES = ['preflop', null, null, 'flop', 'turn', 'river'];
// decideAction に渡すテーブルの状態。他人の手札は空にする
function tableState(hand, seat) {
const list = hand.list;
const index = (s) => list.findIndex((p) => p.seat === s);
const firstToAct = hand.board.length === 0 ? (index(hand.bbSeat) + 1) % list.length : 0;
return {
players: list.map((p, i) => ({
holeCards: p.seat === seat ? p.cards : [],
stack: hand.seats[p.seat].stack,
committedThisStreet: p.bet,
totalCommittedHand: p.committed,
folded: p.folded,
seat: i,
})),
communityCards: hand.board,
phase: PHASES[hand.board.length],
pot: hand.potTotal,
currentBet: hand.currentBet,
buttonSeat: index(hand.button),
smallBlind: hand.sb,
bigBlind: hand.bb,
actingIndex: index(seat),
lastRaiseIncrement: hand.state.lastRaiseAmount || hand.bb,
streetOpeningIndex: firstToAct,
// このストリートで動いた人(直前のアクションの表示はストリートが変わると消える)
actedThisStreet: list.map((p) => Boolean(p.last && p.last.type !== 'FOLD')),
};
}
function decide(hand, seat, { style, rng = Math.random, iterations = 500 } = {}) {
const s = situation(hand, seat);
if (!s) return null;
const { o, p, stack, bb, preflop, opponents, pot } = s;
const villains = Math.max(1, opponents);
// スタックが 12BB 以下ならプッシュかフォールド
if (preflop && stack + p.bet <= 12 * bb) {
const eq = pc.simulateHandOutcome(p.cards, [], iterations, seedFrom(rng), villains);
const breakeven = pc.pushFoldSymmetricBreakevenEquity(stack + p.bet, pot - p.bet);
if (eq >= breakeven + style.pushBias + 0.04 * (villains - 1)) return o.canRaise ? allIn(o) : CALL;
return passive(o);
}
const d = pc.decideAction(tableState(hand, seat), {
aggressionThreshold: style.aggressionThreshold,
riskTolerance: style.riskTolerance,
raisePotFraction: style.raisePotFraction,
monteCarloSimulations: iterations,
monteCarloVillains: villains,
rngSeed: seedFrom(rng),
});
if (d.action === 'raise' && o.canRaise) return raiseToAmount(hand, seat, hand.currentBet + d.raiseBy);
if (d.action === 'raise' || d.action === 'call') return o.canCheck ? CHECK : CALL;
if (d.action === 'check') return passive(o);
return FOLD;
}
module.exports = { decide, randomStyle, available };