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

98 lines
4.4 KiB
JavaScript

'use strict';
// 直感型(ルーズ): pokersolver で「いまできている役」だけを見て打つ。勝率の計算はしない。
// 参加する手が広く、ペアやドローがあればなかなか降りない。強い役ができたときだけ大きく打つ。
const { Hand } = require('pokersolver');
const { situation, CHECK, CALL, FOLD, passive, keepGoing, allIn, raiseToAmount, raisePot, between } = require('./common');
// pokersolver のカードのランク: 2 → 1 … A → 13。役のランク: ハイカード 1、ワンペア 2、ツーペア 3 …
const PAIR = 2;
const TWO_PAIR = 3;
const rankOf = (card) => '23456789TJQKA'.indexOf(card[0]) + 1;
function randomStyle(rng) {
return {
callCapBb: between(rng, 4, 9), // プリフロップでコールする上限(BB)
stickiness: between(rng, 0.8, 1.5), // ペアでコールするベットの上限(ポットに対する割合)
looseness: between(rng, 0.15, 0.4), // 何もなくても小さいベットにはコールする頻度
bluff: between(rng, 0.05, 0.2), // ドローでベットする頻度
betSize: between(rng, 0.5, 0.85),
};
}
function preflop(hand, seat, s, style, rng) {
const { o, p, stack, bb, raises } = s;
const solved = Hand.solve(p.cards);
const [hi, lo] = p.cards.map(rankOf).sort((a, b) => b - a);
const pair = solved.rank === PAIR;
const suited = p.cards[0][1] === p.cards[1][1];
const premium = (pair && hi >= 10) || (hi === 13 && lo === 12); // JJ 以上・AK
const playable = pair || hi === 13 || lo >= 9 || suited || (hi - lo <= 2 && lo >= 4);
// スタックが 10BB 以下なら、ペア・エース・プレミアムはオールイン
if (stack + p.bet <= 10 * bb) {
if (pair || hi === 13 || premium) return o.canRaise ? allIn(o) : CALL;
return passive(o);
}
if (premium && o.canRaise && raises < 2) {
return raiseToAmount(hand, seat, raises === 0 ? Math.max(3 * bb, hand.currentBet * 3) : hand.currentBet * 3);
}
if (premium) return keepGoing(o);
if (playable && o.toCall <= style.callCapBb * bb) {
// たまにリンプではなく小さくオープンする
if (raises === 0 && o.canRaise && rng() < 0.15) return raiseToAmount(hand, seat, 2.5 * bb);
return keepGoing(o);
}
return passive(o);
}
// フラッシュドローかストレートドロー(オープンエンド)があるか。自分の手札を使うものだけ
function hasDraw(cards, board) {
if (board.length >= 5) return false;
const all = cards.concat(board);
for (const suit of 'shdc') {
const n = all.filter((c) => c[1] === suit).length;
if (n === 4 && cards.some((c) => c[1] === suit)) return true;
}
const ranks = new Set(all.map(rankOf));
if (ranks.has(13)) ranks.add(0); // A は 1 としても数える
const mine = new Set(cards.map(rankOf));
for (let lo = 0; lo <= 9; lo++) {
const run = [lo, lo + 1, lo + 2, lo + 3];
if (run.every((r) => ranks.has(r)) && run.some((r) => mine.has(r) || (r === 0 && mine.has(13)))) return true;
}
return false;
}
function decide(hand, seat, { style, rng = Math.random } = {}) {
const s = situation(hand, seat);
if (!s) return null;
if (s.preflop) return preflop(hand, seat, s, style, rng);
const { o, p, pot, raises } = s;
const mine = Hand.solve(p.cards.concat(hand.board));
const onBoard = Hand.solve(hand.board);
const improved = mine.rank > onBoard.rank; // 自分の手札で役が上がっている
const strong = improved && mine.rank >= TWO_PAIR;
const boardTop = Math.max(...hand.board.map(rankOf));
const topPair = improved && mine.rank === PAIR && mine.cards[0].rank >= boardTop;
const pair = improved && mine.rank === PAIR;
const draw = hasDraw(p.cards, hand.board);
if (o.canCheck) {
if (strong && o.canRaise) return raisePot(hand, seat, style.betSize);
if (topPair && o.canRaise && rng() < 0.6) return raisePot(hand, seat, style.betSize * 0.7);
if (draw && o.canRaise && rng() < style.bluff) return raisePot(hand, seat, 0.5);
return CHECK;
}
const bet = o.toCall / Math.max(1, pot - o.toCall); // ポットに対するベットの大きさ
if (strong) return o.canRaise && raises < 2 ? raisePot(hand, seat, style.betSize) : CALL;
if (pair && bet <= style.stickiness * (topPair ? 1.3 : 0.8)) return CALL;
if (draw && bet <= 0.7) return CALL;
if (bet <= 0.35 && rng() < style.looseness) return CALL;
return FOLD;
}
module.exports = { decide, randomStyle, hasDraw };