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:
2026-08-23 10:04:33 +08:00
parent c09395a6d4
commit 3195f67455
7 changed files with 827 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
import { describe, expect, it } from 'vitest'
import { World } from '../src/renderer/game/engine/world'
import { monthlyRate } from '../src/renderer/game/engine/systems/cultivation'
import { combatPowerOf } from '../src/renderer/game/engine/systems/combat'
import { resolveBreakthrough } from '../src/renderer/game/engine/systems/cultivation'
import { computeLegacy, resolveLegacy } from '../src/renderer/game/core/legacy'
import { yearAxis } from '../src/renderer/game/core/yearaxis'
import { buildBiography } from '../src/renderer/game/core/biography'
import { computeGenealogy } from '../src/renderer/game/core/genealogy'
import { marketPrice, buyItem, sellItem } from '../src/renderer/game/engine/market'
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([5, 7, 8, 15, 16, 17, 55, 56, 70].map((a) => [a] as const))('%i 岁修炼速率不越界(0.4-1×)', (a) => {
const w = baseWorld(`age-${a}`)
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.bornYear = 1 - a
const r = monthlyRate(w, c)
expect(r).toBeGreaterThan(0)
expect(r).toBeLessThanOrEqual(10)
if (a < 8) expect(r).toBeLessThan(2)
if (a > 55) expect(r).toBeLessThan(2)
})
})
describe('修为边界矩阵', () => {
it.each([-5, 0, 0.1, 49.9, 99.9, 100, 150].map((p) => [p] as const))('进度 %p 被夹在 0-100', (p) => {
const w = baseWorld(`rp-${p}`)
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.realmProgress = p
w.advanceMonth()
expect(c.realmProgress).toBeGreaterThanOrEqual(0)
expect(c.realmProgress).toBeLessThanOrEqual(100)
})
it('进度 100 且冷却中不自动冲击', () => {
const w = baseWorld('rp-cooldown')
const c = w.state.members['x5']
c.realm = { major: 'qi', minor: 1 }
c.realmProgress = 100
c.lastBreakthroughAttempt = (w.state.year * 12 + w.state.month) - 2
w.advanceMonth()
expect(c.realmProgress).toBeGreaterThanOrEqual(100)
})
})
describe('气血边界矩阵', () => {
it.each([-10, 0, 1, 29, 30, 50, 99, 100, 130].map((h) => [h] as const))('气血 %h 不越 0-100', (h) => {
const w = baseWorld(`hp-${h}`)
const c = w.state.members['x3']
c.health = h
w.advanceMonth()
expect(c.health).toBeGreaterThanOrEqual(0)
expect(c.health).toBeLessThanOrEqual(100)
})
})
describe('库存边界矩阵', () => {
it.each(['lingcao', 'lingkuang', 'beastcore', 'pill-qiyuan', 'pill-ningyuan', 'weapon-qi'].map((i) => [i] as const))(
'库存 %s 交易不为负',
(id) => {
const w = baseWorld(`inv-${id}`)
const before = w.state.family.inventory[id] ?? 0
w.advanceMonth()
expect((w.state.family.inventory[id] ?? 0) - before).toBeGreaterThanOrEqual(-20)
}
)
})
describe('金钱边界矩阵', () => {
it.each([0, 1, 49, 50, 799, 800, 100000].map((s) => [s] as const))('灵石 %s 下买卖不穿仓', (s) => {
const w = baseWorld(`money-${s}`)
const fam = w.state.family
fam.stones = s
const price = marketPrice(w, 'lingcao') * 5
const bought = buyItem(w, 'lingcao', 5)
expect(bought).toBe(s >= price)
expect(fam.stones).toBeGreaterThanOrEqual(0)
if (sellItem(w, 'lingcao', 1)) {
expect(fam.stones).toBeGreaterThanOrEqual(0)
}
})
})
describe('传承与谱系矩阵', () => {
it.each(['x1', 'x2', 'x3', 'x4', 'x5'].map((id) => [id] as const))('%s 列传可生成且不以空结尾', (id) => {
const w = baseWorld(`bio-${id}`)
const lines = buildBiography(w.state, id)
expect(lines.length).toBeGreaterThan(0)
expect(lines[lines.length - 1].label).toBeTruthy()
})
it.each([0, 50, 200].map((y) => [y] as const))('谱系在 %i 年后仍可构建(不抛)', (y) => {
const w = baseWorld(`gen-${y}`)
w.state.year = 1 + y
const rows = computeGenealogy(w)
expect(Array.isArray(rows)).toBe(true)
})
it.each([1, 3, 10, 100].map((y) => [y] as const))('年轴在 %i 年跨度下聚合', (y) => {
const w = baseWorld(`axis-${y}`)
w.state.year = y
w.state.chronicle.push({ id: 'c1', year: Math.max(1, y - 2), month: 1, category: 'breakthrough', text: 'xx', important: false })
const cells = yearAxis(w.state)
expect(cells.length).toBeGreaterThan(0)
})
})
describe('境界战力矩阵', () => {
it.each([
['mortal', 0], ['qi', 0], ['qi', 8], ['foundation', 0], ['core', 0], ['nascent', 0], ['spirit', 0]
] as const)('%s/%i 战力为正且渐强', (major, minor) => {
const w = baseWorld(`pw-${major}`)
const c = w.state.members['x4']
c.realm = { major, minor }
const p = combatPowerOf(w, c)
expect(p).toBeGreaterThan(0)
if (minor > 0 && major === 'qi') {
const c0 = w.state.members['x5']
c0.realm = { major: 'qi', minor: 0 }
expect(p).toBeGreaterThan(combatPowerOf(w, c0))
}
})
it.each(Object.keys(marketStub()).map((it) => [it] as const))('装备 %s 战力增益为正', (item) => {
const w = baseWorld(`eq-${item}`)
const c = w.state.members['x4']
const p0 = combatPowerOf(w, c)
c.equipment = item
expect(combatPowerOf(w, c)).toBeGreaterThan(p0)
})
})
function marketStub(): Record<string, number> {
return { 'weapon-fan': 1, 'weapon-qi': 2, 'weapon-ling': 3, 'weapon-fa': 4 }
}
describe('突破边界矩阵', () => {
it.each([
['mortal', 'qi'], ['qi', 'foundation'], ['foundation', 'core'], ['core', 'nascent'], ['nascent', 'spirit']
] as const)('%s → %s 成功或失败均不破世界', (from, to) => {
const w = baseWorld(`bt-${from}`)
const c = w.state.members['x3']
c.realm = { major: from, minor: from === 'mortal' ? 0 : 8 }
c.realmProgress = 100
c.mind = 8
resolveBreakthrough(w, c, 0.4)
expect(c.realm.major === from || c.realm.major === to).toBe(true)
expect(Number.isNaN(c.realmProgress)).toBe(false)
})
})
describe('评分与结局矩阵', () => {
it.each([0, 30, 55, 85, 120, 160, 200].map((v) => [v] as const))('虚弱档 %i 总评下仍可裁断', (v) => {
const w = baseWorld(`legacy-${v}`)
w.state.stats.repPeak = v
w.state.stats.maxRealmIdx = v
w.state.year = v + 1
w.state.stats.feishengCount = 0
const arch = resolveLegacy(w.state)
expect(arch.title.length).toBeGreaterThan(0)
expect(arch.dims.total).toBeGreaterThan(0)
})
it.each(['renXing', 'daoXing', 'weiMing', 'xiangHuo'].map((k) => [k] as const))('四维 %s 分轴单调', (k) => {
const w = baseWorld(`dim-${k}`)
const d0 = computeLegacy(w.state)
if (k === 'weiMing') w.state.stats.repPeak += 30
if (k === 'renXing') w.state.stats.popPeak += 10
if (k === 'daoXing') w.state.stats.maxRealmIdx += 10
if (k === 'xiangHuo') w.state.year += 50
const d1 = computeLegacy(w.state)
expect((d1 as never as Record<string, number>)[k]).toBeGreaterThan((d0 as never as Record<string, number>)[k])
})
})
describe('跨难度矩阵', () => {
it.each(['easy', 'normal', 'hard'].map((d) => [d] as const))('%s 难度 60 月无崩溃', (d) => {
const w = World.create({ seed: 'diff-' + d, surname: '萧', familyName: `${d}`, motto: 'm', difficulty: d as never })
resetSaveBus()
attachLogSink(w)
for (let i = 0; i < 60; i++) w.advanceMonth()
expect(w.state.family.stones).toBeGreaterThanOrEqual(0)
})
})