feat: persist each player's game so it survives restarts#57
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Per-player game state lived only in memory (
SessionService'sConcurrentHashMap), 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
SessionServiceis now a write-through cache over a newSavedGameRepository: a session is loaded from the DB on a cache miss, and persisted on creation and after every mutation (tick, command, reset, policy change)../databy 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).GameState/Army/Tilegain@NoArgsConstructorso Jackson can read them back (they were previously only ever serialized); a lenient mapper tolerates derived getters (getWidth,isMoving).Army.ensureIdsAboveadvances the static id counter past restored armies so a post-reload split can't reuse an id.Modules touched
backendonly (+ rootdocker-compose.ymland docs).Test plan
./mvnw test— 170 passing, incl.:GameStateSerializationTest— a generated game survives a JSON round trip (grid, armies, policies).SessionPersistenceTest— a saved game is reloaded by a freshSessionService(standing in for a restart)../data/barony.mv.db, serves/state200.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/dataso the H2 file persists across container recreation (will add in the gateway PR).backend/pom.xmlchanged (new deps) — flagging for review.🤖 Generated with Claude Code