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

View File

@ -64,8 +64,11 @@ def validate_action(
prompt = VALIDATION_PROMPT.format(character=char, world=world, log=log_entries, story=recent, action=player_action) prompt = VALIDATION_PROMPT.format(character=char, world=world, log=log_entries, story=recent, action=player_action)
messages = [{"role": "user", "content": prompt}]
for attempt in range(2):
text = call_llm( text = call_llm(
[{"role": "user", "content": prompt}], messages,
max_tokens=1024, max_tokens=1024,
temperature=0.2, temperature=0.2,
label="Action validation", label="Action validation",
@ -89,6 +92,12 @@ def validate_action(
except (json.JSONDecodeError, ValueError): except (json.JSONDecodeError, ValueError):
if on_debug: if on_debug:
on_debug("action_validation", {"valid": True, "reason": "parse_failed", "raw": text[:200]}) 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" return False, "Unrecognized"

View File

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

View File

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