import { describe, expect, it } from 'vitest' import { SaveSlot, SaveDbDriver } from '../src/renderer/game/storage/slots' import { GameState } from '../src/renderer/game/types/domain' import { World } from '../src/renderer/game/engine/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 m = /insert into (\w+)\s*\(([^)]+)\)\s*values\s*\(([^)]+)\)/i.exec(sql) if (m) { const table = m[1] const cols = m[2].split(',').map((c) => c.trim()) const row: Row = {} cols.forEach((c, i) => { row[c] = params[i] }) const t = this.tables.get(table) ?? new Map() t.set(String(row['id'] ?? table + t.size), row) this.tables.set(table, t) } if (/update\s+(\w+)\s+set/i.test(sql)) { const table = /update\s+(\w+)/i.exec(sql)![1] const setCol = /set\s+(\w+)\s*=\s*\?/i.exec(sql)![1] const setVal = params[0] const whereLiteral = /\bwhere\s+(\w+)\s*=\s*'([^']+)'/i.exec(sql) const wherePlaceholder = /where\s+(\w+)\s*=\s*\?/i.exec(sql) const whereCol = (whereLiteral?.[1] ?? wherePlaceholder?.[1]) as string | undefined const whereVal = (whereLiteral?.[2] as string | undefined) ?? (wherePlaceholder ? String(params[1]) : undefined) if (whereCol && whereVal !== undefined) { const t = this.tables.get(table) if (t) { for (const [k, row] of t.entries()) { if (String(row[whereCol]) === whereVal) { t.set(k, { ...row, [setCol]: setVal }) } } } } } const del = /delete from (\w+)\s+where\s+(\w+)\s*=\s*\?/i.exec(sql) if (del) { const table = del[1] const col = del[2] const val = String(params[0]) const t = this.tables.get(table) if (t) { for (const [k, row] of t.entries()) { if (String(row[col]) === val) t.delete(k) } } } return null } async all(sql: string, params: unknown[] = []): Promise { const lower = sql.toLowerCase() const isSnapshot = /from snapshot/.test(lower) const isMeta = /from meta/.test(lower) const isChronicle = /from chronicle/.test(lower) const table = isSnapshot ? 'snapshot' : isMeta ? 'meta' : isChronicle ? 'chronicle' : null 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(lower) if (m) { const col = m[1] const val = String(params[0]) rows = rows.filter((r) => String(r[col]) === val) } } if (isSnapshot && /\border by/i.test(lower)) { rows = rows.sort((a, b) => { const y = Number(b['year']) - Number(a['year']) if (y !== 0) return y return Number(b['month']) - Number(a['month']) }) } if (isSnapshot) { const wantData = /select data/.test(lower) rows = rows.map((r) => (wantData ? { data: String(r['data']) } : r)) } else if (isMeta) { const wantValue = /select value/.test(lower) rows = rows.map((r) => (wantValue ? { value: String(r['value']) } : r)) } 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 } } describe('SaveSlot storage roundtrip', () => { it('saves & loads latest state faithfully', async () => { const slot = new SaveSlot(1, new MemoryDriver()) const w = World.create({ seed: 's1', surname: '陈', familyName: '陈家', motto: 'm', difficulty: 'normal' }) const id = await slot.saveState(w.state, '自动') const loaded = await slot.loadState() expect(loaded).not.toBeNull() expect(loaded!.family.surname).toBe('陈') expect(loaded!.members['x1'].name).toBe(w.state.members['x1'].name) expect(id).toBeTruthy() }) it('lists snapshots newest first', async () => { const slot = new SaveSlot(2, new MemoryDriver()) const w = World.create({ seed: 's2', surname: '周', familyName: '周家', motto: 'm', difficulty: 'normal' }) w.state.year = 1 w.state.month = 1 await slot.saveState(w.state, 'a') const s2 = JSON.parse(JSON.stringify(w.state)) as GameState s2.year = 2 s2.month = 3 await slot.saveState(s2, 'b') const snaps = await slot.listSnapshots() expect(snaps.length).toBe(2) expect(snaps[0].year).toBe(2) expect(snaps[1].year).toBe(1) }) it('loads by id and rounds meta', async () => { const slot = new SaveSlot(3, new MemoryDriver()) const w = World.create({ seed: 's3', surname: '郑', familyName: '郑家', motto: 'm', difficulty: 'normal' }) const id = await slot.saveState(w.state, 'm') const byId = await slot.loadState(id) expect(byId?.family.name).toBe('郑家') await slot.setMeta({ slot: 3, surname: '郑', name: '郑家', estate: w.state.family.estate, year: 1, month: 1, generation: 1, members: 5, reputation: 5, updatedAt: 'now', version: 1 }) const meta = await slot.getMeta() expect(meta?.name).toBe('郑家') expect(meta?.members).toBe(5) }) it('keeps snapshot tail trimmed', async () => { const slot = new SaveSlot(5, new MemoryDriver()) const w = World.create({ seed: 's5', surname: '钱', familyName: '钱家', motto: 'm', difficulty: 'normal' }) for (let i = 0; i < 15; i++) { w.state.year = i + 1 await slot.saveState(w.state, 's' + i) } const snaps = await slot.listSnapshots() expect(snaps.length).toBe(12) expect(snaps[0].year).toBe(15) }) })