Add DELETE /api/games/{id} (creator-only) and DELETE
/api/characters/{id} (owner-only), each 403ing for anyone else.
Deleting a game relies on the existing cascade FKs (game_participants,
messages, world_state, adventure_chunks all cascade on games.id) — a
plain session.delete() is enough.
Deleting a character is trickier: game_participants.character_id and
messages.character_id reference it with no ON DELETE clause (a
character can outlive a game and vice versa), so deleting one that's
been played would hit a FK violation. Null out both references first
— game and message history stay intact, just detached from the
now-gone character, same as how a participant with no character
selected already renders.
Frontend: a reusable ConfirmDialog component, a "Löschen" button on
GameCard/CharacterCard visible only to the owner (checked against
useAuth()'s user id), wired through GamesList/CharactersList so the
list updates locally after a successful delete.
Verified: isolated backend test proved the character delete's FK
nulling works without violation and preserves the game/message rows;
live in the browser, deleting a game removed it from the list with no
orphaned rows left in any of the four related tables, and the
character delete dialog's cancel path correctly leaves everything
untouched. Ownership scoping confirmed live too — no delete button
appears on other users' games.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
The adventure-text field was buried at the bottom of the proposal
form, after the whole interview — the user wanted it to be the very
first thing shown when clicking "Neues Spiel", since pasting an
adventure should drive the proposal rather than follow it.
GameSetupChat now opens on an "Eigenes Abenteuer (optional)" step:
paste the text (or skip) before the wizard chat starts at all. The
setup-chat backend call now receives that text and, when present,
infers genre/world/scope from its opening directly instead of asking
about them — the proposed name/description come out already specific
to the pasted adventure. Removed the now-redundant adventure textarea
from the proposal form (it's collected once, upfront).
Verified live: pasting a short pirate one-shot produced "Klingt nach
einem klassischen Piraten-One-Shot rund um Port Royal – das Genre und
den Umfang leite ich direkt daraus ab", skipping straight to the one
remaining question (experience level), and the final proposal named
Isabella Steel and Blackfin's fortress directly from the text.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Players can now paste the full text of a freely available adventure
when creating a game, and the DM will follow it instead of inventing
its own plot/NPCs/locations. Reuses the existing rulebook RAG
pipeline (chunking, local embedding) rather than injecting the raw
text into every turn, since real adventure modules range from a few
pages to hundreds — far beyond what fits in a prompt.
- New adventure_chunks table (game-scoped, unlike the global
rulebook_chunks) + Game.adventure_text storing the raw source.
- ingest_adventure_text() chunks/embeds synchronously during
POST /api/games when adventure_text is non-empty.
- build_adventure_rag_block() retrieves by similarity to the latest
player message, scoped to game_id — plus always includes chunk 0
(the adventure's opening) regardless of the query, since early
Session-0 messages rarely resemble the adventure's actual hook and
pure similarity search could miss the beginning entirely.
- System prompt instructs the DM to follow provided adventure
excerpts strictly, deviating only when players clearly go off-script.
- Frontend: optional adventure-text field in the game creation form,
a has_adventure flag surfaced as a small badge on the game card and
session header.
Verified with an isolated two-game test that chunks/retrieval never
leak across games (the critical failure mode here), and live against
x.ai: a custom-written one-shot's specific NPCs, location, and hook
appeared verbatim in the DM's opening scene instead of invented ones.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Games now have a status/ended_reason pair. The DM can call the new
end_game tool when the story reaches a real conclusion (victory,
defeat, or a resolved one-shot), and the backend independently ends
the game whenever a character's HP drops to 0 or below, regardless of
whether the DM narrates it. The frontend shows a banner and a
"Beendet" badge once a game ends.
HP moves from the freeform combat_stats bag into dedicated
current_hp/max_hp columns on Character, since reliably detecting 0 HP
requires a real integer rather than parsing strings like "3/10" out of
an LLM-authored key/value dict. Also fixed combat_stats to fully
replace on each upsert instead of merging, matching its documented
contract — the merge was leaving stale keys (old HP/TP text) behind
after the model stopped sending them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Broadcast a "rolling" WS event when the DM invokes roll_dice, and show
a cycling dice-emoji animation with the roll notation in the chat while
waiting for the result, instead of just the generic typing indicator.
Also split the dice-roll prompt rule into skill/save checks (DC shown
to the player) vs. attack rolls (AC stays secret but must still be
labeled as a roll), and made the "always roll yourself, never ask the
player to roll" instruction unconditional.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The server broadcasts a "typing" event to the whole room right when
it starts the DM turn, and broadcasts turn failures to everyone too
(previously only the sender saw them, which would've left other
players' indicators stuck forever). The frontend shows a pulsing
"Dungeon Master schreibt …" line that clears once the real reply or
an error arrives.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RAG: switch Postgres to pgvector, chunk and embed the three D&D
rulebooks locally via sentence-transformers, and retrieve relevant
excerpts per DM turn (query = latest player message) to ground the
system prompt. Retrieval runs off the event loop and is capped by a
relevance threshold and a max character budget so it can't blow up
context size or cost.
Game setup wizard: creating a game now opens a short chat where the
DM asks about genre, length, and the player's experience level, then
proposes a name and description via a tool call. The player can edit
both before creating the game. Stateless endpoint — the frontend
carries the conversation, no DB needed since the game doesn't exist
yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FastAPI + Postgres backend with fastapi-users auth, games/characters
CRUD, and a WebSocket chat endpoint where an x.ai Grok DM narrates
play, rolls dice, and maintains character sheets via tool calls.
React + Tailwind frontend covers registration/login, game list and
creation, participation-code join flow, character viewer, and a
real-time chat session view. Docker Compose skeleton (Postgres +
backend) included; Caddy/frontend container wiring is deferred to
the deploy milestone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>