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,97 @@
|
||||
'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 };
|
||||
Reference in new issue
Block a user