Let the DM reuse an existing character instead of re-creating it
Characters are already owned by the player, not the game, and were meant to be reusable — but the DM had no way to actually retrieve one. upsert_character_sheet is write-only, so telling the DM "I'll bring my character Tillo Eichenherz" led it to create a second, blank "Tillo Eichenherz" (empty stats, thinner ability text) instead of reusing the real one, then ask for all six ability scores again as if nothing existed. Two fixes: 1. New link_existing_character tool: given a player name and a character name, looks it up among that player's own characters (ownership enforced — never someone else's) and links it to game_participants.character_id, returning the full sheet so the DM can continue with real data immediately. System prompt now tells the DM to call this the moment a player names a previously-played character, instead of asking for stats. 2. run_dm_turn now auto-injects the full sheet of every already-linked character into the system prompt every turn (name, stats, combat stats, HP, abilities, equipment, background), with an explicit "already exists, don't ask again" instruction — this is the actual root-cause fix: even a character linked some other way (the existing join-with-character_id flow, or a future UI) now stays visible to the DM turn after turn instead of only living in whatever's left of the truncated chat window. Also manually repaired the user's real "Schwarze Segel" game, which had already hit this bug — re-pointed its participant back to the real Tillo Eichenherz instead of the accidental duplicate (left the duplicate in place rather than deleting data; it's a normal delete away via the character-delete feature if unwanted). Verified live against x.ai: a fresh game where the player said "Ich nehme meinen alten Charakter Tillo Eichenherz" linked to the real character on the first message (no duplicate created — confirmed by row count), and a follow-up question about AC/Strength got the exact real values (RK 18, Stärke 15) pulled from the injected sheet, not re-asked or invented. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,8 @@ from app.config import settings
|
||||
from app.llm.client import get_dm_system_prompt, get_llm_client
|
||||
from app.llm.context import build_context
|
||||
from app.llm.json_utils import fix_double_escaped_unicode
|
||||
from app.llm.tools import character_sheet, dice, end_game, monster, world_state
|
||||
from app.llm.tools import character_sheet, dice, end_game, link_existing_character, monster, world_state
|
||||
from app.models.character import Character
|
||||
from app.models.game import Game, GameParticipant
|
||||
from app.models.message import Message
|
||||
from app.models.user import User
|
||||
@@ -28,6 +29,7 @@ TOOLS = [
|
||||
{"type": "function", "function": end_game.TOOL_SCHEMA},
|
||||
{"type": "function", "function": monster.TOOL_SCHEMA},
|
||||
{"type": "function", "function": world_state.TOOL_SCHEMA},
|
||||
{"type": "function", "function": link_existing_character.TOOL_SCHEMA},
|
||||
]
|
||||
|
||||
|
||||
@@ -47,6 +49,8 @@ async def _execute_tool_call(
|
||||
return await monster.update(session, game_id, tool_input)
|
||||
if tool_name == "update_world_state":
|
||||
return await world_state.update(session, game_id, tool_input)
|
||||
if tool_name == "link_existing_character":
|
||||
return await link_existing_character.link(session, game_id, tool_input)
|
||||
return {"error": f"Unknown tool {tool_name!r}"}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("Tool call %s failed: %s", tool_name, exc)
|
||||
@@ -82,6 +86,29 @@ async def _request_player_roll(
|
||||
}
|
||||
|
||||
|
||||
def _format_character_sheet(character: Character) -> str:
|
||||
parts = [
|
||||
f"- {character.name} ({character.race or '?'} {character.char_class or '?'}, Stufe {character.level})"
|
||||
]
|
||||
if character.stats:
|
||||
parts.append(f" Werte: {character.stats}")
|
||||
hp = None
|
||||
if character.current_hp is not None or character.max_hp is not None:
|
||||
hp = f"{character.current_hp if character.current_hp is not None else '?'}/{character.max_hp if character.max_hp is not None else '?'}"
|
||||
if hp:
|
||||
parts.append(f" TP: {hp}")
|
||||
if character.combat_stats:
|
||||
parts.append(f" Kampfwerte: {character.combat_stats}")
|
||||
if character.abilities:
|
||||
ability_names = ", ".join(a.get("name", "") for a in character.abilities if isinstance(a, dict))
|
||||
parts.append(f" Fähigkeiten: {ability_names}")
|
||||
if character.equipment:
|
||||
parts.append(f" Ausrüstung: {', '.join(character.equipment)}")
|
||||
if character.description:
|
||||
parts.append(f" Hintergrund: {character.description}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
async def _check_hp_game_over(session: AsyncSession, game_id: uuid.UUID, result: dict) -> str | None:
|
||||
"""After an upsert_character_sheet call, auto-ends the game if the character it touched
|
||||
dropped to 0 HP or below. Returns the end reason if the game was just ended, else None."""
|
||||
@@ -159,6 +186,21 @@ async def run_dm_turn(
|
||||
"über den Einladungslink dazukommen; du erkennst sie automatisch am Namen, sobald sie schreiben."
|
||||
)
|
||||
|
||||
linked_characters = (
|
||||
await session.execute(
|
||||
select(Character)
|
||||
.join(GameParticipant, GameParticipant.character_id == Character.id)
|
||||
.where(GameParticipant.game_id == game_id)
|
||||
)
|
||||
).scalars().all()
|
||||
if linked_characters:
|
||||
sheets = "\n".join(_format_character_sheet(c) for c in linked_characters)
|
||||
system_prompt += (
|
||||
"\n\nBekannte Charakterblätter (bereits vollständig vorhanden — NICHT erneut nach "
|
||||
"Werten/Ausrüstung/Hintergrund fragen, nur bei tatsächlichen Änderungen über "
|
||||
f"upsert_character_sheet aktualisieren):\n{sheets}"
|
||||
)
|
||||
|
||||
await world_state.refresh_if_due(session, game_id, client, settings.dm_model)
|
||||
world_summary = await world_state.get_summary(session, game_id)
|
||||
if world_summary:
|
||||
|
||||
Reference in New Issue
Block a user