- WorldSim(engine/sim/WorldSim.ts + worldsim-data.ts): A 资源循环市场(库存池/再平衡/价格弹性——Market 行情联动 sim.mult) B NPC 聚合演化(宗主换代/境界成长/势力=境界+财力+兵员;换代计数) C 秘境灵气(探索消耗/恢复)+ 灵气潮汐(6年周期×修炼倍率) D 灾年签(旱涝蝗疫兽寒,联动市场池) E 天下快讯(节流 24 月滚动 120 条,世界动态 feed) - 相位:新增 worldsim(diplomacy 后 epilogue 前);capabilities 加「天下演序」卡可停用 - 时钟合一:World.clock = Kernel.clock(单一时钟驱动,消除双时钟漂移) - 发现并修复:worldsim 注册被文件搬迁覆盖丢失(修改纪律验证中招——已补回); WorldSim 内 Math.random 根除(改 w.rng) - fingerprint 纳入 worldSim 概要(市场/灵脉/潮汐/换代/快讯数) - 金钟罩三档重固化(0.1.14 正式基线,受控变更) - 验证:35 套件/967 测试全绿;typecheck 0 error
167 lines
5.8 KiB
TypeScript
167 lines
5.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||
import { GameClock, PHASE_ORDER } from '../src/renderer/game/engine/kernel/clock'
|
||
import { buildClock } from '../src/renderer/game/engine/runtime/clocks'
|
||
import { RngHub } from '../src/renderer/game/engine/kernel/rng'
|
||
import { seasonOf, seasonMod, SEASON } from '../src/renderer/game/data/season'
|
||
import { yearAxis, decadeLabel } from '../src/renderer/game/engine/narrative/yearaxis'
|
||
import { stateFingerprint } from './fingerprint.helper'
|
||
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('GameClock 统一时轮', () => {
|
||
it('八个 phase 全被注册且顺序固定(含 worldsim)', () => {
|
||
const clock = buildClock()
|
||
const clock2 = new GameClock()
|
||
clock2.register('production', () => undefined)
|
||
expect(clock.subscriptionCount()).toBeGreaterThanOrEqual(10)
|
||
expect(PHASE_ORDER.length).toBe(8)
|
||
expect(clock2.subscriptionCount()).toBe(1)
|
||
})
|
||
|
||
it('phase 注册后按序执行且消耗确定(同 seed 同指纹)', () => {
|
||
const a = stateFingerprint(longRun('bell-seed-2').state)
|
||
const b = stateFingerprint(longRun('bell-seed-2').state)
|
||
expect(a).toBe(b)
|
||
})
|
||
|
||
it('stepMonthly 返回各 phase 耗时统计(性能预算通道)', () => {
|
||
const w = baseWorld('clk-a')
|
||
const report = w.clock.stepMonthly(w)
|
||
expect(report.length).toBe(8)
|
||
for (const r of report) {
|
||
expect(PHASE_ORDER).toContain(r.phase)
|
||
expect(typeof r.ms).toBe('number')
|
||
}
|
||
})
|
||
|
||
it('年首钩子先于月度执行(族簿于当年首月前发出)', () => {
|
||
const w = baseWorld('clk-b')
|
||
const paperRec: number[] = []
|
||
w.out.push({
|
||
onLog: () => undefined,
|
||
onChronicle: () => undefined,
|
||
onBattle: () => undefined,
|
||
onPendingEvent: () => undefined,
|
||
onGameOver: () => undefined,
|
||
onYearPaper: (r) => paperRec.push(r.year)
|
||
})
|
||
for (let i = 0; i < 14; i++) w.advanceMonth()
|
||
expect(paperRec).toContain(1)
|
||
})
|
||
})
|
||
|
||
describe('RngHub 统一随机收集', () => {
|
||
it('播种为稳定格式且可重现(同 seed 同世界)', () => {
|
||
const seed = RngHub.rollSeed()
|
||
expect(seed.startsWith('seed-')).toBe(true)
|
||
const a = stateFingerprint(longRun(seed).state)
|
||
const b = stateFingerprint(longRun(seed).state)
|
||
expect(a).toBe(b)
|
||
})
|
||
|
||
it('音频随机与引擎随机源完全隔离', () => {
|
||
const w = baseWorld('rng-isolate')
|
||
const before = w.rng.nextCount
|
||
for (let i = 0; i < 100; i++) RngHub.audioNoise01()
|
||
expect(w.rng.nextCount).toBe(before)
|
||
})
|
||
|
||
it('引擎随机计数器随推进增长(audit 可用)', () => {
|
||
const w = baseWorld('rng-audit')
|
||
const c0 = w.rng.nextCount
|
||
for (let i = 0; i < 36; i++) w.advanceMonth()
|
||
expect(w.rng.nextCount).toBeGreaterThan(c0 + 50)
|
||
})
|
||
})
|
||
|
||
describe('season 时节', () => {
|
||
it('四季映射正确', () => {
|
||
expect(seasonOf(1)).toBe('spring')
|
||
expect(seasonOf(4)).toBe('summer')
|
||
expect(seasonOf(7)).toBe('autumn')
|
||
expect(seasonOf(10)).toBe('winter')
|
||
expect(seasonOf(12)).toBe('winter')
|
||
})
|
||
|
||
it('四季加成表方向正确', () => {
|
||
expect(seasonMod(2, 'field')).toBeGreaterThan(0)
|
||
expect(seasonMod(5, 'cult')).toBeGreaterThan(0)
|
||
expect(seasonMod(8, 'market')).toBeGreaterThan(0)
|
||
expect(seasonMod(11, 'meditation')).toBeGreaterThan(0)
|
||
expect(seasonMod(5, 'field')).toBe(0)
|
||
})
|
||
|
||
it('春耕与冬闭实惠落地', () => {
|
||
const w = baseWorld('season-a')
|
||
// 春季灵田月产出高于 12 月
|
||
w.state.family.buildings = { lingtian: 1 }
|
||
w.state.month = 2
|
||
w.advanceMonth()
|
||
const springGain = (w.state.family.inventory['lingcao'] ?? 0) - 60
|
||
const w2 = baseWorld('season-b')
|
||
w2.state.family.buildings = { lingtian: 1 }
|
||
w2.state.month = 11
|
||
w2.advanceMonth()
|
||
const winterGain = (w2.state.family.inventory['lingcao'] ?? 0) - 60
|
||
expect(springGain).toBeGreaterThan(winterGain)
|
||
})
|
||
|
||
it('SEASON 表数据合法', () => {
|
||
for (const s of Object.values(SEASON)) {
|
||
expect(s.field).toBeGreaterThanOrEqual(0)
|
||
expect(s.cult).toBeGreaterThanOrEqual(0)
|
||
expect(s.market).toBeGreaterThanOrEqual(0)
|
||
expect(s.meditation).toBeGreaterThanOrEqual(0)
|
||
}
|
||
})
|
||
})
|
||
|
||
describe('yearAxis 横轴年表', () => {
|
||
it('十年一格聚合生/殇/大事', () => {
|
||
const w = baseWorld('axis-a')
|
||
w.chronicle('birth', '林小生', 'x3')
|
||
w.chronicle('breakthrough', '林公突破至筑基。', 'x3', true)
|
||
w.chronicle('death', '林公辞世。', 'x3', true)
|
||
for (let i = 0; i < 30; i++) w.advanceMonth()
|
||
const cells = yearAxis(w.state)
|
||
expect(cells.length).toBeGreaterThan(0)
|
||
const c1 = cells[0]
|
||
expect(c1.births).toBeGreaterThanOrEqual(1)
|
||
expect(c1.deaths).toBeGreaterThanOrEqual(1)
|
||
expect(c1.events.some((e) => e.kind === '突破')).toBe(true)
|
||
})
|
||
|
||
it('decadeLabel 边界', () => {
|
||
expect(decadeLabel(1, 10)).toBe('1-10年')
|
||
expect(decadeLabel(11, 11)).toBe('11年')
|
||
})
|
||
|
||
it('跨十年事件分布正确', () => {
|
||
const w = baseWorld('axis-b')
|
||
w.state.year = 100
|
||
for (let y = 1; y <= 100; y += 10) {
|
||
w.state.chronicle.push({ id: 'ax' + y, year: y, month: 1, category: 'breakthrough', text: `第${y}年突破`, important: false })
|
||
}
|
||
const cells = yearAxis(w.state)
|
||
expect(cells.length).toBe(10)
|
||
// 头十年第一格
|
||
expect(cells[0].events.length).toBe(1)
|
||
})
|
||
})
|
||
|
||
function longRun(seed: string, months = 560): World {
|
||
const w = World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||
for (let i = 0; i < months; i++) {
|
||
if (w.state.gameOver) break
|
||
w.advanceMonth()
|
||
}
|
||
return w
|
||
}
|