splinter-keep/AGENTS.md
Dejvino 4b9078d41f TUI now owns the full game loop with embedded LLM engine
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
2026-06-25 12:12:04 +02:00

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"
```