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>
88 lines
2.8 KiB
JavaScript
88 lines
2.8 KiB
JavaScript
'use strict';
|
|
|
|
// CPU の思考で共通に使う道具。どの思考も相手の手札は見ない(自分の手札と場札、公開されているベット状況だけを使う)
|
|
|
|
// 手番の状況。手番でなければ null
|
|
function situation(hand, seat) {
|
|
const o = hand.options(seat);
|
|
if (!o) return null;
|
|
const p = hand.players.get(seat);
|
|
const pot = hand.potTotal;
|
|
return {
|
|
o,
|
|
p,
|
|
cards: p.cards,
|
|
board: hand.board,
|
|
stack: hand.seats[seat].stack,
|
|
bb: hand.bb,
|
|
preflop: hand.board.length === 0,
|
|
opponents: hand.live().length - 1,
|
|
pot,
|
|
potOdds: o.toCall / (pot + o.toCall),
|
|
raises: hand.raises || 0,
|
|
};
|
|
}
|
|
|
|
const CHECK = { type: 'check' };
|
|
const CALL = { type: 'call' };
|
|
const FOLD = { type: 'fold' };
|
|
|
|
// チェックできればチェック、できなければフォールド
|
|
const passive = (o) => (o.canCheck ? CHECK : FOLD);
|
|
// チェックできればチェック、できなければコール
|
|
const keepGoing = (o) => (o.canCheck ? CHECK : CALL);
|
|
const allIn = (o) => ({ type: 'raise', amount: o.maxRaiseTo });
|
|
|
|
// 合計 amount までのレイズ。最低額〜オールインに収め、SB 単位に丸め、残りがわずかになるならオールイン
|
|
function raiseToAmount(hand, seat, amount) {
|
|
const o = hand.options(seat);
|
|
const p = hand.players.get(seat);
|
|
const unit = Math.max(1, hand.sb);
|
|
let to = Math.round(amount / unit) * unit;
|
|
to = Math.min(o.maxRaiseTo, Math.max(o.minRaiseTo, to));
|
|
if (to - p.bet >= hand.seats[seat].stack * 0.7) to = o.maxRaiseTo;
|
|
return { type: 'raise', amount: to };
|
|
}
|
|
|
|
// ポット(コールした後)の fraction 倍のレイズ
|
|
function raisePot(hand, seat, fraction) {
|
|
const o = hand.options(seat);
|
|
return raiseToAmount(hand, seat, hand.currentBet + fraction * (hand.potTotal + o.toCall));
|
|
}
|
|
|
|
// 思考の結果を、その場で必ず受け付けられるアクションに直す
|
|
function legalize(hand, seat, d) {
|
|
const o = hand.options(seat);
|
|
if (!o) return null;
|
|
const type = d && d.type;
|
|
if (type === 'raise' && o.canRaise && Number.isFinite(d.amount)) {
|
|
const amount = Math.min(o.maxRaiseTo, Math.max(o.minRaiseTo, Math.round(d.amount)));
|
|
return { type: 'raise', amount };
|
|
}
|
|
if (type === 'raise' || type === 'call') return keepGoing(o);
|
|
if (type === 'check') return passive(o);
|
|
// ただで見られるのに降りることはしない
|
|
return passive(o);
|
|
}
|
|
|
|
// 0 以上 2^31 未満の整数のシード(ネイティブのシミュレーション用)
|
|
const seedFrom = (rng) => Math.floor(rng() * 2147483647);
|
|
|
|
// 範囲内の一様乱数
|
|
const between = (rng, min, max) => min + rng() * (max - min);
|
|
|
|
module.exports = {
|
|
situation,
|
|
CHECK,
|
|
CALL,
|
|
FOLD,
|
|
passive,
|
|
keepGoing,
|
|
allIn,
|
|
raiseToAmount,
|
|
raisePot,
|
|
legalize,
|
|
seedFrom,
|
|
between,
|
|
};
|