Files
thzxx ff6df8054c refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)
结构(旧 game/core+engine → 新 engine/ 域):
- engine/kernel/  时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core)
- engine/narrative/  legacy/报告/列传/谱系/年轴(原 core 叙事族)
- engine/runtime/   World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统)
- engine/sim/     Market(未来 WorldSim 同行)
- 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层)

验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化)
typecheck 0 error
2026-08-23 13:23:25 +08:00

169 lines
6.4 KiB
TypeScript

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))
})
})