diff --git a/prompts/xp-evaluator.md b/prompts/xp-evaluator.md index b4bfb51..d8dc261 100644 --- a/prompts/xp-evaluator.md +++ b/prompts/xp-evaluator.md @@ -81,5 +81,4 @@ Then stop. Don't over-explain. Don't narrate. You're a background process, not a ## Important - Only recommend one level at a time. If they've earned multiple levels, recommend the next one. The evaluator will run again and catch the subsequent level. -- If `advancement_ready` is already set on the character sheet and the DM hasn't acted on it yet, do nothing. Don't stack notifications. - Err slightly on the side of generosity. A player stuck at the same level for too long is worse than leveling one session too early. The goal is to keep the game feeling rewarding and the character's growth matching their story. diff --git a/src/storied/advancement.py b/src/storied/advancement.py index 6dc5435..cc47a2e 100644 --- a/src/storied/advancement.py +++ b/src/storied/advancement.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from pathlib import Path from threading import Thread +from storied import notifications from storied.character import format_character_context, load_character from storied.claude import run_with_tools from storied.engine import load_prompt @@ -32,17 +33,12 @@ def build_advancement_context( ) -> str | None: """Build context for the advancement evaluator. - Returns None if there's nothing to evaluate (no character or - advancement_ready already set). + Returns None if there's no character to evaluate. """ character = load_character(player_id, base_path) if character is None: return None - # If advancement_ready is already set, the DM hasn't acted yet — skip - if character.get("advancement_ready"): - return None - parts: list[str] = [] # Character summary @@ -98,9 +94,28 @@ def evaluate_advancement( start_time = time.monotonic() + character = load_character(player_id, base_path) + if character is None: + progress("Skipped (no character)") + return AdvancementResult(elapsed=time.monotonic() - start_time) + + pending_level = character.get("advancement_ready") + if pending_level: + char_name = character.get("identity", {}).get("name", "The character") + notifications.append( + world_id, + base_path, + f"Reminder: {char_name} is still pending advancement to " + f"level {pending_level}. The next narratively appropriate " + f"moment — a rest, a quiet pause, after a triumph — should " + f"acknowledge their growth before it loses meaning.", + ) + progress(f"Posted reminder: pending level {pending_level}") + return AdvancementResult(elapsed=time.monotonic() - start_time) + context = build_advancement_context(world_id, player_id, base_path) if context is None: - progress("Skipped (no character or advancement already pending)") + progress("Skipped (no context)") return AdvancementResult(elapsed=time.monotonic() - start_time) system_prompt = load_prompt("xp-evaluator") diff --git a/tests/test_advancement.py b/tests/test_advancement.py index 2552186..2d84c4b 100644 --- a/tests/test_advancement.py +++ b/tests/test_advancement.py @@ -107,17 +107,6 @@ class TestBuildAdvancementContext: ) assert result is None - def test_returns_none_when_advancement_ready( - self, ctx: ToolContext, character: dict - ): - character["advancement_ready"] = 4 - save_character("default", character, ctx.base_path) - - result = build_advancement_context( - ctx.world_id, ctx.player_id, ctx.base_path - ) - assert result is None - def test_includes_character_info( self, ctx: ToolContext, character: dict ): @@ -277,7 +266,7 @@ class TestEvaluateAdvancement: ) assert result.evaluated is False - def test_skips_when_advancement_ready( + def test_posts_reminder_when_advancement_pending( self, ctx: ToolContext, character: dict ): character["advancement_ready"] = 4 @@ -288,7 +277,15 @@ class TestEvaluateAdvancement: player_id=ctx.player_id, base_path=ctx.base_path, ) + assert result.evaluated is False + path = ( + ctx.base_path / "worlds" / ctx.world_id / "dm_notifications.md" + ) + assert path.exists() + contents = path.read_text() + assert "Kira" in contents + assert "level 4" in contents @patch("storied.claude.subprocess.Popen") def test_calls_claude_when_character_exists(