Files
ChronicleOfTheImmortalClan/tests/rng.test.ts
T
thzxx 09cbad6449 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 双签名)
2026-08-23 08:47:21 +08:00

119 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import { Rng, seedToRng } from '../src/renderer/game/core/rng'
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('不同种子产生不同序列', () => {
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).toBeGreaterThan(90)
})
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('分布均值接近 0.520000 样本)', () => {
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('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)
}
})
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())
})
})