Making a game

Proving a game

Tests, bots, the simulator, replays and playing it.

A game is shown to work by tests that boot it as its pages do and read what its kinds wrote off the rows, by seats that play themselves, and by playing it.

Which test and which command

FileRuns inCommand
<name>.test.tsnode: kinds, scenes, plugins, addons, pages over the loopbackyarn vitest run <file>
<name>.game.test.ts, <game>.predict.test.tsnode: a whole game booted and played for seconds to minutes of its timeyarn test:game <file>

Run each test beside what changed, one file at a time, from the repo's root. yarn test leaves out the .game.test.ts and .predict.test.ts files, and yarn vitest run on one of them finds no test; yarn test:game runs them. yarn tsc checks types across the repo.

A kind's test

  1. Put it beside the file it tests: <kind>.sim.test.ts beside <kind>.sim.ts, <scene>.sim.test.ts for how a scene plays through.
  2. Boot the manager with boot and load, and take a seat with connection.
  3. Step, and send a message with seat.take as a page would, at the seat's own row found with possessed(rows).
  4. Read the child scene's rows and check them against numbers read from the scene file and the definitions, never typed again.
  5. Read a scene file through scenemanager's child, never by importing <scene>.scene.json: a scene that extends a partial holds only its own part in its file.
const { scene: highlands } = await child(file as unknown as Scene, 'highlands', file.prefabs.scenemanager.scenes.highlands, here)

file is the manager scene and here the game folder's url.

The press test

A game whose seat controls anything has <game>.predict.test.ts beside the manager. It makes pages that run ahead of the server (predict) on a clean wire and bad ones, presses every frame, and checks that on every frame the seat's own player row has taken every press sent by the frame before. It runs with yarn test:game <file>.

Replay

The scene's test plays a real page under the recorder, then seeks back across the run. Each step holds the rows it held when played, and rec.failure is undefined and rec.skipped empty after every step and seek.

A seat that plays itself

A think reads the rows the seat sees and returns a press or nothing. It can be played three ways:

  • a page think on the simulator, with predict: true;
  • playtest(truth, think) from the player addon, for the rules alone, one slot per bot;
  • the playtester plugin, in a scene of its own that lists it, or in dev.html.

A test never changes a scene in memory. To try another value, it calls the function that value feeds, or writes a scene file of its own into a temporary folder.

The simulator

The simulator plays pages against a server on one timeline, with the real code at both ends, and writes every frame, step, packet and message as JSONL. Call simulate(run) in a test, or run it from the repo's root:

node --experimental-strip-types addons/simulator/simulator.addon.ts run &lt;config.json&gt; [out.jsonl]

Give the run a watch, or no rows are recorded. A gym is the same play run over many seeds, networks or starts, with facts stated about all of them.

A plugin's or an addon's test

It writes its own game into a temporary folder, a manager and a child scene, and boots that. A test outside games/ never uses a game in games/.

Playing it

  • In one tab: the game's index.html, opened as in Starting a game.
  • Against its server: node --experimental-strip-types games/<game>/<game>.server.ts and client.html. A key moves the seat's thing on the next frame.
  • In the testbed, dev.html, which may enter any scene and override any value in memory.

Checking it

  • Every test beside what changed passes, the press test on every wire.
  • The game replays under the recorder.
  • yarn tsc shows no error in the files written.
  • The game is played in one tab and against its server.