diff --git a/lib/db/client.ts b/lib/db/client.ts index 54b2984..6c5bb10 100644 --- a/lib/db/client.ts +++ b/lib/db/client.ts @@ -14,12 +14,43 @@ export interface GameDb { file: string; } -// CREATE TABLE statements mirroring lib/db/schema.ts — keep in sync. Game tables land with -// the turn engine; _meta is DB plumbing (not game state): a brand-new sql.js database exports -// as ZERO bytes, so without at least one real table there would be nothing valid to persist -// on the ./db volume from first boot. +// CREATE TABLE statements mirroring lib/db/schema.ts — keep in sync. _meta is DB plumbing +// (not game state): a brand-new sql.js database exports as ZERO bytes, so without at least +// one real table there would be nothing valid to persist on the ./db volume from first boot. const DDL = ` CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT); + +CREATE TABLE IF NOT EXISTS saves ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + seed TEXT NOT NULL, + turn INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS solar_systems ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + save_id INTEGER NOT NULL REFERENCES saves(id), + name TEXT NOT NULL, + x REAL NOT NULL, + y REAL NOT NULL, + discovered INTEGER NOT NULL DEFAULT 0, + is_current INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS jump_routes ( + from_system INTEGER NOT NULL REFERENCES solar_systems(id), + to_system INTEGER NOT NULL REFERENCES solar_systems(id), + PRIMARY KEY (from_system, to_system) +); + +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + save_id INTEGER NOT NULL REFERENCES saves(id), + turn INTEGER NOT NULL, + type TEXT NOT NULL, + message TEXT NOT NULL, + created_at TEXT NOT NULL +); `; // SPACE_WANDERER_DB_PATH override exists so unit tests can point at a temp file instead diff --git a/lib/db/schema.ts b/lib/db/schema.ts index f7f7410..201e40c 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -1,6 +1,47 @@ -// Drizzle schema for game state. EMPTY on purpose at scaffold stage (v0.1.0) — no game -// tables yet; the first turn-engine/ship/crew tables land with the first game systems. +import { integer, primaryKey, real, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +// Drizzle schema for game state. Single-player by design: exactly ONE save row ever exists +// (no auth/accounts), so every table hangs off saves.id but only one save is in play. // Column names/types here are the source of truth; matching CREATE TABLE statements live // in lib/db/client.ts (DDL) and must stay in sync with this file. -export {}; +export const saves = sqliteTable('saves', { + id: integer('id').primaryKey({ autoIncrement: true }), + // Seed for the deterministic world generator — same seed always regenerates the same world. + seed: text('seed').notNull(), + turn: integer('turn').notNull().default(0), + createdAt: text('created_at').notNull(), +}) + +export const solarSystems = sqliteTable('solar_systems', { + id: integer('id').primaryKey({ autoIncrement: true }), + saveId: integer('save_id').notNull().references(() => saves.id), + name: text('name').notNull(), + // 0–100 coordinate space, rendered directly by the NodeGraph component. + x: real('x').notNull(), + y: real('y').notNull(), + discovered: integer('discovered', { mode: 'boolean' }).notNull().default(false), + isCurrent: integer('is_current', { mode: 'boolean' }).notNull().default(false), +}) + +// Jump routes, stored as directed pairs (both directions of each route) so adjacency and +// "reachable from current" are single-row lookups. Read side dedupes for display. +export const jumpRoutes = sqliteTable( + 'jump_routes', + { + fromSystem: integer('from_system').notNull().references(() => solarSystems.id), + toSystem: integer('to_system').notNull().references(() => solarSystems.id), + }, + (t) => [primaryKey({ columns: [t.fromSystem, t.toSystem] })], +) + +// Append-only game log. Writers so far: new-game + jump events; later issues add more +// writers and a read endpoint. +export const events = sqliteTable('events', { + id: integer('id').primaryKey({ autoIncrement: true }), + saveId: integer('save_id').notNull().references(() => saves.id), + turn: integer('turn').notNull(), + type: text('type').notNull(), + message: text('message').notNull(), + createdAt: text('created_at').notNull(), +})