import { describe, expect, it } from 'vitest' import { World, normalizeGameState } from '../src/renderer/game/engine/runtime/World' import { EVENTS, EffectDef } from '../src/renderer/game/data/events' import { applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events' import { POSTS } from '../src/renderer/game/data/posts' import { ASPIRATIONS } from '../src/renderer/game/data/aspirations' 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('事件选项应用矩阵(逐事件逐选项)', () => { it.each(EVENTS.flatMap((e) => e.options.map((o, idx) => [e.id, idx, o.label] as const)))( '事件 %s 选%i(%s)应用后世界不破', (id, idx) => { const w = baseWorld('ev-app-' + id) applyEventChoice(w, id, idx) expect(w.state.family.stones).toBeGreaterThanOrEqual(0) for (const inv of Object.values(w.state.family.inventory)) { if (typeof inv === 'number') expect(inv).toBeGreaterThanOrEqual(0) } } ) }) describe('effect 结构合法矩阵', () => { it.each(EVENTS.flatMap((e) => e.options.map((o) => [e.id, o.eff] as const)))('事件 %s 效果可回放', (id, eff) => { expect(typeof eff).toBe('object') expect(eff).not.toBeNull() }) it.each(EVENTS.flatMap((e) => e.options.map((o, i) => [e.id, i, o.eff.res] as const)).filter(([, , r]) => !!r))( '事件 %s 资源效果引用存在', (_id, _i, res) => { for (const [k] of Object.entries(res!)) { expect(k === 'stones' || ['lingcao', 'lingkuang', 'beastcore'].includes(k) || k.startsWith('pill-') || k.startsWith('weapon-')).toBe(true) } } ) }) describe('职事能力矩阵', () => { // head 由继承链独占(assignPost 拒绝),跳过 const assignable = Object.entries(POSTS).filter(([id]) => id !== 'head') it.each(assignable.map(([id, def]) => [id, def.max] as const))('职事 %s 上限可重复指派', (id, max) => { const w = baseWorld('post-' + id) let assigned = 0 for (const c of Object.values(w.state.members)) { if (assigned >= max) break if (w.assignPost(c.id, id)) assigned++ } expect(assigned).toBe(max) }) it.each(assignable.map(([id]) => [id] as const))('职事 %s 不可指派寄读者', (id) => { const w = baseWorld('post-ap-' + id) const c = w.state.members['x3'] c.state = 'apprentice' expect(w.assignPost(c.id, id)).toBe(false) }) }) describe('志向矩阵', () => { it.each(Object.keys(ASPIRATIONS).map((id) => [id] as const))('志向 %s 生效于世界(无异常)', (id) => { const w = baseWorld('asp-' + id) for (const c of Object.values(w.state.members)) c.aspiration = id w.advanceMonth() expect(true).toBe(true) }) }) describe('normalizeGameState 全残缺矩阵', () => { it.each([ ['finance', 'yearStats', 'yearlyReports', 'stats'].map((k) => [k] as const) ].flat())('%s 缺失补齐', (key) => { const w = baseWorld('norm-' + key) const state = JSON.parse(JSON.stringify(w.state)) delete (state as Record)[key] const fixed = normalizeGameState(state) expect((fixed as Record)[key]).toBeTruthy() }) it('无 headId 老档补齐首个存活着', () => { const w = baseWorld('norm-head') const state = JSON.parse(JSON.stringify(w.state)) state.family.headId = 'x-not-exist' const fixed = normalizeGameState(state) expect(fixed.family.headId).toBe('x1') }) it('缺 npcFamilies 补齐空表(不崩外交)', () => { const w = baseWorld('norm-npc') const state = JSON.parse(JSON.stringify(w.state)) delete state.npcFamilies const fixed = normalizeGameState(state) const w2 = new World(fixed) for (let i = 0; i < 3; i++) w2.advanceMonth() // 不抛 expect(true).toBe(true) }) }) describe('大比寄读渡劫动态事件矩阵', () => { it.each([ ['ev-tournament-10', '称病不出'], ['ev-recruit', '婉拒'], ['ev-centennial', '阖家简庆'] ] as const)('%s 选「%s」无副作用', (id, _label) => { const w = baseWorld('dyn-' + id.slice(0, 6)) const optIdx = 0 // 第一选项最简 applyEventChoice(w, id, optIdx) expect(w.state.pendingEvent).toBeUndefined() }) })