Validate speech and disable dice rolls
This commit is contained in:
parent
527369f280
commit
a0d24c9d44
@ -28,7 +28,6 @@ class GameEngine:
|
|||||||
recent_narrative: str | None = None,
|
recent_narrative: str | None = None,
|
||||||
on_thought: callable = None,
|
on_thought: callable = None,
|
||||||
on_action: callable = None,
|
on_action: callable = None,
|
||||||
on_player_roll: callable = None,
|
|
||||||
) -> TurnResult:
|
) -> TurnResult:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
state.append_llm_log(f"\n{'='*60}")
|
state.append_llm_log(f"\n{'='*60}")
|
||||||
@ -128,9 +127,7 @@ class GameEngine:
|
|||||||
ambience = None
|
ambience = None
|
||||||
if args.get("log_entry"):
|
if args.get("log_entry"):
|
||||||
log_entry = args["log_entry"]
|
log_entry = args["log_entry"]
|
||||||
elif name == "player_roll":
|
else:
|
||||||
pass
|
|
||||||
elif name not in ("roll",):
|
|
||||||
state_changes.append(tc)
|
state_changes.append(tc)
|
||||||
|
|
||||||
# Duplicate check — reject if narrative is 80%+ similar to last book entry
|
# Duplicate check — reject if narrative is 80%+ similar to last book entry
|
||||||
@ -207,11 +204,6 @@ class GameEngine:
|
|||||||
|
|
||||||
if name == "narrative":
|
if name == "narrative":
|
||||||
pass
|
pass
|
||||||
elif name == "player_roll" and on_player_roll:
|
|
||||||
dice = args.get("dice", "1d6")
|
|
||||||
reason = args.get("reason", "a check")
|
|
||||||
roll_val = on_player_roll(dice, reason)
|
|
||||||
result = f"Player rolled {dice} for '{reason}': {roll_val}"
|
|
||||||
else:
|
else:
|
||||||
result = execute_tool(name, args)
|
result = execute_tool(name, args)
|
||||||
|
|
||||||
|
|||||||
@ -19,12 +19,6 @@ Output ONLY ```tool blocks — no prose, no reasoning, no explanation outside to
|
|||||||
|
|
||||||
Wrap each action in its own ```tool block:
|
Wrap each action in its own ```tool block:
|
||||||
|
|
||||||
```tool
|
|
||||||
{"tool": "roll", "args": {"dice": "1d6"}}
|
|
||||||
```
|
|
||||||
```tool
|
|
||||||
{"tool": "player_roll", "args": {"dice": "1d6", "reason": "a check"}}
|
|
||||||
```
|
|
||||||
```tool
|
```tool
|
||||||
{"tool": "modify_vitals", "args": {"current_hp": 5, "cash": 45}}
|
{"tool": "modify_vitals", "args": {"current_hp": 5, "cash": 45}}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .paths import AMBIENCE_PATH, CHAR_PATH, WORLD_PATH
|
from .paths import AMBIENCE_PATH, CHAR_PATH, WORLD_PATH
|
||||||
@ -9,8 +8,6 @@ from .state import read_file, validate_update_size, update_journal, append_llm_l
|
|||||||
|
|
||||||
|
|
||||||
TOOL_REGISTRY: dict[str, dict] = {
|
TOOL_REGISTRY: dict[str, dict] = {
|
||||||
"roll": {"description": "Roll dice.", "args": {"dice": "1d6", "modifier": "+1"}},
|
|
||||||
"player_roll": {"description": "Ask player to roll.", "args": {"dice": "1d6", "reason": "why"}},
|
|
||||||
"modify_traits": {"description": "Change STR/DEX/WIL.", "args": {"str": "optional", "dex": "optional", "wil": "optional"}},
|
"modify_traits": {"description": "Change STR/DEX/WIL.", "args": {"str": "optional", "dex": "optional", "wil": "optional"}},
|
||||||
"modify_vitals": {"description": "Change HP, cash, weapon, armour.", "args": {"current_hp": "optional", "max_hp": "optional", "cash": "optional", "weapon": "optional", "armour": "optional"}},
|
"modify_vitals": {"description": "Change HP, cash, weapon, armour.", "args": {"current_hp": "optional", "max_hp": "optional", "cash": "optional", "weapon": "optional", "armour": "optional"}},
|
||||||
"add_to_inventory": {"description": "Add item to gear.", "args": {"item": "item name and stats"}},
|
"add_to_inventory": {"description": "Add item to gear.", "args": {"item": "item name and stats"}},
|
||||||
@ -34,27 +31,6 @@ def patch_character(pattern: str, repl: str, count: int = 1, flags: int = 0) ->
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def tool_roll(args: dict) -> str:
|
|
||||||
dice_str = (args or {}).get("dice", "1d6")
|
|
||||||
modifier_str = (args or {}).get("modifier", "0")
|
|
||||||
try:
|
|
||||||
count, sides = dice_str.lower().split("d")
|
|
||||||
count = int(count) if count else 1
|
|
||||||
sides = int(sides)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return f"Invalid dice: {dice_str}. Use format like '2d6'."
|
|
||||||
mod = 0
|
|
||||||
if modifier_str:
|
|
||||||
try:
|
|
||||||
mod = int(modifier_str)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
rolls = [random.randint(1, sides) for _ in range(count)]
|
|
||||||
total = sum(rolls) + mod
|
|
||||||
mod_str = f" {'+' if mod >= 0 else ''}{mod}" if mod != 0 else ""
|
|
||||||
return f"Roll: {dice_str}{mod_str} → [{', '.join(str(r) for r in rolls)}] = {total}"
|
|
||||||
|
|
||||||
|
|
||||||
def tool_modify_traits(args: dict) -> str:
|
def tool_modify_traits(args: dict) -> str:
|
||||||
errors = []
|
errors = []
|
||||||
for stat in ("str", "dex", "wil"):
|
for stat in ("str", "dex", "wil"):
|
||||||
@ -185,7 +161,6 @@ def tool_finalize_turn(args: dict) -> str:
|
|||||||
def execute_tool(tool_name: str, args: dict) -> str:
|
def execute_tool(tool_name: str, args: dict) -> str:
|
||||||
"""Execute a tool by name. Returns result string."""
|
"""Execute a tool by name. Returns result string."""
|
||||||
fn_map = {
|
fn_map = {
|
||||||
"roll": tool_roll,
|
|
||||||
"modify_traits": tool_modify_traits,
|
"modify_traits": tool_modify_traits,
|
||||||
"modify_vitals": tool_modify_vitals,
|
"modify_vitals": tool_modify_vitals,
|
||||||
"add_to_inventory": tool_add_to_inventory,
|
"add_to_inventory": tool_add_to_inventory,
|
||||||
|
|||||||
@ -103,6 +103,7 @@ TURN_VALIDATION_PROMPT = """You are a strict RPG game master validating a genera
|
|||||||
4. **Self-Contained Narrative**: The narrative must read clearly on its own — explicitly describe what the character did in response to the action. Do not skip the character's action and jump straight to consequences. Each turn must make sense without referencing the player action line.
|
4. **Self-Contained Narrative**: The narrative must read clearly on its own — explicitly describe what the character did in response to the action. Do not skip the character's action and jump straight to consequences. Each turn must make sense without referencing the player action line.
|
||||||
5. **Log Entry**: Does the log entry accurately summarise the narrative in 1-2 short, dense sentences? Should be specific, factual, and immediately readable.
|
5. **Log Entry**: Does the log entry accurately summarise the narrative in 1-2 short, dense sentences? Should be specific, factual, and immediately readable.
|
||||||
6. **Journal Progress**: Are TODO items being addressed? If the narrative resolves an open TODO, the turn must call `journal_update` to mark it done. Unchecked items left stale too long may need prompting.
|
6. **Journal Progress**: Are TODO items being addressed? If the narrative resolves an open TODO, the turn must call `journal_update` to mark it done. Unchecked items left stale too long may need prompting.
|
||||||
|
7. **Player Speech**: If the player action contains direct speech (quoted text like `"Hello"` or `'Hello'`), the narrative MUST include the player character speaking those words or equivalent dialogue. If the player's speech can be incorporated given the context, the turn should reflect it. Only skip if the speech is completely impossible given the situation.
|
||||||
|
|
||||||
## Character (before changes)
|
## Character (before changes)
|
||||||
{character}
|
{character}
|
||||||
|
|||||||
23
tools/run.py
23
tools/run.py
@ -28,7 +28,7 @@ from run_utils import (
|
|||||||
from run_ambience import AmbiencePlayer
|
from run_ambience import AmbiencePlayer
|
||||||
from run_widgets import (
|
from run_widgets import (
|
||||||
app_ambience_player as _widget_player_ref,
|
app_ambience_player as _widget_player_ref,
|
||||||
RollModal, CharPane, StatusBar, TodoPane, TranscriptPane,
|
CharPane, StatusBar, TodoPane, TranscriptPane,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -107,8 +107,6 @@ class ChaosTUI(App):
|
|||||||
self._thinking_frame = 0
|
self._thinking_frame = 0
|
||||||
self._thinking_timer_handle = None
|
self._thinking_timer_handle = None
|
||||||
self._dm_action = "DM is preparing a response"
|
self._dm_action = "DM is preparing a response"
|
||||||
self._roll_event = threading.Event()
|
|
||||||
self._roll_result: str | None = None
|
|
||||||
self._book_page = 0
|
self._book_page = 0
|
||||||
self._book_pages: list[str] = []
|
self._book_pages: list[str] = []
|
||||||
self._prev_page_count = 0
|
self._prev_page_count = 0
|
||||||
@ -256,7 +254,6 @@ class ChaosTUI(App):
|
|||||||
player_action=player_action,
|
player_action=player_action,
|
||||||
on_thought=on_thought,
|
on_thought=on_thought,
|
||||||
on_action=on_action,
|
on_action=on_action,
|
||||||
on_player_roll=self._on_player_roll,
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.call_from_thread(self._on_generation_error, e, traceback.format_exc())
|
self.call_from_thread(self._on_generation_error, e, traceback.format_exc())
|
||||||
@ -294,24 +291,6 @@ class ChaosTUI(App):
|
|||||||
status.add_class("processing")
|
status.add_class("processing")
|
||||||
status.update(f"✦ {self._spinner_frames[0]} {action} ✦")
|
status.update(f"✦ {self._spinner_frames[0]} {action} ✦")
|
||||||
|
|
||||||
def _on_player_roll(self, dice: str, reason: str) -> str:
|
|
||||||
self.call_from_thread(self._show_roll_modal, dice, reason)
|
|
||||||
self._roll_event.wait()
|
|
||||||
self._roll_event.clear()
|
|
||||||
result = self._roll_result
|
|
||||||
self._roll_result = None
|
|
||||||
return result or "0"
|
|
||||||
|
|
||||||
def _show_roll_modal(self, dice: str, reason: str) -> None:
|
|
||||||
self._roll_event.clear()
|
|
||||||
self._roll_result = None
|
|
||||||
|
|
||||||
def on_dismiss(value: str) -> None:
|
|
||||||
self._roll_result = value
|
|
||||||
self._roll_event.set()
|
|
||||||
|
|
||||||
self.push_screen(RollModal(dice, reason), on_dismiss)
|
|
||||||
|
|
||||||
def _tick_thinking(self) -> None:
|
def _tick_thinking(self) -> None:
|
||||||
if not self._is_processing:
|
if not self._is_processing:
|
||||||
return
|
return
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from textual.app import ComposeResult
|
from textual.widgets import Static
|
||||||
from textual.containers import Vertical
|
|
||||||
from textual.screen import Screen
|
|
||||||
from textual.widgets import Button, Input, Static
|
|
||||||
from rich.markdown import Markdown as RichMarkdown
|
|
||||||
|
|
||||||
from run_utils import (
|
from run_utils import (
|
||||||
CHAR_PATH, TODAY, REFRESH_SECS,
|
CHAR_PATH, TODAY, REFRESH_SECS,
|
||||||
@ -18,69 +14,6 @@ from run_ambience import HAS_AUDIO
|
|||||||
app_ambience_player: object | None = None
|
app_ambience_player: object | None = None
|
||||||
|
|
||||||
|
|
||||||
class RollModal(Screen):
|
|
||||||
CSS = """
|
|
||||||
RollModal {
|
|
||||||
align: center middle;
|
|
||||||
background: rgba(0, 0, 0, 0.75);
|
|
||||||
}
|
|
||||||
#roll-dialog {
|
|
||||||
width: 44;
|
|
||||||
height: auto;
|
|
||||||
padding: 2 3;
|
|
||||||
background: #2a2a3a;
|
|
||||||
border: thick #e0ad4c;
|
|
||||||
}
|
|
||||||
#roll-title {
|
|
||||||
text-style: bold;
|
|
||||||
color: #ffd93d;
|
|
||||||
text-align: center;
|
|
||||||
height: 3;
|
|
||||||
}
|
|
||||||
#roll-reason {
|
|
||||||
color: #c0b090;
|
|
||||||
text-align: center;
|
|
||||||
height: 3;
|
|
||||||
}
|
|
||||||
#roll-input {
|
|
||||||
margin: 1 0;
|
|
||||||
}
|
|
||||||
#roll-submit {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#roll-hint {
|
|
||||||
color: #888888;
|
|
||||||
text-align: center;
|
|
||||||
height: 1;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, dice: str, reason: str) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.dice = dice
|
|
||||||
self.reason = reason
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
with Vertical(id="roll-dialog"):
|
|
||||||
yield Static(f"[bold]🎲 ROLL {self.dice}[/bold]", id="roll-title")
|
|
||||||
yield Static(f"Reason: {self.reason}", id="roll-reason")
|
|
||||||
yield Input(placeholder="Enter the number you rolled...", id="roll-input")
|
|
||||||
yield Button("Submit", id="roll-submit", variant="primary")
|
|
||||||
yield Static("(or press Enter)", id="roll-hint")
|
|
||||||
|
|
||||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
||||||
self._submit(event.value)
|
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
||||||
if event.button.id == "roll-submit":
|
|
||||||
self._submit(self.query_one("#roll-input", Input).value)
|
|
||||||
|
|
||||||
def _submit(self, value: str) -> None:
|
|
||||||
val = value.strip()
|
|
||||||
if val:
|
|
||||||
self.dismiss(val)
|
|
||||||
|
|
||||||
|
|
||||||
class AutoStatic(Static):
|
class AutoStatic(Static):
|
||||||
def load(self):
|
def load(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@ -239,7 +239,7 @@ def test_turn_bad_json(mock_call_llm, mock_truncate_world, mock_read_file):
|
|||||||
"I attack the dragon",
|
"I attack the dragon",
|
||||||
narrative="Dillion swings his sword.",
|
narrative="Dillion swings his sword.",
|
||||||
log_entry="Dillion attacked the dragon.",
|
log_entry="Dillion attacked the dragon.",
|
||||||
changes=[{"tool": "roll", "args": {"dice": "1d6"}}],
|
changes=[{"tool": "modify_vitals", "args": {"current_hp": 10}}],
|
||||||
story="A dragon appears!",
|
story="A dragon appears!",
|
||||||
log="- Dragon spotted",
|
log="- Dragon spotted",
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user