Files
code_of_conquest_dnd/docs/roadmap.md
Phillip Tarrant b639cab0c2 docs(roadmap): advance milestone marker M2 -> M3
M2 (prove aliveness) build-complete + live-proven; feel confirmed in playtest.
Active build focus is now M3 (make it a game), starting with the Adjudicator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 15:47:01 -05:00

12 KiB
Raw Blame History

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 Does bounded AI dialogue feel alive? — the whole POC question (§17). Build complete + live-proven; the feel is confirmed in playtest.
M3 — Make it a gamehere 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 (authored narrator.md body + narrate service + 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 to dev (ddc00d4). §2: text · depends on the contract.

M2 — Prove aliveness (the POC's one question — build complete + live-proven; the feel is confirmed in playtest)

  • Client HTTP loop — client posts its canon log to /dm/narrate, displays the prose, harvests [FACT] via its TagExtractor into 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. Pure DmService core 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 to dev (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/speak with server-owned persona + knowledge[] (spoilers stay off the client) and client-computed available_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.run mirrors narrate.py); the client added a pure MoveValidator (membership is the whole test) + a MoveApplier that writes town-NPC disposition to GameState.npc_dispositions (not the party log), plus NpcContent.available_moves as 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), with reveal/offer_quest moves landing. The live smoke also caught two real tag-format drifts a unit test can't — paren-less zero-arg moves and a dropped MOVE: prefix leaking into prose — both fixed by making TagExtractor tolerant/scrub-safe (backward-compatible; narrator path unaffected). Built on feature/npc-conversation; awaiting the human's --no-ff merge to dev (§18). §2: both (text = the NPC's voice · state = the validated moves) · goal: answer the one question.
  • Considering-state + transport timeout — while a model call is in flight the client rotates authored in-character "the Master considers…" lines (§13 spirit — even loading is content; ConsideringPhrases + ConsideringIndicator, 3.0s cadence) instead of a frozen surface, and HttpTransport sets a request timeout (35s, rejects ≤0) so a hung call degrades to the authored fallback instead of spinning forever. Meets §14's anti-freeze goal by reframing the wait, on the proven non-streaming loop, with no new failure surface. Human-confirmed live including the degrade path (API down → fallback). Merged to dev (099714d); 110/110 client tests. §2: text/UI · goal: the wait is framed (in-character) and bounded (never hangs).
  • Streaming (§14)SHELVED for the POC. Real token streaming would genuinely hide the ~2s call, but it is the POC's largest transport rework (client HTTPRequest → poll-based HTTPClient) and adds mid-stream error/tag-boundary failure surface not worth the risk. The considering-state above reframes the wait instead and is tested and live. Design kept as a record (specs/2026-07-10-narrator-streaming-design.md, banner SHELVED); revive only if playtest proves the wait genuinely hurts. Conscious deviation from §14's "stream everything," recorded per §18.

M3 — Make it a game ◀ here — (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).

NPC & relationship depth (v2)

Three ideas mined from NarrativeEngine-P (MIT) — design borrowed, not code. All three are §2: state (code owns them; the AI only voices from what code decides), so they strengthen the §2 boundary rather than loosen it. None is in POC scope (§17); they fold into the NPC role (§6) and canon log (§11) once bounded dialogue is proven alive. Sequence them after M3's Adjudicator, since they enrich the same conversation surface.

  • Directional dispositions — split the single per-pair disposition into two directed fields (A→B and B→A), so liking is asymmetric. Unrequited regard, undisclosed betrayal (player still trusts the captain who already sold them out), and the Cadwyn↔Brannoc mutual-but-differently-flavored dislike (§9) all become expressible. Player→NPC gates dialogue tone; NPC→player gates offered moves (§6 available_moves) — two arrows, two levers. Extends party[] in the canon log (§11) and npc_dispositions on the client; keep the 100..100 range, just double the field. §2: state · depends on the disposition model existing (M2, done) · goal: relationships that aren't symmetric.
  • Witness tracking — every logged event carries a witnesses: [...] list; a fact enters an NPC's knowledge[] (§6) only if they witnessed it or a witness told them. This is the missing input-side rule for the knowledge list — CLAUDE.md §6 says the list is "the entire content the NPC has to draw on" but never says how a fact gets in. Structurally prevents the blacksmith spoiling a twist he never saw, gives shady acts real "were you seen?" stakes without a stealth system, and turns rumor spread into a code operation (witness meets NPC → code copies the fact, optionally degraded) rather than AI invention. Adds a field to recent_events[]/established_facts[] (§11) and a propagation rule on character encounters. §2: state · depends on the canon log event model (M0, done) + knowledge lists (§6) · goal: NPCs know only what they could plausibly know.
  • Personality Hexagon persona — a richer NPC persona schema: six axes (Drive, Diligence, Boldness, Warmth, Empathy, Composure) supplied to the NPC prompt (§6) as structured voice-shaping input, replacing free-form persona blurbs. Server-owned (like knowledge[]), so it never ships to the client. Purely additive to the §6 prompt contract — it steers how an NPC speaks, not what it may do (the move vocabulary is unchanged). §2: state (the schema) feeding text (the voice) · depends on the NPC role (M2, done) · goal: consistent, distinct NPC voices from data, not prose.