End game rules and player name extraction

This commit is contained in:
Dejvino
2026-07-04 21:50:51 +02:00
parent 1c64ac79f4
commit f36387f1bf
12 changed files with 217 additions and 19 deletions
+3
View File
@@ -4,6 +4,8 @@ from dataclasses import dataclass, field
from typing import Optional
END_MARKER = "### THE END"
@dataclass
class TurnResult:
"""Output of a complete turn."""
@@ -15,3 +17,4 @@ class TurnResult:
debug_info: str = ""
changes: list[str] = field(default_factory=list)
is_meta: bool = False
game_over: bool = False
+3
View File
@@ -26,3 +26,6 @@ AMBIENCE_OPTIONS_PATH = SESSION_DIR / "ambience_options.md"
CHANGES_PATH = SESSION_DIR / "changes.md"
RULES_INJECTION_PATH = SESSION_DIR / "rules_injection.md"
AUDIO_DIR = SESSION_DIR / "audio"
END_GAME_PATH = RULES_DIR / 'end_game.md'
ARCHIVE_DIR = BASE_DIR / 'archive'
+16 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from string import Template
SYSTEM_PROMPT = Template("""You are the DM for "The Chaos". Narrate in 3rd person, vivid but concise. Use the player's name (Dillion) and NPC names explicitly — everything must be parseable on its own without relying on "you" or implied subjects.
SYSTEM_PROMPT = Template("""You are the DM for "The Chaos". Narrate in 3rd person, vivid but concise. Use the player's name and NPC names explicitly — everything must be parseable on its own without relying on "you" or implied subjects.
## Core Rules
$core_rules
@@ -45,12 +45,17 @@ Wrap each action in its own ```tool block:
{"tool": "journal_update", "args": {"add": ["Investigate the mine"], "done": ["Defeat the demon"]}}
```
```tool
{"tool": "finalize_turn", "args": {"ambience": "dungeon", "log_entry": "Dillion explored the dungeon, found a hidden passage, and was ambushed by goblins."}}
{"tool": "finalize_turn", "args": {"ambience": "dungeon", "log_entry": "Kael explored the dungeon, found a hidden passage, and was ambushed by goblins."}}
```
```tool
{"tool": "read_rules", "args": {}}
```
or with a category:
```tool
{"tool": "read_rules", "args": {"category": "end_game"}}
```
(Categories: mechanics, core, character_creation, end_game)
**log_entry**: Provide a short, dense summary (1-2 sentences) of the turn's main events. This becomes the session log — be specific, factual, and concise.
@@ -58,6 +63,15 @@ You are the sole authority over the game state. The player's action is a **propo
**Inventory rule**: If the player wants to use an item, you must first verify it's on their character sheet. If it is, you MUST call `remove_from_inventory` for that item AND apply the effects (e.g. `modify_vitals` for HP potions). If it's not on the sheet, reject the action — do not let them use items they don't have.
## Ending the Game
When the story reaches a definitive end (character death, quest completion, or the player chooses to retire), output the exact marker `### THE END` as a heading in the narrative, then provide:
1. **Why** — why the story ended
2. **What Happened** — summary of final events
3. **The World After** — 2-3 paragraphs describing how the world and characters evolved
After the `### THE END` marker, do NOT call any state-changing tools. The epilogue is narrative-only. Call `read_rules` with `category: "end_game"` for full details.
## State
### Character
+36 -1
View File
@@ -9,13 +9,15 @@ GameEngine or other modules besides paths.
from __future__ import annotations
import re
import shutil
import sys
from datetime import datetime
from pathlib import Path
from .paths import (
CHAR_PATH, WORLD_PATH, BOOK_PATH, JOURNAL_PATH, AMBIENCE_PATH,
LOG_PATH, LLM_LOG_PATH, AMBIENCE_OPTIONS_PATH, CHANGES_PATH,
AUDIO_DIR,
AUDIO_DIR, SESSION_DIR, ARCHIVE_DIR,
)
from .models import TurnResult
@@ -227,3 +229,36 @@ def update_journal(add: list[str] | None = None, done: list[str] | None = None)
cleaned.append(line)
prev_blank = is_blank
JOURNAL_PATH.write_text("\n".join(cleaned) + "\n")
def extract_hero_name() -> str:
"""Extract the hero's name from character.md."""
text = read_file(CHAR_PATH)
if not text:
return "unknown-hero"
for line in text.splitlines():
if line.strip().startswith("**Name:**"):
name = line.split(":", 1)[1].strip().strip("*_ \t")
return name.lower().replace(" ", "-") or "unknown-hero"
return "unknown-hero"
def archive_session() -> str:
"""Archive the current session folder to archive/<hero-name>-<timestamp>/ and clear session state for a fresh start.
Returns the archive path as a string."""
hero = extract_hero_name()
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
archive_dir = ARCHIVE_DIR / f"{hero}-{ts}"
archive_dir.parent.mkdir(parents=True, exist_ok=True)
if SESSION_DIR.exists():
shutil.copytree(SESSION_DIR, archive_dir, dirs_exist_ok=True)
# Clear session state
for child in SESSION_DIR.iterdir():
if child.is_file():
child.unlink()
elif child.is_dir() and child.name != "__pycache__":
shutil.rmtree(child)
return str(archive_dir)
+20 -5
View File
@@ -3,7 +3,10 @@ from __future__ import annotations
import json
import re
from .paths import AMBIENCE_PATH, CHAR_PATH, WORLD_PATH, MECHANICS_PATH
from .paths import (
AMBIENCE_PATH, CHAR_PATH, WORLD_PATH, MECHANICS_PATH,
CORE_RULES_PATH, CHARACTER_CREATION_PATH, END_GAME_PATH,
)
from .state import read_file, validate_update_size, update_journal, append_llm_log, get_valid_ambiences
@@ -18,7 +21,7 @@ TOOL_REGISTRY: dict[str, dict] = {
"world_update": {"description": "Replace world state.", "args": {"content": "full world markdown"}},
"journal_update": {"description": "Update TODO/DONE.", "args": {"add": "[...]", "done": "[...]"}},
"finalize_turn": {"description": "End turn.", "args": {"ambience": "soundscape name", "log_entry": "one-line summary of what happened"}},
"read_rules": {"description": "Read the full mechanics reference (exploration, deck tables, grit, healing, etc.). Call when you need details beyond the Core Rules in the prompt.", "args": {}},
"read_rules": {"description": "Read a rules file by category. Categories: mechanics (full mechanics reference), core (core mechanics), character_creation, end_game (end-game closure rules). Call when you need details beyond the Core Rules in the prompt.", "args": {"category": "optional — one of: mechanics, core, character_creation, end_game (default: mechanics)"}},
}
@@ -159,11 +162,23 @@ def tool_finalize_turn(args: dict) -> str:
return f"Ambience set to {raw}."
RULES_CATEGORIES = {
"mechanics": MECHANICS_PATH,
"core": CORE_RULES_PATH,
"character_creation": CHARACTER_CREATION_PATH,
"end_game": END_GAME_PATH,
}
def tool_read_rules(args: dict) -> str:
"""Read the full mechanics.md and return its content."""
content = read_file(MECHANICS_PATH)
"""Read a rules file by category and return its content."""
category = (args or {}).get("category", "mechanics")
path = RULES_CATEGORIES.get(category)
if not path:
allowed = ", ".join(RULES_CATEGORIES)
return f"**Error:** unknown category '{category}'. Allowed: {allowed}."
content = read_file(path)
if not content:
return "**Error:** rules/mechanics.md not found."
return f"**Error:** {path.name} not found."
return content
+2
View File
@@ -31,6 +31,8 @@ VALIDATION_PROMPT = """You are a strict RPG game master validating whether a pla
- Is the player trying to use an item they don't have? -> invalid
- Are they asserting something that contradicts the state? -> invalid
- Is the action nonsensical given the situation? -> invalid
- Is the player's action or intention unclear or ambiguous? -> invalid (explain what is unclear and why)
- If you are uncertain whether the action is valid, reject it and describe exactly why you are unsure.
- Does the action make sense given the character's abilities and resources? -> valid
- Pay close attention to the Recent Story section — entities like monsters, NPCs, and hazards currently present in the scene ARE valid targets for action.
- If valid, also check: if they're using a consumable item, note that it must be removed from inventory.