Skip to content

feat: persist each player's game so it survives restarts#57

Merged
dmccoystephenson merged 1 commit into
mainfrom
feat/persist-game-state
Jun 8, 2026
Merged

feat: persist each player's game so it survives restarts#57
dmccoystephenson merged 1 commit into
mainfrom
feat/persist-game-state

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

Per-player game state lived only in memory (SessionService's ConcurrentHashMap), so every backend restart or redeploy wiped everyone's in-progress game — and the docs contradicted themselves about whether games were saved. This persists each player's game to a database, keyed by username.

This is the persistence foundation that unblocks #53 (run history) and #55 (per-account layout).

How it works

  • SessionService is now a write-through cache over a new SavedGameRepository: a session is loaded from the DB on a cache miss, and persisted on creation and after every mutation (tick, command, reset, policy change).
  • Storage: embedded H2 written to ./data by default (mount as a volume in prod); DB_URL (+ DB_USERNAME/DB_PASSWORD) switches it to Postgres. Chosen over a separate Postgres container to avoid adding memory pressure on the 4 GB prod box (see gateway#105).
  • Serialization: the game is stored as JSON. GameState/Army/Tile gain @NoArgsConstructor so Jackson can read them back (they were previously only ever serialized); a lenient mapper tolerates derived getters (getWidth, isMoving). Army.ensureIdsAbove advances the static id counter past restored armies so a post-reload split can't reuse an id.

Modules touched

backend only (+ root docker-compose.yml and docs).

Test plan

  • Backend ./mvnw test170 passing, incl.:
    • GameStateSerializationTest — a generated game survives a JSON round trip (grid, armies, policies).
    • SessionPersistenceTest — a saved game is reloaded by a fresh SessionService (standing in for a restart).
  • Runtime smoke: backend boots with file-based H2, creates ./data/barony.mv.db, serves /state 200.
  • Live: play a few turns, redeploy/restart the backend, log back in — the game resumes at the same turn.

Deploy note

The backend image now bundles JPA + H2 + the Postgres driver. The gateway compose needs a data volume mounted at the backend's /app/data so the H2 file persists across container recreation (will add in the gateway PR). backend/pom.xml changed (new deps) — flagging for review.

🤖 Generated with Claude Code

Per-player game state lived only in memory (SessionService's ConcurrentHashMap),
so every backend restart or redeploy wiped everyone's in-progress game — and the
docs contradicted themselves about whether games were saved.

This persists each player's game, keyed by username, as serialized JSON in a
database:

- SessionService is now a write-through cache over a SavedGameRepository: a
  session is loaded from the DB on a cache miss, and persisted on creation and
  after every mutation (tick, command, reset, policy change).
- Storage is an embedded H2 database written to ./data by default (mount as a
  volume in production); DB_URL switches it to Postgres. Chosen over a separate
  Postgres container to avoid adding memory pressure on the small prod box.
- GameState/Army/Tile gain @NoArgsConstructor so Jackson can read them back
  (they were previously only ever serialized); a lenient mapper tolerates the
  derived getters (getWidth, isMoving). Army.ensureIdsAbove advances the static
  id counter past restored armies so a post-reload split can't reuse an id.

Tests: GameState JSON round-trip, and a persistence test that reloads a saved
game in a fresh SessionService (standing in for a restart). docker-compose gets
a data volume; README/PLAYER_GUIDE/CHANGELOG reconcile the save story.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dmccoystephenson dmccoystephenson merged commit 4e9d5c2 into main Jun 8, 2026
2 checks passed
@dmccoystephenson dmccoystephenson deleted the feat/persist-game-state branch June 8, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant