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
149 lines
5.5 KiB
Markdown
149 lines
5.5 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
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)
|
|
|
|
1. **Mount**: Load engine, build system prompt (rules + character + world + log).
|
|
2. **Scene**: Call `engine.generate()` → receive narrative + choices.
|
|
3. **Display**: Show narrative in main pane, render choice buttons.
|
|
4. **Input**: Player clicks a choice or types free text, presses Enter.
|
|
5. **Resolve**: Call `engine.generate(player_action)` → receive outcome + state changes.
|
|
6. **Archive**: Append the full turn (scene + action + outcome) to `book.md`.
|
|
7. **Apply**: Write state changes to `character.md`, `world.md`, `log/`, `ambience.md`, `journal.md`.
|
|
8. **Loop**: Display the next scene → go to step 3.
|
|
|
|
### The Engine (engine.py)
|
|
|
|
- `GameEngine` class loads config from `session/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 parsed `GenerationResult`.
|
|
- `parse_response()` extracts the JSON block from the LLM response.
|
|
- `apply_state()` writes state changes to session files.
|
|
- `archive_turn()` appends the narrative to `book.md`.
|
|
|
|
### LLM Output Format
|
|
|
|
The LLM must end every response with a JSON fenced code block:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```bash
|
|
# 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"
|
|
```
|