diff --git a/.gitignore b/.gitignore index ef7dd07..26daed3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ __pycache__/ session/audio/ llm.log config.json +saves/ +themes/*/audio/*.mp3 diff --git a/AGENTS.md b/AGENTS.md index 433d99f..e7ae066 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,37 +20,49 @@ in that process. ``` the-chaos/ -├── rules/ # LOCKED — game rules, do not modify -│ ├── deck/ # Card tables -│ ├── mechanics.md # Core mechanics reference -│ ├── core_mechanics.md # Condensed core rules (injected into system prompt) -│ ├── character_creation.md # Character creation rules -│ └── end_game.md # End-game closure rules +├── themes/ # SHARED theme packs (versioned) +│ └── default/ +│ ├── theme.json # Theme metadata +│ ├── rules/ # LOCKED — game rules +│ │ ├── deck/ # Card tables +│ │ ├── mechanics.md +│ │ ├── core_mechanics.md +│ │ ├── character_creation.md +│ │ └── end_game.md +│ ├── audio/ # Baseline ambience tracks +│ ├── ambience_options.md +│ └── character_template.md ├── 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) +│ ├── engine.py # Game engine +│ ├── run.py # TUI (Textual app) │ ├── ambience.py # CLI shortcut for ambience switching │ ├── draw_card.py # Card drawing tool │ ├── music-fetch.py # YouTube audio downloader │ ├── roll_dice.py # Dice rolling tool -│ ├── store_turn.py # DEPRECATED — use engine.py archive_turn instead │ ├── test_imports.py # Import validation test │ └── test_runtime.py # Runtime import test ├── 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 +├── session/ # Game state (read/write by engine) +│ ├── config.json # LLM provider config +│ ├── current_theme # Active theme id (e.g. "default") +│ ├── character.md # Player character sheet (evolved) +│ ├── world.md # Keep & Realm state (evolved) +│ ├── book.md # Story book (append-only turn archive) +│ ├── journal.md # TODO / DONE tracking +│ ├── ambience.md # Current ambience name +│ ├── ambience_options.md # Working copy (seeded from theme) +│ ├── ambience_sources.md # Track download URLs +│ ├── tweaks.md # House rules log +│ ├── audio/ # Working copy (seeded from theme) +│ └── log/ # Session logs by date +└── saves/ # Save slots (gitignored, per session) + ├── _autosave/ + │ ├── metadata.json # {hero, theme, timestamp, preview} + │ └── ... # session state snapshot (no audio) + └── my-campaign/ + └── ... ``` ## How It Works @@ -348,15 +360,35 @@ This allows compatibility with OpenAI-compatible servers that return content in ## Session Files - `session/config.json` — LLM config (edit directly) +- `session/current_theme` — Active theme id (written by engine on init) - `session/character.md` — PC state (written by engine) - `session/world.md` — Realm state (written by engine) - `session/book.md` — Story archive (written by engine) - `session/journal.md` — TODO/DONE list (written by engine) - `session/ambience.md` — Current ambience (written by engine) +- `session/ambience_options.md` — Working copy of ambience config (seeded from theme) - `session/log/.md` — Session logs (written by engine) - `session/tweaks.md` — House rules (manual edit) - `archive/-/` — Archived completed games +## Save Slots + +- `saves//` — Full session snapshots (no audio, includes theme ref) +- `saves/_autosave/` — Auto-save before loading another slot +- API in `state.py`: `save_game()`, `load_game()`, `list_saves()`, `delete_save()` + +## Theme Management + +Themes live in `themes//` and contain: +- `theme.json` — `{id, name, version, description}` +- `rules/` — Game mechanics (read by engine at runtime) +- `audio/` — Baseline ambience tracks (seeded to session/ on new game) +- `ambience_options.md` — Baseline ambience → file mapping +- `character_template.md` — Starting character sheet template + +Engine reads rules from `themes//rules/` dynamically. +`session/ambience_options.md` and `session/audio/` are working copies seeded from the theme on init. + ## LLM Strategies Explained ### "conversational" Strategy diff --git a/themes/default/ambience_options.md b/themes/default/ambience_options.md new file mode 100644 index 0000000..38a5105 --- /dev/null +++ b/themes/default/ambience_options.md @@ -0,0 +1,82 @@ +# Ambience Options + +Set the current ambience by writing its name into `session/ambience.md`. +The TUI polls this file and crossfades to the matching track. + +Music files go in `session/audio/`. Supported formats: `.mp3`, `.ogg`, `.wav`. +When multiple files are listed, one is chosen at random each time the ambience activates. + +## Requirements + +```bash +pip install miniaudio yt-dlp +``` +`ffmpeg` must also be installed on your system. + +## Fetching New Tracks + +Use the music-fetch tool to search YouTube and download tracks: + +```bash +# Auto-search for a tavern track +python3 tools/music-fetch.py tavern + +# Custom query +python3 tools/music-fetch.py "deep fen" "swamp ambience D&D" + +# Specific video +python3 tools/music-fetch.py tavern --url "https://youtu.be/..." + +# Replace all tracks for an ambience +python3 tools/music-fetch.py tavern --replace + +# Preview without downloading +python3 tools/music-fetch.py tavern --dry-run + +# Allow tracks longer than 10 minutes +python3 tools/music-fetch.py tavern --allow-long-songs +``` + +Sources are recorded in `session/ambience_sources.md` so tracks can be +re-downloaded without keeping audio files in git. + +## Available Ambiences + +| Ambience | Files | +|----------|-------| +| silence | (stops all music) | +| calm | calm_01.ogg | +| combat | combat_01.ogg | +| dungeon | dungeon_01.ogg, dungeon_02.ogg | +| forest | forest_01.ogg | +| tavern | tavern_01.ogg | +| tension | tension_01.ogg | +| town | town_01.ogg | +| wilds | wilds_01.ogg | + +## Usage (DM) + +```bash +# Switch to forest ambience +echo "forest" > session/ambience.md + +# Stop music +echo "silence" > session/ambience.md +``` + +Or use the companion CLI shortcut: + +```bash +python3 tools/ambience.py forest +python3 tools/ambience.py silence +``` + +## Status Display + +The TUI status bar shows the current ambience: + +``` +Dillion ❤ 10 │ 42 entries │ 3 todo │ 2026-06-24 │ ♫ tavern +``` + +If miniaudio is not installed, the status bar shows a hint and no audio plays. diff --git a/themes/default/audio/calm_01.ogg b/themes/default/audio/calm_01.ogg new file mode 100644 index 0000000..848cae9 Binary files /dev/null and b/themes/default/audio/calm_01.ogg differ diff --git a/themes/default/audio/combat_01.ogg b/themes/default/audio/combat_01.ogg new file mode 100644 index 0000000..544d51e Binary files /dev/null and b/themes/default/audio/combat_01.ogg differ diff --git a/themes/default/audio/dungeon_01.ogg b/themes/default/audio/dungeon_01.ogg new file mode 100644 index 0000000..5bcf68d Binary files /dev/null and b/themes/default/audio/dungeon_01.ogg differ diff --git a/themes/default/audio/dungeon_02.ogg b/themes/default/audio/dungeon_02.ogg new file mode 100644 index 0000000..dd8a81b Binary files /dev/null and b/themes/default/audio/dungeon_02.ogg differ diff --git a/themes/default/audio/forest_01.ogg b/themes/default/audio/forest_01.ogg new file mode 100644 index 0000000..9d95d5d Binary files /dev/null and b/themes/default/audio/forest_01.ogg differ diff --git a/themes/default/audio/tavern_01.ogg b/themes/default/audio/tavern_01.ogg new file mode 100644 index 0000000..08e377f Binary files /dev/null and b/themes/default/audio/tavern_01.ogg differ diff --git a/themes/default/audio/tension_01.ogg b/themes/default/audio/tension_01.ogg new file mode 100644 index 0000000..906bffd Binary files /dev/null and b/themes/default/audio/tension_01.ogg differ diff --git a/themes/default/audio/town_01.ogg b/themes/default/audio/town_01.ogg new file mode 100644 index 0000000..ca369bc Binary files /dev/null and b/themes/default/audio/town_01.ogg differ diff --git a/themes/default/audio/wilds_01.ogg b/themes/default/audio/wilds_01.ogg new file mode 100644 index 0000000..a82bf9e Binary files /dev/null and b/themes/default/audio/wilds_01.ogg differ diff --git a/themes/default/character_template.md b/themes/default/character_template.md new file mode 100644 index 0000000..33dace6 --- /dev/null +++ b/themes/default/character_template.md @@ -0,0 +1,28 @@ +# Character + +**Name:** +**Thing:** +**Failed Career:** +**What's Gone Wrong:** + +## Traits + +- **STR:** 10 +- **DEX:** 10 +- **WIL:** 10 + +## Vitals + +- **Max Health:** 10 +- **Current Health:** 10 +- **Armour:** None +- **Weapon:** Unarmed (1d4) +- **Cash:** 0 + +## Chains + +## Discords + +## Gear + +## Notes & Scribbles diff --git a/rules/character_creation.md b/themes/default/rules/character_creation.md similarity index 100% rename from rules/character_creation.md rename to themes/default/rules/character_creation.md diff --git a/rules/core_mechanics.md b/themes/default/rules/core_mechanics.md similarity index 100% rename from rules/core_mechanics.md rename to themes/default/rules/core_mechanics.md diff --git a/rules/deck/cook.yaml b/themes/default/rules/deck/cook.yaml similarity index 100% rename from rules/deck/cook.yaml rename to themes/default/rules/deck/cook.yaml diff --git a/rules/deck/creatures.yaml b/themes/default/rules/deck/creatures.yaml similarity index 100% rename from rules/deck/creatures.yaml rename to themes/default/rules/deck/creatures.yaml diff --git a/rules/deck/curiosities.yaml b/themes/default/rules/deck/curiosities.yaml similarity index 100% rename from rules/deck/curiosities.yaml rename to themes/default/rules/deck/curiosities.yaml diff --git a/rules/deck/souls.yaml b/themes/default/rules/deck/souls.yaml similarity index 100% rename from rules/deck/souls.yaml rename to themes/default/rules/deck/souls.yaml diff --git a/rules/end_game.md b/themes/default/rules/end_game.md similarity index 100% rename from rules/end_game.md rename to themes/default/rules/end_game.md diff --git a/rules/mechanics.md b/themes/default/rules/mechanics.md similarity index 100% rename from rules/mechanics.md rename to themes/default/rules/mechanics.md diff --git a/themes/default/theme.json b/themes/default/theme.json new file mode 100644 index 0000000..2e5662a --- /dev/null +++ b/themes/default/theme.json @@ -0,0 +1,6 @@ +{ + "id": "default", + "name": "The Chaos — Default", + "version": "1.0.0", + "description": "The base game: a dark fantasy realm of wild magic, ancient ruins, and fragile hope." +} diff --git a/tools/draw_card.py b/tools/draw_card.py index af28606..6c06f71 100755 --- a/tools/draw_card.py +++ b/tools/draw_card.py @@ -20,7 +20,7 @@ import yaml import random import os -DECK_DIR = os.path.join(os.path.dirname(__file__), '..', 'rules', 'deck') +DECK_DIR = os.path.join(os.path.dirname(__file__), '..', 'themes', 'default', 'rules', 'deck') DECKS = { 'souls': 'souls.yaml', diff --git a/tools/engine.py b/tools/engine.py index 17b6369..6c88448 100644 --- a/tools/engine.py +++ b/tools/engine.py @@ -16,7 +16,7 @@ from engine_lib.tools_handler import execute_tool, describe_change, extract_tool from engine_lib.parsing import log_turn_details from engine_lib import state from engine_lib.llm import call_llm -from engine_lib.paths import CHARACTER_CREATION_PATH, RULES_INJECTION_PATH +from engine_lib.paths import get_character_creation_path, RULES_INJECTION_PATH class GameEngine: @@ -74,7 +74,7 @@ class GameEngine: is_new_game = not player_action and not recent_narrative system = build_system_prompt(recent_narrative=recent_narrative, recent_log=session_log) if is_new_game: - cc = state.read_file(CHARACTER_CREATION_PATH) + cc = state.read_file(get_character_creation_path()) if cc: system += f"\n\n## Character Creation Reference\n{cc}" state.append_llm_log(f"\n[NEW GAME] injected character_creation.md ({len(cc)} chars)") diff --git a/tools/engine_lib/context.py b/tools/engine_lib/context.py index d2c7ca8..c7db22e 100644 --- a/tools/engine_lib/context.py +++ b/tools/engine_lib/context.py @@ -1,6 +1,6 @@ from __future__ import annotations -from .paths import CHAR_PATH, WORLD_PATH, JOURNAL_PATH, CORE_RULES_PATH, RULES_INJECTION_PATH +from .paths import CHAR_PATH, WORLD_PATH, JOURNAL_PATH, get_core_rules_path, RULES_INJECTION_PATH from .prompts import SYSTEM_PROMPT from . import state @@ -12,7 +12,7 @@ def build_system_prompt(recent_narrative: str | None = None, recent_log: str | N log = recent_log if recent_log is not None else state.read_recent_log() journal = state.read_file(JOURNAL_PATH) or "*No journal entries.*" story = recent_narrative if recent_narrative is not None else state.read_recent_book(2) - core_rules = state.read_file(CORE_RULES_PATH) or "*No core rules file.*" + core_rules = state.read_file(get_core_rules_path()) or "*No core rules file.*" extra = state.read_file(RULES_INJECTION_PATH) extra_section = f"\n\n## Full Mechanics Reference\n{extra}" if extra else "" return SYSTEM_PROMPT.substitute( diff --git a/tools/engine_lib/paths.py b/tools/engine_lib/paths.py index e5a250a..8832178 100644 --- a/tools/engine_lib/paths.py +++ b/tools/engine_lib/paths.py @@ -1,18 +1,11 @@ -#!/usr/bin/env python3 -""" -paths.py — Path constants for The Chaos game engine. -""" - from __future__ import annotations from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent -RULES_DIR = BASE_DIR / 'rules' -CORE_RULES_PATH = RULES_DIR / 'core_mechanics.md' -MECHANICS_PATH = RULES_DIR / 'mechanics.md' -CHARACTER_CREATION_PATH = RULES_DIR / 'character_creation.md' +THEMES_DIR = BASE_DIR / 'themes' + SESSION_DIR = BASE_DIR / 'session' CONFIG_PATH = SESSION_DIR / 'config.json' CHAR_PATH = SESSION_DIR / 'character.md' @@ -27,6 +20,48 @@ CHANGES_PATH = SESSION_DIR / "changes.md" RULES_INJECTION_PATH = SESSION_DIR / "rules_injection.md" META_LOG_PATH = SESSION_DIR / "meta_log.md" AUDIO_DIR = SESSION_DIR / "audio" +ACTIVE_THEME_PATH = SESSION_DIR / "current_theme" -END_GAME_PATH = RULES_DIR / 'end_game.md' ARCHIVE_DIR = BASE_DIR / 'archive' + + +def get_active_theme_id() -> str: + if ACTIVE_THEME_PATH.exists(): + return ACTIVE_THEME_PATH.read_text().strip() or "default" + return "default" + + +def get_theme_dir(theme_id: str | None = None) -> Path: + return THEMES_DIR / (theme_id or get_active_theme_id()) + + +def get_rules_dir(theme_id: str | None = None) -> Path: + return get_theme_dir(theme_id) / "rules" + + +def get_core_rules_path(theme_id: str | None = None) -> Path: + return get_rules_dir(theme_id) / "core_mechanics.md" + + +def get_mechanics_path(theme_id: str | None = None) -> Path: + return get_rules_dir(theme_id) / "mechanics.md" + + +def get_character_creation_path(theme_id: str | None = None) -> Path: + return get_rules_dir(theme_id) / "character_creation.md" + + +def get_end_game_path(theme_id: str | None = None) -> Path: + return get_rules_dir(theme_id) / "end_game.md" + + +def get_character_template_path(theme_id: str | None = None) -> Path: + return get_theme_dir(theme_id) / "character_template.md" + + +def get_theme_audio_dir(theme_id: str | None = None) -> Path: + return get_theme_dir(theme_id) / "audio" + + +def get_theme_ambience_options_path(theme_id: str | None = None) -> Path: + return get_theme_dir(theme_id) / "ambience_options.md" diff --git a/tools/engine_lib/state.py b/tools/engine_lib/state.py index 98c6737..2a2e67a 100644 --- a/tools/engine_lib/state.py +++ b/tools/engine_lib/state.py @@ -17,7 +17,8 @@ from pathlib import Path from .paths import ( CHAR_PATH, WORLD_PATH, BOOK_PATH, JOURNAL_PATH, AMBIENCE_PATH, LOG_PATH, LLM_LOG_PATH, AMBIENCE_OPTIONS_PATH, CHANGES_PATH, - META_LOG_PATH, AUDIO_DIR, SESSION_DIR, ARCHIVE_DIR, + META_LOG_PATH, AUDIO_DIR, SESSION_DIR, ARCHIVE_DIR, THEMES_DIR, + ACTIVE_THEME_PATH, ) from .models import TurnResult @@ -277,3 +278,185 @@ def archive_session() -> str: shutil.rmtree(child) return str(archive_dir) + + +# ── Theme management ───────────────────────────────────────── + +def list_themes() -> list[dict]: + """Scan themes/ and return metadata for each available theme.""" + if not THEMES_DIR.exists(): + return [{"id": "default", "name": "Default", "description": "Built-in theme"}] + themes = [] + for d in sorted(THEMES_DIR.iterdir()): + if not d.is_dir(): + continue + meta_path = d / "theme.json" + if meta_path.exists(): + try: + import json + meta = json.loads(meta_path.read_text()) + except Exception: + meta = {} + else: + meta = {} + themes.append({ + "id": meta.get("id", d.name), + "name": meta.get("name", d.name), + "version": meta.get("version", "0.0.0"), + "description": meta.get("description", ""), + }) + return themes + + +def get_active_theme() -> str: + """Read the active theme id from session/current_theme.""" + if ACTIVE_THEME_PATH.exists(): + return ACTIVE_THEME_PATH.read_text().strip() or "default" + return "default" + + +def set_active_theme(theme_id: str) -> None: + """Write the active theme id to session/current_theme.""" + ACTIVE_THEME_PATH.parent.mkdir(parents=True, exist_ok=True) + ACTIVE_THEME_PATH.write_text(theme_id.strip() + "\n") + + +def init_session_from_theme(theme_id: str | None = None) -> None: + """Seed session files from a theme. Only copies files that don't exist yet.""" + from .paths import ( + get_theme_dir, get_character_template_path, get_theme_audio_dir, + get_theme_ambience_options_path, + ) + tid = theme_id or get_active_theme() + theme_dir = get_theme_dir(tid) + + set_active_theme(tid) + + # Character template + tmpl = get_character_template_path(tid) + if tmpl.exists() and not CHAR_PATH.exists(): + shutil.copy2(tmpl, CHAR_PATH) + + # World template (optional) + world_tmpl = theme_dir / "world_template.md" + if world_tmpl.exists() and not WORLD_PATH.exists(): + shutil.copy2(world_tmpl, WORLD_PATH) + + # Ambience options baseline + ao = get_theme_ambience_options_path(tid) + if ao.exists() and not AMBIENCE_OPTIONS_PATH.exists(): + shutil.copy2(ao, AMBIENCE_OPTIONS_PATH) + + # Audio files (seed if session/audio/ is empty) + theme_audio = get_theme_audio_dir(tid) + if theme_audio.exists(): + AUDIO_DIR.mkdir(parents=True, exist_ok=True) + existing = set(f.name for f in AUDIO_DIR.iterdir()) if AUDIO_DIR.exists() else set() + for f in theme_audio.iterdir(): + if f.is_file() and f.name not in existing: + shutil.copy2(f, AUDIO_DIR) + + +# ── Save / Load ───────────────────────────────────────────── + +SAVES_DIR = Path(__file__).resolve().parent.parent.parent / 'saves' + + +def save_game(slot_name: str) -> str: + """Save current session to saves//. + Excludes audio/ (seeded from theme). Returns the save path.""" + slot_dir = SAVES_DIR / slot_name + slot_dir.mkdir(parents=True, exist_ok=True) + + # Copy session files (excluding audio/) + for child in SESSION_DIR.iterdir(): + if child.name == "audio": + continue + if child.is_file(): + shutil.copy2(child, slot_dir / child.name) + elif child.is_dir(): + shutil.copytree(child, slot_dir / child.name, dirs_exist_ok=True) + + # Write metadata + hero = extract_hero_name() + theme = get_active_theme() + narrative = read_recent_book(1) + preview = narrative.strip()[:200] if narrative else "" + meta = { + "hero": hero, + "theme": theme, + "timestamp": datetime.now().isoformat(), + "preview": preview, + } + import json + (slot_dir / "metadata.json").write_text(json.dumps(meta, indent=2) + "\n") + return str(slot_dir) + + +def load_game(slot_name: str) -> str: + """Load session from saves// back into session/. + Returns the hero name for UI reload.""" + slot_dir = SAVES_DIR / slot_name + if not slot_dir.exists(): + raise FileNotFoundError(f"Save slot not found: {slot_name}") + + # Clear current session (same as archive_session) + for child in list(SESSION_DIR.iterdir()): + if child.is_file(): + child.unlink() + elif child.is_dir() and child.name != "__pycache__": + shutil.rmtree(child) + + # Restore save files + for child in slot_dir.iterdir(): + if child.name == "metadata.json": + continue + if child.is_file(): + shutil.copy2(child, SESSION_DIR / child.name) + elif child.is_dir(): + shutil.copytree(child, SESSION_DIR / child.name, dirs_exist_ok=True) + + # Re-seed audio from theme (audio is never in saves) + theme = get_active_theme() + from .paths import get_theme_audio_dir + theme_audio = get_theme_audio_dir(theme) + if theme_audio.exists(): + AUDIO_DIR.mkdir(parents=True, exist_ok=True) + for f in theme_audio.iterdir(): + if f.is_file() and not (AUDIO_DIR / f.name).exists(): + shutil.copy2(f, AUDIO_DIR) + + return extract_hero_name() + + +def list_saves() -> list[dict]: + """Return list of save slots with metadata.""" + if not SAVES_DIR.exists(): + return [] + slots = [] + import json + for d in sorted(SAVES_DIR.iterdir()): + if not d.is_dir(): + continue + meta_path = d / "metadata.json" + meta = {} + if meta_path.exists(): + try: + meta = json.loads(meta_path.read_text()) + except Exception: + pass + slots.append({ + "slot": d.name, + "hero": meta.get("hero", "?"), + "theme": meta.get("theme", "?"), + "timestamp": meta.get("timestamp", ""), + "preview": meta.get("preview", ""), + }) + return slots + + +def delete_save(slot_name: str) -> None: + """Remove a save slot.""" + slot_dir = SAVES_DIR / slot_name + if slot_dir.exists(): + shutil.rmtree(slot_dir) diff --git a/tools/engine_lib/tools_handler.py b/tools/engine_lib/tools_handler.py index 424c400..a50ff68 100644 --- a/tools/engine_lib/tools_handler.py +++ b/tools/engine_lib/tools_handler.py @@ -4,8 +4,9 @@ import json import re from .paths import ( - AMBIENCE_PATH, CHAR_PATH, WORLD_PATH, MECHANICS_PATH, - CORE_RULES_PATH, CHARACTER_CREATION_PATH, END_GAME_PATH, + AMBIENCE_PATH, CHAR_PATH, WORLD_PATH, + get_mechanics_path, get_core_rules_path, + get_character_creation_path, get_end_game_path, ) from .state import read_file, validate_update_size, update_journal, append_llm_log, get_valid_ambiences @@ -301,10 +302,10 @@ def tool_finalize_turn(args: dict) -> str: RULES_CATEGORIES = { - "mechanics": MECHANICS_PATH, - "core": CORE_RULES_PATH, - "character_creation": CHARACTER_CREATION_PATH, - "end_game": END_GAME_PATH, + "mechanics": get_mechanics_path(), + "core": get_core_rules_path(), + "character_creation": get_character_creation_path(), + "end_game": get_end_game_path(), } def tool_read_rules(args: dict) -> str: diff --git a/tools/run.py b/tools/run.py index ac7af03..6e75ff6 100755 --- a/tools/run.py +++ b/tools/run.py @@ -182,6 +182,7 @@ class ChaosTUI(App): def on_mount(self): clear_llm_log() ensure_log() + state.init_session_from_theme() self.console._theme = MARKDOWN_THEME self._init_book() self.set_interval(REFRESH_SECS, self._check_ambience)