From 2382d6f8d39a29f3e925a0ce4c30b4a8a0981f39 Mon Sep 17 00:00:00 2001 From: thibaud-leclere Date: Fri, 3 Apr 2026 21:26:47 +0200 Subject: [PATCH] docs: add victory condition design spec Co-Authored-By: Claude Sonnet 4.6 --- .../2026-04-03-victory-condition-design.md | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-03-victory-condition-design.md diff --git a/docs/superpowers/specs/2026-04-03-victory-condition-design.md b/docs/superpowers/specs/2026-04-03-victory-condition-design.md new file mode 100644 index 0000000..3d91a28 --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-victory-condition-design.md @@ -0,0 +1,131 @@ +# Victory Condition — Design Spec + +**Date:** 2026-04-03 +**Status:** Approved + +--- + +## Overview + +When all highlighted cells (the intersection column that spells the main actor's name) are filled by the player, a server-side check is triggered automatically. If all letters match the main actor's name, the game is won: all rows are revealed, the inputs are disabled, and a victory card is shown above the grid. + +--- + +## 1. Data Model + +### Actor entity — new column +Add a nullable `profile_path` (varchar 255) column to the `actor` table. This stores the TMDB image path (e.g. `/abc123.jpg`) used to construct photo URLs. + +- A Doctrine migration is required. +- `ActorSyncer` is updated to populate `profile_path` via the new `TMDBGateway::getPersonDetails()` method during future imports. +- A console command (`app:actor:backfill-profile-path`) fetches `profile_path` for existing actors that have a `tmdbId` but no `profile_path`. + +### Game entity — new status +Add `STATUS_WON = 'won'` constant. The `Game::win()` method sets `status = 'won'` and `endedAt = now()`. + +--- + +## 2. TMDB Integration + +New `TMDBGateway::getPersonDetails(int $tmdbId): ?TMDBPerson` method calling `GET /person/{id}`. +`TMDBPerson` model exposes `profilePath` (mapped from `profile_path`). + +Photo URL format: `https://image.tmdb.org/t/p/w500/{profile_path}`. This URL is constructed server-side and returned in the API response. If `profile_path` is null, `actorPhotoUrl` is null in the response and the victory card shows a placeholder. + +--- + +## 3. API Endpoint + +`POST /api/game/{id}/check` + +**Authorization:** same ownership rules as the abandon endpoint (user match or session match). +**CSRF:** not required (JSON API, not a form submission). + +**Request body:** +```json +{ "letters": ["T", "O", "M", " ", "H", "O", "L", "L", "A", "N", "D"] } +``` +One letter per letter of the main actor's name (spaces included, matching the separator positions). + +**Validation:** array length must match the number of alphabetic characters in the main actor's name (one entry per highlighted cell, in row order). + +**Success response (won):** +```json +{ + "won": true, + "actorName": "Tom Holland", + "actorPhotoUrl": "https://image.tmdb.org/t/p/w500/abc123.jpg", + "rows": [ + { + "actorName": "Tom Hardy", + "letters": ["T", "O", "M", " ", "H", "A", "R", "D", "Y"] + } + ] +} +``` +`rows` is in the same order as the grid rows (excluding separator rows). Each `letters` array contains one entry per character of the actor name (letters and non-letters), matching the grid column layout exactly. + +**Response (not won):** +```json +{ "won": false } +``` +The game is not modified server-side when `won` is false. + +**Side effects on win:** `Game::win()` is called and flushed to DB. + +--- + +## 4. React — State Lift & Detection + +`GameGrid` becomes stateful. All letter input values are lifted up: +- `GameGrid` holds a `letters` state: a 2D structure indexed by `[rowIndex][charIndex]`. +- `GameRow` receives `onLetterChange(charIndex, value)` callback and reports changes upward. +- `LetterInput` receives its current `value` and reports changes via `onChange`. + +**Trigger condition:** when every highlighted cell (one per non-separator row) has a non-empty value, `GameGrid` calls `POST /api/game/{id}/check` with the highlighted letters in row order. + +`gameId` and `gameStatus` are passed as new props to `GameGrid` from the Twig template. + +--- + +## 5. Reveal Logic (on win) + +Applied immediately after a successful `won: true` response: + +1. **Highlighted cells** (main actor column): class `letter-correct` (green background). +2. **All other cells per row**: filled with the correct letter from `rows[i].letters`. If the player had typed a different (non-empty) value at that position, the cell gets class `letter-wrong` (red background) and is replaced with the correct letter. +3. **All inputs**: `disabled = true`. +4. **VictoryCard**: rendered above the grid. + +--- + +## 6. VictoryCard Component + +New React component `VictoryCard` rendered at the top of `GameGrid` when `gameWon` state is true. + +Contents: +- Actor photo (`` from `actorPhotoUrl`, or a placeholder avatar SVG if null) +- Actor name (large, styled) +- "Nouvelle partie" button (anchor to `/`) + +Styled as a card consistent with the existing SUNRISE design system (warm orange palette, `var(--surface)` background, `var(--radius-lg)` border radius). + +--- + +## 7. CSS additions + +| Class | Description | +|---|---| +| `.letter-correct` | Green border + green tint background for correct highlighted cells | +| `.letter-wrong` | Red border + red tint background for incorrect revealed cells | +| `.victory-card` | Card above the grid with photo, name, and new-game button | +| `.victory-card__photo` | Actor photo, circular crop | +| `.victory-card__name` | Actor name, large serif font (Fraunces) | + +--- + +## 8. Out of Scope + +- What happens when the player submits wrong letters (the `won: false` path) — no UI change beyond the server returning false. Future feature. +- Per-row progressive validation as the player types. +- Score, timer, or share functionality.