Files
ChronicleOfTheImmortalClan/tests/save.test.ts
T
thzxx ff6df8054c refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)
结构(旧 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
2026-08-23 13:23:25 +08:00

163 lines
5.9 KiB
TypeScript

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/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 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<string, Row>()
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<T>(sql: string, params: unknown[] = []): Promise<T[]> {
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<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
}
}
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)
})
})