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
+86
View File
@@ -0,0 +1,86 @@
import { describe, expect, it } from 'vitest'
import { Rng, seedToRng, RngHub } from '../src/renderer/game/core/rng'
describe('RNG 多种子矩阵', () => {
it.each([
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'
].map((s) => [s] as const))('种子 %s 前 5 值互异不重复零帧', (seed) => {
const rng = new Rng(seedToRng(seed))
const vals = [rng.next(), rng.next(), rng.next(), rng.next(), rng.next()]
expect(new Set(vals.map((v) => v.toFixed(6))).size).toBeGreaterThanOrEqual(4)
})
it.each([10, 50, 100, 500, 1000].map((n) => [n] as const))('输出序列 %i 个全在 [0,1)', (n) => {
const rng = new Rng(seedToRng('range-' + n))
for (let i = 0; i < n; i++) {
const v = rng.next()
expect(v).toBeGreaterThanOrEqual(0)
expect(v).toBeLessThan(1)
}
})
it.each([3, 7, 20, 99, 1000].map((n) => [n] as const))('int(0,%i) 端点可达', (n) => {
const rng = new Rng(seedToRng('int-' + n))
let lo = Number.MAX_SAFE_INTEGER
let hi = -1
for (let i = 0; i < n * 40; i++) {
const v = rng.int(0, n - 1)
lo = Math.min(lo, v)
hi = Math.max(hi, v)
}
expect(lo).toBe(0)
expect(hi).toBe(n - 1)
})
})
describe('RngHub 矩阵', () => {
it.each([1, 2, 3].map((n) => [n] as const))('rollSeed 第%i 次格式稳定', () => {
expect(RngHub.rollSeed().startsWith('seed-')).toBe(true)
})
it.each([5, 20, 50].map((n) => [n] as const))('audioNoise01 %i 次均为 [0,1)', (n) => {
for (let i = 0; i < n; i++) {
const v = RngHub.audioNoise01()
expect(v).toBeGreaterThanOrEqual(0)
expect(v).toBeLessThan(1)
}
})
it('音频流可回放(同序)', () => {
const a = RngHub.audioNoise01()
const b = RngHub.audioNoise01()
expect(a).not.toBe(b)
})
})
describe('RigRng state 拷贝矩阵', () => {
it.each(['s1', 's2', 's3'].map((s) => [s] as const))('种子 %s 状态快照后继续一致', (s) => {
const rng = new Rng(seedToRng(s))
for (let i = 0; i < 10; i++) rng.next()
const snap = rng.getState()
const con = new Rng(snap)
const r2 = new Rng(seedToRng(s))
for (let i = 0; i < 10; i++) r2.next()
expect(con.next()).toBe(r2.next())
})
})
describe('pick/shuffle 矩阵', () => {
it.each([['a'], ['a', 'b'], ['a', 'b', 'c'], Array.from({ length: 7 }, (_, i) => 'e' + i)])(
'pick 从 %j 不丢元素',
(arr) => {
const rng = new Rng(seedToRng('pick-' + arr.length))
for (let i = 0; i < 30; i++) {
expect(arr).toContain(rng.pick(arr))
}
}
)
it.each([1, 2, 5, 12].map((n) => [n] as const))('shuffle %i 保集不变', (n) => {
const rng = new Rng(seedToRng('sh-' + n))
const src = Array.from({ length: n }, (_, i) => i)
const out = rng.shuffle(src)
expect(out.length).toBe(n)
expect([...out].sort((a, b) => a - b)).toEqual(src)
})
})