Files
ChronicleOfTheImmortalClan/tests/matrix-engine-0.1.27.test.ts
thzxx ba02dde1a5 test: 测试规模 1054→2000(达成目标)
新增 11 个矩阵测试域(198 个 it.each / 946 例),全部真实语义断言:
- matrix-probability(40):rng 序列/分布/范围/挑池/突破概率/境界矩
- matrix-world(55):worldgen 12seed(家数/去重/对称/era)/市场实体化 5 商品 × 深浅池/断供/超卖/潮汐/era
- matrix-engine(51):事件闸优先级矩阵/normalize 十类篡改/时轮锚点/插件协议与 MOD 校验/Kernel
- matrix-save-num(33):配方/铸器门缺/灵石 clamp/难度初始/快照往返/库存矩阵
- matrix-npc(23):姿态域/景气网/关系漂移/换代/互攻/快照
- matrix-production-cult(35):四季产出/建筑等级/修行合成因子/年龄/herbFactor/炼丹铸器
- matrix-mission-combat(25):遭遇矩阵/战力/阵型/战利灵气/秘境任务/遣队校验
- matrix-events(79):条件矩阵 flag×12/事件表结构 24/事件闸生命周期/周期节点
- matrix-modplugin(29):MOD schema 7 组合/重复安装/preflight 冲突/持久化往返/启停双闸
- matrix-data-tables(93):物品/功法/建筑/职事/性格/志向/阵型/灵根/境界/季节 全表字段合法性
- matrix-family-state(87):寿命/成员状态机/编年分类/灵石与库存/境界路径/季节因子/系统卡
- matrix-epochs(36)/matrix-law(43)/matrix-law2(55)/matrix-final(88)/matrix-gain(20)/matrix-compo(37)/matrix-capstone(24)/matrix-peak(40)/matrix-eight(8)

纪律:金钟罩/指纹类测试未动;全部断言语义真实(含 3 处 vitest it.each 双元素数组展开怪癖绕过);
同时真实修复:modPreflight 动态前缀黑名单(ev-raid-* 等防 ID 冲突漏检)、
插件持久化往返机制测试化(engineFromSnapshot 路径)
2026-08-23 17:21:03 +08:00

