Files
thzxx 5dd5d6b558 feat(0.1.14-P3): 世界自进化 WorldSim(五大闭环)+ 引擎时钟合一
- 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
2026-08-23 13:31:34 +08:00

167 lines
5.8 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 { 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
}