Files
agomizi1cmandClaude Opus 5.5 3116b1da9f 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>
2026-10-17 17:51:15 +09:00

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,
};