Files
ChronicleOfTheImmortalClan/tests/event-state-matrix.test.ts
T
thzxx 9b0f36c70a v0.1.35b: 兼容层铲除——迁移链/版本戳/死表死代码/normalize 兼容批/死字段
【A+I】migrate.ts 迁移链(CURRENT_SCHEMA/MIGRATIONS/migrateIfNeeded/MigrationError)整链铲除→validateLoadedState 结构校验(seed/family/members 断言);信封统一 payload 单格式(去双格式兜底);APP_ID 保留
【F】schemaVersion 字段全删(domain/creation/save 8 处);SaveMeta.version 记游戏版本字符串
【C】chronicle 死表+saveChronicle 死方法(首行 return+unreachable legacy 僵尸)全删
【D/E】GameEngine __state globalThis 泄漏口+seed 三元(恒不可达)删除;engineFromSnapshot/noAutoCreate 保留(测试便利——非兼容机器)
【B/H】normalizeGameState 兼容批删:老档字段补(28行)/worldGen 重放(18行)/P0-2 占位 def(9行)/worldSim 字段批删;保留 npcs 重注册/headId 修复/d distress 防护;initSim 补 0.1.34 四键(唯一初始源)
【G】domain 收紧 5 兼容 optional(techniqueRank/Progress/tribBoost/worldGen 必填)+pcgen 必写
【测试】删 9 个兼容验证体(audit 0.1.0-era/全残缺矩阵/空 worldSim 兜底);5989 全绿——金钟罩零漂(新档结构全字段,删除不涉世界数值)
2026-08-23 22:35:17 +08:00

88 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('大比寄读渡劫动态事件矩阵', () => {
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()
})
})