This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine.
What I Built
Okuribi is a terminal-based, story-driven investigation game where the past refuses to stay buried.
The player is caught in a repeating loop around a mysterious incident. They explore locations, question people, investigate documents, connect clues, and eventually attempt a ritual that can bring the truth to light. Each loop gives the player another chance to notice something they missed, while failed attempts and corruption make the investigation more unsettling.
The Ghost in the Machine idea here is a haunting hidden inside a system: old records, repeated conversations, secrets people are reluctant to reveal, and traces that survive when the world resets. The player has to work out what happened instead of simply being told the answer.
The plot of this game is taken from the anime series: Another (2012)
For this submission, I focused on getting the core gameplay and agent workflow in place first. The current interface is terminal-based. I am prioritizing reliable game mechanics, NPC interactions, and archive retrieval before adding a UI.
Demo
The current version is designed to run locally in the terminal.
Suggested demo path: start the game, talk to an NPC, ask two questions in the same conversation, type
/back, then perform another game action.
Code
- Repository: Github Repo
- World Bible: The canonical source for the premise, NPC profiles, clues, documents, game rules, and ending.
-
Key implementation files:
workflow.py,game/npc_subgraph.py,game/engine_main.py,game/llm_context.py, and the World Bible.
How I Built It
I wanted the LLM to make the game feel alive without letting it decide the actual game rules. So I separated deterministic game mechanics from the language-model parts.
Current architecture
flowchart TD
PLAYER["Player<br/>Terminal input"] --> WORKFLOW
subgraph ORCHESTRATION["1. LangGraph Orchestration"]
WORKFLOW["Top-level Okuribi workflow"]
BEGIN["Begin turn<br/>Reset turn-local outputs"]
ACTIVE{"NPC conversation active?"}
WORKFLOW --> BEGIN --> ACTIVE
ACTIVE -->|No| PLANNER["Planner node<br/>Gemini + structured ActionPlan"]
PLANNER --> CLARIFY{"Plan needs clarification?"}
CLARIFY -->|Yes| ASK["Return clarification prompt"]
ASK --> PLAYER
CLARIFY -->|No| PLAN["Normalize plan into action request"]
ACTIVE -->|Yes| NPC_INPUT["Read input as active NPC question"]
NPC_INPUT --> EXIT_CHECK{"Input is /back or exit command?"}
EXIT_CHECK -->|Yes| CLEAR["Clear active_npc_id"]
CLEAR --> PLAYER
EXIT_CHECK -->|No| NPC_REQUEST["Build question_npc request<br/>for the active NPC"]
end
PLAN --> ACTION_REQUEST
NPC_REQUEST --> ACTION_REQUEST
subgraph ACTION_LAYER["2. ActionType Separation and Typed Actions"]
ACTION_REQUEST["ActionRequest<br/>Pydantic TypeAdapter validation"]
ACTION_TYPE{"Discriminate by action_type"}
ACTION_REQUEST --> ACTION_TYPE
ACTION_TYPE --> MOVE["MoveLocationAction"]
ACTION_TYPE --> INSPECT["InspectLocationAction"]
ACTION_TYPE --> TALK["TalkNPCAction"]
ACTION_TYPE --> SEARCH["SearchDocAction"]
ACTION_TYPE --> CROSS["CrossCheckAction"]
ACTION_TYPE --> RITUAL["PerformRitualAction"]
end
subgraph ENGINE["3. Deterministic Game Engine"]
VALIDATE["Common action validation<br/>and action-specific validation"]
AP["Calculate and deduct AP cost"]
DISPATCH{"Dispatch by typed action"}
EXECUTE["Execute matching action handler"]
RESULT["Authoritative ActionResult<br/>and candidate GameState"]
COMMIT["Return updated state on success<br/>Preserve original state on failure"]
LOOP_CHECK{"AP reaches zero<br/>and game not already over?"}
CALLING["Trigger The Calling<br/>Update corruption and ending state"]
VALIDATE --> AP --> DISPATCH --> EXECUTE --> RESULT --> COMMIT
COMMIT --> LOOP_CHECK
LOOP_CHECK -->|Yes| CALLING
LOOP_CHECK -->|No| ROUTE_RESULT
CALLING --> ROUTE_RESULT
end
MOVE --> VALIDATE
INSPECT --> VALIDATE
TALK --> VALIDATE
SEARCH --> VALIDATE
CROSS --> VALIDATE
RITUAL --> VALIDATE
subgraph NPC_SYSTEM["4. NPC Conversation Subgraphs"]
NPC_ROUTE{"Successful question_npc<br/>and NPC selected?"}
ENTER["Enter conversation<br/>Set active_npc_id"]
SELECT["Select subgraph using NPC ID"]
subgraph NPC_SAE["NPC_SAE Subgraph — example"]
SAE_CONTEXT["Build constrained context<br/>Persona + allowed facts + revealed clues"]
SAE_LLM["Local Ollama<br/>Gemma 3 1B"]
SAE_HISTORY["NPC-specific dialogue history<br/>NPC-scoped thread ID + MemorySaver"]
SAE_CONTEXT --> SAE_LLM --> SAE_HISTORY
end
OTHER_NPC["Other NPC-specific subgraphs<br/>Own persona, context and history"]
SELECT --> SAE_CONTEXT
SELECT --> OTHER_NPC
end
ROUTE_RESULT{"Route by action result"}
ROUTE_RESULT -->|Successful question_npc<br/>with no topic| ENTER
ENTER --> PLAYER
ROUTE_RESULT -->|Successful question_npc<br/>with topic| NPC_ROUTE
NPC_ROUTE --> SELECT
ROUTE_RESULT -->|Other action or failure| NARRATOR
SAE_HISTORY --> NARRATOR
OTHER_NPC --> NARRATOR
subgraph ARCHIVE["5. Document RAG MCP — Archive Retrieval"]
ENGINE_CLIENT["GameEngine archive_search callback<br/>search_docs_sync"]
MCP_TOOL["Document RAG MCP server<br/>query_docs tool"]
RETRIEVAL["Retrieve relevant archive documents<br/>and normalize results"]
DOC_MAP["Canonical document_clue_map<br/>from World Bible / engine config"]
EVIDENCE["Return document evidence<br/>and eligible clue IDs"]
ENGINE_CLIENT --> MCP_TOOL --> RETRIEVAL
RETRIEVAL --> EVIDENCE
DOC_MAP --> EVIDENCE
end
EXECUTE -.->|search_doc| ENGINE_CLIENT
EXECUTE -.->|cross_check may need archive evidence| ENGINE_CLIENT
EVIDENCE --> RESULT
subgraph CANONICAL["6. Canonical Game Data and State"]
WORLD["World Bible / engine configuration<br/>Locations, NPCs, documents, rules"]
STATE["GameState<br/>Location, AP, notebook, discovered clues,<br/>NPC state, corruption and phase"]
RULES["Deterministic rule mappings<br/>Action costs, clue unlock rules,<br/>ritual solution and endings"]
end
WORLD --> VALIDATE
WORLD --> EXECUTE
RULES --> VALIDATE
RULES --> EXECUTE
STATE --> VALIDATE
COMMIT --> STATE
subgraph NARRATION["7. Player-Facing Response"]
NARRATOR["Narrator node<br/>Gemini"]
OUTPUT["Format event, result and permitted evidence"]
TERMINAL["Display response in terminal"]
NARRATOR --> OUTPUT --> TERMINAL --> PLAYER
end
COMMIT --> NARRATOR
WORKFLOW -.-> CHECKPOINT["Top-level MemorySaver<br/>In-memory workflow checkpoint"]
SAE_HISTORY -.-> NPC_CHECKPOINT["Separate in-memory NPC checkpointer<br/>Scoped by session and NPC ID"]
classDef model fill:#e8e5ff,stroke:#7163a8,color:#17152b
classDef engine fill:#dff3e4,stroke:#43815a,color:#102a19
classDef retrieval fill:#fff0d9,stroke:#b47a29,color:#38250b
classDef state fill:#e0efff,stroke:#4c7da8,color:#12263a
class PLANNER,SAE_LLM,NARRATOR model
class VALIDATE,AP,DISPATCH,EXECUTE,RESULT,COMMIT,LOOP_CHECK,CALLING engine
class ENGINE_CLIENT,MCP_TOOL,RETRIEVAL,DOC_MAP,EVIDENCE retrieval
class WORLD,STATE,RULES state
The planner interprets a new request as one game action: moving, inspecting a location, talking to an NPC, searching a document, cross-checking documents, or attempting the ritual. The action is converted into a typed ActionRequest and passed to the engine.
The GameEngine is the authority. It validates actions, applies action-point costs, checks game conditions, and updates state. The LLM cannot directly unlock clues, change corruption, or decide whether a ritual succeeds.
The World Bible provides the canonical story and game data. For archive investigation, the workflow reuses my existing MCP document-search tool instead of treating the LLM's generated text as evidence.
Why Gemma?
For NPC dialogue, I use Gemma3:1B locally through Ollama. The planner and narrator currently use Gemini; Gemma is the model that plays the characters.
I chose this setup so I could experiment with NPC behavior locally, control the prompts, and keep character dialogue separate from game mechanics. Each NPC gets its own persona and knowledge context, rather than loading a separate model for every character. The same local Gemma model can play different roles through different prompts.
NPC conversations and subgraphs
Talking to an NPC is a two-stage interaction:
- The player selects an NPC, for example
Talk to NPC_SAE. - The workflow records the active NPC and asks the player what they want to ask.
- The next input is treated as a question for that NPC, not as a new global action.
- The engine validates the question action. If it succeeds, the workflow calls that NPC's subgraph.
- The NPC subgraph generates a response using Gemma and that character's recent conversation history. Typing
/backreturns to the main game planner.
The code creates a separate subgraph for each NPC ID in the World Bible, with a separate in-memory checkpointer and a thread ID scoped to the session and NPC. This keeps character conversation histories isolated from each other.
The memory is currently in-memory, not durable across application restarts. A SQLite or PostgreSQL checkpointer is a future improvement.
NPCs also have knowledge boundaries and allowed clue IDs. The engine remains responsible for whether a clue is actually discovered; NPC dialogue does not mutate the game state.
Current implementation and next steps
- In the current code: terminal interaction, typed action planning, deterministic action execution, NPC conversation routing, individual NPC subgraphs, in-memory per-NPC conversation history, World Bible-backed game data, and archive retrieval through MCP.
- Next: run end-to-end tests across all action types and NPCs, harden topic/clue unlocking, and improve fallback behavior for malformed model outputs.
- UI work: add a Streamlit interface with a separate chat branch/panel for each NPC.
- Memory work: replace in-memory checkpointers with durable storage so NPC histories survive process restarts.
Why Does Open Innovation Matter?
Open innovation makes this project practical to experiment with. I can run Gemma locally through Ollama, inspect and change prompts, define what each character is allowed to know, and keep the core game rules in code that I can test.
I can also combine tools instead of locking the project into one provider: LangGraph coordinates the workflow, Pydantic validates actions, the game engine owns deterministic mechanics, MCP exposes archive retrieval, and Gemma generates NPC dialogue locally. I can improve one part without rewriting the whole game.
For a mystery game, this separation matters. I want the model to create believable conversations, but I don't want the truth of the story to change just because a model generated something plausible.
The Spirit of It
The feeling I want to build toward is that the game remembers something the player almost forgot. A loop resets the immediate situation, but the investigation should not feel like starting from zero. Notes and discovered evidence help the player push back against the repetition, while NPCs can react through their own conversations.
I am keeping the first version small on purpose: make the rules reliable, make the NPCs feel distinct, and make the mystery worth investigating. The current terminal version is the foundation; a richer UI and durable character memory can come next.
Prize Categories
Best Use of Gemma
I am submitting Okuribi for Best Use of Gemma. Gemma 3 1B runs locally through Ollama and generates the dialogue for the game's NPCs. Each NPC has a distinct persona, dialogue style, and constrained knowledge context, and each NPC ID has its own conversation subgraph and isolated in-memory dialogue history. The deterministic game engine—not the model—controls action validation and game-state changes.
This is an MVP, and I am building it incrementally: first a reliable game loop and character interactions, then a richer interface and durable NPC memory.




Top comments (0)