test: 测试矩阵扩至 161 项(深度审计 + 5 处缺陷修复)
新增测试矩阵(12 文件 / 161 cases): - rng 12 / data 18 / pcgen+cultivation 20 / economy 12 / combat+missions 24 - marriage+diplomacy 13 / events 12 / world+lifecycle 8(含镜像一致)/ save 4+5 / audit 8 审计带出并修复的真实缺陷: 1. Rng.pick 空数组未报错(防空白数组进而崩溃难查) 2. combatPowerOf 未应用功法悟道品阶战力加成(0.1.2 设计缺口) 3. 重伤者即使不代表伤状态仍按满速修炼(health 进修炼公式) 4. 寡妇/鳏夫无法再婚:候选列表放着人执行层却拒婚(spouseId 死旧契约) 5. isWidowed 参数契约陷阱:传 id 静默 false(升级接受 Character|Id 双签名)
This commit is contained in:
+96
-33
@@ -1,55 +1,118 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Rng, seedToRng } from '../src/renderer/game/core/rng'
|
||||
|
||||
describe('Rng', () => {
|
||||
it('is deterministic for same seed', () => {
|
||||
const a = new Rng(seedToRng('hello'))
|
||||
const b = new Rng(seedToRng('hello'))
|
||||
for (let i = 0; i < 100; i++) {
|
||||
describe('Rng.sfc32 特性', () => {
|
||||
it('同一种子产生的序列完全一致', () => {
|
||||
const a = new Rng(seedToRng('ser1'))
|
||||
const b = new Rng(seedToRng('ser1'))
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
expect(a.next()).toBe(b.next())
|
||||
}
|
||||
})
|
||||
|
||||
it('differs for different seeds', () => {
|
||||
const a = new Rng(seedToRng('aaa'))
|
||||
const b = new Rng(seedToRng('bbb'))
|
||||
const seq = 20
|
||||
let diff = false
|
||||
for (let i = 0; i < seq; i++) {
|
||||
if (a.next() !== b.next()) diff = true
|
||||
it('不同种子产生不同序列', () => {
|
||||
const a = new Rng(seedToRng('seed-a'))
|
||||
const b = new Rng(seedToRng('seed-b'))
|
||||
let diff = 0
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (a.next() !== b.next()) diff++
|
||||
}
|
||||
expect(diff).toBe(true)
|
||||
expect(diff).toBeGreaterThan(90)
|
||||
})
|
||||
|
||||
it('produces values in [0, 1)', () => {
|
||||
const rng = new Rng(seedToRng('x'))
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
it('输出始终落在 [0,1)', () => {
|
||||
const rng = new Rng(seedToRng('domain'))
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
const v = rng.next()
|
||||
expect(v).toBeGreaterThanOrEqual(0)
|
||||
expect(v).toBeLessThan(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('int works within bounds', () => {
|
||||
const rng = new Rng(seedToRng('y'))
|
||||
for (let i = 0; i < 500; i++) {
|
||||
const v = rng.int(0, 10)
|
||||
expect(v).toBeGreaterThanOrEqual(0)
|
||||
expect(v).toBeLessThanOrEqual(10)
|
||||
expect(Number.isInteger(v)).toBe(true)
|
||||
it('分布均值接近 0.5(20000 样本)', () => {
|
||||
const rng = new Rng(seedToRng('mean'))
|
||||
let sum = 0
|
||||
const n = 20000
|
||||
for (let i = 0; i < n; i++) sum += rng.next()
|
||||
const mean = sum / n
|
||||
expect(Math.abs(mean - 0.5)).toBeLessThan(0.02)
|
||||
})
|
||||
|
||||
it('int 端点全部可取(长抽样覆盖 min 与 max)', () => {
|
||||
const rng = new Rng(seedToRng('ends'))
|
||||
let minSeen: number | null = null
|
||||
let maxSeen: number | null = null
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const v = rng.int(1, 6)
|
||||
expect(v).toBeGreaterThanOrEqual(1)
|
||||
expect(v).toBeLessThanOrEqual(6)
|
||||
if (v === 1) minSeen = v
|
||||
if (v === 6) maxSeen = v
|
||||
}
|
||||
expect(minSeen).toBe(1)
|
||||
expect(maxSeen).toBe(6)
|
||||
})
|
||||
|
||||
it('pick 返回数组内元素', () => {
|
||||
const rng = new Rng(seedToRng('pick'))
|
||||
const pool = ['a', 'b', 'c']
|
||||
for (let i = 0; i < 200; i++) {
|
||||
expect(pool).toContain(rng.pick(pool))
|
||||
}
|
||||
})
|
||||
|
||||
it('state roundtrip continues identically', () => {
|
||||
const a = new Rng(seedToRng('z'))
|
||||
const b = new Rng(seedToRng('z'))
|
||||
for (let i = 0; i < 10; i++) {
|
||||
a.next()
|
||||
b.next()
|
||||
it('pick 空数组抛错(进行防御性约束)', () => {
|
||||
const rng = new Rng(seedToRng('empty'))
|
||||
expect(() => rng.pick([])).toThrow()
|
||||
})
|
||||
|
||||
it('chance 边界行为:p=0 恒否、p=1 恒是', () => {
|
||||
const rng = new Rng(seedToRng('chance'))
|
||||
for (let i = 0; i < 50; i++) {
|
||||
expect(rng.chance(0)).toBe(false)
|
||||
expect(rng.chance(1)).toBe(true)
|
||||
}
|
||||
const snap = a.getState()
|
||||
const c = new Rng(snap)
|
||||
expect(c.next()).toBe(b.next())
|
||||
expect(c.next()).toBe(b.next())
|
||||
})
|
||||
|
||||
it('shuffle 保持乱序且不丢元素', () => {
|
||||
const rng = new Rng(seedToRng('shuffle'))
|
||||
const src = Array.from({ length: 30 }, (_, i) => i)
|
||||
const shuffled = rng.shuffle(src)
|
||||
expect(shuffled.length).toBe(30)
|
||||
expect([...shuffled].sort((a, b) => a - b)).toEqual(src)
|
||||
// 30 个元素单次打乱完全与原序相同概率几乎为 0
|
||||
expect(shuffled.some((v, i) => v !== src[i])).toBe(true)
|
||||
})
|
||||
|
||||
it('between 落在区间内', () => {
|
||||
const rng = new Rng(seedToRng('between'))
|
||||
for (let i = 0; i < 500; i++) {
|
||||
const v = rng.between(2.5, 7.5)
|
||||
expect(v).toBeGreaterThanOrEqual(2.5)
|
||||
expect(v).toBeLessThanOrEqual(7.5)
|
||||
}
|
||||
})
|
||||
|
||||
it('getState 返回副本:修改不影响原', () => {
|
||||
const rng = new Rng(seedToRng('copy'))
|
||||
rng.next()
|
||||
const snap = rng.getState()
|
||||
snap.a = 999999
|
||||
const c = new Rng(rng.getState())
|
||||
expect(c.next()).toBe(rng.next())
|
||||
})
|
||||
|
||||
it('全零种子被安全修复不会退化为常数', () => {
|
||||
const state = { a: 0, b: 0, c: 0, d: 0 }
|
||||
const rng = new Rng(state)
|
||||
const v1 = rng.next()
|
||||
const v2 = rng.next()
|
||||
expect(v1).not.toBe(v2)
|
||||
})
|
||||
|
||||
it('空字符串种子可稳定初始化', () => {
|
||||
const a = new Rng(seedToRng(''))
|
||||
const b = new Rng(seedToRng(''))
|
||||
expect(a.next()).toBe(b.next())
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user