v0.1.28: 稳石——第三轮全量审计歼灭(2008 全绿 + 指纹补盲)
【P0 批(7 项,实证驱动)】
- P0-1 世界词库池无回滚(真实漂移实证)→ WorldGenPools 来源追踪 + removePool 精确回滚
+ MOD uninstall 自动摘除(池/灾因/配方/数值四类全局注册——P2-3 一并根治)
- P0-2 新贵 def 不落档(僵尸实证:power 恒 120 零演化)→ 生成即写 worldGen.npcs
+ normalize 占位 def 双保险 + 新贵 id 掺 seed 指纹(P1-10 防跨档碰撞)
- P0-3 校验死代码化 → installModText 装前 modPreflight(冲突拒+警告 toast)
- P0-4 MOD 读档静默消失 → state.pluginsMissing 收集提示(与 MOD_GUIDE 承诺对齐)
- P0-5 救济按钮乌龙(哈希序首个+直改 state)→ World.reliefCalamity(景气最低+双检)
- P0-6 天下快讯永久冻结(memo 依赖恒 120 length)→ 去 memo 每次 reverse
- P0-7 MOD 导出单行道 → modParse 识别 {app:'cotymod',bundle} 逐包递归(roundtrip 闭环)
【P1 批】
- P1-8 指纹补盲(worldGen/sagaAnnals/distress/declineYears 入 hash——0.1.27 成果真实锁定)+ 双基线重算
- P1-9 重放同步 worldGen.npcs(自捕获);P1-11 worldNum 字段接线(install/uninstall 复位)
- P1-13 落盘兜底(beforeunload/visibilitychange 突发 save,堵 12-tick 节流窗口)
- P1-14 onGameOver 停 timer + IPC mods 路径白名单(扩展名正则防穿越)
- P1-15 DataPack 摘除 npcs(覆写死路删除,与五口宣言对齐);P1-4 techniques 双源统一(techniquePrice 读包)
【P2 扫尾】tauntCD 清理/sagaAnnals 上限 40/worldGenMods 落空写入/validateMod 深度校验随 P0-3 提交
【测试】69 套件 2008 全绿(audit-0.1.28 八例:池回滚/僵尸/preflight/bundle/freeze/指纹敏感/worldNum/救济);
金钟罩 0.1.28 基线重算(指纹补盲+受控时序变更);build 通过
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
import { WorldGenPools, generateWorld } from '../src/renderer/game/engine/sim/worldgen'
|
||||
import { engineFromSnapshot } from '../src/renderer/game/engine/GameEngine'
|
||||
import { npcById } from '../src/renderer/game/data/npcs'
|
||||
import { modParse } from '../src/renderer/game/engine/runtime/modSchema'
|
||||
import { modToPlugin, modPreflight } from '../src/renderer/game/engine/runtime/modManager'
|
||||
import { stateFingerprint } from './fingerprint.helper'
|
||||
import { worldNum } from '../src/renderer/game/engine/sim/worldsim-data'
|
||||
|
||||
function mk(seed: string): World {
|
||||
return World.create({ seed, surname: '钟', familyName: '钟家', motto: 'm', difficulty: 'normal' })
|
||||
}
|
||||
|
||||
describe('0.1.28 稳石·审计歼灭', () => {
|
||||
it('P0-1 词库池回滚:注入→回滚后同 seed 生成复原', () => {
|
||||
WorldGenPools.reset()
|
||||
const before = generateWorld('replay-1').npcs.map((n) => n.id).join(',')
|
||||
WorldGenPools.add({ fams: ['蟋'], regions: ['蟋谷'] }, 'unit-test')
|
||||
const injected = generateWorld('replay-1').npcs.map((n) => n.id).join(',')
|
||||
expect(injected).not.toBe(before)
|
||||
WorldGenPools.removePool('unit-test')
|
||||
const after = generateWorld('replay-1').npcs.map((n) => n.id).join(',')
|
||||
expect(after).toBe(before)
|
||||
})
|
||||
|
||||
it('P0-2 新贵落档:生成即入 worldGen.npcs,读档非僵尸', () => {
|
||||
const w = mk('zombie-a')
|
||||
const ws = w.state.worldSim as { npcDyn: Record<string, never> } | undefined
|
||||
void ws
|
||||
// 直接驱动补位(构造弱世+短 run)
|
||||
const target = Object.keys(w.state.npcFamilies)[0]!
|
||||
w.state.npcFamilies[target].power = 40
|
||||
for (let i = 0; i < 120 && Object.keys(w.state.npcFamilies).length >= 4; i++) w.advanceMonth()
|
||||
// 读档往返
|
||||
const snap = JSON.parse(JSON.stringify(w.state)) as never
|
||||
const e2 = engineFromSnapshot(snap)
|
||||
for (const id of Object.keys(e2.world.state.npcFamilies)) {
|
||||
expect(npcById(id)).toBeTruthy() // 无一缺 def
|
||||
}
|
||||
})
|
||||
|
||||
it('P0-3 preflight 生产路径:事件撞核心拒装', () => {
|
||||
const w = mk('pf-1')
|
||||
const pr = modParse(JSON.stringify({
|
||||
id: 'coll-x', name: 'c', version: '1', data: {
|
||||
events: [{ id: 'ev-legacypass', name: 'x', category: 'daily', weight: 1, text: 't', options: [{ label: 'a', eff: {} }] }]
|
||||
}
|
||||
}))
|
||||
if (!pr.ok) return expect(pr.ok).toBe(true)
|
||||
const r = modPreflight(w, pr.pack)
|
||||
expect(r.ok).toBe(false)
|
||||
})
|
||||
|
||||
it('P0-7 bundle roundtrip:导出→解析→安装', () => {
|
||||
const w = mk('rt-1')
|
||||
const pr = modParse(JSON.stringify({ id: 'round-a', name: 'rt', version: '1.0.0', data: { worldgen: { fams: ['鹿'] } } }))
|
||||
if (!pr.ok) return expect(pr.ok).toBe(true)
|
||||
w.installPlugin(modToPlugin(pr.pack))
|
||||
const bundleText = w.exportModBundle()!
|
||||
expect(bundleText).toContain('round-a')
|
||||
const re = modParse(bundleText) // bundle 分支:应逐包解析成功并对首包合法
|
||||
expect(re.ok).toBe(true)
|
||||
})
|
||||
|
||||
it('P0-6 快讯冻结:newsFeed 维持 120 时面板数据源仍新鲜(改底层切表验证)', () => {
|
||||
// 结构性断言:WorldPanel 已去 memo——用指纹敏感的 feed 长度变化即可(news 在 snapshot 仍在)
|
||||
const w = mk('freeze-1')
|
||||
for (let i = 0; i < 260; i++) w.advanceMonth()
|
||||
const feed = (w.state.worldSim as { newsFeed: unknown[] }).newsFeed
|
||||
expect(feed.length).toBeGreaterThanOrEqual(3)
|
||||
expect(feed[feed.length - 1]).toBeTruthy()
|
||||
})
|
||||
|
||||
it('P1-8 指纹敏感:sagaAnnals 变更会红', () => {
|
||||
const w = mk('fp-8')
|
||||
for (let i = 0; i < 620; i++) w.advanceMonth()
|
||||
const f1 = stateFingerprint(w.state)
|
||||
const ws = w.state.worldSim as { sagaAnnals: { year: number }[] }
|
||||
ws.sagaAnnals.push({ year: 1 }, { year: 2 })
|
||||
const f2 = stateFingerprint(w.state)
|
||||
expect(f1).not.toBe(f2)
|
||||
})
|
||||
|
||||
it('P1-11 worldNum 接线:MOD 装→覆写生效→卸→复位', () => {
|
||||
const w = mk('wn-1')
|
||||
const pr = modParse(JSON.stringify({ id: 'num-x', name: 'n', version: '1', data: { worldNum: { calamityChance: 0.5 } } }))
|
||||
if (!pr.ok) return expect(pr.ok).toBe(true)
|
||||
w.installPlugin(modToPlugin(pr.pack))
|
||||
expect(worldNum('calamityChance')).toBe(0.5)
|
||||
w.removePlugin('mod-num-x')
|
||||
expect(worldNum('calamityChance')).toBe(0.18)
|
||||
})
|
||||
|
||||
it('P0-5 救济:目标为景气最低家(稳定性)', () => {
|
||||
const w = mk('relief-1')
|
||||
const ok = w.reliefCalamity(50)
|
||||
expect(typeof ok).toBe('boolean')
|
||||
expect(w.state.family.stones).toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
})
|
||||
@@ -82,7 +82,7 @@ describe('审计回归:P0 修复固化', () => {
|
||||
})
|
||||
|
||||
it('防御性修补后金钟罩不变(行为等价确认)', () => {
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('27c79b36')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('9812188b')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('b111633d')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('99fc8b01')
|
||||
})
|
||||
})
|
||||
|
||||
+8
-8
@@ -7,20 +7,20 @@ import { World } from '../src/renderer/game/engine/runtime/World'
|
||||
* 任何改动(重构日程/调平衡/加系统)若改变了确定性序列或结果,此测试立刻报红。
|
||||
* 更新规则:仅当**有意**变更序列逻辑时,三枚 seed 指纹同版更新并注明原因。
|
||||
*/
|
||||
// 0.1.27 活水长流基线(指纹含全世界轴+worldgen+sagaAnnals):
|
||||
// 生灭可达化(门槛放开/era 调制增长/顶峰衰)+ 修为↔市场回环 + 世鉴 50/100 年 + worldNum 聚合口后固化。
|
||||
// 0.1.28 稳石基线(指纹含全世界轴+worldgen+sagaAnnals+declineYears 汇总+distress):
|
||||
// 第三轮全量审计歼灭——词库池回滚/新贵落档/repression 双保险/指纹补盲重算后固化。
|
||||
const GOLDEN: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: '27c79b36', 1200: '94a91084', 2160: 'aa444dbe' },
|
||||
'bell-seed-2': { 560: 'b1ed2bd7', 1200: '03c43016', 2160: '6267adfe' },
|
||||
'bell-seed-3': { 560: '9812188b', 1200: 'f6b9a3e1', 2160: '429fdbe4' }
|
||||
'bell-seed-1': { 560: 'b111633d', 1200: '7aaad380', 2160: '312ab6e6' },
|
||||
'bell-seed-2': { 560: '0cad22aa', 1200: '54d97978', 2160: '7538f86e' },
|
||||
'bell-seed-3': { 560: '99fc8b01', 1200: 'd125ca18', 2160: '079c3eff' }
|
||||
}
|
||||
|
||||
/** 第二金钟罩:自动 resolve 长跑("现实"世界——每 tick 处理待决事件;
|
||||
* 锁事件闸/事件流全程,防"冻结世界"指纹漏锁)。 */
|
||||
const GOLDEN_RESOLVED: Record<string, Record<number, string>> = {
|
||||
'bell-seed-1': { 560: 'b7a1fcac', 1200: '61a9dffd', 2160: '7e51b410' },
|
||||
'bell-seed-2': { 560: '036df3ac', 1200: 'd8df0c0c', 2160: 'bfd564c3' },
|
||||
'bell-seed-3': { 560: 'ffedbbc6', 1200: '69201a4e', 2160: '5ff6deb3' }
|
||||
'bell-seed-1': { 560: 'cf3a2ea3', 1200: '2045e95b', 2160: '1ddcb672' },
|
||||
'bell-seed-2': { 560: 'faa4e88c', 1200: '33650d8d', 2160: '2a22e4b7' },
|
||||
'bell-seed-3': { 560: 'fe0657fa', 1200: 'f3678400', 2160: 'bd813ee1' }
|
||||
}
|
||||
|
||||
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.27')
|
||||
expect(info.version).toContain('0.1.28')
|
||||
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('27c79b36')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('9812188b')
|
||||
expect(stateFingerprint(longRun('bell-seed-1').state)).toBe('b111633d')
|
||||
expect(stateFingerprint(longRun('bell-seed-3').state)).toBe('99fc8b01')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,6 +33,15 @@ export function stateFingerprint(s: GameState): string {
|
||||
parts.push('ws:none')
|
||||
}
|
||||
parts.push(`pe:${s.pendingEvent ?? '-'}:${s.eventQueue ? s.eventQueue.length : 0}`)
|
||||
// P1-8 指纹补盲(0.1.28):世界轴线全量——sagaAnnals/distress/declineYears/worldGen 摘 req
|
||||
const wg = s.worldGen as { npcCount?: number; eras?: string; npcs?: unknown[] } | undefined
|
||||
parts.push(`wg:${wg?.npcCount ?? 0}:${wg?.eras ?? '-'}:${wg?.npcs?.length ?? 0}`)
|
||||
parts.push(`saga:${(ws?.sagaAnnals ?? []).length}:${((ws?.sagaAnnals ?? []) as { year: number }[]).slice(-1).map((a) => a.year).join(',')}`)
|
||||
parts.push(`distress:${ws?.distress?.id ?? '-'}`)
|
||||
const declSum = Object.values(ws?.npcDyn ?? {})
|
||||
.map((d) => (d as { declineYears?: number })?.declineYears ?? 0)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
parts.push(`decl:${declSum}`)
|
||||
return hash32(parts.join('\u0001'))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user