5 KiB
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.
ActorSynceris updated to populateprofile_pathvia the newTMDBGateway::getPersonDetails()method during future imports.- A console command (
app:actor:backfill-profile-path) fetchesprofile_pathfor existing actors that have atmdbIdbut noprofile_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:
{ "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):
{
"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):
{ "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:
GameGridholds alettersstate: a 2D structure indexed by[rowIndex][charIndex].GameRowreceivesonLetterChange(charIndex, value)callback and reports changes upward.LetterInputreceives its currentvalueand reports changes viaonChange.
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:
- Highlighted cells (main actor column): class
letter-correct(green background). - 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 classletter-wrong(red background) and is replaced with the correct letter. - All inputs:
disabled = true. - 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 (
<img>fromactorPhotoUrl, 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: falsepath) — no UI change beyond the server returning false. Future feature. - Per-row progressive validation as the player types.
- Score, timer, or share functionality.