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>() async open(_name: string): Promise {} async close(): Promise {} async run(sql: string, params: unknown[] = []): Promise { 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() 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(sql: string, params: unknown[] = []): Promise { 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 { 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 {} } }