Fix missing story in turn llm

This commit is contained in:
Dejvino 2026-07-01 22:09:02 +02:00
parent c5c40225a3
commit 0140e2e8d9
5 changed files with 62 additions and 77 deletions

View File

@ -24,7 +24,7 @@ class GameEngine:
def generate_turn(
self,
player_action: str | None = None,
last_prompt: str | None = None,
recent_narrative: str | None = None,
on_thought: callable = None,
on_action: callable = None,
on_player_roll: callable = None,
@ -36,8 +36,8 @@ class GameEngine:
state.append_llm_log(f"{'='*60}")
if player_action:
state.append_llm_log(f"Player: {player_action}")
elif last_prompt:
state.append_llm_log(f"Resume from: {last_prompt[:120]}")
if recent_narrative is None:
recent_narrative = state.read_recent_book(2)
die_roll = random.randint(1, 6)
state.append_llm_log(f"Dice: {die_roll} (1d6)")
@ -66,11 +66,11 @@ class GameEngine:
system = build_system_prompt()
parts = []
if last_prompt:
parts.append(f"## Situation\n{last_prompt}")
if recent_narrative:
parts.append(f"## Recent Narrative\n{recent_narrative}")
if player_action:
parts.append(f"## Player's Request\n{player_action}")
if not player_action and not last_prompt:
if not player_action and not recent_narrative:
parts.append(
"## Instructions\n"
"This is a new story. Welcome the player and guide them through the game setup."
@ -166,8 +166,8 @@ class GameEngine:
})
log_turn_details(
player_action=player_action or last_prompt or "",
last_prompt=last_prompt or "",
player_action=player_action or "",
last_prompt=recent_narrative or "",
strategy_name="tools",
die_roll=die_roll,
model=model,
@ -196,13 +196,11 @@ def main():
parser = argparse.ArgumentParser(description="The Chaos Game Engine (CLI)")
parser.add_argument("--action", "-a", help="Player action text")
parser.add_argument("--last", "-l", help="Last narrative text")
args = parser.parse_args()
engine = GameEngine()
result = engine.generate_turn(
player_action=args.action,
last_prompt=args.last,
)
if result.error:

View File

@ -64,32 +64,41 @@ def validate_action(
prompt = VALIDATION_PROMPT.format(character=char, world=world, log=log_entries, story=recent, action=player_action)
text = call_llm(
[{"role": "user", "content": prompt}],
max_tokens=1024,
temperature=0.2,
label="Action validation",
on_debug=on_debug,
)
messages = [{"role": "user", "content": prompt}]
if not text:
return False, "Not sure"
for attempt in range(2):
text = call_llm(
messages,
max_tokens=1024,
temperature=0.2,
label="Action validation",
on_debug=on_debug,
)
cleaned = text.strip()
m = re.search(r"```(?:json)?\s*\n?(.*?)```", cleaned, re.DOTALL)
if m:
cleaned = m.group(1).strip()
try:
data = json.loads(cleaned)
valid = data.get("valid", True)
reason = data.get("reason", "")
if on_debug:
on_debug("action_validation", {"valid": valid, "reason": reason, "action": player_action})
return valid, reason
except (json.JSONDecodeError, ValueError):
if on_debug:
on_debug("action_validation", {"valid": True, "reason": "parse_failed", "raw": text[:200]})
return False, "Unrecognized"
if not text:
return False, "Not sure"
cleaned = text.strip()
m = re.search(r"```(?:json)?\s*\n?(.*?)```", cleaned, re.DOTALL)
if m:
cleaned = m.group(1).strip()
try:
data = json.loads(cleaned)
valid = data.get("valid", True)
reason = data.get("reason", "")
if on_debug:
on_debug("action_validation", {"valid": valid, "reason": reason, "action": player_action})
return valid, reason
except (json.JSONDecodeError, ValueError):
if on_debug:
on_debug("action_validation", {"valid": True, "reason": "parse_failed", "raw": text[:200]})
if attempt == 0:
messages.append({
"role": "system",
"content": "Your previous response was not valid JSON. Reply with ONLY a JSON object in exactly this format, nothing else:\n\n```json\n{\"valid\": true, \"reason\": \"ok\"}\n```\nor\n```json\n{\"valid\": false, \"reason\": \"brief explanation\"}\n```"
})
return False, "Unrecognized"
def auto_prompt(book_log: str = "") -> str:

View File

@ -21,7 +21,7 @@ from engine import GameEngine
from engine_lib.models import TurnResult
from engine_lib import state
from run_utils import (
BOOK_PATH, CHAR_PATH, LAST_PROMPT_PATH, CHANGES_PATH, SETTINGS_PATH,
BOOK_PATH, CHAR_PATH, CHANGES_PATH, SETTINGS_PATH,
TODAY, REFRESH_SECS, clear_llm_log, ensure_log,
load_book_pages,
)
@ -105,7 +105,6 @@ class ChaosTUI(App):
run_widgets.app_ambience_player = app_ambience_player
self.engine = GameEngine()
self._last_prompt: str = ""
self._last_result: TurnResult | None = None
self._is_processing: bool = False
self._spinner_frames = ["", "", "", ""]
@ -205,23 +204,18 @@ class ChaosTUI(App):
def _begin_game(self):
self._last_narrative: str = ""
if LAST_PROMPT_PATH.exists():
saved = LAST_PROMPT_PATH.read_text().strip()
if saved:
self._last_prompt = saved
pages = load_book_pages()
parts = []
if pages:
parts.append(pages[-1])
if CHANGES_PATH.exists():
changes = [l.strip() for l in CHANGES_PATH.read_text().splitlines() if l.strip()]
if changes:
changes_text = "\n".join(f"> {c}" for c in changes)
parts.append(f"> **Last turn changes:**\n{changes_text}")
parts.append(f"---\n\n{saved}")
self._set_narrative("\n\n".join(parts))
self._enable_input()
return
pages = load_book_pages()
if pages and pages != ["*The story has not begun.*"]:
parts = []
parts.append(pages[-1])
if CHANGES_PATH.exists():
changes = [l.strip() for l in CHANGES_PATH.read_text().splitlines() if l.strip()]
if changes:
changes_text = "\n".join(f"> {c}" for c in changes)
parts.append(f"> **Last turn changes:**\n{changes_text}")
self._set_narrative("\n\n".join(parts))
self._enable_input()
return
self._call_llm()
def _check_ambience(self):
@ -261,8 +255,6 @@ class ChaosTUI(App):
def _run_generation(self, player_action: str | None) -> None:
import traceback
last_prompt = self._last_prompt or None
def on_thought(thought: str) -> None:
self.call_from_thread(self._on_thought, thought)
@ -275,7 +267,6 @@ class ChaosTUI(App):
try:
result = self.engine.generate_turn(
player_action=player_action,
last_prompt=last_prompt,
on_thought=on_thought,
on_action=on_action,
on_player_roll=self._on_player_roll,
@ -458,9 +449,6 @@ class ChaosTUI(App):
self._display_scene(result)
if result.book_log:
self._last_result = result
if result.user_prompt:
LAST_PROMPT_PATH.write_text(result.user_prompt.strip())
self._last_prompt = result.user_prompt
self._append_debug("✔ turn complete")
def _on_generation_error(self, error: Exception, traceback_str: str) -> None:

View File

@ -8,7 +8,6 @@ JOURNAL_PATH = SESSION / 'journal.md'
AMBIENCE_PATH = SESSION / 'ambience.md'
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'

View File

@ -70,40 +70,35 @@ def section(name: str):
def main():
section("First turn — no player action (story opening)")
r = engine.generate_turn()
check("Story opening", r, expect_error=False, expect_book=True, expect_prompt=True)
check("Story opening", r, expect_error=False, expect_book=True, expect_prompt=False)
section("Valid action — buy a drink")
r = engine.generate_turn(
player_action="I buy a mug of ale at the Splintered Tankard",
last_prompt="What do you do?",
)
check("Buy ale", r, expect_error=False, expect_book=True, expect_prompt=True)
check("Buy ale", r, expect_error=False, expect_book=True, expect_prompt=False)
section("Valid action — talk to an NPC")
r = engine.generate_turn(
player_action="I ask Mistress Otta about recent rumours",
last_prompt="What do you do?",
)
check("Ask Otta", r, expect_error=False, expect_book=True, expect_prompt=True)
check("Ask Otta", r, expect_error=False, expect_book=True, expect_prompt=False)
section("Valid action — use inventory item")
r = engine.generate_turn(
player_action="I apply my healing salve to restore HP",
last_prompt="What do you do?",
)
check("Use healing salve", r, expect_error=False, expect_book=True, expect_prompt=True)
check("Use healing salve", r, expect_error=False, expect_book=True, expect_prompt=False)
section("Valid action — explore")
r = engine.generate_turn(
player_action="I head to the Market Square to look around",
last_prompt="What do you do?",
)
check("Visit market", r, expect_error=False, expect_book=True, expect_prompt=True)
check("Visit market", r, expect_error=False, expect_book=True, expect_prompt=False)
section("Invalid action — use non-existent item")
r = engine.generate_turn(
player_action="I drink a potion of invisibility",
last_prompt="What do you do?",
)
check("Potion of invisibility", r, expect_error=False, expect_book=False)
if r.log_entry:
@ -112,7 +107,6 @@ def main():
section("Invalid action — cast spell (not a weaver)")
r = engine.generate_turn(
player_action="I cast a fireball at the tavern ceiling",
last_prompt="What do you do?",
)
check("Fireball spell", r, expect_error=False, expect_book=False)
if r.log_entry:
@ -121,17 +115,14 @@ def main():
section("Invalid action — nonsensical")
r = engine.generate_turn(
player_action="I fly to the moon",
last_prompt="What do you do?",
)
check("Fly to moon", r, expect_error=False, expect_book=False)
if r.log_entry:
print(f" log: {r.log_entry}")
section("Resume from last_prompt (no player action)")
r = engine.generate_turn(
last_prompt="You stand in the market square, surrounded by stalls and bustle. What do you do?",
)
check("Resume scene", r, expect_error=False, expect_book=True, expect_prompt=True)
section("Resume scene (no player action)")
r = engine.generate_turn()
check("Resume scene", r, expect_error=False, expect_book=True, expect_prompt=False)
print(f"\n{'=' * 60}")
print(f" Results: {PASS} passed, {FAIL} failed")