v0.1.20: 万世无谬(第二轮全量审计歼灭,1011 测试+双金钟罩)
【事件闸(机制重锤)】 - fire(w,id,priority):单闸+优先级——高优(raid/渡劫/大比)顶替被占者、被顶替入 eventQueue 下月兑现不丢失;普通被占返回 false 调用方冷却(raid 被吞也写 CD) - pending 唯一写入入口:渡劫/破境丹改走 fire(防覆盖丢弃);名宿传薪年锚 flag(防同年重复 12 次) - applyEventChoice 前置校验 pendingEvent===id(陈旧 Modal 二次结算防线) 【世界模型四修(真·活世界)】 - era 延续档:flow 权重和<1(留白=续任)——盛世能撑 30 年而非固定 25 - 潮汐供给符号修正:灵涨万物丰(原实现反了)+tide 双界 clamp - driftMarket 弱锚定:rebalance 0.1→0.025——供需/era/灾年真实撬动价格(原五池被锚死±5%) - 断供/池底单常量 poolFloorPct=0.35(消灭禁买暗雷带) 【知识防线(审计虫洞修复)】 - 指纹扩展:era/stance/prosperity/relationsWithOthers/calamityLeft/pendingEvent 全入 hash - 第二金钟罩 GOLDEN_RESOLVED:自动 resolve 长跑锁“现实”世界(原指纹锁的只是“事件流冻结”世界) 【性能与存储】 - 存档节流(疾12/常6/缓4,手动即时)+ saveChronicle O(N)→O(1) 单查增量写(2160 月档告别每 tick 数千 OPFS) - battles≤220/chronicle≤520 截断(stringify 线性膨胀止血);startNewGame/openState 停 timer 摘旧 bus 【杂项】actDirect 对齐 ACT_CATALOG 全 24 项;about 三版本统一 0.1.20;kernel.bus 死总线删;seed 改 RngHub.rollSeed;normalize 补 eraStartYear/overflux/distress;WorldPanel 买卖条件反转修复;音效与视效同闸 【测试】1011 全绿(42 套件):audit-0.1.20 9 例(事件闸/传薪/二次结算/era延续/潮汐符号/断供一致/弱锚定/指纹敏感)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { fire, applyEventChoice } from '../src/renderer/game/engine/runtime/Systems/events'
|
||||
import { ERA_CONF, ERA_DURA, WORLDSIM, WorldSimState } from '../src/renderer/game/engine/sim/worldsim-data'
|
||||
import { stateFingerprint } from './fingerprint.helper'
|
||||
|
||||
describe('0.1.20 万世无谬', () => {
|
||||
it('fire 高优可顶替普通 pending,被顶替者入队不丢失', () => {
|
||||
const w = World.create({ seed: 'a1', surname: '岳', familyName: '岳家', motto: 'm', difficulty: 'normal' })
|
||||
w.advanceMonth()
|
||||
w.state.pendingEvent = undefined // 清月度事件闸(测试隔离)
|
||||
fire(w, 'ev-legacypass') // 普通占位
|
||||
expect(w.state.pendingEvent).toBe('ev-legacypass')
|
||||
const ok = fire(w, 'ev-raid-n-nulei', 1) // 高优顶替
|
||||
expect(ok).toBe(true)
|
||||
expect(w.state.pendingEvent).toBe('ev-raid-n-nulei')
|
||||
expect(w.state.eventQueue).toContain('ev-legacypass') // 未丢失
|
||||
})
|
||||
|
||||
it('fire 普通被占则 false(调用方可冷却)', () => {
|
||||
const w = World.create({ seed: 'a2', surname: '满', familyName: '满家', motto: 'm', difficulty: 'normal' })
|
||||
w.advanceMonth()
|
||||
w.state.pendingEvent = undefined
|
||||
fire(w, 'ev-legacypass')
|
||||
expect(fire(w, 'ev-legacypass')).toBe(false)
|
||||
})
|
||||
|
||||
it('名宿传薪同一年仅一次(年锚 flag)', () => {
|
||||
const w = World.create({ seed: 'a3', surname: '贺', familyName: '贺家', motto: 'm', difficulty: 'normal' })
|
||||
w.state.year = 4 // year%8===4
|
||||
w.state.completedEvents = []
|
||||
let n = 0
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (w.state.year % 8 === 4 && !w.state.family.flag[`legacyDone-${w.state.year}`]) {
|
||||
if (fire(w, 'ev-legacypass')) {
|
||||
w.state.family.flag[`legacyDone-${w.state.year}`] = true
|
||||
n++
|
||||
}
|
||||
w.applyEventChoice(w.state.pendingEvent ?? 'ev-legacypass', 0)
|
||||
}
|
||||
w.state.month++
|
||||
if (w.state.month > 12) { w.state.month = 1; w.state.year++ }
|
||||
}
|
||||
expect(n).toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('二次结算防线:陈旧 Modal 选择被忽略(pending 已清/易主)', () => {
|
||||
const w = World.create({ seed: 'a4', surname: '冼', familyName: '冼家', motto: 'm', difficulty: 'normal' })
|
||||
w.advanceMonth()
|
||||
fire(w, 'ev-legacypass')
|
||||
const before = w.state.pendingEvent
|
||||
// 恶意重复选择:pending 已易主(自动压制)时 apply 不得落盘
|
||||
w.state.pendingEvent = undefined
|
||||
let threw = false
|
||||
try {
|
||||
applyEventChoice(w, before!, 0)
|
||||
} catch {
|
||||
threw = true
|
||||
}
|
||||
expect(threw).toBe(false) // 不崩
|
||||
expect(w.state.pendingEvent).toBeUndefined() // 未二次应用
|
||||
})
|
||||
|
||||
it('era 有延续档:flow 权重和 < 1(不再每届必换)', () => {
|
||||
for (const k of Object.keys(ERA_CONF) as Array<keyof typeof ERA_CONF>) {
|
||||
const sum = ERA_CONF[k].flow.reduce((a, [, wgt]) => a + wgt, 0)
|
||||
expect(sum).toBeLessThan(1)
|
||||
}
|
||||
expect(ERA_DURA.pingshi[0]).toBeLessThan(ERA_DURA.pingshi[1])
|
||||
})
|
||||
|
||||
it('潮汐供给正号:tide>1 → 供给乘 >1(灵涨必丰)', () => {
|
||||
const multAt = (tide: number): number => Math.max(0.75, Math.min(1.35, 1 + (tide - 1) * WORLDSIM.tideSupplySpan))
|
||||
expect(multAt(1.3)).toBeGreaterThan(1)
|
||||
expect(multAt(0.7)).toBeLessThan(1)
|
||||
})
|
||||
|
||||
it('断供/池底单常量一致:拒单阈值 == poolFloorPct', () => {
|
||||
expect(WORLDSIM.tradeFloorPct).toBe(WORLDSIM.poolFloorPct)
|
||||
})
|
||||
|
||||
it('rebalance 弱锚定:供需可撬动价格(1200 月池有超过 ±25% 振幅的时机)', () => {
|
||||
const w = World.create({ seed: 'a9', surname: '郄', familyName: '郄家', motto: 'm', difficulty: 'normal' })
|
||||
let min = Infinity
|
||||
let max = -Infinity
|
||||
for (let i = 0; i < 1200; i++) {
|
||||
w.advanceMonth()
|
||||
const pool = (w.state.worldSim as WorldSimState | undefined)?.marketPool?.['lingcao'] ?? 600
|
||||
min = Math.min(min, pool / 600)
|
||||
max = Math.max(max, pool / 600)
|
||||
}
|
||||
expect(max - min).toBeGreaterThan(0.5) // 弱锚定下真实起伏
|
||||
})
|
||||
|
||||
it('指纹含世界轴(era/stance 变更会红)', () => {
|
||||
const w = World.create({ seed: 'a10', surname: '苻', familyName: '苻家', motto: 'm', difficulty: 'normal' })
|
||||
for (let i = 0; i < 240; i++) w.advanceMonth()
|
||||
const f1 = stateFingerprint(w.state)
|
||||
const ws = w.state.worldSim as WorldSimState
|
||||
ws.era = ws.era === 'mofa' ? 'shengshi' : 'mofa' // 确保改成异值
|
||||
ws.npcDyn['n-xuanying']!.stance = ws.npcDyn['n-xuanying']!.stance === 'expand' ? 'endure' : 'expand'
|
||||
const f2 = stateFingerprint(w.state)
|
||||
expect(f1).not.toBe(f2)
|
||||
})
|
||||
})
|
||||
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
|
||||
})
|
||||
|
||||
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('aeed3b65')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('39892725')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('3b784f49')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('0bf7ec62')
|
||||
})
|
||||
})
|
||||
|
||||
+13
-5
@@ -7,12 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
||||
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
||||
*/
|
||||
// 0.1.19 八方风雨基线:NPC 战略姿态(stance 四态+年首重估+全行为调制)
|
||||
// + 超卖潮(谷贱伤农产能回调)+ 盟约连坐/求援 + 调停/资助干预后固化。
|
||||
// 0.1.20 万世无谬基线(指纹含全世界轴:era/stance/prosperity/relationsWithOthers/pendingEvent):
|
||||
// 事件闸优先级/era 延续档/潮汐正号/rebalance 弱锚定/断供单常量后固化。
|
||||
const GOLDEN: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: 'aeed3b65', 1200: '0cbd4079', 2160: '7d1efba9' },
|
||||
'bell-seed-2': { 560: '6bb6ff89', 1200: 'a73b5655', 2160: 'f3537f46' },
|
||||
'bell-seed-3': { 560: '39892725', 1200: '4b69c491', 2160: '950e8410' }
|
||||
'bell-seed-1': { 560: '3b784f49', 1200: 'b62fada2', 2160: '999782cc' },
|
||||
'bell-seed-2': { 560: '0f73bffb', 1200: '454eedc9', 2160: '426c967f' },
|
||||
'bell-seed-3': { 560: '0bf7ec62', 1200: '06cf0213', 2160: '51feb3ee' }
|
||||
}
|
||||
|
||||
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
|
||||
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
|
||||
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: 'b28b99f9', 1200: 'ffd53c37', 2160: 'd7eadca0' },
|
||||
'bell-seed-2': { 560: 'c712ad0f', 1200: '2304c1c4', 2160: '76519353' },
|
||||
'bell-seed-3': { 560: '6878460d', 1200: '6633cdde', 2160: 'f285188f' }
|
||||
}
|
||||
|
||||
const TIERS = [
|
||||
|
||||
@@ -162,7 +162,7 @@ describe('GameFacade 门面', () => {
|
||||
const f = new GameFacade(w, 1)
|
||||
const info = f.about()
|
||||
expect(info.title).toBe('仙途家族志')
|
||||
expect(info.version).toContain('0.1.19')
|
||||
expect(info.version).toContain('0.1.20')
|
||||
expect(info.modules).toBeGreaterThanOrEqual(11)
|
||||
expect(info.systems).toBeGreaterThan(0)
|
||||
expect(info.plugins).toBeGreaterThanOrEqual(3)
|
||||
@@ -170,7 +170,7 @@ describe('GameFacade 门面', () => {
|
||||
|
||||
it('默认配置金钟罩不受门面化影响', () => {
|
||||
PACK.reset()
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('aeed3b65')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('39892725')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('3b784f49')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('0bf7ec62')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,10 +25,14 @@ export function stateFingerprint(s: GameState): string {
|
||||
parts.push(s.yearlyReports.length)
|
||||
const ws = s.worldSim
|
||||
if (ws) {
|
||||
parts.push(`ws:${JSON.stringify(ws.marketPool ?? {})}:${JSON.stringify(ws.secretQi ?? {})}:${ws.tide ?? 0}:${ws.npcSuccessions ?? 0}:${(ws.newsFeed ?? []).length}`)
|
||||
const dyns = Object.entries(ws.npcDyn ?? {})
|
||||
.map(([k, d]) => `${k}:${(d as { stance?: string; prosperity?: number }).stance ?? '-'}:${Math.floor((d as { prosperity?: number }).prosperity ?? 0) * 7}:${JSON.stringify((d as { relationsWithOthers?: Record<string, number> }).relationsWithOthers ?? {})}`)
|
||||
.join(';')
|
||||
parts.push(`ws:${JSON.stringify(ws.marketPool ?? {})}:${JSON.stringify(ws.secretQi ?? {})}:${ws.tide ?? 0}:${ws.npcSuccessions ?? 0}:${(ws.newsFeed ?? []).length}:${ws.era ?? '-'}:${ws.eraStartYear ?? 0}:${ws.calamity ?? '-'}:${ws.calamityLeft ?? 0}:${dyns}`)
|
||||
} else {
|
||||
parts.push('ws:none')
|
||||
}
|
||||
parts.push(`pe:${s.pendingEvent ?? '-'}:${s.eventQueue ? s.eventQueue.length : 0}`)
|
||||
return hash32(parts.join('\u0001'))
|
||||
}
|
||||
|
||||
@@ -67,3 +71,18 @@ export function longRun(seed: string, months = 560): World {
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
/** 自动 resolve 长跑("现实"世界:每 tick 处理待决事件——与直调冻结世界对照) */
|
||||
export function longRunResolved(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()
|
||||
let guard = 0
|
||||
while (w.state.pendingEvent && guard < 4) {
|
||||
w.applyEventChoice(w.state.pendingEvent, 0)
|
||||
guard++
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
@@ -47,21 +47,21 @@ describe('GameEngine 引擎门面', () => {
|
||||
expect(e.status()).toBe('running')
|
||||
})
|
||||
|
||||
it('about 元数据含 plugins(≥3)与版本 0.1.14', () => {
|
||||
it('about 元数据含 plugins(≥3)与版本 0.1.20', () => {
|
||||
const e = new GameEngine({ seed: 'engine-5' })
|
||||
const a = e.about()
|
||||
expect(a.plugins).toBeGreaterThanOrEqual(3)
|
||||
expect(a.version).toContain('0.1.14')
|
||||
expect(a.version).toContain('0.1.20')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Kernel 三合一', () => {
|
||||
it('时钟/随机/总线协同(同 seed 同随机序列)', () => {
|
||||
it('时钟/随机协同(同 seed 同随机序列);bus 已并入 World.out', () => {
|
||||
const k1 = makeKernel('kernel-a')
|
||||
const k2 = makeKernel('kernel-a')
|
||||
expect(k1.rng.next()).toBe(k2.rng.next())
|
||||
expect(k1.clock).toBeTruthy()
|
||||
expect(typeof k1.bus.onLog).toBe('function')
|
||||
expect((k1 as unknown as { bus?: unknown }).bus).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -80,9 +80,9 @@ describe('PluginCore 插件协议', () => {
|
||||
})
|
||||
|
||||
it('默认管线金钟罩不受插件层影响', () => {
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('aeed3b65')
|
||||
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('6bb6ff89')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('39892725')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('3b784f49')
|
||||
expect(stateFingerprint(longRun('bell-seed-2').state)).toBe('0f73bffb')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('0bf7ec62')
|
||||
})
|
||||
|
||||
it('facade 插件查询与 about.plugins', () => {
|
||||
|
||||
Reference in New Issue
Block a user