【事件闸(机制重锤)】 - 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延续/潮汐符号/断供一致/弱锚定/指纹敏感)
89 lines
3.9 KiB
TypeScript
89 lines
3.9 KiB
TypeScript
import { World } from '../src/renderer/game/engine/runtime/World'
|
|
import { GameState } from '../src/renderer/game/types/domain'
|
|
|
|
export function stateFingerprint(s: GameState): string {
|
|
const parts = [
|
|
s.year, s.month, s.seq, s.totalTicks,
|
|
s.family.stones,
|
|
s.family.reputation,
|
|
s.family.generation,
|
|
JSON.stringify(sortedInv(s.family.inventory)),
|
|
JSON.stringify(sortedBld(s.family.buildings)),
|
|
s.family.headId,
|
|
s.family.techniques.join(',')
|
|
]
|
|
const members = Object.values(s.members)
|
|
.map((c) => `${c.id}:${c.alive}:${c.realm.major}/${c.realm.minor}:${Math.floor(c.realmProgress * 10) / 10}:${Math.floor(c.health * 10) / 10}:${c.state}:${c.bornYear}:${c.techniqueRank ?? 0}:${Math.floor((c.techniqueProgress ?? 0) * 10) / 10}:${c.post ?? ''}:${c.aspiration ?? ''}:${c.spouseId ?? ''}:${c.children.join('-')}`)
|
|
.sort()
|
|
parts.push(members.join('|'))
|
|
parts.push(JSON.stringify(sortedNpc(s.npcFamilies)))
|
|
parts.push(s.missions.length + ':' + s.missions.map((m) => `${m.defId}:${m.stage}:${m.stageMonth}:${m.done}:${m.memberIds.join('+')}`).join(';'))
|
|
parts.push(s.chronicle.length + ':' + s.chronicle.slice(-5).map((e) => `${e.year}${e.month}${e.category}`).join(','))
|
|
parts.push(s.battles.length)
|
|
parts.push(Object.keys(s.completedEvents).length + ':' + s.completedEvents.slice(-3).join(','))
|
|
parts.push(JSON.stringify(sortedFlags(s.family.flag)))
|
|
parts.push(s.yearlyReports.length)
|
|
const ws = s.worldSim
|
|
if (ws) {
|
|
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'))
|
|
}
|
|
|
|
function sortedInv(inv: Record<string, number>): Array<[string, number]> {
|
|
return Object.entries(inv).sort((a, b) => (a[0] < b[0] ? -1 : 1))
|
|
}
|
|
function sortedBld(b: Record<string, number>): Array<[string, number]> {
|
|
return Object.entries(b).sort((a, b) => (a[0] < b[0] ? -1 : 1))
|
|
}
|
|
function sortedFlags(f: Record<string, number | boolean | string>): Array<[string, string]> {
|
|
return Object.entries(f)
|
|
.map(([k, v]) => [k, String(v)] as [string, string])
|
|
.sort((a, b) => (a[0] < b[0] ? -1 : 1))
|
|
}
|
|
function sortedNpc(n: GameState['npcFamilies']): Array<[string, string]> {
|
|
return Object.entries(n)
|
|
.map(([k, v]) => [k, `${v.relation}:${v.power}:${v.allied}`] as [string, string])
|
|
.sort((a, b) => (a[0] < b[0] ? -1 : 1))
|
|
}
|
|
|
|
function hash32(str: string): string {
|
|
let h = 2166136261
|
|
for (let i = 0; i < str.length; i++) {
|
|
h ^= str.charCodeAt(i)
|
|
h = Math.imul(h, 16777619)
|
|
h = (h << 13) | (h >>> 19)
|
|
}
|
|
return (h >>> 0).toString(16).padStart(8, '0')
|
|
}
|
|
|
|
export 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
|
|
}
|
|
|
|
/** 自动 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
|
|
}
|