Files
otivm/data/create_player_db.sql

419 lines
25 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- OTIVM Per-Player Database Schema
-- File: data/create_player_db.sql
-- Version: OTIVM-V (schema version 5)
-- 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 = 5; -- OTIVM-V 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).
--
-- UNIQUE (actor_id): required so that child tables can hold a valid
-- foreign key to actor_id alone. The composite PRIMARY KEY
-- (actor_id, recorded_at) supports the append-only supersession
-- pattern; the UNIQUE constraint makes actor_id independently
-- referenceable. In practice, each player database file holds
-- exactly one actor, so actor_id is always unique within the file.
--
-- 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 5,
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),
UNIQUE (actor_id) -- required for FK references from child tables
);
-- ===================================================================
-- 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 (PORTORIUM)
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'),
(5, 'OTIVM-V: Add UNIQUE(actor_id) to actor_profile to fix FK references', '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)
-- sqlite3 data/saves/{actor_id}.sqlite3 "PRAGMA foreign_key_check;"
-- Expected: (empty — no violations)
--
-- 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.
-- ===================================================================