/** * 마작 무작위 대국 ③ 3인(산마) 360판 + 엔진 randomPlayout(직렬화 왕복·정보 누출 검사 내장) + 매 단계 누출 검사 * + 재현성 (docs/09-game-engine.md §7). */ import { describe, expect, test } from 'bun:test'; import { randomPlayout, seedN } from '@bg/engine'; import { mahjong } from './index'; import { checkPoints, checkTiles, emptyStats, playGame, variedOptions } from './playout'; const stats = emptyStats(); describe('무작위 대국: 3인 산마', () => { for (let chunk = 0; chunk < 18; chunk++) { test(`시드 ${2000 + chunk * 20 + 1}~${2000 + chunk * 20 + 20}`, () => { for (let i = 1; i <= 20; i++) { const seed = 2000 + chunk * 20 + i; playGame({ seed, players: 3, options: { length: seed % 2 ? 'tonpuu' : 'hanchan', ...variedOptions(seed, 3) }, prefs: seed % 4 === 0, leaveAt: seed % 11 === 0 ? 150 : undefined, stats }); } }); } test('북 뽑기·화료·유국이 모두 나왔다', () => { expect(stats.kita).toBeGreaterThan(500); expect(stats.tsumo).toBeGreaterThan(50); expect(stats.ron).toBeGreaterThan(50); expect(stats.exhaustive).toBeGreaterThan(50); expect(stats.maxApplyMs).toBeLessThan(25); }); }); describe('엔진 randomPlayout (매 단계 직렬화 왕복 + 정보 누출 검사)', () => { for (const [players, seed] of [ [['A', 'B', 'C', 'D'], 1], [['A', 'B', 'C'], 2], ] as const) { test(`${players.length}인 동풍전 시드 ${seed}`, () => { const r = randomPlayout(mahjong, { players: [...players], options: mahjong.optionsSchema.parse({ length: 'tonpuu', westEntry: false }), seed: seedN(seed), maxSteps: 4000, maxApplyMs: 25, invariant: (s) => { checkTiles(s as never); checkPoints(s as never); }, }); expect(r.steps).toBeGreaterThan(100); }); } }); describe('매 단계 누출 검사 (울기·리치·화료가 있는 대국)', () => { test('4인 1판 + 3인 1판, 매 단계 모든 시점의 view 검사', () => { const a = emptyStats(); playGame({ seed: 7, players: 4, options: { length: 'tonpuu', westEntry: false }, deep: true, stats: a }); playGame({ seed: 8, players: 3, options: { length: 'tonpuu', westEntry: false }, deep: true, stats: a }); expect(a.calls + a.riichi).toBeGreaterThan(0); }); }); describe('재현성', () => { test('같은 시드면 대국 전체가 같다', () => { for (const seed of [3, 4]) { const a = JSON.stringify(playGame({ seed, players: 4, options: variedOptions(seed, 4) })); const b = JSON.stringify(playGame({ seed, players: 4, options: variedOptions(seed, 4) })); expect(a).toBe(b); } }); });