Add meta language

This commit is contained in:
Dejvino 2026-07-04 16:38:58 +02:00
parent 83a83dd421
commit e002bafbc8
4 changed files with 79 additions and 14 deletions

View File

@ -49,10 +49,20 @@ class GameEngine:
on_action("DM is preparing a response")
system = build_system_prompt(recent_narrative=recent_narrative, recent_log=session_log)
is_meta = bool(player_action and player_action.strip().startswith(">"))
base_parts = []
if player_action:
base_parts.append(f"## Player's Request\n{player_action}")
if not player_action and not recent_narrative:
if is_meta:
base_parts.append(
"## Instructions\n"
"The player's message starts with `>` — this is a meta out-of-character question to the DM. "
"Do NOT advance the story. Respond as the DM in meta language, starting the response with `>`. "
"Use the `narrative` tool to output your meta response. Do NOT call any other tools (no journal_update, no finalize_turn, no rolls, no state changes)."
)
elif not player_action and not recent_narrative:
base_parts.append(
"## Instructions\n"
"This is a new story. Welcome the player and guide them through the game setup."
@ -63,6 +73,7 @@ class GameEngine:
"Advance the story based on the player's request. "
"All state is shown above — write the outcome directly."
)
if not is_meta:
base_parts.append(f"\n*A die is cast: **{die_roll}** (1d6).*")
base_user = "\n\n".join(base_parts)
@ -132,8 +143,19 @@ class GameEngine:
else:
state_changes.append(tc)
# Meta check — reject if state changes produced for a meta action
if is_meta and state_changes:
state.append_llm_log(f"\n[TURN META REJECTED] state changes not allowed for meta action")
if attempt < MAX_RETRIES:
feedback = "This is a meta action. Do NOT call any state-changing tools. Respond only with meta text (starting with `>`) and no tool calls beyond a finalize_turn."
state.append_llm_log(f"\n[TURN REGENERATE] (meta) attempt {attempt + 2}")
if on_action:
on_action("DM is consulting the fates...")
continue
state.append_llm_log(f"\n[TURN META EXCEEDED] accepting despite state changes")
# Duplicate check — reject if narrative is 80%+ similar to last book entry
if book_log:
if not is_meta and book_log:
prev = state.read_recent_book(1)
if prev and prev not in ("*No prior story.*",):
prev_text = re.sub(r"^## Turn \d+\n\n", "", prev, flags=re.MULTILINE).strip()
@ -164,6 +186,7 @@ class GameEngine:
changes=state_changes,
story=recent_narrative,
log=session_log,
meta=is_meta,
)
if valid:
@ -203,6 +226,9 @@ class GameEngine:
# Accept this turn — execute all tool calls
break
if is_meta:
tool_calls = [tc for tc in tool_calls if tc.get("tool") == "narrative"]
# Second pass — execute all tool calls
extr_start = datetime.now()
@ -256,6 +282,7 @@ class GameEngine:
ambience=ambience,
debug_info="; ".join(errors) if errors else "",
changes=changes,
is_meta=is_meta,
)

View File

@ -14,3 +14,4 @@ class TurnResult:
error: Optional[str] = None
debug_info: str = ""
changes: list[str] = field(default_factory=list)
is_meta: bool = False

View File

@ -171,6 +171,33 @@ Regenerate (turn had fixable issues like wrong state changes or minor inconsiste
```
"""
META_VALIDATION_PROMPT = """You are validating a meta (out-of-character) DM response. The player's action starts with `>` — they are talking to the DM, not to a character.
## Player Action (Meta)
{action}
## Generated Meta Response
{narrative}
## Instructions
1. **Meta Format**: The entire response must start with `>` and use meta language (DM addressing the player directly).
2. **State Changes**: There MUST be no state changes. This is a meta conversation, not story progression.
3. **Answer Quality**: The response should address the player's meta question and be helpful.
4. **No Story Advancement**: The response must not advance the game narrative.
Reply with ONLY a ```tool block. Examples:
Valid:
```tool
{{"tool": "validate", "args": {{"valid": true, "reason": "ok", "action": "ok"}}}}
```
Regenerate (response didn't start with `>` or tried to change state):
```tool
{{"tool": "validate", "args": {{"valid": false, "reason": "describe the issue", "action": "regenerate"}}}}
```
"""
def _format_changes(changes: list[dict]) -> str:
"""Format tool calls into a readable change list for the validation prompt."""
@ -193,6 +220,7 @@ def validate_turn(
changes: list[dict] | None = None,
story: str = "",
log: str = "",
meta: bool = False,
) -> tuple[bool, str, str]:
"""Validate a complete generated turn.
@ -208,6 +236,12 @@ def validate_turn(
journal = state.read_file(JOURNAL_PATH) or "*No journal entries.*"
change_summary = _format_changes(changes or [])
if meta:
prompt = META_VALIDATION_PROMPT.format(
action=player_action,
narrative=narrative,
)
else:
prompt = TURN_VALIDATION_PROMPT.format(
character=char, world=world, story=recent,
log=log_entries, journal=journal, action=player_action,

View File

@ -67,6 +67,7 @@ class ChaosTUI(App):
#char-content { background: #1e1e2a; color: #c0c0c0; padding: 0 1; }
#transcript { background: #1a2a1a; color: #c8c8c8; padding: 0 1; }
#play-narrative { background: #161616; color: #d8d8d8; padding: 1 2; height: auto; }
#play-narrative.meta { background: #1a1a2e; color: #b0a0e0; border-top: solid #6b4fa0; border-bottom: solid #6b4fa0; }
#play-status { background: #1a2a1a; color: #e0b060; padding: 0 2; height: 1; text-style: bold italic; text-align: center; }
#play-status.processing { background: #2a1a0a; color: #ffd93d; }
#play-input { height: 3; background: #222; color: #e0d0c0; border: solid #555; padding: 0 1; }
@ -304,7 +305,7 @@ class ChaosTUI(App):
if result.error:
self._show_error(result.error, result.debug_info)
return
if result.book_log:
if result.book_log and not result.is_meta:
turn_num = state.archive_turn(result.book_log)
if result.log_entry:
state.append_log(f"- **Turn {turn_num}** — {result.log_entry}")
@ -312,7 +313,7 @@ class ChaosTUI(App):
summary = result.book_log.strip().split(chr(10))[0][:80]
state.append_log(f"- **Turn {turn_num}** — {summary}")
result.book_log = load_book_pages()[-1]
elif result.log_entry:
elif result.log_entry and not result.is_meta:
state.append_log(f"- {result.log_entry}")
state.apply_state(result)
if result.book_log or not result.user_prompt:
@ -344,7 +345,7 @@ class ChaosTUI(App):
parts.append(self._render_changes(result.changes))
if result.user_prompt:
parts.append(f"---\n\n{result.user_prompt}")
self._set_narrative("\n\n".join(parts) if parts else "")
self._set_narrative("\n\n".join(parts) if parts else "", meta=result.is_meta)
self._enable_input()
def _enable_input(self, value: str = "") -> None:
@ -354,9 +355,11 @@ class ChaosTUI(App):
inp.value = value
inp.focus()
def _set_narrative(self, text: str) -> None:
def _set_narrative(self, text: str, meta: bool = False) -> None:
self._last_narrative = text
self.query_one("#play-narrative", Static).update(RichMarkdown(text))
widget = self.query_one("#play-narrative", Static)
widget.set_class(meta, "meta")
widget.update(RichMarkdown(text))
self.query_one("#play-scroll", VerticalScroll).scroll_home(animate=False)
def _show_error(self, error: str, debug_info: str = "") -> None: