adventure: tell the story of a run, not just how it ended
Pete only ever heard that an expedition happened once it was over — a zone cleared, a retreat, a death. The run itself was narrated into one Matrix DM and thrown away. The map on the adventurer page has always shown where somebody is; this shows what happened there. Beats arrive on their own channel, append-only and idempotent on (run_id, seq). They are the one thing gogobee pushes that is history rather than state, so they accumulate instead of replacing — and they stay off the dispatch queue so a chatty run can never spend the retry budget a death dispatch depends on. The run header is derived from the beats rather than pushed: a run whose start beat never arrived still gets a readable, unattributed log instead of being dropped for want of a name. An unknown beat kind renders as its own noun rather than 400ing. That is the same lesson the dispatch ingest learned the hard way, and the regression test covers the class, not the case. Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
This commit is contained in:
@@ -144,6 +144,54 @@ CREATE TABLE IF NOT EXISTS adventure_siege_history (
|
||||
ended_at INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- The expedition liveblog. Two tables, and — unlike everything else gogobee
|
||||
-- pushes — these are append-only history, not a replaceable snapshot. A beat is
|
||||
-- something that HAPPENED; there is no later truth that corrects it, only more
|
||||
-- of it.
|
||||
--
|
||||
-- adventure_run is the header, assembled from the run's beats rather than pushed
|
||||
-- as its own object: the "start" beat opens it and the "end" beat closes it.
|
||||
-- That means a run whose start beat never arrived still gets a row (created by
|
||||
-- whatever beat did arrive) and is simply unattributed — the log survives
|
||||
-- nameless instead of being dropped for want of a name.
|
||||
CREATE TABLE IF NOT EXISTS adventure_run (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
token TEXT NOT NULL DEFAULT '', -- public board token; '' = unattributed
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
level INTEGER NOT NULL DEFAULT 0,
|
||||
zone TEXT NOT NULL DEFAULT '',
|
||||
total_rooms INTEGER NOT NULL DEFAULT 0,
|
||||
started_at INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0,
|
||||
ended_at INTEGER NOT NULL DEFAULT 0, -- 0 = still walking
|
||||
outcome TEXT NOT NULL DEFAULT '' -- cleared|died|retreated|abandoned
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_adv_run_token ON adventure_run(token, started_at DESC);
|
||||
|
||||
-- (run_id, seq) is the identity, so a re-sent batch collapses on the primary key
|
||||
-- and needs no content comparison. seq is gogobee's monotonic counter, which is
|
||||
-- also the render order — beats can arrive out of order across two batches and
|
||||
-- still read correctly.
|
||||
CREATE TABLE IF NOT EXISTS adventure_run_beat (
|
||||
run_id TEXT NOT NULL,
|
||||
seq INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
occurred_at INTEGER NOT NULL DEFAULT 0,
|
||||
room INTEGER NOT NULL DEFAULT 0,
|
||||
total_rooms INTEGER NOT NULL DEFAULT 0,
|
||||
room_kind TEXT NOT NULL DEFAULT '',
|
||||
target TEXT NOT NULL DEFAULT '',
|
||||
outcome TEXT NOT NULL DEFAULT '',
|
||||
amount INTEGER NOT NULL DEFAULT 0,
|
||||
qty INTEGER NOT NULL DEFAULT 0,
|
||||
hp INTEGER NOT NULL DEFAULT 0,
|
||||
hp_max INTEGER NOT NULL DEFAULT 0,
|
||||
crits INTEGER NOT NULL DEFAULT 0,
|
||||
fumbles INTEGER NOT NULL DEFAULT 0,
|
||||
region TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (run_id, seq)
|
||||
);
|
||||
|
||||
-- A signed-in buyer's own euro balance, as of the last snapshot gogobee pushed.
|
||||
-- Keyed by localpart (== Authentik preferred_username == the session's Username),
|
||||
-- a *separate keyspace* from the anonymous roster tokens on purpose: it is only
|
||||
|
||||
Reference in New Issue
Block a user