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 "; ".join(parts) if parts else ""
|
||||||
return ""
|
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:
|
def _execute_tool(self, tool_name: str, args: dict) -> str:
|
||||||
fn_map = {
|
fn_map = {
|
||||||
"roll": self._tool_roll,
|
"roll": self._tool_roll,
|
||||||
@ -945,7 +1023,24 @@ class GameEngine:
|
|||||||
user_prompt = self._auto_prompt(book_log)
|
user_prompt = self._auto_prompt(book_log)
|
||||||
ambience = None
|
ambience = None
|
||||||
phase3_errors = []
|
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}
|
previous_attempt = None # {output, feedback}
|
||||||
phase3_ok = False
|
phase3_ok = False
|
||||||
for p3_attempt in range(5):
|
for p3_attempt in range(5):
|
||||||
@ -959,8 +1054,9 @@ class GameEngine:
|
|||||||
)
|
)
|
||||||
if changes_block.strip():
|
if changes_block.strip():
|
||||||
phase3_prompt += (
|
phase3_prompt += (
|
||||||
f"## Changes to apply\n{changes_block}\n\n"
|
f"## Changes already applied\n{changes_block}\n\n"
|
||||||
f"Convert the listed changes into tool calls:\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:
|
else:
|
||||||
phase3_prompt += (
|
phase3_prompt += (
|
||||||
@ -1043,7 +1139,7 @@ class GameEngine:
|
|||||||
if not errors:
|
if not errors:
|
||||||
phase3_ok = True
|
phase3_ok = True
|
||||||
debug_info = ""
|
debug_info = ""
|
||||||
changes = attempt_changes
|
changes.extend(attempt_changes)
|
||||||
if on_debug:
|
if on_debug:
|
||||||
on_debug("phase", {"phase": 3, "status": "done", "applied": len([tc for tc in tool_calls if tc.get("tool") != "finalize_turn"])})
|
on_debug("phase", {"phase": 3, "status": "done", "applied": len([tc for tc in tool_calls if tc.get("tool") != "finalize_turn"])})
|
||||||
break
|
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'
|
BOOK_PATH = SESSION / 'book.md'
|
||||||
LAST_PROMPT_PATH = SESSION / 'last_prompt.md'
|
LAST_PROMPT_PATH = SESSION / 'last_prompt.md'
|
||||||
CHANGES_PATH = SESSION / 'changes.md'
|
CHANGES_PATH = SESSION / 'changes.md'
|
||||||
|
SETTINGS_PATH = SESSION / 'settings.json'
|
||||||
AUDIO_DIR = SESSION / 'audio'
|
AUDIO_DIR = SESSION / 'audio'
|
||||||
TODAY = date.today().isoformat()
|
TODAY = date.today().isoformat()
|
||||||
LOG_PATH = LOG_DIR / f'{TODAY}.md'
|
LOG_PATH = LOG_DIR / f'{TODAY}.md'
|
||||||
@ -640,6 +641,36 @@ class ChaosTUI(App):
|
|||||||
# Debug log
|
# Debug log
|
||||||
self._debug_lines: list[str] = []
|
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 ──────────────────────────────────────────
|
# ── Compose ──────────────────────────────────────────
|
||||||
def compose(self):
|
def compose(self):
|
||||||
yield Static(f"⚔ The Chaos ╎ {TODAY}", id="banner")
|
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._check_ambience)
|
||||||
self.set_interval(REFRESH_SECS, self._reload_book)
|
self.set_interval(REFRESH_SECS, self._reload_book)
|
||||||
self.call_after_refresh(self._update_mute_button)
|
self.call_after_refresh(self._update_mute_button)
|
||||||
|
self.call_after_refresh(self._apply_settings)
|
||||||
# Start the game
|
# Start the game
|
||||||
self.call_after_refresh(self._begin_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):
|
def _begin_game(self):
|
||||||
"""Resume from last saved prompt or generate an opening scene."""
|
"""Resume from last saved prompt or generate an opening scene."""
|
||||||
if LAST_PROMPT_PATH.exists():
|
if LAST_PROMPT_PATH.exists():
|
||||||
@ -719,6 +767,7 @@ class ChaosTUI(App):
|
|||||||
if app_ambience_player:
|
if app_ambience_player:
|
||||||
app_ambience_player.toggle_mute()
|
app_ambience_player.toggle_mute()
|
||||||
self._update_mute_button()
|
self._update_mute_button()
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
def _update_mute_button(self) -> None:
|
def _update_mute_button(self) -> None:
|
||||||
btn = self.query_one("#mute-btn", Button)
|
btn = self.query_one("#mute-btn", Button)
|
||||||
@ -1099,10 +1148,15 @@ class ChaosTUI(App):
|
|||||||
@on(Button.Pressed, "#book-prev")
|
@on(Button.Pressed, "#book-prev")
|
||||||
def on_book_prev(self):
|
def on_book_prev(self):
|
||||||
self.action_prev_page()
|
self.action_prev_page()
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
@on(Button.Pressed, "#book-next")
|
@on(Button.Pressed, "#book-next")
|
||||||
def on_book_next(self):
|
def on_book_next(self):
|
||||||
self.action_next_page()
|
self.action_next_page()
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
|
def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user