结构(旧 game/core+engine → 新 engine/ 域): - engine/kernel/ 时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core) - engine/narrative/ legacy/报告/列传/谱系/年轴(原 core 叙事族) - engine/runtime/ World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统) - engine/sim/ Market(未来 WorldSim 同行) - 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层) 验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化) typecheck 0 error
146 lines
5.0 KiB
TypeScript
146 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { SaveSlot, SaveDbDriver } from '../src/renderer/game/storage/slots'
|
|
import { World } from '../src/renderer/game/engine/runtime/World'
|
|
|
|
interface Row {
|
|
[k: string]: unknown
|
|
}
|
|
|
|
class MemoryDriver implements SaveDbDriver {
|
|
tables = new Map<string, Map<string, Row>>()
|
|
|
|
async open(_name: string): Promise<void> {}
|
|
|
|
async close(): Promise<void> {}
|
|
|
|
async run(sql: string, params: unknown[] = []): Promise<unknown> {
|
|
const ins = /insert into (\w+)\s*\(([^)]+)\)\s*values\s*\(([^)]+)\)/i.exec(sql)
|
|
if (ins) {
|
|
const cols = ins[2].split(',').map((c) => c.trim())
|
|
const row: Row = {}
|
|
cols.forEach((c, i) => (row[c] = params[i]))
|
|
const t = this.tables.get(ins[1]) ?? new Map<string, Row>()
|
|
t.set(String(row['id'] ?? ins[1] + t.size), row)
|
|
this.tables.set(ins[1], t)
|
|
}
|
|
const upd = /update\s+(\w+)\s+set/i.exec(sql)
|
|
if (upd) {
|
|
const setCol = /set\s+(\w+)\s*=\s*\?/i.exec(sql)![1]
|
|
const setVal = params[0]
|
|
const lit = /\bwhere\s+(\w+)\s*=\s*'([^']+)'/i.exec(sql)
|
|
const ph = /where\s+(\w+)\s*=\s*\?/i.exec(sql)
|
|
const col = (lit?.[1] ?? ph?.[1]) as string
|
|
const val = (lit?.[2] as string | undefined) ?? String(params[1])
|
|
const t = this.tables.get(upd[1])
|
|
if (t) {
|
|
for (const [k, row] of t.entries()) {
|
|
if (String(row[col]) === val) t.set(k, { ...row, [setCol]: setVal })
|
|
}
|
|
}
|
|
}
|
|
const del = /delete from (\w+)\s+where\s+(\w+)\s*=\s*\?/i.exec(sql)
|
|
if (del) {
|
|
const t = this.tables.get(del[1])
|
|
if (t) {
|
|
for (const [k, row] of t.entries()) {
|
|
if (String(row[del[2]]) === String(params[0])) t.delete(k)
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
async all<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
const lower = sql.toLowerCase()
|
|
const table = /from (\w+)/i.exec(sql)?.[1]
|
|
if (!table) return []
|
|
let rows = [...(this.tables.get(table)?.values() ?? [])].map((r) => ({ ...r }))
|
|
if (/\bwhere\b/.test(lower)) {
|
|
const m = /where\s+(\w+)\s*=\s*\?/i.exec(sql)
|
|
if (m && params[0] !== undefined) {
|
|
rows = rows.filter((r) => String(r[m[1]]) === String(params[0]))
|
|
}
|
|
}
|
|
if (/\border by/i.test(lower)) {
|
|
rows = rows.sort((a, b) => Number(b['year']) - Number(a['year']) || Number(b['month']) - Number(a['month']))
|
|
}
|
|
if (/select data/i.test(lower)) rows = rows.map((r) => ({ data: String(r['data']) }))
|
|
if (/select value/i.test(lower)) rows = rows.map((r) => ({ value: String(r['value']) }))
|
|
return rows as T[]
|
|
}
|
|
|
|
async exec(sql: string): Promise<unknown> {
|
|
const create = /create table (?:if not exists )?(\w+)/i.exec(sql)
|
|
if (create && !this.tables.has(create[1])) this.tables.set(create[1], new Map())
|
|
const del = /delete from (\w+)/i.exec(sql)
|
|
if (del) this.tables.set(del[1], new Map())
|
|
return null
|
|
}
|
|
}
|
|
|
|
function world(seed: string) {
|
|
return World.create({ seed, surname: '楚', familyName: '楚家', motto: 'm', difficulty: 'normal' })
|
|
}
|
|
|
|
describe('SaveSlot 存储扩展', () => {
|
|
it('snapshot 保留最近 12 份并淘汰最旧', async () => {
|
|
const driver = new MemoryDriver()
|
|
const file = new SaveSlot(9, driver)
|
|
const w = world('st-9')
|
|
for (let i = 0; i < 20; i++) {
|
|
w.state.year = i + 1
|
|
await file.saveState(w.state, 'x')
|
|
}
|
|
const snaps = await file.listSnapshots()
|
|
expect(snaps.length).toBe(12)
|
|
expect(snaps[0].year).toBe(20)
|
|
})
|
|
|
|
it('wipe 后库中没有快照与 meta', async () => {
|
|
const driver = new MemoryDriver()
|
|
const file = new SaveSlot(10, driver)
|
|
await file.saveState(world('st-10').state, 'x')
|
|
await file.wipe()
|
|
const state = await file.loadState()
|
|
expect(state).toBeNull()
|
|
})
|
|
|
|
it('非法 json 导入返回 false 且原数据不动', async () => {
|
|
const driver = new MemoryDriver()
|
|
const file = new SaveSlot(11, driver)
|
|
await file.saveState(world('st-11').state, 'x')
|
|
const ok = await file.importAll('not json at all')
|
|
expect(ok).toBe(false)
|
|
const still = await file.loadState()
|
|
expect(still).toBeTruthy()
|
|
})
|
|
|
|
it('export→import 完全恢复(含 rng 状态推进一致)', async () => {
|
|
const driver = new MemoryDriver()
|
|
const file = new SaveSlot(12, driver)
|
|
const w = world('st-12')
|
|
for (let i = 0; i < 10; i++) w.advanceMonth()
|
|
w.syncRng()
|
|
await file.saveState(w.state, 'y')
|
|
const json = await file.exportAll()
|
|
const d2 = new MemoryDriver()
|
|
const f2 = new SaveSlot(12, d2)
|
|
await f2.importAll(json)
|
|
const restored = await f2.loadState()
|
|
expect(restored?.totalTicks).toBe(10)
|
|
// 此后推进与原始世界分叉前提一致(同 rng 同初始 = 同序列)
|
|
const w2 = new World(restored!)
|
|
w.advanceMonth()
|
|
w2.advanceMonth()
|
|
expect(w2.state.family.stones).toBe(w.state.family.stones)
|
|
})
|
|
})
|
|
|
|
// 修正语法:listSnapshots async
|
|
declare global {
|
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
namespace NodeJS {
|
|
interface ProcessEnv {}
|
|
}
|
|
}
|