From 1eb7817182c5e196b36f0e564514760fe2d587be Mon Sep 17 00:00:00 2001 From: thibaud-leclere Date: Mon, 30 Mar 2026 19:38:29 +0200 Subject: [PATCH] docs: add game grid persistence design spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-30-game-persistence-design.md | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-30-game-persistence-design.md diff --git a/docs/superpowers/specs/2026-03-30-game-persistence-design.md b/docs/superpowers/specs/2026-03-30-game-persistence-design.md new file mode 100644 index 0000000..fdff2c7 --- /dev/null +++ b/docs/superpowers/specs/2026-03-30-game-persistence-design.md @@ -0,0 +1,108 @@ +# 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