Files
thzxx ff6df8054c refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)
结构(旧 game/core+engine → 新 engine/ 域):
- engine/kernel/  时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core)
- engine/narrative/  legacy/报告/列传/谱系/年轴(原 core 叙事族)
- engine/runtime/   World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统)
- engine/sim/     Market(未来 WorldSim 同行)
- 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层)

验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化)
typecheck 0 error
2026-08-23 13:23:25 +08:00

119 lines
3.5 KiB
TypeScript
Raw Permalink 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/engine/kernel/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())
})
})