Skip to content

Save & Restore ​

simulatorClient.matches.save() captures a full state snapshot of one live match into a wire-format MatchSavePayload. simulatorClient.matches.restore() reconstitutes that payload into a fresh live session — new match id, fresh seat tokens, AI driver respawned if the original session had one.

This is not the same as the older debug replay flow. Replay re-runs every command from match creation; save captures the engine state directly, works for AI-opponent matches, and does not re-execute any commands.

When to use this ​

  • "Bookmark" a match position so testers or developers can return to it.
  • Move an in-progress match between simulator instances (export from one server, import on another running the same release).
  • Snapshot a tricky state for a bug report — paired with the wire-format schema, the payload is everything an investigator needs to reproduce.

Save ​

ts
const 
payload
= await
simulatorClient
.
matches
.
save
(
hostAccess
)

The returned payload includes:

  • version: wire-format version. Compare to your SDK's schemaVersion if you persist payloads across releases.
  • sourceMatchId: original match id, retained for traceability.
  • seats: per-seat decks. Used at restore to rebuild the catalog.
  • aiOpponents: one AiOpponentConfig per AI seat the source match had.
  • state: opaque engine state. Treat as a black box; round-trip it byte-for-byte.

The simulator does not persist the payload. Save it wherever fits your deployment — localStorage, IndexedDB, a file download, or a server upload sit equally well. JSON serialization is the only requirement.

ts
const 
payload
= await
simulatorClient
.
matches
.
save
(
hostAccess
)
// localStorage example. Use the schema version in the key so a future SDK // release does not silently load an incompatible payload.
localStorage
.
setItem
('swu-save',
JSON
.
stringify
(
payload
))

Restore ​

Public HTTP save and restore require SWU_SIMULATOR_SAVE_SIGNING_SECRET. The server signs every payload field with HMAC-SHA256 and binds that signature to the seat token supplied outside the payload. Keep this server-only secret stable across restarts; changing it intentionally invalidates prior public saves. The save payload never contains the seat token.

ts
import type { MatchSavePayload } from '@my-swu/simulator-client'

const 
persisted
=
JSON
.
parse
(
localStorage
.
getItem
('swu-save') ?? '{}',
) as MatchSavePayload // The source access proves the caller owned the saved match: the server // checks the payload signature against this seat token. const
restored
= await
simulatorClient
.
matches
.
restore
(
hostAccess
,
persisted
)

restored carries everything you need to reconnect:

  • matchId: freshly minted id for the restored session.
  • accesses: one MatchAccessResponse per seat, with new seatTokens. Original tokens are not preserved.
  • snapshot: public board state at restore time, identical in shape to a GET /api/matches/{id} response.

Connect the restored seat tokens the same way you would for a freshly created match:

ts
const 
persistedSave
=
JSON
.
parse
(
localStorage
.
getItem
('swu-save') ?? '{}',
) as import('@my-swu/simulator-client').
MatchSavePayload
const
restored
= await
simulatorClient
.
matches
.
restore
(
hostAccess
,
persistedSave
)
const
restoredHost
=
restored
.
accesses
.
find
(
access
=>
access
.
seat
=== 'one')
if (!
restoredHost
) throw new
Error
('seat one missing')
const
session
=
simulatorClient
.
session
.
create
(
restoredHost
)
await
session
.
connect
()

What is preserved ​

  • Engine state (board, hands, decks, prompt, phase, round number).
  • MatchSeed-driven RNG (the saved seed continues to drive any subsequent shuffle or AI decision).
  • AI configuration: an AI-opponent match comes back with the AI driver re-spawned from saved AiOpponentConfig entries. The MCTS tree is rebuilt fresh; only the configuration rides along, not the search state.

What is not preserved ​

  • Match id and seat tokens. Both are minted fresh on restore. Hand the new tokens out to your seats; the old ones do not authorize anything.
  • The websocket transport. Restore is HTTP-only; reopen sockets against the new match id and seat tokens after the call returns.
  • AI search state (MCTS tree, RNG step). The rebuilt driver starts cold.

Validation ​

Restore rejects payloads early so the HTTP surface returns 400 rather than 500. The current rejections are:

  • Wire-format version mismatch.
  • Empty seats list.
  • Duplicate seats in seats.
  • Seat one missing from seats.
  • A seat referenced in seats that is not configured for the match's playerCount.
  • A joined seat whose deck is not present in seats.

These are surface-level integrity checks. Beyond them, restore re-validates every seat's deck against the catalog, which catches stale saves built against a different card set.

Limitations ​

  • Save and restore are HTTP-only. Saving from a websocket client requires a separate POST /api/matches/{id}/save HTTP call.
  • The simulator stores nothing on disk. Persistence is the SDK consumer's responsibility.
  • Save payloads are tied to the wire format. Bumping schemaVersion invalidates older payloads.
  • Restore re-validates decks against the running server's catalog. A save from a release with a card the new server does not know will fail.

Released under the MIT License.