The game is now self-contained: run.sh starts the TUI, which calls the LLM directly via engine.py. No external agent (OpenCode) needed. - tools/engine.py: Game engine with prompt builder, litellm client, response parser (JSON block extraction), and state persistence - tools/run.py: Refactored TUI with PLAY/CHAR/LOG/BOOK tabs. PLAY tab has streaming narrative pane, dynamic choice buttons, and text input. Game loop: scene -> input -> resolve -> archive -> apply -> scene - session/config.json: LLM provider configuration (model, api_key, etc.) - AGENTS.md: Updated to document the new architecture - tools/__init__.py: Package marker for clean imports - session/turn_description.md, turn_reaction.md: Deprecated - no longer needed now that the TUI drives the game loop internally
5.5 KiB
5.5 KiB
The Chaos — Game Architecture
This document describes the system architecture for developers and AI agents working on the codebase.
Design Principle
The Chaos is a self-contained terminal game. The TUI owns the full game loop — including LLM calls — so there is no split between a "DM agent" in chat and a "dashboard" in the terminal. The player runs one command:
python3 tools/run.py
Everything — narrative, choices, character sheet, log, archive, ambience — lives in that process.
Project Layout
the-chaos/
├── rules/ # LOCKED — game rules, do not modify
│ ├── deck/ # Card tables
│ └── mechanics.md # Core mechanics reference
├── tools/ # Game system code
│ ├── __init__.py
│ ├── engine.py # Game engine (prompt builder, LLM client, parser, state)
│ ├── run.py # TUI (Textual app, game loop, narrative, input)
│ ├── ambience.py # CLI shortcut for ambience switching
│ ├── draw.py # Card drawing tool
│ ├── music-fetch.py # YouTube audio downloader
│ ├── roll.py # Dice rolling tool
│ └── store_turn.py # DEPRECATED — use engine.py archive_turn instead
├── scripts/ # UNLOCKED — helper scripts
├── run.sh # Entry point (just calls tools/run.py)
└── session/ # Game state (read/write by engine)
├── config.json # LLM provider config
├── character.md # Player character sheet
├── world.md # Keep & Realm state
├── book.md # Story book (append-only turn archive)
├── journal.md # TODO / DONE tracking
├── ambience.md # Current ambience name
├── ambience_options.md # Ambience → file mapping
├── ambience_sources.md # Track source URLs
├── tweaks.md # House rules log
├── audio/ # Music files
└── log/ # Session logs by date
How It Works
Tools
| Tool | Role |
|---|---|
tools/engine.py |
Game engine. Owns the LLM interaction, prompt assembly, response parsing, and state persistence. Can be used standalone from the CLI for debugging. |
tools/run.py |
TUI (Textual app). Owns the game loop: display narrative → get player input → call engine → display result. |
The Game Loop (run.py)
- Mount: Load engine, build system prompt (rules + character + world + log).
- Scene: Call
engine.generate()→ receive narrative + choices. - Display: Show narrative in main pane, render choice buttons.
- Input: Player clicks a choice or types free text, presses Enter.
- Resolve: Call
engine.generate(player_action)→ receive outcome + state changes. - Archive: Append the full turn (scene + action + outcome) to
book.md. - Apply: Write state changes to
character.md,world.md,log/,ambience.md,journal.md. - Loop: Display the next scene → go to step 3.
The Engine (engine.py)
GameEngineclass loads config fromsession/config.json.build_system_prompt()assembles the DM prompt from game rules + current state.build_user_message()builds the per-turn message with player action context.generate()calls litellm, returns parsedGenerationResult.parse_response()extracts the JSON block from the LLM response.apply_state()writes state changes to session files.archive_turn()appends the narrative tobook.md.
LLM Output Format
The LLM must end every response with a JSON fenced code block:
{
"choices": ["Choice 1", "Choice 2"],
"log_entry": "- **time** — description.",
"ambience": "ambience_name_or_null",
"character_updates": null,
"world_updates": null,
"journal_add": [],
"journal_done": []
}
choices: 2-4 action options for the player.log_entry: Single-line summary appended to today's log.ambience: One of: silence, calm, combat, dungeon, forest, tavern, tension, town, wilds.character_updates: Full character sheet markdown only if HP/cash/gear/stats changed.world_updates: Full world markdown only if NPCs/locations/threads changed.journal_add/journal_done: TODO list management.
Session Config
{
"llm": {
"model": "ollama/llama3.1",
"api_key": null,
"api_base": null,
"temperature": 0.8
}
}
The model field accepts any litellm provider string: openai/gpt-4,
anthropic/claude-sonnet-4-20250514, ollama/llama3.1, groq/llama3-70b-8192,
etc. Set api_key for remote providers.
Files Still Used By Tools
| File | Purpose | Written By |
|---|---|---|
session/config.json |
LLM provider config | Manual edit |
session/character.md |
PC state | engine.py |
session/world.md |
Realm state | engine.py |
session/book.md |
Story archive | engine.py |
session/journal.md |
TODO/DONE | engine.py |
session/ambience.md |
Current ambience | engine.py |
session/log/<date>.md |
Session log | engine.py |
session/tweaks.md |
House rules | Manual edit |
Running
# Start the game
./run.sh
# Or directly
python3 tools/run.py
# No music
python3 tools/run.py --no-music
# Test a generation from CLI (no TUI)
python3 tools/engine.py --action "I head to the market"