Move the game from a prototype architecture to a production-grade, type-safe system for AI-driven gameplay.
The main constraints for this roadmap are:
- no string-matching control flow
- no UI copy embedded in domain logic
- no direct LLM-to-world mutation path
- world state remains authoritative
- AI context and memory are explicit and testable
Completed phases:
- Phase 1: typed read-model and presenter boundary
- Phase 2: typed command/event application boundary
- Phase 3: simplified world model and validation
- Phase 4: canonical semantic vocabularies
- Phase 5: AI dialogue boundary without direct world mutation
- Phase 6: typed AI context contracts
- Phase 7: conversation memory
Not started:
- Phase 8: persistence versioning
- Phase 9: TUI runtime state machine hardening
What is true in the code now:
src/tui.rsconsumes typedUiSnapshotread models fromGameService::snapshot()src/presenter.rsowns world prose and event notice renderingsrc/domain/commands.rsandsrc/domain/events.rsdefine the typed application boundarysrc/app/service.rsowns command handling and emits typedGameEventssrc/app/read_model.rsownsUiSnapshotprojection fromGameStatesrc/app/query.rsowns shared query helpers used by both service validation and read-model projectionsrc/world.rsowns a simple, game-local authoritative world model for cities, places, NPCs, entities, player state, and active processesWorld::validate()checks world consistency directly without graph/BFO infrastructuresrc/domain/vocab.rsowns canonical enums for city and NPC semanticsGameService::newandGameService::loadvalidate worlds before gameplay begins- world generation now samples typed semantic vocabularies rather than raw string tables
src/ai/context.rsdefines the typed AI context contracts used by NPC dialoguesrc/ai/prompting.rsdefines the prompt builders that consume those typed contractssrc/llm.rsnow consumesNpcDialogueContextinstead of assembling ad hoc prompt-facing request structssrc/domain/memory.rsdefinesConversationMemoryGameStatestores per-NPC conversation memorysrc/llm.rssummarizes dialogue into conversation memory only- AI context and UI read models now include conversation history summaries without any affinity/disposition mechanic
Note:
- Earlier roadmap phases discussed AI proposal validation and affinity mechanics. Those systems were removed in favor of a simpler production baseline: NPCs only retain conversation memory, and LLM output does not propose or apply world mutations.
Save/load works, but persisted state is not schema-versioned yet. That makes future refactors riskier than they need to be.
The command boundary is typed, but modal behavior in src/tui.rs is still encoded directly in event-handler branches rather than a dedicated typed UI state machine.
Split the codebase into four layers.
Owns:
- ids
- game-local world state
- canonical enums and value types
- invariants
- domain commands and domain events
Must not own:
- UI text
- prompt strings
- provider-specific AI logic
Owns:
- orchestration
- command handling
- validation
- service coordination
- state transitions
Must expose:
- typed commands in
- typed events and read models out
Owns:
- dialogue context building
- prompt construction
- provider adapters
- conversation memory summarization
Must not:
- mutate game state directly
Owns:
- keybindings
- menus
- input modes
- rendering
Must consume:
- typed read models only
Goal:
Remove presentation formatting from simulation.
Progress:
- renamed
climodule totui - replaced
UiSnapshotstring bags likeknown_info,people,cars, andthings - added typed read-model structs for status, city, place, actors, entities, routes, and context feed
- moved world text and menu label formatting into a presenter module
- updated the TUI runtime to render from the presenter
- removed remaining duplicated display-oriented fields from the read model where nested typed views were sufficient
- added presenter-focused tests that do not instantiate the full game runtime
Status:
- Phase 1 is complete.
- Remaining UI cleanup now belongs to later phases, especially the command/event split in Phase 2 and deeper domain normalization in Phase 3 and Phase 4.
Work:
- replace
UiSnapshotwith typed view structs - remove
known_info: Vec<String>,people: Vec<String>,things: Vec<String>, and other display-oriented bags - add typed snapshot summaries such as:
PlaceSummaryRouteViewActorViewEntitySummaryContextEntry
- move all prose and label formatting into a new presenter module
Deliverables:
src/presenter.rs- typed read-model structs
tui.rsrenders from typed data only
Acceptance criteria:
- simulation no longer formats user-facing strings
- TUI no longer strips prefixes like
Time: - UI rendering tests can be written without instantiating
Game
Goal:
Stop treating Game as a monolithic service.
Progress:
- replaced string-based command return values with typed
CommandResult - introduced typed application events for travel, dialogue lifecycle, vehicle entry/exit, inspection, and waiting
- updated the TUI to render notices from typed events through the presenter
- split commands and events into dedicated modules outside
simulation.rs - replaced the monolithic
simulation::Gameservice withapp::service::GameService - made system context feed entries structured state instead of service-formatted UI strings
- removed the remaining string-returning helper methods from the application layer
Status:
- Phase 2 is complete.
- The application boundary now consists of typed commands, typed events, and a dedicated
GameService. simulation.rsis now state/read-model focused, while the TUI and presenter consume the command/event boundary instead of calling string-based action paths.
Work:
- add typed application commands:
StartDialogueSubmitDialogueLineLeaveDialogueTravelEnterVehicleExitVehicleInspectEntityWait
- add typed domain/application events:
DialogueStartedDialogueLineRecordedTravelCompletedVehicleEnteredVehicleExitedContextFeedAppended
- replace
CommandOutput { text, should_quit }with typed results
Deliverables:
src/domain/commands.rssrc/domain/events.rssrc/app/service.rs
Acceptance criteria:
- CLI dispatches typed commands only
- state transitions emit typed events
- command handlers contain validation and no UI prose
Completion notes:
src/domain/commands.rsnow owns the gameplay command surface.src/domain/events.rsnow owns the application event/result surface.src/app/service.rsis now the authoritative application service boundary.src/app/read_model.rsnow ownsUiSnapshotprojection fromGameState.src/app/query.rsnow owns shared state query helpers used by both command validation and read-model projection.src/simulation.rsnow contains state and typed read models only.- system context feed entries were converted from
{ label, text }strings into typed variants rendered bysrc/presenter.rs. - NPC reply submission now goes through
GameCommand::SubmitDialogueLinerather than a side-channel service method. GameEventpayloads now use dedicated domain refs/value types instead ofsimulationview structs.- dialogue and system feed mutations now emit typed events (
DialogueLineRecorded/ContextAppended) instead of being invisible side effects. - the TUI now consumes
GameService::snapshot()instead of accessing rawGameState. - service/query read paths were deduplicated through
src/app/query.rs.
Goal:
Replace the graph/BFO-backed prototype model with a simpler game-local world.
Progress:
- removed the text game's direct dependency on
riggy_model,riggy_ontology,bfo, andpetgraph - moved domain primitives needed by the game into local modules under
src/domain - replaced node/edge traversal with direct collections and typed ids in
src/world.rs - kept explicit
World::validate()checks for containment, residency, route, and process consistency - preserved regression coverage for invalid generated and loaded world states
- enforced world validation in
GameService::newandGameService::load
Status:
- Phase 3 is complete.
- The game now uses a simple authoritative world model, and world validation is explicit instead of implicit.
Work:
- keep only the domain state the text game actually uses
- make containment, residency, and routing direct data instead of derived graph edges
- preserve explicit validation for invalid save snapshots and bad procgen output
Deliverables:
src/world.rs- world validation API
Acceptance criteria:
- world can be validated explicitly
- invalid world states are detectable in tests
- construction code no longer depends on graph/node machinery
Completion notes:
Worldnow stores cities, places, NPCs, entities, player state, and processes directly.- local domain modules now own time, seed, vocab, records, and conversation memory for the text game.
World::validate()checks containment, residency, route endpoint rules, and NPC resident-city vs present-place-city consistency.GameService::newnow rejects invalid generated worlds before runtime starts.GameService::loadnow rejects invalid saved worlds before they can enter gameplay.- regression coverage exists for both invalid in-memory world states and invalid load-time snapshots.
Goal:
Turn world semantics into typed vocabularies.
Progress:
- added canonical enums in
src/domain/vocab.rsforBiome,Economy,Culture,NpcArchetype,Occupation,TraitTag, andGoalTag - updated
src/world.rsto store typed semantics inCityandNpc - updated procgen to sample canonical vocab values directly rather than generating semantic strings first
- updated typed read models to carry canonical semantic values into the presenter
- updated
DialogueRequestto carry canonical typed semantic fields - moved human-readable rendering of those semantics to presenter/prompt code through
.label()methods
Status:
- Phase 4 is complete.
- Core world semantics now exist as canonical types in domain state, read models, and AI request contracts.
Work:
- add enums/newtypes for:
BiomeEconomyCultureNpcArchetypeOccupationTraitTagGoalTag
- distinguish canonical tags from flavor text
- update procgen to generate typed tags first, then human-readable strings second
Deliverables:
src/domain/vocab.rs- updated world generation and AI context generation
Acceptance criteria:
- domain rules use canonical types instead of matching strings
- prompts are built from typed fields
- future simulation systems can branch on enums, not prose
Completion notes:
src/domain/vocab.rsnow owns the canonical semantic vocabulary for the world layer.City.biome,City.economy, andCity.cultureare now typed enums instead ofString.Npc.archetype,Npc.occupation,Npc.personality_traits, andNpc.goalare now typed enums/tags instead ofString.src/app/read_model.rsnow projects those canonical types directly intoUiSnapshot.src/presenter.rsnow renders semantic labels from canonical types instead of receiving preformatted semantics.src/llm.rsnow works with typed semantic fields through the typed AI context contract.- procgen still produces freeform flavor text such as district descriptions, place descriptions, names, and landmarks, but canonical simulation semantics are now separate from that flavor text.
Goal:
Ensure the LLM cannot directly mutate authoritative state.
Progress:
- removed direct LLM-to-world mutation paths
- constrained the AI layer to dialogue generation and conversation-memory summarization
- kept authoritative state mutation inside the application service
- added regression coverage for dialogue submission and memory persistence without AI-driven world actions
Status:
- Phase 5 is complete.
- The LLM boundary no longer attempts to mutate the world at all.
Work:
- remove any direct mutation path from the LLM layer
- keep LLM responsibilities limited to:
- dialogue text generation
- conversation summarization
- route all durable state mutation through typed application code
Deliverables:
- simplified
src/llm.rs - typed AI context integration through
src/ai/context.rs
Acceptance criteria:
- no LLM output is applied directly to state
- AI responsibilities are narrow and testable
Completion notes:
src/llm.rsnow returns dialogue text only.- conversation summarization is the only non-dialogue output from the AI layer.
src/app/service.rsremains the only layer that mutates durable gameplay state.- no proposal, policy, or validation submodules remain in the AI layer.
Goal:
Make AI context explicit, stable, and testable.
Progress:
- added
src/ai/context.rswithNpcDialogueContext,DialogueTurnContext, andConversationMemoryView - added
src/ai/prompting.rswith prompt builders that consume only typed context structs - updated
src/llm.rsto acceptNpcDialogueContextinstead of an ad hoc request type - moved prompt-shape tests to context-fixture tests that do not require live world state
- updated the application service to build typed AI context through
build_npc_dialogue_context
Status:
- Phase 6 is complete.
- The LLM boundary now consumes an explicit typed context contract instead of a prompt-facing request struct assembled directly inside
src/llm.rs.
Work:
- add typed context objects:
NpcDialogueContextDialogueTurnContextConversationMemoryView
- add a prompt builder that consumes only those contracts
- separate authoritative facts from presentation text
Deliverables:
src/ai/context.rssrc/ai/prompting.rs
Acceptance criteria:
- AI requests are built from typed context structs
- prompt tests use context fixtures instead of live game state
- changing prompt shape does not require touching simulation logic
Completion notes:
src/ai/context.rsnow owns the dialogue context contract.- the AI contract now owns its own transcript line and speaker types instead of embedding simulation transcript structs.
src/ai/prompting.rsnow owns dialogue prompt rendering forNpcDialogueContext.src/llm.rsnow consumesNpcDialogueContextdirectly, and no longer owns the prompt-facing request contract.- prompt rendering tests now use hand-built context fixtures instead of constructing a live
World. - the application service now builds AI context through
build_npc_dialogue_context, keeping prompt-shape assembly out of the LLM adapter layer. build_npc_dialogue_contextnow derives city and NPC facts from authoritative world ids and rejects incoherent city/NPC/session combinations.- dialogue clock values in
NpcDialogueContextnow come from authoritative game time instead of transcript-length heuristics.
Goal:
Store durable conversation state in a form the game can reason about.
Work:
- replace ephemeral transcript-only memory with a typed conversation summary object
- merge new summaries across conversations without overwriting prior context
- keep the shape minimal and focused on what was discussed
Deliverables:
src/domain/memory.rs- updated summarization path
Acceptance criteria:
- NPC memory contains durable conversation context
- AI context can include conversation summaries
Status:
- Phase 7 is complete.
- Conversation memory is now a typed domain object instead of a single summary string.
Completion notes:
src/domain/memory.rsnow ownsConversationMemorywith normalization helpers.- conversation memory updates are merged durably across conversations instead of overwriting prior context.
NpcMemoryStatestoresmemory: ConversationMemory.src/llm.rsnow summarizes conversations intoConversationMemoryfor both mock and Rig backends.src/ai/context.rsnow includes conversation memory inNpcDialogueContext.src/ai/prompting.rsnow renders conversation memory into dialogue prompts.src/app/read_model.rsandsrc/presenter.rsnow project and render conversation memory.- dialogue exit now preserves the active session if summarization fails, instead of discarding the conversation before the await succeeds.
- tests now cover memory normalization, AI context mapping, and service-level persistence of memory after dialogue.
Goal:
Make saves resilient to schema changes.
Work:
- add a save schema version
- separate persisted state from runtime-only UI/application state
- add migrations where needed
Deliverables:
src/persistence/mod.rs- versioned save schema
Acceptance criteria:
- saves can be migrated intentionally
- UI-only state is not persisted accidentally
Goal:
Make UI state machine logic explicit and testable.
Work:
- replace coarse
Mode/Menuhandling with a typed UI state machine - separate:
- focus state
- overlay state
- pending async state
- dialogue input state
- remove incidental behavior from event handlers
Deliverables:
src/ui/state.rssrc/ui/events.rs
Acceptance criteria:
Esc,Enter, and modal transitions are driven by typed state transitions- UI behavior can be tested without full terminal rendering
src/
domain/
commands.rs
events.rs
ids.rs
invariants.rs
memory.rs
vocab.rs
world.rs
app/
read_model.rs
service.rs
ai/
context.rs
prompting.rs
ui/
events.rs
presenter.rs
state.rs
tui.rs
persistence/
mod.rs
- world validation tests
- procgen determinism tests
- command precondition tests
- event emission tests
- context fixture tests
- prompt construction tests
- conversation summarization tests
- presenter tests
- UI state-machine tests
- modal transition tests
- save/load round-trip tests
- version migration tests
Recommended order for implementation:
- typed read models and presenter extraction
- typed commands and events
- simple world model and validation layer
- semantic vocab types
- simplified AI boundary
- typed AI context contracts
- conversation memory
- persistence versioning
- TUI state machine hardening
This refactor is complete when:
- no gameplay logic depends on formatted strings
- no AI output can mutate world state directly
- world invariants are explicit and testable
- UI consumes typed read models only
- AI prompt input is a typed contract
- persistence is versioned
- key gameplay flows are covered by deterministic tests
Start with Phase 8.
Reason:
- the AI and memory contracts are now typed enough that save schema churn becomes the main structural risk
- persistence is still the weakest architectural boundary left in the core runtime
- versioned saves are the cleanest next step before more schema-heavy work lands
The first concrete code change should be:
- add a versioned persisted save wrapper
- separate persisted gameplay state from runtime-only UI/application state
- define explicit migration entry points for future schema changes