import { describe, expect, it } from 'vitest' import { World } from '../src/renderer/game/engine/runtime/World' import { stateFingerprint, longRun } from './fingerprint.helper' import { computeGenealogy } from '../src/renderer/game/engine/narrative/genealogy' import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events' import { resetSaveBus, attachLogSink } from './world.helpers' describe('多 seed 长跑矩阵', () => { it.each([1, 2, 3, 4, 5].map((n) => [`seed-matrix-${n}`] as const))('%s 240 月可存活推进', (seed) => { const w = World.create({ seed, surname: '官', familyName: '官家', motto: 'm', difficulty: 'normal' }) resetSaveBus() attachLogSink(w) for (let i = 0; i < 240; i++) { if (w.state.gameOver) break w.advanceMonth() } expect(w.state.year).toBeGreaterThan(5) }) }) describe('各代际矩阵', () => { it.each([1, 10, 30, 50, 100].map((g) => [g] as const))('第%i 代后谱系与成员状态合法', (g) => { const w = World.create({ seed: 'gen-' + g, surname: '关', familyName: '关家', motto: 'm', difficulty: 'normal' }) for (const c of Object.values(w.state.members)) c.generation = g w.state.family.generation = g const rows = computeGenealogy(w) expect(rows.every((r: { gen: number }) => r.gen >= 1)).toBe(true) }) }) describe('继承链矩阵(多路径)', () => { it('出生顺序长子继位', () => { const w = World.create({ seed: 'heir-a', surname: '韩', familyName: '韩家', motto: 'm', difficulty: 'normal' }) w.state.members['x1'].alive = false w.advanceMonth() expect(w.state.family.headId).toBe('x3') // first-born }) it('同代男子无后时取高境界', () => { const w = World.create({ seed: 'heir-b', surname: '韩', familyName: '韩家', motto: 'm', difficulty: 'normal' }) w.state.members['x1'].alive = false w.state.members['x3'].alive = false w.state.members['x5'].alive = false w.state.members['x4'].realm = { major: 'foundation', minor: 0 } w.state.members['x2'].realm = { major: 'qi', minor: 2 } w.advanceMonth() expect(w.state.family.headId).toBe('x4') // 高境界优先 }) }) describe('gameOver 路径守卫', () => { it.each([ ['全员死亡', (w: World) => Object.values(w.state.members).forEach((c) => (c.alive = false))], ['家主死亡且无后人', (w: World) => { w.state.members['x1'].alive = false w.state.members['x3'].alive = false }] ] as const)('%s → 世界终止标记', (_name, fn) => { const w = World.create({ seed: 'go-' + _name.length, surname: '欧阳', familyName: '欧阳家', motto: 'm', difficulty: 'normal' }) fn(w) resetSaveBus() attachLogSink(w) if (_name.startsWith('全员')) { // 全员死亡:无活人即 gameover w.advanceMonth() expect(w.state.gameOver).toBeTruthy() } else { // 有活人(x2 寡)则继承;x2 就是继承人 w.advanceMonth() expect(w.state.gameOver || w.state.family.headId).toBeTruthy() } }) }) describe('系统级固定周期事件', () => { it('每年必有岁簿(paper 触发统计)', () => { const w = World.create({ seed: 'paper-a', surname: '吴', familyName: '吴家', motto: 'm', difficulty: 'normal' }) const yearsSeen: number[] = [] w.out.push({ onLog: () => undefined, onChronicle: () => undefined, onBattle: () => undefined, onPendingEvent: () => undefined, onGameOver: () => undefined, onYearPaper: (r) => yearsSeen.push(r.year) }) for (let i = 0; i < 26; i++) w.advanceMonth() expect(yearsSeen).toContain(1) expect(yearsSeen).toContain(2) }) it('每五年大比征召(第10/15/20年)', () => { const w = World.create({ seed: 'tourn-b', surname: '邵', familyName: '邵家', motto: 'm', difficulty: 'normal' }) const seen: string[] = [] w.out.push({ onLog: () => undefined, onChronicle: () => undefined, onBattle: () => undefined, onPendingEvent: (id) => { if (id.startsWith('ev-tournament-')) seen.push(id) }, onGameOver: () => undefined }) for (let i = 0; i < 20 * 12; i++) { if (w.state.pendingEvent) { applyEventChoice(w, w.state.pendingEvent, 0) w.state.pendingEvent = undefined } w.advanceMonth() } expect(seen).toContain('ev-tournament-10') expect(seen).toContain('ev-tournament-15') expect(seen).toContain('ev-tournament-20') }) it('偶数年四邻回声整年单发', () => { const w = World.create({ seed: 'echo-b', surname: '邱', familyName: '邱家', motto: 'm', difficulty: 'normal' }) const sounds: number[] = [] w.out.push({ onLog: () => undefined, onChronicle: () => undefined, onBattle: () => undefined, onPendingEvent: (id) => { if (id.startsWith('ev-echo-')) sounds.push(Date.now() % 1000) }, onGameOver: () => undefined }) for (const y of [12, 14, 16]) { w.state.year = y w.state.month = 1 w.state.family.flag = {} w.state.pendingEvent = undefined // 直接调用 eventRoll 检验年内单发 w.advanceMonth() if (w.state.pendingEvent) { sounds.push(1) w.state.pendingEvent = undefined } w.advanceMonth() if (w.state.pendingEvent) sounds.push(2) } // 每偶数年均至少一打一(flag 封缄) expect(sounds.length).toBeGreaterThanOrEqual(3) }) }) describe('确定性复跑矩阵', () => { it.each([1, 2, 3].map((n) => [`det-replay-${n}`] as const))('%s 两次 120 月轨迹一致', (seed) => { const a = World.create({ seed, surname: '洪', familyName: '洪家', motto: 'm', difficulty: 'normal' }) const b = World.create({ seed, surname: '洪', familyName: '洪家', motto: 'm', difficulty: 'normal' }) resetSaveBus() attachLogSink(a) resetSaveBus() attachLogSink(b) for (let i = 0; i < 120; i++) { a.advanceMonth() b.advanceMonth() } a.state.pendingEvent = undefined b.state.pendingEvent = undefined const fa = JSON.parse(JSON.stringify({ ...a.state, pendingEvent: undefined })) const fb = JSON.parse(JSON.stringify({ ...b.state, pendingEvent: undefined })) expect(JSON.stringify(fa)).toBe(JSON.stringify(fb)) }) }) describe('fingerprint 跨版本稳定性锚点', () => { it('保存后重放同世界(存档点回复)', () => { const w = longRun('det-anchor') expect(stateFingerprint(w.state)).toBe(stateFingerprint(w.state)) }) })