Files
ltbxd-actorle/docs/superpowers/specs/2026-03-30-game-persistence-design.md
thibaud-leclere 1eb7817182 docs: add game grid persistence design spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 19:38:29 +02:00

109 lines
4.9 KiB
Markdown

# Game Grid Persistence
## Problem
Game grids are currently generated on the fly at every page load in `HomepageController`. There is no persistence — refreshing the page gives a new grid. Players cannot resume a game.
## Goal
Persist game grids so players can resume their current game. Display a "Start a game" button when no game is in progress. Allow players to abandon a game.
## Scope
- Grid generation and persistence only
- No win/loss logic (will be added later)
- No saving of player input (filled letters)
## Data Model
### Entity: `Game`
| Field | Type | Description |
|-------------|-------------------------|------------------------------------------------|
| `id` | int (auto) | Primary key |
| `user` | ManyToOne → User, nullable | Null for anonymous players |
| `mainActor` | ManyToOne → Actor | The actor to guess |
| `status` | string | `in_progress` or `abandoned` (extensible to `won` later) |
| `startedAt` | DateTimeImmutable | When the game started |
| `endedAt` | DateTimeImmutable, nullable | When the game ended (abandon or win) |
### Entity: `GameRow`
| Field | Type | Description |
|------------|--------------------|------------------------------------------------|
| `id` | int (auto) | Primary key |
| `game` | ManyToOne → Game | Parent game |
| `actor` | ManyToOne → Actor | The actor for this row |
| `position` | int | Index of the target letter in the actor's name |
| `rowOrder` | int | Order of this row in the grid |
`GameRow` is a separate entity (not JSON) to allow attaching hints/clues per row in a future iteration.
## Service: `GameGridGenerator`
Extracted from the current `HomepageController::index` logic:
1. Select a random main actor via `ActorRepository::findOneRandom()`
2. For each letter (a-z) in the main actor's name, find a random actor containing that letter
3. Create a `Game` entity with `status = in_progress` and `startedAt = now`
4. Create `GameRow` entities with the actor, letter position, and row order
5. Persist and flush
6. Return the `Game` entity
Also provides a method to compute grid display data (width, middle) from a `Game` and its rows, for passing to the React component.
## Routes
### `GET /` — Homepage (modified)
Current behavior replaced:
1. If user is connected → query `GameRepository` for a `Game` with `status = in_progress` for this user
2. If anonymous → get `current_game_id` from the Symfony session, then look up the `Game` in DB
3. If a game is found and still `in_progress` → render the grid with an "Abandon" button above it
4. If no game is found → render a "Start a game" button
### `POST /game/start` — Start a new game (new)
1. Verify no game is already in progress for this player (connected or session)
2. Call `GameGridGenerator` to create the game
3. If user is connected → the game is linked to the user
4. If anonymous → store `$game->getId()` in session key `current_game_id`
5. Redirect to `/`
### `POST /game/{id}/abandon` — Abandon a game (new)
1. Verify the game belongs to the current player (user match or session match)
2. Set `status = abandoned` and `endedAt = now`
3. If anonymous → remove `current_game_id` from session
4. Redirect to `/`
Both POST routes use CSRF protection.
## Anonymous Session Handling
- Session key: `current_game_id` (stores the `Game` ID)
- Set on game creation, removed on abandon
- On homepage load: if the stored game ID no longer exists or is not `in_progress`, clean up the session key
- No migration of anonymous games on login — the anonymous game is lost when the user logs in. Acceptable since we don't save letter input.
## Frontend
### Template changes (`homepage/index.html.twig`)
Three states:
1. **No game in progress**: centered "Start a game" button (Twig form, POST to `/game/start`)
2. **Game in progress**: action bar above the grid with an "Abandon" button (Twig form, POST to `/game/{id}/abandon` with CSRF token), then the React `GameGrid` component below
3. **After abandon**: redirect to `/`, which shows state 1
### React components
No changes to `GameGrid`, `GameRow`, `LetterInput`, or `ActorPopover`. They receive the same props (`grid`, `width`, `middle`) — the data source changes from on-the-fly generation to database lookup.
## Future Extensibility
- **Win logic**: add `won` to the status enum, set `endedAt` on win
- **Hints/clues per row**: add a relation on `GameRow` (e.g., `GameRowHint` entity)
- **Game history/stats**: query `Game` entities by user with status filters