'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 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}`); });