Files
Poker/test/seating.test.js
T
agomizi1cmandClaude Opus 5.5 dd39c043c4 Add player stats, in-table ranking, host seating and review return
- Stats page (/stats): play style radar (TAG/LAG/rock/fish), VPIP, PFR,
  ATS, 3bet, WTSD, W$SD, WWSF, per-street fold rate, aggression
  frequency, hand win rate and net in BB, per table type, each with a
  bands-and-explanation sheet. Hands are counted per human player when
  they finish and stored as totals under data/stats/, keyed by a hash
  of the player ID (sent in a POST body, never in the URL).
- Ranking sheet at the table: everyone seated, ordered by stack or by
  net (stack minus buy-ins), updating live.
- The host can reorder seats by dragging (with a mini table preview and
  shuffle). Takes effect immediately between hands, otherwise at the
  start of the next hand; the dealer button stays with the same player.
- The full-session review opens in the same tab and its back button
  returns to the table it was opened from.

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

91 lines
3.6 KiB
JavaScript

'use strict';
// node --test で実行(ACCESS_KEY はテスト用のダミー、統計の保存先は一時ディレクトリ)
const fs = require('fs');
const os = require('os');
const path = require('path');
process.env.ACCESS_KEY = process.env.ACCESS_KEY || '000000';
process.env.STATS_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'holdem-stats-'));
const test = require('node:test');
const assert = require('node:assert/strict');
const { Table } = require('../server/tables');
const T = (n) => 'q'.repeat(31) + n; // 32文字のトークン
function makeTable(n) {
const t = new Table({
id: 'x',
name: 'test',
startingStack: 1000,
smallBlind: 10,
bigBlind: 20,
blindTimerEnabled: false,
blindIntervalMin: 10,
hostToken: T(0),
mode: 'friends',
});
for (let i = 0; i < n; i++) t.join(T(i), 'p' + i);
return t;
}
// 席の並び(名前)
const names = (t) => t.seats.filter(Boolean).map((s) => s.name);
test('席順: 開始前はすぐに並べ替え、ホスト以外や着席者と合わない指定は受け付けない', () => {
const t = makeTable(4);
assert.equal(t.setSeating(T(1), [3, 2, 1, 0]), 'ホストのみ操作できます');
assert.match(t.setSeating(T(0), [3, 2, 1]), /もう一度/);
assert.match(t.setSeating(T(0), [3, 3, 1, 0]), /もう一度/);
assert.match(t.setSeating(T(0), [3, 2, 1, 5]), /もう一度/);
assert.equal(t.setSeating(T(0), [0, 3, 1, 2]), null);
assert.deepEqual(names(t), ['p0', 'p3', 'p1', 'p2']);
assert.equal(t.pendingSeating, null);
// 席は詰めて座らせ、新しく座った人は空いている次の席(並びの最後)に入る
t.leave(T(1));
t.join(T(9), 'p9');
assert.deepEqual(names(t), ['p0', 'p3', 'p9', 'p2']);
});
test('席順: ハンド中は次のハンドの始めに並べ替え、ボタンは同じ人に付いたまま次へ回る', () => {
const t = makeTable(4);
t.start(T(0), 0);
const h = t.hand;
const buttonName = t.seats[h.button].name;
const order = [0, 2, 1, 3]; // p0 → p2 → p1 → p3
assert.equal(t.setSeating(T(0), order), null);
assert.deepEqual(names(t), ['p0', 'p1', 'p2', 'p3']); // まだ変わらない
assert.deepEqual(t.publicState(T(0), new Set()).pendingSeating, order);
assert.equal(t.publicState(T(1), new Set()).pendingSeating, null); // ホスト以外には送らない
// 全員降りてハンドを終わらせ、次のハンドへ
for (let i = 0; i < 10 && !h.finished; i++) t.act(t.seats[h.toAct].token, 'fold', 0);
assert.ok(h.finished);
t.tick(10 ** 6);
t.tick(10 ** 7);
assert.equal(t.hand.number, 2);
assert.deepEqual(names(t), ['p0', 'p2', 'p1', 'p3']);
assert.equal(t.pendingSeating, null);
// 前のハンドのボタンの人の、新しい並びでの次の人がボタン
const after = ['p0', 'p2', 'p1', 'p3'];
const next = after[(after.indexOf(buttonName) + 1) % 4];
assert.equal(t.seats[t.hand.button].name, next);
// 参加者の順もボタンの次から新しい並びどおり
const seq = t.hand.order.map((s) => t.seats[s].name);
const k = after.indexOf(next);
assert.deepEqual(seq, [1, 2, 3, 4].map((d) => after[(k + d) % 4]));
});
test('席順: 決めた後に座った人は最後に加え、いなくなった人は外す', () => {
const t = makeTable(3);
t.start(T(0), 0);
const h = t.hand;
assert.equal(t.setSeating(T(0), [2, 1, 0]), null);
t.join(T(5), 'p5'); // ハンド中に新しく座る
t.leave(T(1)); // ハンド中に退席
for (let i = 0; i < 10 && !h.finished; i++) t.act(t.seats[h.toAct].token, 'fold', 0);
t.tick(10 ** 6);
t.tick(10 ** 7);
assert.deepEqual(names(t), ['p2', 'p0', 'p5']);
});