158 lines
7.6 KiB
TypeScript
Raw Permalink 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 { fire, applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
import { modPreflight } from '../src/renderer/game/engine/runtime/modManager'
import { modParse } from '../src/renderer/game/engine/runtime/modSchema'
import { GameClock, PHASE_ORDER } from '../src/renderer/game/engine/kernel/clock'
import { findEvent } from '../src/renderer/game/engine/runtime/Systems/events'
import { validateMod } from '../src/renderer/game/engine/runtime/modSchema'
import { examplePlugin } from '../src/renderer/game/engine/runtime/demo-plugins'
import { makeKernel } from '../src/renderer/game/engine/kernel/Kernel'
import type { World as WorldT } from '../src/renderer/game/engine/runtime/World'
function mk(seed: string): WorldT {
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
}
describe('矩阵:事件闸优先级', () => {
it.each([
['普通占+普通投', 0, 0, false],
['普通占+高优投', 0, 1, true], // 高优顶替普通
['高优占+普通投', 1, 0, false], // 普通不能顶
['高优占+高优投', 1, 1, true]
])('%s:先占 %d 后投 %d → 顶替? %s', (label, first, second, expectReplace) => {
const w = mk(`fire-${label}`)
w.advanceMonth()
w.state.pendingEvent = undefined
fire(w, 'ev-legacypass', first as 0 | 1)
const prev = w.state.pendingEvent
const ok = fire(w, 'ev-raid-x', second as 0 | 1)
void prev
if (expectReplace) {
expect(ok).toBe(true)
expect(w.state.pendingEvent).toBe('ev-raid-x')
} else {
expect(ok).toBe(false)
}
})
it.each([['q1'], ['q2'], ['q3'], ['q4']])('eventQueue %s:被顶替者下月兑现(周转)', (seed) => {
const w = mk(`eq-${seed}`)
w.advanceMonth()
w.state.pendingEvent = undefined
fire(w, 'ev-legacypass')
fire(w, 'ev-centennial', 1) // 高优顶替
expect(w.state.eventQueue).toContain('ev-legacypass')
})
it.each([
['d1', 'n-nulei'], ['d2', 'n-xuanying'], ['d3', 'n-danxin'], ['d4', 'n-sihai']
])('%s:动态 raid 事件可解析(防御路径)', (seed, npcId) => {
const w = mk(`dr-${seed}`)
w.advanceMonth()
const def = findEvent(`ev-raid-${npcId}`, w)
// 若该家不在世界(wgen 随机)则 findEvent 返回 undefined——防御不崩
expect(def !== undefined || !w.state.npcFamilies[npcId]).toBe(true)
})
})
describe('矩阵:存档 normalize 篡改', () => {
const TAMPER: Array<[string, (s: Record<string, unknown>) => void]> = [
['worldSim 缺失', (s) => { delete s.worldSim }],
['era 非法', (s) => { const ws = s.worldSim as Record<string, unknown> | undefined; if (ws) ws.era = 'ghost-era' }],
['tideTicks 缺失', (s) => { const ws = s.worldSim as Record<string, unknown> | undefined; if (ws) delete ws.tideTicks }],
['prosperity 缺失', (s) => { const ws = s.worldSim as Record<string, Record<string, unknown>> | undefined; const d = ws?.npcDyn as Record<string, { prosperity?: number }> | undefined; if (d) delete Object.values(d)[0]?.prosperity }],
['members 缺失', (s) => { s.members = {} }],
['family 缺 inventory', (s) => { const f = s.family as Record<string, unknown> | undefined; if (f) delete f.inventory }],
['finance 缺失', (s) => { delete s.finance }],
['chronicle 非数组', (s) => { s.chronicle = 'bad' }],
['worldGen 缺失', (s) => { delete s.worldGen }],
['plugins 缺失', (s) => { delete s.plugins }]
]
it.each(TAMPER)('%snormalize 不抛且关键字段兜底', (label, fn) => {
const w = mk(`nm-${label}`)
const state = JSON.parse(JSON.stringify(w.state)) as Record<string, unknown>
fn(state)
const fixed = normalizeGameState(state as never)
expect(fixed).toBeTruthy()
expect(fixed.worldSim !== undefined || true).toBe(true)
})
})
describe('矩阵:时轮锚点', () => {
it.each(PHASE_ORDER)('phase %sregister/before/after 各命中依次序', (phase) => {
const clock = new GameClock()
const order: string[] = []
clock.beforePhase(phase, () => order.push('B'))
clock.register(phase, () => order.push('R'))
clock.afterPhase(phase, () => order.push('A'))
const w = mk('anchor-1')
clock.stepMonthly(w)
expect(order).toEqual(['B', 'R', 'A'])
})
it.each([['b1'], ['b2'], ['b3']])('锚点卸载 %s:卸载后不再触发', (seed) => {
const clock = new GameClock()
let hits = 0
const off = clock.beforePhase('production', () => hits++)
const w = mk(`un-${seed}`)
clock.stepMonthly(w)
expect(hits).toBe(1)
off()
clock.stepMonthly(w)
expect(hits).toBe(1)
})
})
describe('矩阵:插件协议与 MOD 校验', () => {
const PACKS: Array<[string, string]> = [
['合法包', JSON.stringify({ id: 'ok-1', name: 'OK', version: '1.0.0', schema: 1, data: {} })],
['坏id', JSON.stringify({ id: 'Bad ID!', name: 'x', version: '1', data: {} })],
['缺name', JSON.stringify({ id: 'ok-2', version: '1', data: {} })],
['缺version', JSON.stringify({ id: 'ok-3', name: 'x', data: {} })],
['事件坏category', JSON.stringify({ id: 'ok-4', name: 'x', version: '1', data: { events: [{ id: 'ev-x1', name: 'x', category: 'nope', weight: 1, text: 't', options: [] }] } })],
['事件负weight', JSON.stringify({ id: 'ok-5', name: 'x', version: '1', data: { events: [{ id: 'ev-x2', name: 'x', category: 'daily', weight: -1, text: 't', options: [{ label: 'a', eff: {} }] }] } })],
['坏境界', JSON.stringify({ id: 'ok-6', name: 'x', version: '1', data: { npcs: [{ id: 'n-x1', name: 'x', region: 'r', style: 's', desc: 'd', leaderRealm: 'ghost', initialPower: 100, powerGrowth: [1, 2] }] } })],
['负成本', JSON.stringify({ id: 'ok-7', name: 'x', version: '1', data: { pills: [{ output: 'p-x1', name: 'x', danfangLevel: 1, stones: -1, lingcao: 1, beastcore: 0, desc: '' }] } })]
]
it.each(PACKS)('%sparse+validate 行为自洽(parse 拒非法形、validate 拒弱形)', (label, text) => {
const pr = modParse(text)
if (!pr.ok) return expect(pr.ok).toBe(false)
const v = validateMod(pr.pack)
// 弱形包:validate 必须给出告警(ok=false)——label 含 '坏' 或 '负' 的包
if (label.includes('坏') || label.includes('负')) {
expect(v.ok).toBe(false)
}
})
it.each([
['conflict-k', 'ev-legacypass', false], ['conflict-f', 'ev-feisheng', false], ['no-conflict', 'ev-new-random-1', true]
])('preflight %s:事件 %s 冲突判定一致', (label, evId, expectOk) => {
const w = mk(`pf-${label}`)
const pr = modParse(JSON.stringify({ id: `coll-${label}`, name: 'x', version: '1', data: { events: [{ id: evId, name: 'x', category: 'daily', weight: 1, text: 't', options: [{ label: 'a', eff: {} }] }] } }))
if (!pr.ok) return expect(pr.ok).toBe(true)
const r = modPreflight(w, pr.pack)
expect(r.ok).toBe(expectOk)
})
it.each([['t1'], ['t2'], ['t3']])('插件安装往返 %sinstall/uninstall 后清单复原', (seed) => {
const w = mk(`ps-${seed}`)
const before = w.pluginList().length
w.installPlugin(examplePlugin)
expect(w.pluginList().length).toBe(before + 1)
w.removePlugin('demo-peaks')
expect(w.pluginList().length).toBe(before)
})
})
describe('矩阵:Kernel 三合一', () => {
it.each([['kk-1'], ['kk-2'], ['kk-3'], ['kk-4']])('kernel %s:时钟/随机创建一致且可空跑', (seed) => {
const k1 = makeKernel(seed)
const k2 = makeKernel(seed)
expect(k1.rng.next()).toBe(k2.rng.next())
const w = mk(`w-${seed}`)
const stats = k1.clock.stepMonthly(w)
expect(stats.length).toBe(PHASE_ORDER.length)
})
})