DEV Community

Cover image for Okuribi — A Ghost in the Machine
Harshit Rai
Harshit Rai

Posted on Edited on

Okuribi — A Ghost in the Machine

Hacktoberfest Open-Source AI Challenge: Ghost in the Machine Submission 👻🌼

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.

  • Demo video: Watch the video

  • 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.

Gemma3:1B is configured through Ollama
Local Gemma3 invoked by the NPC conversation subgraph

Gemma3:1B is available in my local Ollama environment

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:

  1. The player selects an NPC, for example Talk to NPC_SAE.
  2. The workflow records the active NPC and asks the player what they want to ask.
  3. The next input is treated as a question for that NPC, not as a new global action.
  4. The engine validates the question action. If it succeeds, the workflow calls that NPC's subgraph.
  5. The NPC subgraph generates a response using Gemma and that character's recent conversation history. Typing /back returns 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)