test: 测试矩阵扩至 861 项(参数化全覆盖)
新增 5 个矩阵套件: - data-matrix 242:物品/功法/建筑/秘境/敌人/势力/事件选项/职事/秉性/灵根/境界/时节 逐行 - balance-edge 81:年龄/修为/气血/库存/金钱边界、传承谱系年轴、境界战力、突破矩阵、评分分档、难度 - world-matrix 21:多 seed 长跑、继承链多路径、gameOver 守卫、岁簿/大比/回声周期、确定性复跑 - rng-matrix 38:多种子性质、范围分桶、RngHub 回放/隔离、state 拷贝、pick/shuffle 保集 - api-matrix 32:act 全目录逐项、query 全 ref、subscribe 全事件矩阵 - event-state-matrix 193:逐事件逐选项应用不破、effect 引用、职事上限/寄读、志向、normalize 残缺矩阵、动态事件 引擎附带优化:月底 clampState(修为/气血/库存/金钱永不越界——负血量负库存负钱被矩阵钓出后修复) 总测试 256 → 861;金钟罩复验通过(clamp 零漂移)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/world'
|
||||
import { stateFingerprint, longRun } from './fingerprint.helper'
|
||||
import { computeGenealogy } from '../src/renderer/game/core/genealogy'
|
||||
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++) {
|
||||
w.advanceMonth()
|
||||
if (w.state.pendingEvent) w.state.pendingEvent = undefined
|
||||
}
|
||||
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))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user