feat: gamemaster

This commit is contained in:
Ilia Denisov
2026-05-03 07:59:03 +02:00
committed by GitHub
parent a7cee15115
commit 3e2622757e
229 changed files with 41521 additions and 1098 deletions
@@ -0,0 +1,136 @@
-- +goose Up
-- Initial Game Master PostgreSQL schema.
--
-- Four tables cover the durable surface of the service:
-- * runtime_records — one row per game with the latest known runtime
-- status, scheduling state, and engine health summary;
-- * engine_versions — the deployable engine version registry consumed
-- by Lobby's start flow and the GM admin/patch flow;
-- * player_mappings — the (game_id, user_id) → (race_name,
-- engine_player_uuid) projection installed at register-runtime;
-- * operation_log — append-only audit of every register-runtime,
-- turn-generation, force-next-turn, banish, stop, patch, and
-- engine-version mutation GM performed.
--
-- Schema and the matching `gamemasterservice` role are provisioned
-- outside this script (in tests via cmd/jetgen/main.go::provisionRoleAndSchema;
-- in production via an ops init script). This migration runs as the
-- schema owner with `search_path=gamemaster` and only contains DDL for
-- the service-owned tables and indexes. ARCHITECTURE.md §Database topology
-- mandates that the per-service role's grants stay restricted to its own
-- schema; consequently this file deliberately deviates from PLAN.md
-- Stage 09's literal `CREATE SCHEMA IF NOT EXISTS gamemaster;` instruction.
-- runtime_records holds one durable record per game with the latest
-- known runtime status, scheduling state, and engine health summary.
-- The status enum is enforced by a CHECK so domain code can rely on it
-- without reading every callsite. The composite (status,
-- next_generation_at) index drives the scheduler ticker scan that
-- selects `status='running' AND next_generation_at <= now()` once per
-- second. next_generation_at is nullable: a row enters with
-- status='starting' and a null tick, and only acquires a tick when the
-- register-runtime CAS flips it to 'running'.
CREATE TABLE runtime_records (
game_id text PRIMARY KEY,
status text NOT NULL,
engine_endpoint text NOT NULL,
current_image_ref text NOT NULL,
current_engine_version text NOT NULL,
turn_schedule text NOT NULL,
current_turn integer NOT NULL DEFAULT 0,
next_generation_at timestamptz,
skip_next_tick boolean NOT NULL DEFAULT false,
engine_health text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
started_at timestamptz,
stopped_at timestamptz,
finished_at timestamptz,
CONSTRAINT runtime_records_status_chk
CHECK (status IN (
'starting', 'running', 'generation_in_progress',
'generation_failed', 'stopped', 'engine_unreachable',
'finished'
))
);
CREATE INDEX runtime_records_status_next_gen_idx
ON runtime_records (status, next_generation_at);
-- engine_versions is the deployable engine version registry. Each row
-- ties a semver string to a Docker reference and a free-form options
-- document; the status enum gates the start flow (active versions are
-- accepted by Lobby's resolve, deprecated versions are rejected on new
-- starts but remain valid for already-running games). `options` is
-- jsonb: v1 stores it verbatim and never element-filters.
CREATE TABLE engine_versions (
version text PRIMARY KEY,
image_ref text NOT NULL,
options jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
CONSTRAINT engine_versions_status_chk
CHECK (status IN ('active', 'deprecated'))
);
-- player_mappings carries the (game_id, user_id) → (race_name,
-- engine_player_uuid) projection installed at register-runtime. The
-- composite primary key both serves the lookups by (game_id, user_id)
-- on every command/order/report request and as a leftmost-prefix index
-- for the per-game roster reads (`WHERE game_id = $1`). The partial
-- UNIQUE index on (game_id, race_name) enforces the one-race-per-game
-- invariant at the storage boundary.
CREATE TABLE player_mappings (
game_id text NOT NULL,
user_id text NOT NULL,
race_name text NOT NULL,
engine_player_uuid text NOT NULL,
created_at timestamptz NOT NULL,
PRIMARY KEY (game_id, user_id)
);
CREATE UNIQUE INDEX player_mappings_game_race_uniq
ON player_mappings (game_id, race_name);
-- operation_log is an append-only audit of every operation Game Master
-- performed against a game's runtime or against the engine version
-- registry. The (game_id, started_at DESC) index drives audit reads
-- from the GM/Admin REST surface. finished_at is nullable for in-flight
-- rows even though the service layer always finalises the row. The
-- op_kind / op_source / outcome enums are enforced by CHECK constraints
-- to keep the audit schema honest without a separate Go validator.
CREATE TABLE operation_log (
id bigserial PRIMARY KEY,
game_id text NOT NULL,
op_kind text NOT NULL,
op_source text NOT NULL,
source_ref text NOT NULL DEFAULT '',
outcome text NOT NULL,
error_code text NOT NULL DEFAULT '',
error_message text NOT NULL DEFAULT '',
started_at timestamptz NOT NULL,
finished_at timestamptz,
CONSTRAINT operation_log_op_kind_chk
CHECK (op_kind IN (
'register_runtime', 'turn_generation', 'force_next_turn',
'banish', 'stop', 'patch',
'engine_version_create', 'engine_version_update',
'engine_version_deprecate', 'engine_version_delete'
)),
CONSTRAINT operation_log_op_source_chk
CHECK (op_source IN (
'gateway_player', 'lobby_internal', 'admin_rest'
)),
CONSTRAINT operation_log_outcome_chk
CHECK (outcome IN ('success', 'failure'))
);
CREATE INDEX operation_log_game_started_idx
ON operation_log (game_id, started_at DESC);
-- +goose Down
DROP TABLE IF EXISTS operation_log;
DROP TABLE IF EXISTS player_mappings;
DROP TABLE IF EXISTS engine_versions;
DROP TABLE IF EXISTS runtime_records;