adventure: give the realm a map, a board and a history

Everything Pete published so far was either the present moment (the roster,
the Siege bar) or one thing that happened (a dispatch, a run report). None of
it said what this place IS — how many zones there are, which are harder, or
whether anybody has ever actually beaten them.

Three pages off one snapshot, because they are one question and every number
on all three comes off the same scan of the same run history upstream:

  /adventure/realm      the world, in difficulty order, with who is inside it
  /adventure/standings  the board, plus Pete's own duel record
  /adventure/firsts     the hall of firsts, as a dated history

The map is deliberately not who_map.go's layout engine. That lays out a graph,
and the realm has no edges — zones aren't connected, you pick one and go.
Forcing a graph onto a set would imply a topology the game doesn't have. What
it has is an order, so tier bands are what get drawn.

A zone nobody has ever cleared looks different rather than saying so in small
text, and that styling keys off the clear count, never off whether a name is
attached — otherwise an opt-out would silently redraw a conquered place as one
nobody has come out of.

The per-kind first dot goes through a custom property instead of a
.firsts-entry-zone::before rule: input.css is in Tailwind's content glob, so a
hand-written class survives the purge only if its literal name can be lifted
out of the file, and a name glued to ::before cannot. It failed silently.

Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
This commit is contained in:
prosolis
2026-07-24 17:54:49 -07:00
parent b4a276da36
commit 1dfd3ac9fb
12 changed files with 1745 additions and 7 deletions
+14 -1
View File
@@ -102,7 +102,7 @@ func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled boo
shared []string
pages []string
}{
{"layout", []string{"_card"}, []string{"index", "channel", "weather", "bookmarks", "for-you", "status", "story", "who", "siege", "run_report"}},
{"layout", []string{"_card", "_realmnav"}, []string{"index", "channel", "weather", "bookmarks", "for-you", "status", "story", "who", "siege", "run_report", "realm", "standings", "firsts"}},
{"games_layout", []string{"_chipbar"}, []string{"games", "games_door", "blackjack", "hangman", "solitaire", "trivia", "uno", "holdem"}},
}
tpls := make(map[string]*template.Template)
@@ -251,6 +251,19 @@ func New(cfg config.WebConfig, sources []config.SourceConfig, postingEnabled boo
mux.HandleFunc("GET /api/adventure/siege", s.handleSiegeAPI)
mux.HandleFunc("GET /adventure/siege", s.handleSiegePage)
// The realm: the world map, the board, and the hall of firsts. One
// bearer-authed ingest behind all three, and all three public — every number
// on them is already public on the board or in a dispatch.
//
// Same literal-two-segment reasoning as /adventure/siege: "realm",
// "standings" and "firsts" beat /adventure/{guid} on Go's most-specific-match
// rule, and nothing is shadowed because a dispatch guid is
// "<type>:<hash>:<ts>" and can never be a bare word.
mux.HandleFunc("POST /api/ingest/realm", s.handleRealmIngest)
mux.HandleFunc("GET /adventure/realm", s.handleRealmPage)
mux.HandleFunc("GET /adventure/standings", s.handleStandingsPage)
mux.HandleFunc("GET /adventure/firsts", s.handleFirstsPage)
// Per-dispatch permalink (the article_url every ingested story points at).
// Public GET; self-gates on adv.Enabled. Distinct from GET /adventure (the
// channel listing, registered in the channels loop above).