-- OTIVM Per-Player Database Schema -- File: data/create_player_db.sql -- Version: OTIVM-IV (schema version 4) -- 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 -- -- Simulation clock: see docs/architecture/simulation-clock.md -- session_anchor_at is the real UTC timestamp at which the player's -- simulation clock started (set at first dispatch, NULL until then). -- All simulated date arithmetic uses this field as the epoch anchor -- per the integer constraint documented in simulation-clock.md. -- -- 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 = 4; -- OTIVM-IV 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-IV but the structure supports it). -- -- session_anchor_at: NULL until first dispatch. Set once at the -- moment the player fires their first venture. All simulation -- clock arithmetic is relative to this timestamp. -- See docs/architecture/simulation-clock.md Section 7. -- ================================================================ 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 4, recorded_at TEXT NOT NULL, -- ISO 8601 UTC — row creation time session_anchor_at TEXT, -- ISO 8601 UTC — first dispatch time -- NULL until player fires first venture -- Never updated after it is set 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. -- -- duration_days: integer simulated days per simulation-clock.md. -- Must be a positive integer. Never a decimal. -- Derived from duration_ms / MS_PER_SIM_DAY at write time. -- See docs/architecture/simulation-clock.md Section 2. -- ================================================================ 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 INTEGER, -- simulated days (INTEGER, never REAL) -- NULL until leg completes 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 INTEGER, -- deviation from expected duration (INTEGER) 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'), (4, 'OTIVM-IV: Add session_anchor_at to actor_profile for simulation clock','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) -- -- Note on venture_legs.duration_days and delay_days: -- Declared INTEGER, not REAL. This enforces the integer clock -- constraint from docs/architecture/simulation-clock.md. -- A fractional value here is a schema violation. -- ================================================================