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

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)

  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:

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