DM speaks first automatically; stop asking who's playing

Two issues from live testing: (1) the DM only ever spoke after a
player sent something, so starting a fresh game meant typing a
throwaway message like "start" just to get it going; (2) Session 0
asked for player headcount and names even though that's already
known — everyone who wants in has already logged in and joined via
the invite link/code, tracked in game_participants.

Fix 1: ws_game.py now runs a DM turn immediately on the first-ever
WebSocket connection to a game with zero messages, before entering
the normal receive loop — guarded by the game's existing lock plus a
"does this game have messages yet" check, so two tabs opening around
the same time can't produce two greetings. Extracted the
run-turn-then-broadcast logic (typing/rolling/game_ended/message)
into _run_and_broadcast_dm_turn, shared between the kickoff and the
normal per-message path instead of duplicated.

Fix 2: orchestrator.run_dm_turn() now queries game_participants and
injects the actual joined-player names into the system prompt
alongside the game's name/description. dm_system_prompt.txt drops the
"ask for player count and names" question entirely and instructs the
DM to greet whoever's already listed instead, since more players can
join mid-session and get recognized automatically once they speak.

Verified live: a freshly created game showed "Dungeon Master schreibt
…" and then a real opening message addressed to "Thorsten" by name
before any player message existed, skipped straight to the character
question, and reloading the page didn't produce a duplicate greeting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thorsten
2026-09-02 14:04:42 +02:00
parent 07665595a2
commit 7bbdee868a
3 changed files with 70 additions and 33 deletions
+17 -1
View File
@@ -3,6 +3,7 @@ import logging
import uuid
from collections.abc import Awaitable, Callable
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -10,8 +11,9 @@ 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.models.game import Game
from app.models.game import Game, GameParticipant
from app.models.message import Message
from app.models.user import User
from app.rag.adventure_retrieval import build_adventure_rag_block
from app.rag.retrieval import build_rag_block
@@ -89,6 +91,20 @@ async def run_dm_turn(
f"Name: {game.name}\nBeschreibung: {game.description}"
)
participant_names = (
await session.execute(
select(User.name)
.join(GameParticipant, GameParticipant.user_id == User.id)
.where(GameParticipant.game_id == game_id)
)
).scalars().all()
if participant_names:
system_prompt += (
"\n\nBereits beigetretene Spieler (über Login/Einladungslink — NICHT nach Anzahl "
"oder Namen fragen): " + ", ".join(participant_names) + ". Weitere können jederzeit "
"über den Einladungslink dazukommen; du erkennst sie automatisch am Namen, sobald sie schreiben."
)
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: