docs: add victory condition design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
843009e193
commit
2382d6f8d3
1 changed files with 131 additions and 0 deletions
131
docs/superpowers/specs/2026-04-03-victory-condition-design.md
Normal file
131
docs/superpowers/specs/2026-04-03-victory-condition-design.md
Normal 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.
|
||||||
Loading…
Reference in a new issue