import { describe, expect, it } from 'vitest' import { World } from '../src/renderer/game/engine/world' import { GameClock, PHASE_ORDER } from '../src/renderer/game/core/clock' import { buildClock } from '../src/renderer/game/engine/clocks' import { RngHub } from '../src/renderer/game/core/rng' import { seasonOf, seasonMod, SEASON } from '../src/renderer/game/data/season' import { yearAxis, decadeLabel } from '../src/renderer/game/core/yearaxis' import { stateFingerprint } from './fingerprint.helper' import { resetSaveBus, attachLogSink } from './world.helpers' function baseWorld(seed: string): World { const w = World.create({ seed, surname: '经', familyName: '经家', motto: 'm', difficulty: 'normal' }) resetSaveBus() attachLogSink(w) return w } describe('GameClock 统一时轮', () => { it('七个 phase 全被注册且顺序固定', () => { const clock = buildClock() const clock2 = new GameClock() clock2.register('production', () => undefined) expect(clock.subscriptionCount()).toBeGreaterThanOrEqual(10) expect(PHASE_ORDER.length).toBe(7) expect(clock2.subscriptionCount()).toBe(1) }) it('phase 注册后按序执行且消耗确定(同 seed 同指纹)', () => { const a = stateFingerprint(longRun('bell-seed-2').state) const b = stateFingerprint(longRun('bell-seed-2').state) expect(a).toBe(b) }) it('stepMonthly 返回各 phase 耗时统计(性能预算通道)', () => { const w = baseWorld('clk-a') const report = w.clock.stepMonthly(w) expect(report.length).toBe(7) for (const r of report) { expect(PHASE_ORDER).toContain(r.phase) expect(typeof r.ms).toBe('number') } }) it('年首钩子先于月度执行(族簿于当年首月前发出)', () => { const w = baseWorld('clk-b') const paperRec: number[] = [] w.out.push({ onLog: () => undefined, onChronicle: () => undefined, onBattle: () => undefined, onPendingEvent: () => undefined, onGameOver: () => undefined, onYearPaper: (r) => paperRec.push(r.year) }) for (let i = 0; i < 14; i++) w.advanceMonth() expect(paperRec).toContain(1) }) }) describe('RngHub 统一随机收集', () => { it('播种为稳定格式且可重现(同 seed 同世界)', () => { const seed = RngHub.rollSeed() expect(seed.startsWith('seed-')).toBe(true) const a = stateFingerprint(longRun(seed).state) const b = stateFingerprint(longRun(seed).state) expect(a).toBe(b) }) it('音频随机与引擎随机源完全隔离', () => { const w = baseWorld('rng-isolate') const before = w.rng.nextCount for (let i = 0; i < 100; i++) RngHub.audioNoise01() expect(w.rng.nextCount).toBe(before) }) it('引擎随机计数器随推进增长(audit 可用)', () => { const w = baseWorld('rng-audit') const c0 = w.rng.nextCount for (let i = 0; i < 36; i++) w.advanceMonth() expect(w.rng.nextCount).toBeGreaterThan(c0 + 50) }) }) describe('season 时节', () => { it('四季映射正确', () => { expect(seasonOf(1)).toBe('spring') expect(seasonOf(4)).toBe('summer') expect(seasonOf(7)).toBe('autumn') expect(seasonOf(10)).toBe('winter') expect(seasonOf(12)).toBe('winter') }) it('四季加成表方向正确', () => { expect(seasonMod(2, 'field')).toBeGreaterThan(0) expect(seasonMod(5, 'cult')).toBeGreaterThan(0) expect(seasonMod(8, 'market')).toBeGreaterThan(0) expect(seasonMod(11, 'meditation')).toBeGreaterThan(0) expect(seasonMod(5, 'field')).toBe(0) }) it('春耕与冬闭实惠落地', () => { const w = baseWorld('season-a') // 春季灵田月产出高于 12 月 w.state.family.buildings = { lingtian: 1 } w.state.month = 2 w.advanceMonth() const springGain = (w.state.family.inventory['lingcao'] ?? 0) - 60 const w2 = baseWorld('season-b') w2.state.family.buildings = { lingtian: 1 } w2.state.month = 11 w2.advanceMonth() const winterGain = (w2.state.family.inventory['lingcao'] ?? 0) - 60 expect(springGain).toBeGreaterThan(winterGain) }) it('SEASON 表数据合法', () => { for (const s of Object.values(SEASON)) { expect(s.field).toBeGreaterThanOrEqual(0) expect(s.cult).toBeGreaterThanOrEqual(0) expect(s.market).toBeGreaterThanOrEqual(0) expect(s.meditation).toBeGreaterThanOrEqual(0) } }) }) describe('yearAxis 横轴年表', () => { it('十年一格聚合生/殇/大事', () => { const w = baseWorld('axis-a') w.chronicle('birth', '林小生', 'x3') w.chronicle('breakthrough', '林公突破至筑基。', 'x3', true) w.chronicle('death', '林公辞世。', 'x3', true) for (let i = 0; i < 30; i++) w.advanceMonth() const cells = yearAxis(w.state) expect(cells.length).toBeGreaterThan(0) const c1 = cells[0] expect(c1.births).toBeGreaterThanOrEqual(1) expect(c1.deaths).toBeGreaterThanOrEqual(1) expect(c1.events.some((e) => e.kind === '突破')).toBe(true) }) it('decadeLabel 边界', () => { expect(decadeLabel(1, 10)).toBe('1-10年') expect(decadeLabel(11, 11)).toBe('11年') }) it('跨十年事件分布正确', () => { const w = baseWorld('axis-b') w.state.year = 100 for (let y = 1; y <= 100; y += 10) { w.state.chronicle.push({ id: 'ax' + y, year: y, month: 1, category: 'breakthrough', text: `第${y}年突破`, important: false }) } const cells = yearAxis(w.state) expect(cells.length).toBe(10) // 头十年第一格 expect(cells[0].events.length).toBe(1) }) }) function longRun(seed: string, months = 560): World { const w = World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' }) for (let i = 0; i < months; i++) { if (w.state.gameOver) break w.advanceMonth() } return w }