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.
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.
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 |
- Python 3.11+
- An Anthropic API key
# Clone and install
git clone <repo-url>
cd lmtext
pip install -e .
# Set your API key
export ANTHROPIC_API_KEY="sk-ant-..."# 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 lmtextType natural language commands to play. Claude interprets your intent and responds with narration and game actions.
| 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.
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.webThen open http://127.0.0.1:8000 in your browser.
| 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.
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.
| 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). |
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- 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_flagis set (e.g., examining a bookshelf reveals a hidden key). use_withmaps a target item ID to the flag that gets set when this item is used on that target.use_requiresgates item usage behind a prerequisite flag (e.g., oil the gears before placing the crystal).consume_on_useremoves the item from inventory after a successfuluse_with. Pair withtransforms_intoto 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
endingslist. Each ending maps a flag to its ownoutro_text. The game ends when any ending flag (orwin_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_flagis set (or any flag inendings).
MIT