ltbxd-actorle/docs/superpowers/specs/2026-04-03-victory-condition-design.md
thibaud-leclere 2382d6f8d3 docs: add victory condition design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 21:26:47 +02:00

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.
  • 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:

{ "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:

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