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

4.9 KiB

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