Settings store
This commit is contained in:
parent
c0e8fd8522
commit
25fb5fd729
5
session/settings.json
Normal file
5
session/settings.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"active_tab": "play-tab",
|
||||
"music_muted": true,
|
||||
"book_page": 16
|
||||
}
|
||||
102
tools/engine.py
102
tools/engine.py
@ -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
|
||||
|
||||
54
tools/run.py
54
tools/run.py
@ -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():
|
||||
|
||||
Loading…
Reference in New Issue
Block a user