Settings store

This commit is contained in:
Dejvino 2026-06-28 18:53:33 +02:00
parent c0e8fd8522
commit 25fb5fd729
3 changed files with 158 additions and 3 deletions

5
session/settings.json Normal file
View File

@ -0,0 +1,5 @@
{
"active_tab": "play-tab",
"music_muted": true,
"book_page": 16
}

View File

@ -691,6 +691,84 @@ class GameEngine:
return "; ".join(parts) if parts else ""
return ""
@staticmethod
def _parse_changes_block(changes_block: str) -> list[dict]:
"""Parse a ### Changes block into tool call dicts.
Handles the standard format:
- Current Health: N
- Cash: N
- Max Health: N
- Added to inventory: item1, item2
- Removed from inventory: item1, item2
- Replaced gear: X Y
- Note: text
- Journal add: item1, item2
- Journal done: item1, item2
"""
calls = []
for raw_line in changes_block.split("\n"):
line = raw_line.strip()
if not line.startswith("- "):
continue
content = line[2:].strip()
m = re.match(r"Current Health:\s*(\d+)", content, re.IGNORECASE)
if m:
calls.append({"tool": "modify_vitals", "args": {"current_hp": m.group(1)}})
continue
m = re.match(r"Cash:\s*(\d+)", content, re.IGNORECASE)
if m:
calls.append({"tool": "modify_vitals", "args": {"cash": m.group(1)}})
continue
m = re.match(r"Max Health:\s*(\d+)", content, re.IGNORECASE)
if m:
calls.append({"tool": "modify_vitals", "args": {"max_hp": m.group(1)}})
continue
m = re.match(r"Add(?:ed)? to inventory:\s*(.+)", content, re.IGNORECASE)
if m:
for item in [i.strip() for i in m.group(1).split(",") if i.strip()]:
calls.append({"tool": "add_to_inventory", "args": {"item": item}})
continue
m = re.match(r"Remov(?:e|ed) from inventory:\s*(.+)", content, re.IGNORECASE)
if m:
for item in [i.strip() for i in m.group(1).split(",") if i.strip()]:
calls.append({"tool": "remove_from_inventory", "args": {"item": item}})
continue
m = re.match(r"Replace(?:d)? gear:\s*(.+?)\s*[→➜]\s*(.+)", content, re.IGNORECASE)
if m:
calls.append({"tool": "replace_gear", "args": {"before": m.group(1).strip(), "after": m.group(2).strip()}})
continue
m = re.match(r"Note:\s*(.+)", content, re.IGNORECASE)
if m:
calls.append({"tool": "add_note", "args": {"note": m.group(1).strip()}})
continue
m = re.match(r"Journal add:\s*(.+)", content, re.IGNORECASE)
if m:
calls.append({"tool": "journal_update", "args": {"add": [i.strip() for i in m.group(1).split(",") if i.strip()]}})
continue
m = re.match(r"Journal done:\s*(.+)", content, re.IGNORECASE)
if m:
calls.append({"tool": "journal_update", "args": {"done": [i.strip() for i in m.group(1).split(",") if i.strip()]}})
continue
# Looted from X: Y — narrative fallback, extract what we can
m = re.match(r"Looted from .+:\s*(.+)", content, re.IGNORECASE)
if m:
items_text = m.group(1).strip()
calls.append({"tool": "add_note", "args": {"note": f"Looted: {items_text}"}})
continue
return calls
def _execute_tool(self, tool_name: str, args: dict) -> str:
fn_map = {
"roll": self._tool_roll,
@ -945,7 +1023,24 @@ class GameEngine:
user_prompt = self._auto_prompt(book_log)
ambience = None
phase3_errors = []
changes = [] # Reset for this outer attempt
# Step 1: Parse ### Changes block directly (deterministic, no LLM)
if changes_block.strip():
for tc in self._parse_changes_block(changes_block):
name = tc["tool"]
args = tc.get("args", {})
if name == "finalize_turn":
continue
result = self._execute_tool(name, args)
if result.startswith("**Error:") or result.startswith("Tool error") or result.startswith("Unknown"):
phase3_errors.append(f"{name}: {result}")
else:
desc = self._describe_change(name, args)
if desc:
changes.append(desc)
# Step 2: LLM Phase 3 for finalize_turn + any extra changes
previous_attempt = None # {output, feedback}
phase3_ok = False
for p3_attempt in range(5):
@ -959,8 +1054,9 @@ class GameEngine:
)
if changes_block.strip():
phase3_prompt += (
f"## Changes to apply\n{changes_block}\n\n"
f"Convert the listed changes into tool calls:\n\n"
f"## Changes already applied\n{changes_block}\n\n"
f"Output the finalize_turn tool to end the turn. "
f"Add extra tool calls if you spot changes the list above missed.\n\n"
)
else:
phase3_prompt += (
@ -1043,7 +1139,7 @@ class GameEngine:
if not errors:
phase3_ok = True
debug_info = ""
changes = attempt_changes
changes.extend(attempt_changes)
if on_debug:
on_debug("phase", {"phase": 3, "status": "done", "applied": len([tc for tc in tool_calls if tc.get("tool") != "finalize_turn"])})
break

View File

@ -47,6 +47,7 @@ AMBIENCE_OPTIONS_PATH = SESSION / 'ambience_options.md'
BOOK_PATH = SESSION / 'book.md'
LAST_PROMPT_PATH = SESSION / 'last_prompt.md'
CHANGES_PATH = SESSION / 'changes.md'
SETTINGS_PATH = SESSION / 'settings.json'
AUDIO_DIR = SESSION / 'audio'
TODAY = date.today().isoformat()
LOG_PATH = LOG_DIR / f'{TODAY}.md'
@ -640,6 +641,36 @@ class ChaosTUI(App):
# Debug log
self._debug_lines: list[str] = []
# Settings guard — prevent save during init before restore
self._settings_loaded = False
# ── Settings persistence ──────────────────────────────
def _load_settings(self) -> dict:
defaults = {"active_tab": "play-tab", "music_muted": False, "book_page": 0}
if SETTINGS_PATH.exists():
try:
data = json.loads(SETTINGS_PATH.read_text())
if isinstance(data, dict):
defaults.update(data)
except (json.JSONDecodeError, OSError):
pass
return defaults
def _save_settings(self) -> None:
if not self._settings_loaded:
return
tabs = self.query_one("#main-tabs", TabbedContent)
active = tabs.active if tabs else "play-tab"
data = {
"active_tab": active,
"music_muted": app_ambience_player.is_muted if app_ambience_player else False,
"book_page": self._book_page,
}
try:
SETTINGS_PATH.write_text(json.dumps(data, indent=2) + "\n")
except OSError:
pass
# ── Compose ──────────────────────────────────────────
def compose(self):
yield Static(f"⚔ The Chaos ╎ {TODAY}", id="banner")
@ -685,9 +716,26 @@ class ChaosTUI(App):
self.set_interval(REFRESH_SECS, self._check_ambience)
self.set_interval(REFRESH_SECS, self._reload_book)
self.call_after_refresh(self._update_mute_button)
self.call_after_refresh(self._apply_settings)
# Start the game
self.call_after_refresh(self._begin_game)
def _apply_settings(self) -> None:
"""Load and apply persisted UI settings."""
settings = self._load_settings()
self._book_page = settings.get("book_page", 0)
self._prev_page_count = len(self._book_pages)
self._reload_book()
if settings.get("music_muted") and app_ambience_player and not app_ambience_player.is_muted:
app_ambience_player.toggle_mute()
self._update_mute_button()
try:
tabs = self.query_one("#main-tabs", TabbedContent)
tabs.active = settings.get("active_tab", "play-tab")
except Exception:
pass
self._settings_loaded = True
def _begin_game(self):
"""Resume from last saved prompt or generate an opening scene."""
if LAST_PROMPT_PATH.exists():
@ -719,6 +767,7 @@ class ChaosTUI(App):
if app_ambience_player:
app_ambience_player.toggle_mute()
self._update_mute_button()
self._save_settings()
def _update_mute_button(self) -> None:
btn = self.query_one("#mute-btn", Button)
@ -1099,10 +1148,15 @@ class ChaosTUI(App):
@on(Button.Pressed, "#book-prev")
def on_book_prev(self):
self.action_prev_page()
self._save_settings()
@on(Button.Pressed, "#book-next")
def on_book_next(self):
self.action_next_page()
self._save_settings()
def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
self._save_settings()
def main():