CPU match persistence
Scope
Issue #75 adds the server and database boundary for Basic CPU matches. The
browser submits an intended action identifier; the API loads the match state,
validates the action with @wits-world/game-rules, performs CPU actions with
server-side randomness, and commits the resulting state and audit records in
one transaction.
The player can have one active CPU match. Completed matches remain private to the authenticated player and are never recalculated from a later deck or card catalogue change.
Stored data
Migration 0012_create_cpu_matches.sql adds:
cpu_matchesfor the current authoritative state, status, version, timestamps, winner, and an optional source deck reference;cpu_match_cardsfor immutable player and CPU card snapshots;cpu_match_roundsfor before/after state and the results of one submitted transition; andcpu_match_actionsfor each player and server CPU action, including ordering, submission id, action input, and result.
The current state and round snapshots are JSONB because they are the complete state accepted by the pure rules package. The relational match, player, side, sequence, and status columns remain queryable and constrained by PostgreSQL. The source deck reference is nullable on deletion so removing a saved deck does not remove or invalidate a match snapshot.
Transaction and concurrency boundary
Every player action runs in a transaction:
- Lock the owning
cpu_matchesrow withFOR UPDATE. - Return the previously stored response when the submission id was already completed.
- Reject a completed match or stale expected version.
- Apply the player action through the shared rules package.
- Apply all required CPU actions using the server random source.
- Insert the round and ordered action audit rows.
- Update the match state, version, timer, and terminal result.
- Commit the complete transition.
Any failure rolls back the match state and all audit rows. The unique
(match_id, submission_id) constraint and the match row lock prevent one
client submission from being applied twice.
The API uses a 90-second server timer for a player's active CPU turn. A late
active-turn request becomes the rules engine's trusted timeout action. CPU
turns are resolved synchronously by the API and are never delegated to the
browser.
HTTP contract
Protected routes are exposed under /api/v1:
POST /matches/cpucreates a match from the player's selected deck;GET /matches/:matchIdreturns the owner's safe current state;POST /matches/:matchId/actionsaccepts a submission id, expected version, and one intended action; andGET /matches/historyreturns the authenticated player's completed records.
Current responses expose the player's cards and legal actions. CPU cards that have not been revealed remain hidden; only the active and defeated CPU Creatures and aggregate remaining counts are returned. The browser cannot send card stats, effects, random outcomes, winners, or completion state.
Verification
The API route tests cover authentication, identity derivation, rejection of
client-authoritative result fields, legal action payloads, timeout rejection,
and history isolation. Service tests cover a player action followed by a
server CPU action in one transaction and stale-version rollback. PostgreSQL
integration suites apply and re-run migration 0012 with the existing clean
database migration coverage.
This document was generated and edited with assistance from ChatGPT-Web[GPT-5.6 Sol].