docs: add victory condition design spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thibaud-leclere 2026-04-03 21:26:47 +02:00
parent 843009e193
commit 2382d6f8d3

View file

@ -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 (`<img>` 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.