Give tests a ready-made world with named fixtures, save a running game as a snapshot, and start a scenario from either, instead of playing up to the interesting moment every time.
Fixtures: worlds built in code
A fixture is a named function that fills a new world. Add one to the apple tree from Getting started, with a sim method on Tree to build it:
// Sim code a fixture calls to build a world; not a command, so no player can send it. public void Ripen(int apples) => Apples = apples;
And in OrchardGame:
// Ready-made worlds that scenarios, tests and live control load by name. public override void DefineFixtures(FixtureSet fixtures) => fixtures .Add("heavy-tree", world => world.Add(new Tree()).Ripen(40)) .Add("bare-tree", world => world.Add(new Tree()));
A scenario starts from it with fixture::
name: heavy-tree description: Start from a tree with 40 apples and pick ten. fixture: heavy-tree steps: - command: { entity: Tree, name: Pick, args: [10] } - step: 1 expect: - state: { entity: Tree, property: Apples, equals: 30 } - ui: { id: apples, text: "30 on the tree, 10 picked" }
PASS heavy-tree (32 ms, tick 1, hash 78e6a5eeffa093c0) 1 passed, 0 failed
fixture: also works as a step, to switch worlds mid-scenario, and live control loads one with fixture_load.
Snapshots: a saved game
A snapshot is the whole world at one tick: every entity, the random streams and the tick, with its state hash. Save one from a running game over live control. This session uses the small MCP client from Drive a running game over MCP, which prints each tool call and its result:
fixture_load {"name": "heavy-tree"} -> {"frame":0,"tick":0,"hash":"5a1f6ada5d6e7e3e"} command_send {"entity": "Tree", "command": "Pick", "args": ["5"]} -> {"entity":"Tree#1","command":"Pick","status":"Queued"} step {"frames": 120} -> {"frame":120,"tick":120,"hash":"8984cf483a6e339b"} state_query {"entity": "Tree"} -> {"id":1,"type":"Tree","Apples":43,"Picked":5,"Growth":0} snapshot_save {"path": "snapshots/after-pick.ogs"} -> {"path":"/tmp/games/orchard/artifacts/snapshots/after-pick.ogs","bytes":103,"hash":"8984cf483a6e339b","tick":120} reset {} -> {"frame":120,"tick":0,"hash":"6554bfe7b94da5ca"} state_query {"entity": "Tree"} -> {"id":1,"type":"Tree","Apples":0,"Picked":0,"Growth":0} snapshot_load {"path": "snapshots/after-pick.ogs"} -> {"frame":120,"tick":120,"hash":"8984cf483a6e339b"} state_query {"entity": "Tree"} -> {"id":1,"type":"Tree","Apples":43,"Picked":5,"Growth":0} quit {} -> {"frame":120,"tick":120,"hash":"8984cf483a6e339b"}
The loaded world has the hash it was saved with. File paths are relative to the game’s output folder (artifacts/ unless the game was started with --artifacts), and a path that leaves it is refused.
Copy the file next to the scenarios and start from it with snapshot::
name: from-snapshot description: Carry on from a saved game with 43 apples and 5 picked, at tick 120. snapshot: snapshots/after-pick.ogs steps: - command: { entity: Tree, name: Pick, args: [3] } - step: 1 expect: - state: { entity: Tree, property: Picked, equals: 8 } - tick: 121
PASS from-snapshot (28 ms, tick 121, hash 4ad2aa9eebde135b) 1 passed, 0 failed
The scenario carries on from tick 120, so after one step it is at tick 121.
Good to know
- A snapshot loads only into a game with the same seed. After a
resetwith another seed,snapshot_loadfails withbad_data: “The save could not be loaded: SeedMismatch (None).” - In a single-player game a save is a snapshot:
GameInstance.SaveSnapshot()returns the bytes andLoadSnapshot(bytes)restores them. Snapshots are versioned. A[Migrate]method on an entity loads state saved by an older schema. Determinism.SnapshotRestoreproves a game survives a save and load mid-run: see Test a game from C#.- Loading a fixture or a snapshot replaces the world, so a running recording starts a new segment there and replays still match.