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:
agomizi1cmandClaude Opus 5.5 committed 2026-10-17 17:51:15 +09:00
1 parent d92953a270
commit 3116b1da9f
11 files changed
+702 -131

No files matched your search

+70
View File
@@ -708,6 +708,76 @@ test('CPU: どんな状況でも正しいアクションを返す', () => {
assert.ok(actions > 300, `actions=${actions}`);
});
test('CPU: どの思考(ライブラリ)も自分で正しいアクションを決める', () => {
const brains = ai.availableBrains();
// 少なくとも純粋な JavaScript のライブラリの思考は必ず使える
for (const b of ['montecarlo', 'range', 'madehand']) assert.ok(brains.includes(b), b);
for (const brain of brains) {
const rng = seeded(brain.length * 101);
let decisions = 0;
for (let game = 0; game < 6; game++) {
const n = 2 + (game % 5);
const stacks = Array.from({ length: n }, () => 100 + Math.floor(rng() * 1500));
const t = makeTable(stacks, { mode: 'friends' });
t.start(T(0), 0);
let now = 0;
for (let step = 0; step < 3000 && t.hand && t.hand.number <= 8; step++) {
const h = t.hand;
if (!h.finished && h.toAct != null) {
const style = ai.randomStyle(rng, brain);
assert.equal(style.brain, brain);
// 思考そのものがエラーにならず、何かを返す(バランス型に頼らない)
assert.ok(ai.BRAINS[brain].decide(h, h.toAct, { style, rng, iterations: 80 }), brain);
const d = ai.decide(h, h.toAct, { style, rng, iterations: 80 });
assert.equal(h.act(h.toAct, d.type, d.amount), null, `${brain} ${JSON.stringify(d)}`);
decisions++;
} else {
t.tick((now += 10 ** 5));
}
}
}
assert.ok(decisions > 60, `${brain} decisions=${decisions}`);
}
});
test('CPU: 思考の性格は弱い手で降り、強い手では降りない', () => {
for (const brain of ai.availableBrains()) {
const rng = seeded(3);
const t = makeTable([1000, 1000, 1000], { mode: 'friends' });
t.start(T(0), 0);
const h = t.hand;
const [sb, bb, btn] = h.order;
rig(h, [['7c', '2d'], ['8h', '3s'], ['As', 'Ah']], ['Kd', '9c', '4s', '5h', 'Jd']);
const style = ai.randomStyle(rng, brain);
assert.notEqual(ai.decide(h, btn, { style, rng }).type, 'fold', brain); // AA
t.act(tk(t, btn), 'raise', 1000); // オールイン
assert.equal(ai.decide(h, sb, { style, rng }).type, 'fold', brain); // 72o
}
});
test('CPU戦: CPU ごとに違う思考が割り当てられ、どれを使っているかは画面に出ない', () => {
const brains = ai.availableBrains();
assert.deepEqual(new Set(ai.makeStyles(brains.length).map((s) => s.brain)), new Set(brains));
const t = new Table({
id: 'c',
name: 'cpu',
startingStack: 1000,
smallBlind: 10,
bigBlind: 20,
blindTimerEnabled: false,
blindIntervalMin: 10,
hostToken: T(0),
mode: 'cpu',
cpuCount: 8,
});
const used = new Set(t.seats.filter((s) => s && s.bot).map((s) => s.style.brain));
assert.equal(used.size, brains.length);
t.join(T(0), 'me');
t.start(T(0), 0);
const json = JSON.stringify([t.publicState(T(0), new Set()), t.summary()]);
for (const word of ['brain', 'style', ...brains, 'aggression', 'Threshold']) assert.ok(!json.includes(word), word);
});
test('CPU戦: CPU が席に着き、ホストは人間のまま、CPU は自動で行動する', () => {
const t = new Table({
id: 'c',