diff --git a/data/create_player_db.sql b/data/create_player_db.sql new file mode 100644 index 0000000..ee25259 --- /dev/null +++ b/data/create_player_db.sql @@ -0,0 +1,381 @@ +-- OTIVM Per-Player Database Schema +-- File: data/create_player_db.sql +-- Version: OTIVM-III +-- Status: Game schema — transitional, designed to be throw-away +-- Replaces: data/saves/{session_id}.json (OTIVM-I/II) +-- Parallel to: data/create_otivm_db.sql (TESSERA world substrate) +-- +-- Naming conventions: terminology.md Layer 3 (snake_case, period-neutral) +-- Timestamp column: recorded_at (never created_at, never timestamp) +-- Rows are never deleted — only archived or superseded +-- value_true and value_perceived are always separate columns +-- confidence_tag is always a first-class column, never a comment +-- +-- When this schema is retired, it becomes a read-only layer +-- readable by the Simulator via the existing API translation layer. +-- The API is the universal wiring — this schema does not need to +-- anticipate the Simulator schema. +-- +-- TheRON — single contributor. AI assistants implement, document, +-- flag — do not direct. + +PRAGMA journal_mode = WAL; +PRAGMA foreign_keys = ON; +PRAGMA user_version = 3; -- OTIVM-III schema version + +-- ============================================================ +-- TABLE: actor_profile +-- One row per actor. The static anchor. +-- Created at session initialisation. Never updated — superseded +-- by a new row if background is changed (not currently possible +-- in OTIVM-III but the structure supports it). +-- ============================================================ + +CREATE TABLE IF NOT EXISTS actor_profile ( + actor_id TEXT NOT NULL, -- uuid, matches save file naming + session_id TEXT NOT NULL, -- uuid, links to session chain + background_id TEXT NOT NULL, -- former_legionary | freedman_trader | + -- noble_younger_son | failed_magistrate | + -- camp_logistician | guild_scribe + actor_name TEXT NOT NULL, -- display name chosen by participant + epoch TEXT NOT NULL -- roman_14bce (Layer 3 code token) + DEFAULT 'roman_14bce', + schema_version INTEGER NOT NULL DEFAULT 3, + recorded_at TEXT NOT NULL, -- ISO 8601 UTC + PRIMARY KEY (actor_id, recorded_at) +); + +-- ============================================================ +-- TABLE: actor_parameters +-- The core parameter table. +-- One row per parameter per actor per recorded moment. +-- Append-only — new row on every change, old row retained. +-- +-- Parameters with observable: partial or observable: hidden +-- carry both value_true (server-side ground truth) and +-- value_perceived (what the actor believes or can infer). +-- These are never merged into one column. +-- +-- For parameters where perceived == true (observable: full), +-- value_perceived is set equal to value_true at write time. +-- +-- confidence_tag vocabulary (from parameter-registry.md): +-- measured | indicated | inferred | estimated | unknown +-- +-- observable_level vocabulary: +-- full | partial | hidden +-- +-- value columns are TEXT to support ordinal bands (low/medium/high), +-- enums, floats, integers, and JSON structures without schema changes. +-- The parameter_token identifies what type to expect. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS actor_parameters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + actor_id TEXT NOT NULL, + parameter_token TEXT NOT NULL, -- from parameter-registry.md token field + scope TEXT NOT NULL, -- actor | scenario | relation + layer TEXT NOT NULL, -- roman | mesolithic | universal + value_true TEXT NOT NULL, -- server-side ground truth + value_perceived TEXT NOT NULL, -- actor-side belief (= value_true when full) + value_social TEXT, -- market consensus (auctoritas only) + confidence_tag TEXT NOT NULL -- measured | indicated | inferred | + DEFAULT 'estimated', -- estimated | unknown + observable_level TEXT NOT NULL, -- full | partial | hidden + drift_source TEXT, -- what caused this value (event_id, or NULL if initial) + recorded_at TEXT NOT NULL, -- ISO 8601 UTC + superseded_at TEXT, -- NULL if current; set when a newer row replaces this one + FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) +); + +-- Index for fast current-value lookup +CREATE INDEX IF NOT EXISTS idx_actor_parameters_current + ON actor_parameters (actor_id, parameter_token, superseded_at); + +-- ============================================================ +-- TABLE: parameter_drift_log +-- Append-only event log of every parameter change. +-- What changed, why, what triggered it, old and new values. +-- The behavioral record. Rows are never deleted. +-- This is the source of truth for the Simulator when it +-- reads this database as a read-only layer. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS parameter_drift_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + actor_id TEXT NOT NULL, + parameter_token TEXT NOT NULL, + trigger_type TEXT NOT NULL, -- venture_complete | leg_complete | + -- interval_complete | exchange_complete | + -- scenario_event | session_start | + -- background_drift | manual + trigger_ref TEXT, -- venture_id, leg_id, scenario_id, or NULL + value_before TEXT NOT NULL, + value_after TEXT NOT NULL, + delta_note TEXT, -- human-readable reason for drift + recorded_at TEXT NOT NULL, -- ISO 8601 UTC + FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) +); + +CREATE INDEX IF NOT EXISTS idx_drift_log_actor + ON parameter_drift_log (actor_id, recorded_at); + +-- ============================================================ +-- TABLE: ventures +-- One row per venture (NEGOTIVM in Roman layer). +-- Status tracks the lifecycle. Outcome recorded on completion. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS ventures ( + venture_id TEXT PRIMARY KEY, -- uuid + actor_id TEXT NOT NULL, + venture_label TEXT NOT NULL, -- human-readable, e.g. "Ostia to Capua" + status TEXT NOT NULL -- planned | active | complete | abandoned + DEFAULT 'planned', + cargo_type TEXT, -- amphora type, modius goods, etc. + cargo_quantity REAL, + cargo_unit TEXT, -- amphora | modius | talent | unit + cost_total REAL, -- denarii-equivalent, sum of all legs + revenue_total REAL, -- denarii-equivalent, at settlement + outcome_net REAL, -- revenue_total - cost_total (NULL until complete) + outcome_note TEXT, -- narrative summary of outcome + recorded_at TEXT NOT NULL, -- ISO 8601 UTC — venture created + started_at TEXT, -- ISO 8601 UTC — first leg begun + completed_at TEXT, -- ISO 8601 UTC — final settlement + FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) +); + +CREATE INDEX IF NOT EXISTS idx_ventures_actor + ON ventures (actor_id, status); + +-- ============================================================ +-- TABLE: venture_legs +-- One row per leg (ITER in Roman layer). +-- Indivisible unit of movement within a venture. +-- Cost components recorded separately for Simulator readability. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS venture_legs ( + leg_id TEXT PRIMARY KEY, -- uuid + venture_id TEXT NOT NULL, + leg_sequence INTEGER NOT NULL, -- 1, 2, 3... within the venture + origin_h3 TEXT NOT NULL, -- H3 cell ID (TESSERA-compatible) + destination_h3 TEXT NOT NULL, -- H3 cell ID (TESSERA-compatible) + mode TEXT NOT NULL, -- road | sea | river | overland + vessel_type TEXT, -- navis_oneraria | actuaria | NULL if land + duration_days REAL, -- actual duration (NULL until complete) + cost_vectura REAL, -- freight charge (VECTVRA) + cost_portoria REAL, -- customs duty (PORTORIVM) + cost_other REAL, -- horreum, incidentals + cost_total REAL, -- sum of above + status TEXT NOT NULL -- planned | active | complete | failed + DEFAULT 'planned', + delay_days REAL, -- deviation from expected duration + delay_cause TEXT, -- weather | congestion | dispute | NULL + recorded_at TEXT NOT NULL, -- ISO 8601 UTC — leg created + started_at TEXT, + completed_at TEXT, + FOREIGN KEY (venture_id) REFERENCES ventures(venture_id) +); + +CREATE INDEX IF NOT EXISTS idx_legs_venture + ON venture_legs (venture_id, leg_sequence); + +-- ============================================================ +-- TABLE: scenario_state +-- Active and archived scenario parameters. +-- Transient during active window, archived on close. +-- Must not persist into actor_parameters as permanent values — +-- scenario parameters are pressures, not conditions. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS scenario_state ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + scenario_id TEXT NOT NULL, -- e.g. SCENARIO-MERCHANT-0001 + actor_id TEXT NOT NULL, + parameter_token TEXT NOT NULL, -- from parameter-registry.md + value_true TEXT NOT NULL, + value_perceived TEXT NOT NULL, + confidence_tag TEXT NOT NULL DEFAULT 'estimated', + observable_level TEXT NOT NULL, + recorded_at TEXT NOT NULL, -- ISO 8601 UTC — parameter set + archived_at TEXT, -- NULL if active; set on scenario close + FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) +); + +CREATE INDEX IF NOT EXISTS idx_scenario_active + ON scenario_state (actor_id, scenario_id, archived_at); + +-- ============================================================ +-- TABLE: events +-- Append-only chronological record of all simulation state +-- changes. The atomic unit of history. +-- Every event has a recorded_at. No event is undated. +-- This table is the substrate for the behavioral model — +-- when this database becomes a read-only Simulator layer, +-- this table is the primary read target. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + actor_id TEXT NOT NULL, + event_type TEXT NOT NULL, -- from terminology.md Layer 3 event_type values: + -- venture_start | venture_complete | + -- leg_start | leg_complete | + -- interval_start | interval_complete | + -- exchange_complete | session_abandoned | + -- chapter_advance | journal_unlock | + -- scenario_trigger | scenario_close | + -- parameter_drift + ref_id TEXT, -- venture_id, leg_id, scenario_id, or NULL + ref_type TEXT, -- venture | leg | scenario | actor | NULL + payload TEXT, -- JSON — event-specific detail + recorded_at TEXT NOT NULL, -- ISO 8601 UTC + FOREIGN KEY (actor_id) REFERENCES actor_profile(actor_id) +); + +CREATE INDEX IF NOT EXISTS idx_events_actor_time + ON events (actor_id, recorded_at); + +CREATE INDEX IF NOT EXISTS idx_events_type + ON events (event_type, recorded_at); + +-- ============================================================ +-- TABLE: background_starting_values +-- Seed data — the canonical starting parameter values for +-- each of the six backgrounds, per parameter-registry.md. +-- Read at actor initialisation to populate actor_parameters. +-- Never modified after initial insert. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS background_starting_values ( + background_id TEXT NOT NULL, + parameter_token TEXT NOT NULL, + value_true TEXT NOT NULL, + value_perceived TEXT NOT NULL, + confidence_tag TEXT NOT NULL DEFAULT 'indicated', + observable_level TEXT NOT NULL, + notes TEXT, + PRIMARY KEY (background_id, parameter_token) +); + +-- ============================================================ +-- SEED DATA: background_starting_values +-- Source: parameter-registry.md, section 1 (Actor Parameters) +-- Ordinal bands: low | medium | high | distinguished | extensive +-- ============================================================ + +INSERT OR IGNORE INTO background_starting_values VALUES +-- FORMER LEGIONARY +('former_legionary', 'auctoritas', 'medium', 'medium', 'indicated', 'partial', 'Reliable but not distinguished'), +('former_legionary', 'clientela', 'low', 'low', 'indicated', 'partial', 'Military network, limited commercial reach'), +('former_legionary', 'liquiditas', '200', '200', 'measured', 'full', 'Denarii — modest savings'), +('former_legionary', 'fama', 'neutral','neutral','indicated', 'partial', NULL), +('former_legionary', 'disciplina', 'high', 'high', 'indicated', 'full', NULL), +('former_legionary', 'mercatus_scientia', 'low', 'low', 'indicated', 'full', 'Military logistics, not commercial markets'), +('former_legionary', 'itineris_scientia', 'high', 'high', 'indicated', 'full', 'Extensive road and route knowledge'), +('former_legionary', 'ius_accessus', 'medium', 'medium', 'indicated', 'partial', 'Citizen standing'), +('former_legionary', 'periculum_tolerantia', 'high', 'high', 'indicated', 'full', NULL), +('former_legionary', 'negotiatio', 'low', 'low', 'indicated', 'full', NULL), +('former_legionary', 'litterae', 'medium', 'medium', 'indicated', 'full', NULL), +('former_legionary', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL), + +-- FREEDMAN TRADER +('freedman_trader', 'auctoritas', 'low', 'low', 'indicated', 'partial', 'Practical reputation growing faster than social recognition'), +('freedman_trader', 'clientela', 'medium', 'medium', 'indicated', 'partial', 'Commercial network, active'), +('freedman_trader', 'liquiditas', '350', '350', 'measured', 'full', 'Denarii — working capital'), +('freedman_trader', 'fama', 'neutral','neutral','indicated', 'partial', NULL), +('freedman_trader', 'disciplina', 'medium', 'medium', 'indicated', 'full', NULL), +('freedman_trader', 'mercatus_scientia', 'high', 'high', 'indicated', 'full', NULL), +('freedman_trader', 'itineris_scientia', 'medium', 'medium', 'indicated', 'full', NULL), +('freedman_trader', 'ius_accessus', 'low', 'low', 'indicated', 'partial', 'Freedman limits on contract enforceability'), +('freedman_trader', 'periculum_tolerantia', 'medium', 'medium', 'indicated', 'full', NULL), +('freedman_trader', 'negotiatio', 'high', 'high', 'indicated', 'full', NULL), +('freedman_trader', 'litterae', 'high', 'high', 'indicated', 'full', NULL), +('freedman_trader', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL), + +-- NOBLE YOUNGER SON +('noble_younger_son','auctoritas', 'high', 'high', 'indicated', 'partial', NULL), +('noble_younger_son','clientela', 'high', 'high', 'indicated', 'partial', 'Inherited network, not personally built'), +('noble_younger_son','liquiditas', '150', '150', 'measured', 'full', 'Denarii — constrained by elder sibling priority'), +('noble_younger_son','fama', 'good', 'good', 'indicated', 'partial', NULL), +('noble_younger_son','disciplina', 'low', 'low', 'indicated', 'full', NULL), +('noble_younger_son','mercatus_scientia', 'low', 'low', 'indicated', 'full', NULL), +('noble_younger_son','itineris_scientia', 'low', 'low', 'indicated', 'full', NULL), +('noble_younger_son','ius_accessus', 'high', 'high', 'indicated', 'partial', NULL), +('noble_younger_son','periculum_tolerantia', 'low', 'low', 'indicated', 'full', NULL), +('noble_younger_son','negotiatio', 'medium', 'medium', 'indicated', 'full', NULL), +('noble_younger_son','litterae', 'high', 'high', 'indicated', 'full', NULL), +('noble_younger_son','officia_burden', 'high', 'medium', 'indicated', 'partial', 'Underestimates informal obligations'), + +-- FAILED MAGISTRATE +('failed_magistrate','auctoritas', 'low', 'medium', 'indicated', 'partial', 'True value lower than perceived — falling'), +('failed_magistrate','clientela', 'medium', 'medium', 'indicated', 'partial', 'Legal contacts, politically exposed'), +('failed_magistrate','liquiditas', '100', '100', 'measured', 'full', 'Denarii — depleted by failed campaign'), +('failed_magistrate','fama', 'mixed', 'mixed', 'indicated', 'partial', NULL), +('failed_magistrate','disciplina', 'medium', 'medium', 'indicated', 'full', NULL), +('failed_magistrate','mercatus_scientia', 'low', 'low', 'indicated', 'full', NULL), +('failed_magistrate','itineris_scientia', 'low', 'low', 'indicated', 'full', NULL), +('failed_magistrate','ius_accessus', 'high', 'high', 'indicated', 'partial', 'Formal standing intact, practical access eroding'), +('failed_magistrate','periculum_tolerantia', 'low', 'low', 'indicated', 'full', NULL), +('failed_magistrate','negotiatio', 'medium', 'medium', 'indicated', 'full', NULL), +('failed_magistrate','litterae', 'high', 'high', 'indicated', 'full', NULL), +('failed_magistrate','officia_burden', 'high', 'high', 'indicated', 'partial', NULL), + +-- CAMP LOGISTICIAN +('camp_logistician', 'auctoritas', 'low', 'low', 'indicated', 'partial', NULL), +('camp_logistician', 'clientela', 'low', 'low', 'indicated', 'partial', 'Supply chain contacts, narrow'), +('camp_logistician', 'liquiditas', '180', '180', 'measured', 'full', 'Denarii — steady savings'), +('camp_logistician', 'fama', 'neutral','neutral','indicated', 'partial', NULL), +('camp_logistician', 'disciplina', 'high', 'high', 'indicated', 'full', NULL), +('camp_logistician', 'mercatus_scientia', 'high', 'high', 'indicated', 'full', 'Bulk goods, logistics pricing'), +('camp_logistician', 'itineris_scientia', 'high', 'high', 'indicated', 'full', NULL), +('camp_logistician', 'ius_accessus', 'medium', 'medium', 'indicated', 'partial', NULL), +('camp_logistician', 'periculum_tolerantia', 'medium', 'medium', 'indicated', 'full', NULL), +('camp_logistician', 'negotiatio', 'medium', 'medium', 'indicated', 'full', NULL), +('camp_logistician', 'litterae', 'medium', 'medium', 'indicated', 'full', NULL), +('camp_logistician', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL), + +-- GUILD SCRIBE +('guild_scribe', 'auctoritas', 'low', 'low', 'indicated', 'partial', NULL), +('guild_scribe', 'clientela', 'low', 'low', 'indicated', 'partial', 'Document and account network'), +('guild_scribe', 'liquiditas', '120', '120', 'measured', 'full', 'Denarii — modest, careful'), +('guild_scribe', 'fama', 'neutral','neutral','indicated', 'partial', NULL), +('guild_scribe', 'disciplina', 'medium', 'medium', 'indicated', 'full', NULL), +('guild_scribe', 'mercatus_scientia', 'medium', 'medium', 'indicated', 'full', 'Account knowledge, not market instinct'), +('guild_scribe', 'itineris_scientia', 'low', 'low', 'indicated', 'full', NULL), +('guild_scribe', 'ius_accessus', 'medium', 'medium', 'indicated', 'partial', 'Document access, limited enforcement'), +('guild_scribe', 'periculum_tolerantia', 'low', 'low', 'indicated', 'full', NULL), +('guild_scribe', 'negotiatio', 'low', 'low', 'indicated', 'full', NULL), +('guild_scribe', 'litterae', 'high', 'high', 'indicated', 'full', NULL), +('guild_scribe', 'officia_burden', 'low', 'low', 'indicated', 'partial', NULL); + +-- ============================================================ +-- TABLE: schema_changelog +-- Documents schema version history. +-- Required: if this database becomes a read-only Simulator +-- layer, the Simulator must be able to identify schema version +-- and adapt its reader accordingly. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS schema_changelog ( + version INTEGER PRIMARY KEY, + description TEXT NOT NULL, + applied_at TEXT NOT NULL -- ISO 8601 UTC +); + +INSERT OR IGNORE INTO schema_changelog VALUES +(1, 'OTIVM-I: JSON save files (not SQLite — recorded here for continuity)', '2026-04-25T00:00:00Z'), +(2, 'OTIVM-II: JSON save files with TESSERA map integration', '2026-04-28T00:00:00Z'), +(3, 'OTIVM-III: Per-player SQLite schema, parameter registry v1', '2026-05-02T00:00:00Z'); + +-- ============================================================ +-- END OF SCHEMA +-- To initialise a player database: +-- sqlite3 data/saves/{actor_id}.sqlite3 < data/create_player_db.sql +-- +-- To verify integrity after creation: +-- sqlite3 data/saves/{actor_id}.sqlite3 "PRAGMA integrity_check;" +-- sqlite3 data/saves/{actor_id}.sqlite3 "SELECT COUNT(*) FROM background_starting_values;" +-- Expected: 72 rows (12 parameters × 6 backgrounds) +-- ============================================================