Skip to content

shaman247/lmtext

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lmtext

An LLM-powered text adventure engine that uses Claude as the dungeon master. Define your world in YAML, and Claude brings it to life — narrating scenes, voicing characters, and managing game mechanics through structured tool calls.

Play in the terminal or in a browser with the built-in web UI (game text, inventory, interactive map, and debug panes).

Ships with three adventures:

  • The Tomb of the Clockmaker — Explore an underground tomb, solve puzzles, and repair a mysterious clock for the ghost of its creator.
  • The Tides of Salthollow — Play as an imperial tax collector sent to a remote island. Investigate a governor's death, interrogate five suspects, and uncover a murder conspiracy. Features three distinct endings based on what evidence you gather and how you file your report.
  • The House That Hungers — Wake with no memory inside a living, predatory house that absorbs its guests. Solve escape-room puzzles, survive encounters with the fused-to-his-chair Host, and find a way out — or take his place. Three endings ranging from freedom to something far worse.

How it works

The engine maintains all game state (rooms, items, inventory, flags) in Python, while Claude handles narration and player intent. Claude never modifies state directly — it calls tools (move_player, add_to_inventory, set_flag, etc.) that the engine validates and executes. This keeps the game consistent while giving Claude full creative freedom in storytelling. The system prompt includes only items and characters relevant to the current context (visible in the room, in inventory, or referenced by use_with targets), keeping token usage low even in large worlds.

Architecture

Player input
    │
    ▼
┌──────────┐     tool calls      ┌──────────┐
│  Claude   │ ──────────────────▶ │  Engine  │
│  (DM)     │ ◀──────────────────  │  (State) │
└──────────┘     tool results     └──────────┘
    │
    ▼
Narration output

Key modules:

Module Role
models.py Data models — World, Room, Item, Character, Exit, Ending
state.py Runtime game state — position, inventory, flags, visited rooms
llm.py Claude integration — system prompt, conversation history, tool definitions
actions.py Tool handlers — validates and executes state changes
engine.py Main game loop — ties everything together
loader.py YAML world file parser with validation
checker.py Solvability checker — verifies the puzzle chain is completable
save.py Save/load — persists game state to JSON
display.py Terminal UI — colored text, HUD, text wrapping
web.py Web UI — FastAPI backend, WebSocket game sessions

Requirements

Setup

# Clone and install
git clone <repo-url>
cd lmtext
pip install -e .

# Set your API key
export ANTHROPIC_API_KEY="sk-ant-..."

Terminal UI

# Run with a specific world
lmtext worlds/tomb_of_the_clockmaker/world.yaml

# Or auto-discover worlds (scans worlds/**/world.yaml)
lmtext

# Run as a module
python -m lmtext

Type natural language commands to play. Claude interprets your intent and responds with narration and game actions.

In-game commands

Command Effect
save Save current game state
load Restore last saved game
debug Toggle debug mode (shows tool calls and token usage)
quit Exit the game

Games auto-save after every turn to ~/.lmtext/saves/. On startup, the engine offers to resume from an existing save. Save files are deleted upon winning.

Web UI

The web UI provides a multi-pane browser interface with real-time streaming narration, an interactive map, inventory tracking, and a debug panel.

# Install with web dependencies
pip install -e ".[web]"

# Start the server
lmtext-web

# Or run as a module
python -m lmtext.web

Then open http://127.0.0.1:8000 in your browser.

Panes

Pane Description
Game text Streams narration as Claude generates it. Items, characters, and directions are color-highlighted. Each turn is visually separated.
Map SVG graph of explored rooms. Current room is highlighted blue, visited rooms are solid, adjacent unexplored rooms are dashed outlines. Locked exits shown in red. The map grows as you explore.
Inventory Live-updated list of held items with any annotations (e.g., "broken", "soaking wet").
Debug Collapsible panel showing tool calls, parameters, success/failure, and state changes for each turn. Hidden by default — click "show" to expand.

The sidebar also shows the current room name, available exits, and any characters present.

How it works

The web UI reuses the same core engine (models, state, actions, LLM client, loader, save system) as the terminal version. A FastAPI server handles HTTP and WebSocket connections. Each browser tab gets an independent game session. Narration is streamed in real-time over WebSocket — the synchronous Anthropic client runs in a thread and bridges chunks through an async queue.

