Files
History/server/db.ts
T
kamilkrzywda a129c2ab81
CI / build + unit + e2e (pull_request) Successful in 2m36s
v0.27.0 (#44): LLM-placed named map point markers
- shared: 8 MarkerTypes + MARKER_TYPE_INFO (label/color/glyph); MapMarker;
  GameState.markers (required, [] at init); TurnResult.markers {add,remove};
  MarkerSpec/MarkerInfo; FNV-1a 4-char markerId; MAX_MARKERS=400.
- data: new public/region-centroids.json (1,800 anchors = largest-part centroid
  + spread ~0.1x min-bbox) built by new idempotent scripts/build-region-centroids.mjs.
- server: regions.ts centroid loader (public/ then dist/); engine applyTurnResult
  marker ops (remove -> owner-recompute -> add, jitter, cap, never throws);
  prompt documents the contract; buildUserPrompt carries markers; stub marker-free.
- client: WorldMap symbol layer rendered as canvas per-type icons (glyph CDN dead);
  hover tooltip shows name; click focuses owning nation.
- tests: 168 -> 189 (centroids script, regions centroids, db legacy, engine markers).
2026-10-14 22:37:29 +00:00

67 lines
2.2 KiB
TypeScript

// SQLite persistence for games (one row per game id; the full GameState as JSON).
import { mkdirSync } from 'node:fs';
import path from 'node:path';
import Database from 'better-sqlite3';
import type { GameState } from '../shared/types';
export interface Db {
save(state: GameState): void;
load(id: string): GameState | null;
listIds(): string[];
delete(id: string): boolean;
close(): void;
}
export function openDb(filePath: string): Db {
const dir = path.dirname(filePath);
if (dir) mkdirSync(dir, { recursive: true });
const db = new Database(filePath);
db.pragma('journal_mode = WAL');
db.exec(
`CREATE TABLE IF NOT EXISTS games (
id TEXT PRIMARY KEY,
data TEXT NOT NULL,
updated_at TEXT NOT NULL
)`,
);
const upsert = db.prepare(
`INSERT INTO games (id, data, updated_at) VALUES (?, ?, ?)
ON CONFLICT(id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`,
);
const byId = db.prepare('SELECT data FROM games WHERE id = ?');
const all = db.prepare('SELECT id FROM games ORDER BY updated_at DESC');
const del = db.prepare('DELETE FROM games WHERE id = ?');
return {
save(state) {
upsert.run(state.id, JSON.stringify(state), new Date().toISOString());
},
load(id) {
const row = byId.get(id) as { data: string } | undefined;
if (!row) return null;
try {
const state = JSON.parse(row.data) as GameState;
// (v0.22, #16 P3) Rows saved before `GameState.unowned` existed load
// with it defaulted: a legacy game has no no-man's-land regions.
if (state.unowned === undefined) state.unowned = [];
// (v0.23, #16 P4a) Same for pending nation-op proposals: a legacy game
// has none awaiting confirm/reject.
if (state.pendingNationOps === undefined) state.pendingNationOps = [];
// (v0.27, #44) Same for map markers: a legacy game has none placed.
if (state.markers === undefined) state.markers = [];
return state;
} catch {
return null;
}
},
listIds() {
return (all.all() as { id: string }[]).map((r) => r.id);
},
delete(id) {
return del.run(id).changes > 0;
},
close() {
db.close();
},
};
}