# 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.