Save/resume works across sessions: if a save exists for a world, you're prompted to resume or start fresh when selecting it.

Environment variables

Variable Effect
ANTHROPIC_API_KEY Required. Your Anthropic API key.
LMTEXT_DEBUG Debug mode is on by default. Set to 0 to disable.
LMTEXT_HOST Web UI bind address (default: 127.0.0.1).
LMTEXT_PORT Web UI port (default: 8000).

Creating your own world

Worlds are defined in a single YAML file. Here's the structure:

title: "My Adventure"
start_room: entrance
win_flag: quest_complete       # Flag that triggers the win (default: "game_complete")

intro_text: |
  Text displayed when the game starts.

outro_text: |
  Default text displayed when the game is won.

# Optional: multiple endings based on which flag triggers the win
endings:
  - flag: quest_complete
    outro_text: |
      The good ending.
  - flag: quest_failed
    outro_text: |
      The bad ending.

rooms:
  entrance:
    name: "The Entrance"
    description: |
      A description of the room.
    exits:
      - direction: north
        target: next_room
      - direction: east
        target: locked_room
        locked: true
        required_flag: has_key
    items: [item_id]
    characters: [npc_id]

    # Optional: description changes when flags are set
    conditional_descriptions:
      - condition: room_transformed
        description: |
          The room looks completely different now.

    # Optional: triggers that fire when the player enters
    on_enter:
      - message: "A chill runs down your spine."
        condition: has_amulet        # Only fires if this flag is set (null = always)
        sets_flag: entered_crypt     # Sets this flag when triggered

items:
  item_id:
    name: "a shiny item"
    description: "Short description shown in room."
    examine_text: |
      Detailed text shown when the player examines the item.
    portable: true              # Can the player pick this up?
    hidden: false               # Hidden until revealed?
    reveal_flag: some_flag      # Flag that reveals this item
    use_with:
      target_item: flag_to_set  # Using this item on target sets a flag
    use_requires:
      target_item: prerequisite_flag  # Must have this flag before use
    consume_on_use: false       # Remove from inventory after use?
    transforms_into: new_item   # If consumed, add this item instead

characters:
  npc_id:
    name: "a mysterious figure"
    description: |
      What the player sees when they look at this character.
    personality: |
      Instructions for how Claude should voice this character.
    dialogue:
      - condition: null          # Always available (first meeting)
        text: "Hello, traveler."
        sets_flag: met_npc
      - condition: met_npc       # Only after first meeting
        text: "You again!"
    # Optional: topic-based dialogue ("ask npc about weather")
    topics:
      weather:
        text: "Looks like rain."
        condition: met_npc       # Only available after meeting
        sets_flag: discussed_weather

Key concepts

  • Flags are string tokens that track game progression. Items can set flags when used, dialogue can set flags when spoken, and exits can require flags to unlock.
  • Hidden items don't appear until a reveal_flag is set (e.g., examining a bookshelf reveals a hidden key).
  • use_with maps a target item ID to the flag that gets set when this item is used on that target.
  • use_requires gates item usage behind a prerequisite flag (e.g., oil the gears before placing the crystal).
  • consume_on_use removes the item from inventory after a successful use_with. Pair with transforms_into to replace it with another item (e.g., a full bottle becomes an empty bottle).
  • Conditional descriptions let rooms change their description based on active flags.
  • Room entry triggers fire automatically when a player enters a room, optionally gated by a condition flag.
  • Topic-based dialogue lets players ask characters about specific subjects. Falls back to progression-based dialogue if no topic matches.
  • Multiple endings are supported via the endings list. Each ending maps a flag to its own outro_text. The game ends when any ending flag (or win_flag) is set.
  • Annotations are freeform strings the LLM attaches to items or characters to track dynamic state (e.g., "broken", "following player"). Annotated items can't be used until the annotation is cleared.
  • Solvability checking runs automatically at load time. The engine traces the puzzle dependency graph and warns if the win condition is unreachable.
  • The game ends when the win_flag is set (or any flag in endings).

License

MIT

About

LLM-powered text adventure engine

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors