Files
Poker/test/engine.test.js
T
agomizi1cmandClaude Opus 5.5 66907345ff Add optional card mode: server deals cards and settles showdowns automatically
- New "カードを配る" switch on the create-table screen (default off); the lobby
  shows "カードあり" for tables that use it
- Card mode (server/cards.js restored): crypto-shuffled deck kept server-side,
  hole cards sent only to their owner until showdown/all-in reveal, board dealt
  per street with burns, all-in runout on a timer (frozen while paused)
- Showdown is evaluated and every pot (incl. side pots, split pots) is paid out
  automatically; host winner selection is only used when card mode is off
- Play UI: board, hole cards, own cards in the action bar, hand names, winning
  cards highlighted; layout tuned for 7+ seats and short screens
- Playing-card styles use .pcard so they don't clash with the existing .card panel
- Tests for hand evaluation, card privacy, runout, side pots, ties and random play

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:44:22 +09:00

573 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// node --test で実行(ACCESS_KEY はテスト用のダミー)
process.env.ACCESS_KEY = process.env.ACCESS_KEY || '000000';
const test = require('node:test');
const assert = require('node:assert/strict');
const { Table } = require('../server/tables');
const { bestHand, compareScore } = require('../server/cards');
const config = require('../server/config');
const T = (n) => 't'.repeat(31) + n; // 32文字のトークン
function makeTable(stacks, opts = {}) {
const table = new Table({
id: 'x',
name: 'test',
startingStack: 1000,
smallBlind: 10,
bigBlind: 20,
blindTimerEnabled: false,
blindIntervalMin: 10,
hostToken: T(0),
...opts,
});
stacks.forEach((st, i) => {
table.join(T(i), 'p' + i);
table.seats[i].stack = st;
});
return table;
}
const tk = (t, seat) => t.seats[seat].token;
// ショーダウンまでチェック/コールで進める
function checkDown(t) {
const h = t.hand;
for (let guard = 0; guard < 100 && h.toAct != null; guard++) {
const o = h.options(h.toAct);
assert.equal(t.act(tk(t, h.toAct), o.canCheck ? 'check' : 'call', 0), null);
}
}
test('ヘッズアップ: ボタンがSBで先に動き、フォールドでBBが勝つ', () => {
const t = makeTable([1000, 1000]);
assert.equal(t.start(T(0), 0), null);
const h = t.hand;
assert.equal(h.sbSeat, h.button);
assert.equal(h.toAct, h.sbSeat);
const sb = h.sbSeat;
const bb = h.bbSeat;
assert.equal(t.seats[sb].stack, 990);
assert.equal(t.seats[bb].stack, 980);
assert.equal(t.act(tk(t, sb), 'fold', 0), null);
assert.ok(h.finished && !h.showdown);
assert.equal(t.seats[sb].stack, 990);
assert.equal(t.seats[bb].stack, 1010);
});
test('BBのオプションとチェックでストリートが進む', () => {
const t = makeTable([1000, 1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
const [sb, bb, btn] = h.order;
assert.equal(h.toAct, btn); // UTG = ボタン(3人)
assert.equal(t.act(tk(t, btn), 'call', 0), null);
assert.equal(t.act(tk(t, sb), 'call', 0), null);
assert.equal(h.street, 'PREFLOP');
assert.equal(h.toAct, bb); // BB はまだ動ける
assert.ok(h.options(bb).canRaise);
assert.equal(t.act(tk(t, bb), 'check', 0), null);
assert.equal(h.street, 'FLOP');
assert.equal(h.pot, 60);
assert.equal(h.toAct, sb); // ポストフロップは SB から
checkDown(t);
assert.equal(t.phase, 'SHOWDOWN');
assert.deepEqual(h.view().pots, [{ amount: 60, eligible: [sb, bb, btn] }]);
});
test('ミニマムレイズと、足りないオールインは再レイズ権を与えない', () => {
const t = makeTable([1000, 1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
const [sb, bb, btn] = h.order;
// 最低レイズ額は 40(20 + 20)
assert.ok(t.act(tk(t, btn), 'raise', 39));
assert.equal(t.act(tk(t, btn), 'raise', 60), null); // +40 のレイズ
assert.equal(h.options(sb).minRaiseTo, 100);
assert.equal(t.act(tk(t, sb), 'call', 0), null);
// BB は 75 しか持っていないとする → 75 オールインは 100 未満なので不完全なレイズ
t.seats[bb].stack = 75 - 20;
assert.equal(t.act(tk(t, bb), 'raise', 75), null);
assert.equal(h.currentBet, 75);
assert.equal(h.toAct, btn);
assert.equal(h.options(btn).canRaise, false); // コールかフォールドのみ
assert.equal(t.act(tk(t, btn), 'raise', 200), 'レイズできません');
assert.equal(t.act(tk(t, btn), 'call', 0), null);
assert.equal(t.act(tk(t, sb), 'call', 0), null);
assert.equal(h.street, 'FLOP');
});
test('3人オールイン: すぐショーダウンになり、サイドポットと返金', () => {
const t = makeTable([100, 300, 500]);
t.start(T(0), 0);
const h = t.hand;
for (let guard = 0; guard < 10 && h.toAct != null; guard++) {
const o = h.options(h.toAct);
assert.equal(t.act(tk(t, h.toAct), o.canRaise ? 'raise' : 'call', o.canRaise ? o.maxRaiseTo : 0), null);
}
assert.equal(h.street, 'SHOWDOWN');
// 席2 の超過 200 は返金済み。メイン 300(全員)、サイド 400(席1・席2)
assert.equal(t.seats[2].stack, 200);
assert.deepEqual(
h.view().pots.map((p) => [p.amount, p.eligible.slice().sort()]),
[[300, [0, 1, 2]], [400, [1, 2]]]
);
// 席0 がメイン、席1 がサイドを獲得
assert.equal(t.award(T(0), [[0], [1]]), null);
assert.ok(h.finished && h.showdown);
assert.equal(t.seats[0].stack, 300);
assert.equal(t.seats[1].stack, 400);
assert.equal(t.seats[2].stack, 200);
});
test('勝者の指定: 山分け(端数はボタンの左から)と不正な指定の拒否', () => {
const t = makeTable([1000, 1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
const [sb, bb, btn] = h.order;
// SB をフォールドさせて、BB とボタンで 50 のポットを争う(SB の 10 + 20 + 20)
assert.equal(t.act(tk(t, btn), 'call', 0), null);
assert.equal(t.act(tk(t, sb), 'fold', 0), null);
assert.equal(t.act(tk(t, bb), 'check', 0), null);
checkDown(t);
assert.equal(h.street, 'SHOWDOWN');
const host = T(0);
// ホスト以外は、ショーダウンに残った人でも分配できない
for (const s of [sb, bb, btn]) {
if (tk(t, s) !== host) assert.equal(t.award(tk(t, s), [[bb]]), 'ポットの分配はホストのみ行えます');
}
assert.ok(t.award(T(8), [[bb]])); // 着席していない
assert.ok(t.award(host, []));
assert.ok(t.award(host, [[]]));
assert.ok(t.award(host, [[sb]])); // フォールドした人は勝てない
assert.ok(t.award(host, [[bb, bb]]));
assert.ok(t.award(host, [[bb], [btn]])); // ポットは1つ
assert.ok(!h.finished);
assert.equal(t.award(host, [[bb, btn]]), null);
// 50 を2人で → 25 ずつ
assert.equal(t.seats[bb].stack, 1005);
assert.equal(t.seats[btn].stack, 1005);
assert.equal(t.award(host, [[bb]]), 'ショーダウンではありません');
});
test('ホストが退席したら次のホストが分配する', () => {
const t = makeTable([1000, 1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
// ホスト(席0)以外の2人と一緒にショーダウンまで進め、ホストが退席する
checkDown(t);
assert.equal(h.street, 'SHOWDOWN');
t.leave(T(0));
assert.notEqual(t.hostToken, T(0));
const live = h.live().map((p) => p.seat);
assert.equal(t.award(T(0), [[live[0]]]), 'ポットの分配はホストのみ行えます');
assert.equal(t.award(t.hostToken, [[live[0]]]), null);
assert.ok(h.finished);
});
test('端数はボタンの左から配る', () => {
const t = makeTable([1000, 1000, 1000], { smallBlind: 5, bigBlind: 10 });
t.start(T(0), 0);
const h = t.hand;
const [sb, bb, btn] = h.order;
t.act(tk(t, btn), 'call', 0);
t.act(tk(t, sb), 'fold', 0);
t.act(tk(t, bb), 'check', 0);
checkDown(t);
// 25 を2人で → ボタンの左に近い BB が 13、ボタンが 12
assert.equal(t.award(T(0), [[btn, bb]]), null);
assert.equal(t.seats[bb].stack, 990 + 13);
assert.equal(t.seats[btn].stack, 990 + 12);
});
test('ホストはショーダウンに参加していなくても勝者を選べる', () => {
const t = makeTable([1000, 1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
// ホスト(席0)だけフォールドさせる
for (let guard = 0; guard < 50 && h.toAct != null; guard++) {
const o = h.options(h.toAct);
t.act(tk(t, h.toAct), h.toAct === 0 ? 'fold' : o.canCheck ? 'check' : 'call', 0);
}
assert.equal(h.street, 'SHOWDOWN');
const other = h.live().map((p) => p.seat);
assert.equal(t.canAward(T(0)), true);
assert.equal(t.award(T(0), [[other[0]]]), null);
assert.ok(h.finished);
});
test('ショーダウン中に1人を残して退席したら残った人が勝つ', () => {
const t = makeTable([1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
checkDown(t);
assert.equal(h.street, 'SHOWDOWN');
const [a, b] = h.order;
t.leave(tk(t, a));
assert.ok(h.finished && !h.showdown);
assert.equal(t.seats[b].stack, 1020);
});
test('離席中の人の手番は自動でチェック/フォールド、次のハンドから外れる', () => {
const t = makeTable([1000, 1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
const first = h.toAct;
assert.equal(t.setSittingOut(tk(t, first), true), null);
assert.ok(h.players.get(first).folded);
assert.notEqual(h.toAct, first);
t.act(tk(t, h.toAct), 'fold', 0); // 残り1人で終了
assert.ok(h.finished);
t.tick(10 ** 6);
t.tick(10 ** 7);
assert.equal(t.hand.number, 2);
assert.ok(!t.hand.players.has(first));
assert.equal(t.setSittingOut(tk(t, first), false), null);
assert.ok(t.eligibleSeats().includes(first));
});
test('一時停止中はアクションも勝者の決定も次のハンドもできない', () => {
const t = makeTable([1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
t.togglePause(T(0), 1000);
assert.equal(t.act(tk(t, h.toAct), 'fold', 0), '一時停止中です');
t.togglePause(T(0), 2000);
checkDown(t);
t.togglePause(T(0), 3000);
assert.equal(t.award(T(0), [[h.order[0]]]), '一時停止中です');
t.togglePause(T(0), 4000);
assert.equal(t.award(T(0), [[h.order[0]]]), null);
t.togglePause(T(0), 5000);
t.tick(10 ** 7);
t.tick(10 ** 8);
assert.equal(t.hand.number, 1); // 止まっている
t.togglePause(T(0), 10 ** 8);
t.tick(10 ** 8 + 10 ** 5);
t.tick(10 ** 8 + 10 ** 6);
assert.equal(t.hand.number, 2);
});
test('ハンド中の退席: フォールド扱いで席はハンド終了まで予約', () => {
const t = makeTable([1000, 1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
const [sb, bb, btn] = h.order;
t.act(tk(t, btn), 'call', 0);
assert.ok(t.leave(tk(t, sb))); // 手番の SB が退席
assert.ok(h.players.get(sb).folded);
assert.ok(t.seats[sb].left);
assert.equal(t.playerCount, 2);
// 新しく来た人は予約席には座らない
assert.notEqual(t.join(T(9), 'new'), sb);
assert.equal(t.act(tk(t, bb), 'check', 0), null);
assert.equal(h.street, 'FLOP');
});
test('2人のうち片方が退席したら残った人が勝つ(BB は返らない)', () => {
const t = makeTable([1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
const stay = h.toAct; // SB(ボタン)
const other = h.order.find((s) => s !== stay);
t.leave(tk(t, other));
assert.ok(h.finished);
assert.equal(t.seats[stay].stack, 1020);
});
test('他人の手番では操作できない・不正な額は拒否', () => {
const t = makeTable([1000, 1000, 1000]);
t.start(T(0), 0);
const h = t.hand;
const notTurn = h.order.find((s) => s !== h.toAct);
assert.equal(t.act(tk(t, notTurn), 'fold', 0), 'あなたの番ではありません');
const me = tk(t, h.toAct);
assert.ok(t.act(me, 'raise', 10 ** 9));
assert.ok(t.act(me, 'raise', 40.5));
assert.ok(t.act(me, 'check', 0)); // BB に対してチェックはできない
assert.ok(t.act(T(8), 'fold', 0)); // 着席していない
});
test('リバイ', () => {
const t = makeTable([1000, 1000]);
t.seats[1].stack = 0;
assert.equal(t.rebuy(tk(t, 0)), 'チップが残っています');
assert.equal(t.rebuy(tk(t, 1)), null);
assert.equal(t.seats[1].stack, 1000);
assert.equal(t.seats[1].buyIns, 2);
});
test('ランダムに遊んでもチップの合計が変わらず、必ず終わる', () => {
let rnd = 12345;
const rand = (n) => {
rnd = (rnd * 1103515245 + 12345) % 2147483648;
return rnd % n;
};
let showdowns = 0;
let sidePots = 0;
for (let game = 0; game < 60; game++) {
const n = 2 + (game % 8);
const stacks = Array.from({ length: n }, () => 20 + rand(2000));
const t = makeTable(stacks);
const sum = stacks.reduce((a, b) => a + b, 0);
t.start(T(0), 0);
let now = 0;
for (let step = 0; step < 20000 && t.hand; step++) {
const h = t.hand;
if (!h.finished && h.toAct != null) {
const o = h.options(h.toAct);
const r = rand(10);
const me = tk(t, h.toAct);
let err;
if (r < 2) err = t.act(me, 'fold', 0);
else if (r < 6 || !o.canRaise) err = t.act(me, o.canCheck ? 'check' : 'call', 0);
else err = t.act(me, 'raise', o.minRaiseTo + rand(o.maxRaiseTo - o.minRaiseTo + 1));
assert.equal(err, null);
} else if (h.awaitingWinners) {
const pots = h.view().pots;
showdowns++;
if (pots.length > 1) sidePots++;
const winners = pots.map((p) => {
const w = p.eligible.filter(() => rand(3) === 0);
return w.length ? w : [p.eligible[rand(p.eligible.length)]];
});
assert.equal(t.award(T(0), winners), null);
} else {
t.tick((now += 10 ** 5));
}
for (const s of t.seats) if (s) assert.ok(s.stack >= 0);
const inPot = t.hand && !t.hand.finished ? t.hand.view().potTotal : 0;
assert.equal(t.seats.reduce((a, s) => a + (s ? s.stack : 0), 0) + inPot, sum);
if (t.hand && t.hand.number > 40) break;
}
}
assert.ok(showdowns > 100 && sidePots > 20, `showdowns=${showdowns} sidePots=${sidePots}`);
});
// ---------- カード機能あり ----------
// 山札の上(pop される順)に指定のカードを積む。holes は h.order の順
function rig(hand, holes, board) {
const used = new Set([...holes.flat(), ...board]);
const rest = hand.deck.concat(...[...hand.players.values()].map((p) => p.cards)).filter((c) => !used.has(c));
hand.order.forEach((seat, k) => (hand.players.get(seat).cards = holes[k].slice()));
// dealStreet はバーン1枚 + 場札を pop する
const seq = ['X', board[0], board[1], board[2], 'X', board[3], 'X', board[4]];
hand.deck = rest.slice(0, rest.length - 3).concat(seq.map((c) => (c === 'X' ? rest.pop() : c)).reverse());
}
const SPECTATOR = 's'.repeat(32);
const viewOf = (t, token) => t.publicState(token, new Set()).hand;
test('役の判定と比較', () => {
const h = (s) => bestHand(s.split(' '));
assert.equal(h('As Ks Qs Js Ts 2d 3c').name, 'ロイヤルフラッシュ');
assert.equal(h('5s 4s 3s 2s As Kd Kc').name, 'ストレートフラッシュ');
assert.equal(h('9h 9d 9s 9c 2d 3c 4h').name, 'フォーカード');
assert.equal(h('9h 9d 9s 2c 2d 3c 4h').name, 'フルハウス');
assert.equal(h('Ah 9h 7h 4h 2h Kd Kc').name, 'フラッシュ');
assert.equal(h('Ah 2d 3c 4s 5h Kd Kc').name, 'ストレート');
assert.equal(h('Qh Qd Qs 7c 2d 3c 4h').name, 'スリーカード');
assert.equal(h('Qh Qd 7s 7c 2d 3c 4h').name, 'ツーペア');
assert.equal(h('Qh Qd 8s 7c 2d 3c 5h').name, 'ワンペア');
assert.equal(h('Ah Qd 8s 7c 2d 3c 5h').name, 'ハイカード');
// A-5 は最弱のストレート
assert.ok(compareScore(h('Ah 2d 3c 4s 5h').score, h('2h 3d 4c 5s 6h').score) < 0);
// キッカー
assert.ok(compareScore(h('Ah Ad Ks 7c 2d').score, h('As Ac Qs 7d 2h').score) > 0);
// 同点
assert.equal(compareScore(h('Ah Kd Qs Jc 9d').score, h('As Kc Qd Jh 9s').score), 0);
// ツーペアは上のペア → 下のペア → キッカー
assert.ok(compareScore(h('Kh Kd 3s 3c Ad').score, h('Qh Qd Js Jc Ad').score) > 0);
});
test('カードなしのテーブルでは手札も場札も扱わない', () => {
const t = makeTable([1000, 1000]);
t.start(T(0), 0);
assert.equal(t.publicState(T(0), new Set()).cardsEnabled, false);
const v = viewOf(t, T(0));
assert.deepEqual(v.board, []);
assert.equal('cards' in v.players[0], false);
});
test('カードあり: 自分の手札だけが見え、ストリートごとに場札が配られ、勝者は自動で決まる', () => {
const t = makeTable([1000, 1000, 1000], { cardsEnabled: true });
t.start(T(0), 0);
const h = t.hand;
const [sb, bb, btn] = h.order;
assert.equal(t.publicState(tk(t, sb), new Set()).cardsEnabled, true);
let v = viewOf(t, tk(t, sb));
assert.equal(v.players[sb].cards.length, 2);
assert.equal(v.players[bb].cards, null);
assert.equal(v.players[btn].cards, null);
assert.equal(viewOf(t, SPECTATOR).players[sb].cards, null);
t.act(tk(t, btn), 'call', 0);
t.act(tk(t, sb), 'call', 0);
t.act(tk(t, bb), 'check', 0);
assert.equal(h.street, 'FLOP');
assert.equal(h.board.length, 3);
assert.ok(viewOf(t, tk(t, sb)).players[sb].handName);
t.act(tk(t, sb), 'check', 0);
t.act(tk(t, bb), 'check', 0);
t.act(tk(t, btn), 'check', 0);
assert.equal(h.board.length, 4);
checkDown(t);
assert.equal(h.board.length, 5);
assert.ok(h.finished && h.showdown);
assert.equal(h.awaitingWinners, false);
assert.equal(t.award(T(0), [[sb]]), 'ショーダウンではありません');
assert.ok(h.results[0].handName && h.results[0].bestCards.length === 5);
// ショーダウン後は残った人の手札が公開される
v = viewOf(t, SPECTATOR);
for (const s of [sb, bb, btn]) assert.equal(v.players[s].cards.length, 2);
// すべて別のカード(バーンの3枚は山札から消えている)
const all = [...h.deck, ...h.board, ...h.list.flatMap((p) => p.cards)];
assert.equal(new Set(all).size, all.length);
assert.equal(all.length, 52 - 3);
});
test('カードあり: フォールドした人の手札は公開されない', () => {
const t = makeTable([1000, 1000, 1000], { cardsEnabled: true });
t.start(T(0), 0);
const h = t.hand;
const [sb, bb, btn] = h.order;
t.act(tk(t, btn), 'fold', 0);
t.act(tk(t, sb), 'call', 0);
t.act(tk(t, bb), 'check', 0);
checkDown(t);
assert.ok(h.finished);
const v = viewOf(t, tk(t, sb));
assert.equal(v.players[btn].cards, null);
assert.equal(v.players[bb].cards.length, 2);
assert.equal(viewOf(t, tk(t, btn)).players[btn].cards.length, 2); // 本人には見える
});
test('カードあり: 3人オールインで場札を順にめくり、サイドポットを自動で分配', () => {
const t = makeTable([100, 300, 500], { cardsEnabled: true });
t.start(T(0), 0);
const h = t.hand;
// 席0 が最強、席1 が2番目、席2 が最弱
const holes = { 0: ['As', 'Ah'], 1: ['Ks', 'Kh'], 2: ['7c', '2d'] };
rig(h, h.order.map((s) => holes[s]), ['Ad', 'Kd', '9c', '5s', '3h']);
for (let guard = 0; guard < 10 && h.toAct != null; guard++) {
const o = h.options(h.toAct);
assert.equal(t.act(tk(t, h.toAct), o.canRaise ? 'raise' : 'call', o.canRaise ? o.maxRaiseTo : 0), null);
}
assert.ok(h.runout && h.revealed);
assert.equal(viewOf(t, SPECTATOR).players[2].cards.length, 2);
const step = config.RUNOUT_STEP_MS;
t.tick(0);
// 一時停止中はめくらない
t.togglePause(T(0), 0);
t.tick(10 * step);
assert.equal(h.board.length, 0);
t.togglePause(T(0), 10 * step);
t.tick(10 * step + step - 1);
assert.equal(h.board.length, 0);
t.tick(11 * step);
assert.equal(h.board.length, 3);
t.tick(12 * step);
assert.equal(h.board.length, 4);
t.tick(13 * step);
assert.equal(h.board.length, 5);
assert.ok(!h.finished);
t.tick(14 * step);
assert.ok(h.finished && h.showdown);
// メイン 300 → 席0、サイド 400 → 席1、席2 の超過 200 は返金
assert.equal(t.seats[0].stack, 300);
assert.equal(t.seats[1].stack, 400);
assert.equal(t.seats[2].stack, 200);
assert.deepEqual(
h.results.map((r) => [r.amount, r.winners, r.handName]),
[
[300, [0], 'スリーカード'],
[400, [1], 'スリーカード'],
]
);
});
test('カードあり: 引き分けは山分け', () => {
const t = makeTable([1000, 1000, 1000], { cardsEnabled: true });
t.start(T(0), 0);
const h = t.hand;
const [sb, bb, btn] = h.order;
rig(h, [['2c', '3d'], ['2d', '3c'], ['4h', '5h']], ['As', 'Ks', 'Qs', 'Js', 'Ts']); // 全員ロイヤル
t.act(tk(t, btn), 'raise', 40);
t.act(tk(t, sb), 'call', 0);
t.act(tk(t, bb), 'call', 0);
checkDown(t);
assert.ok(h.finished);
assert.equal(t.seats[sb].stack, 1000);
assert.equal(t.seats[bb].stack, 1000);
assert.equal(t.seats[btn].stack, 1000);
assert.deepEqual(h.results[0].winners, [sb, bb, btn]);
assert.equal(h.results[0].handName, 'ロイヤルフラッシュ');
});
test('カードあり: めくっている途中に1人を残して退席したら残った人が勝つ', () => {
const t = makeTable([1000, 1000], { cardsEnabled: true });
t.start(T(0), 0);
const h = t.hand;
t.act(tk(t, h.toAct), 'raise', h.options(h.toAct).maxRaiseTo);
t.act(tk(t, h.toAct), 'call', 0);
assert.ok(h.runout);
t.leave(T(1));
assert.ok(h.finished && !h.showdown && !h.runout);
assert.equal(t.seats[0].stack, 2000);
});
test('カードあり: ランダムに遊んでもチップの合計が変わらず、必ず終わる', () => {
let rnd = 777;
const rand = (n) => {
rnd = (rnd * 1103515245 + 12345) % 2147483648;
return rnd % n;
};
let showdowns = 0;
let runouts = 0;
for (let game = 0; game < 40; game++) {
const n = 2 + (game % 8);
const stacks = Array.from({ length: n }, () => 20 + rand(2000));
const t = makeTable(stacks, { cardsEnabled: true });
const sum = stacks.reduce((a, b) => a + b, 0);
t.start(T(0), 0);
let now = 0;
let prev = null;
for (let step = 0; step < 20000 && t.hand; step++) {
const h = t.hand;
if (h !== prev) {
if (prev && prev.showdown) {
showdowns++;
assert.equal(prev.board.length, 5);
}
prev = h;
assert.equal(new Set([...h.deck, ...h.list.flatMap((p) => p.cards)]).size, 52);
}
if (!h.finished && h.toAct != null) {
const o = h.options(h.toAct);
const r = rand(10);
const me = tk(t, h.toAct);
let err;
if (r < 2) err = t.act(me, 'fold', 0);
else if (r < 6 || !o.canRaise) err = t.act(me, o.canCheck ? 'check' : 'call', 0);
else err = t.act(me, 'raise', o.minRaiseTo + rand(o.maxRaiseTo - o.minRaiseTo + 1));
assert.equal(err, null);
} else {
assert.equal(h.awaitingWinners, false);
if (h.runout && t.runoutAt == null) runouts++;
t.tick((now += 10 ** 5));
}
for (const s of t.seats) if (s) assert.ok(s.stack >= 0);
const inPot = t.hand && !t.hand.finished ? t.hand.view().potTotal : 0;
assert.equal(t.seats.reduce((a, s) => a + (s ? s.stack : 0), 0) + inPot, sum);
if (t.hand && t.hand.number > 40) break;
}
}
assert.ok(showdowns > 100 && runouts > 20, `showdowns=${showdowns} runouts=${runouts}`);
});