8.3 KiB
Roadmap
Source of truth for scope is CLAUDE.md. This tracks sequencing.
POC — the one question (charter §17)
Does bounded AI dialogue feel alive, or does it feel like a chatbot in a costume?
Test that before building anything else. ~30 minutes of play.
POC scope
- One town, two NPCs with authored knowledge lists
- One dungeon: three fights, one boss
- Two companions: Cadwyn, Brannoc
- Three playable classes: Sellsword, Assassin, Priest
- Luck fully wired: generation, drift, one shrine, one cursed item
- Full proxy, full role split, full canon log
- Authored fallback text for every AI surface
Out of scope (charter §17)
AI-generated story skeletons · multiplayer (never) · mobile · credits/payments/auth · companions leaving · positional combat · more than three classes.
Sequencing
Status: ✅ done · ▶ next · ○ planned. Each item is its own spec → plan → implementation cycle. Each carries its §2 side (state = code owns it · text = AI owns it · both = it is two systems), what it depends on, and the milestone goal it advances.
The milestones are ordered to reach the POC's one question as early as possible, then build the game around a validated answer:
| Milestone | Goal it proves |
|---|---|
| M0 — Foundations ✅ | The contract and the client-side engine exist and are trustworthy. |
| M1 — Prove the server loop ✅ | A real model sits behind /dm/narrate and the pipeline is reusable. |
| M2 — Prove aliveness ◀ here | Does bounded AI dialogue feel alive? — the whole POC question (§17). |
| M3 — Make it a game | The dialogue lives inside real stakes: a playable ~30-min dungeon slice. |
| M4 — POC ship-readiness | Every AI surface degrades with a voice, never an error (§13). |
M0 — Foundations ✅
- ✅ Canon log + origin JSON-Schema contract — api validation, unified 422 envelope (Plan A). §2: state · foundation.
- ✅ Client canon log engine — typed GDScript model (invariants in the model), new-game construction (origin + world + creation → canon log), turn-to-turn maintenance mutators, pure
[FACT]/[MOVE]/[ADJUST_DISPOSITION]tag extractor (Plan B). §2: state · depends on the contract.
M1 — Prove the server loop ✅
- ✅ Narrator / Ollama pipeline (server) — the shared server-side model-call pipeline (Ollama httpx client, role→model routing config, prompt loader + canon-log digest renderer, §10 call logging, single-retry → typed 502) wired through
/dm/narrate. Non-streaming. Both phases landed: (1) pipeline foundation (config/routing/prompts/ollama_client/call_log), (2) Narrator online (authorednarrator.mdbody +narrateservice + gated live smoke). Server returns raw prose; the client extracts tags. The other four roles become thin drop-ins on this pipeline. Live wire proven against qwen3.5; 49 tests. Merged todev(ddc00d4). §2: text · depends on the contract.
M2 — Prove aliveness ◀ (the POC's one question)
- ✅ Client HTTP loop — client posts its canon log to
/dm/narrate, displays the prose, harvests[FACT]via itsTagExtractorinto its own log, and shows the degraded-DM authored fallback (§13) on any non-200. First end-to-end vertical slice: state → HTTP → model → prose → screen → fact-harvest. PureDmServicecore behind an injectable transport seam; throwaway harness scene for the first on-screen prose. Live-proven against qwen3.5 through Docker (grounded scene prose, no numbers per §7). 67/67 client tests; merged todev(b949cd2). Surfaced + fixed two Docker-dev gaps in M1 (prompts not packaged, no container→host-Ollama route) — M1's live proof had been venv-on-host only. §2: state (client owns the loop, consumes text) · goal: put the Narrator on screen. - ✅ Bounded NPC conversation —
/npc/speakwith server-owned persona +knowledge[](spoilers stay off the client) and client-computedavailable_moves[]; the client validates each emitted move against live state, applies the valid, silently drops the invalid, and always keeps the prose (§6). Server role is a thin drop-in on the M1 pipeline (npc.runmirrorsnarrate.py); the client added a pureMoveValidator(membership is the whole test) + aMoveApplierthat writes town-NPC disposition toGameState.npc_dispositions(not the party log), plusNpcContent.available_movesas the single legality home. Free text goes straight to the NPC prompt (no Adjudicator needed — the moves are the NPC's). Live-proven against qwen3.5: Fenn answered in-voice, grounded strictly in his authored knowledge, no Luck leak (§7), withreveal/offer_questmoves landing. The live smoke also caught two real tag-format drifts a unit test can't — paren-less zero-arg moves and a droppedMOVE:prefix leaking into prose — both fixed by makingTagExtractortolerant/scrub-safe (backward-compatible; narrator path unaffected). Built onfeature/npc-conversation; awaiting the human's--no-ffmerge todev(§18). §2: both (text = the NPC's voice · state = the validated moves) · goal: answer the one question. - ▶ Streaming (§14) — server streams Ollama → client; latency reads as the DM is speaking rather than the game is frozen, hiding the ~2s call. Gated on aliveness: landed only after the dialogue experiment above says the prose is worth polishing. Additive to
ollama_client(shaped for it) + an SSE/chunked response + the client stream consumer. §2: text · depends on the client loop · goal: polish, not proof.
M3 — Make it a game (the playable ~30-min slice — dungeon first, per the sequencing call)
- ○ Adjudicator —
/dm/adjudicate: free text → a legal action or an in-world rejection, strict JSON (§12), small/fast model, the one-retry-with-error-appended path. Unlocks the hybrid input model (§15) the playable slice needs — the "what do you do?" boundary. §2: both (text = the in-world reason · state = the chosen action) · depends on the pipeline · goal: free-text world actions. - ○ Combat — deterministic, seeded per encounter from save state (§10); positionless turn-based; resource-management as the one interesting axis; Luck influences only crit thresholds + borderline status checks (§7). The dungeon: three fights, one boss. Combat flavor text is async, optional Narrator calls (reuses M1). "Ship the boring one" (§10) — mostly state work, low AI risk. §2: state (flavor text = a thin AI layer on top) · depends on the pipeline for flavor · goal: real stakes.
- ○ Luck fully wired — generation exists (Plan B); this adds drift events (sleeping in a real bed, a shrine's blessing, killing something praying), one shrine, one cursed item (e.g. +STR / −LCK). Visible only in prose, never as a number (§7). Gates the Improviser. §2: state · depends on the item + event systems · goal: the signature system, live.
- ○ Improviser —
/dm/improvise: minor off-script events, §7-sandboxed by construction — output passes a validator that can only apply a whitelisted set of state changes (dignity, never progress; structurally incapable of ending a quest). Fires on Luck; hooks Cadwyn. §2: both (text = the event · state = the whitelisted change) · depends on Luck being wired · goal: bad luck as a story, not a fine. - ○ Banter —
/party/banter: companions comment on recent events; low stakes, high charm, aggressively cacheable; reads Brannoc's humiliation log (§9), stacking not overwriting. Prose only, no tags. The cheapest role, landed last. §2: text · depends on the humiliation log (Plan B) + events to comment on · goal: the comedy loop closes.
M4 — POC ship-readiness
- ○ Authored fallback text — full sweep — every AI-dependent surface has authored fallback content (§13), written as writing, not error handling. Each role lands with its own fallback; this is the final pass that guarantees no surface can show the player an error. §2: text · depends on every surface existing · goal: a degraded DM still has a voice.
Deferred beyond POC
Replicate provider (prod) · auth · metering · credits/payments (§4) — retrofit onto the pipeline later, the game doesn't notice. AI-generated story skeletons (v2 — the seeded story is authored for now).