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:
1 parent
d92953a270
commit
3116b1da9f
11 files changed
+702
-131
No files matched your search
@@ -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 };
|
||||
Reference in new issue
Block a user