Skip to content

@gameable/test-harness ​

Classes ​

PlayersInput ​

Seats 0..count-1, each with its own mutable input, and the room events queued for the next step.

Example ​

ts
import { createPlayersInput, press } from 'gameable/test';

const tape = createPlayersInput(2);
tape.join(0);
press(tape.input(0), 'W');
const { players, events } = tape.frame();

Constructors ​

Constructor ​
ts
new PlayersInput(count): PlayersInput;
Parameters ​
ParameterTypeDescription
countnumberHow many seats; ids are 0..count-1.
Returns ​

PlayersInput

Properties ​

count ​
ts
readonly count: number;

How many seats; ids are 0..count-1.

Methods ​

endFrame() ​
ts
endFrame(): void;

Clear every seat's per-frame edges, keeping held keys held. Call it after each step.

Returns ​

void

entityOf() ​
ts
entityOf(seat): number;
Parameters ​
ParameterTypeDescription
seatnumberA seat id.
Returns ​

number

The entity the authority last said that seat controls (set-player-entity), or 0.

frame() ​
ts
frame(): PlayerLanes;

Take this step's lanes: every joined seat's input (its seq counts its steps since it joined, from 1) and the queued events, which are cleared.

Returns ​

PlayerLanes

The lanes for createFrameInput.

input() ​
ts
input(seat): MutableInputState;
Parameters ​
ParameterTypeDescription
seatnumberA seat id.
Returns ​

MutableInputState

That seat's input, the same object every step; mutate it with press and friends.

isJoined() ​
ts
isJoined(seat): boolean;
Parameters ​
ParameterTypeDescription
seatnumberA seat id.
Returns ​

boolean

True while that seat is joined.

join() ​
ts
join(
   seat, 
   name?, 
   data?
): void;

Seat a player from the next step on, with a player-joined event.

Parameters ​
ParameterTypeDescription
seatnumberThe seat id.
namestringThe display name; default p<seat>.
data?stringThe saved document as JSON, if any.
Returns ​

void

joined() ​
ts
joined(): number[];
Returns ​

number[]

The joined seats, ascending.

leave() ​
ts
leave(seat, reason?): void;

Free a seat on the next step, with a player-left event.

Parameters ​
ParameterTypeDefault valueDescription
seatnumberundefinedThe seat id.
reasonstring'left'Why; default left.
Returns ​

void

message() ​
ts
message(
   seat, 
   name, 
   payload?
): void;

Queue a game message from a player.

Parameters ​
ParameterTypeDefault valueDescription
seatnumberundefinedThe sender.
namestringundefinedThe message name.
payloadstring'null'JSON; default null.
Returns ​

void

observe() ​
ts
observe(commands): void;

Read a step's commands for the set-player-entitys, so entityOf follows spawns and possess.

Parameters ​
ParameterTypeDescription
commandsreadonly Command[]The step's frame-output.commands.
Returns ​

void

push() ​
ts
push(event): void;

Queue any room event. A player-joined seats its player and a player-left frees the seat, exactly as join and leave.

Parameters ​
ParameterTypeDescription
eventGameEventThe event.
Returns ​

void

Throws ​

On a seat out of range, a join to a taken seat, or a leave from an empty one.

Interfaces ​

FrameInputOverrides ​

Fields createFrameInput accepts.

Properties ​

bodies? ​
ts
optional bodies?: Float32Array<ArrayBufferLike>;

Packed body rows, stride 15.

contacts? ​
ts
optional contacts?: readonly Contact[];

Reported contacts.

dt? ​
ts
optional dt?: number;

Fixed timestep in seconds. Default 1 / 60.

elapsed? ​
ts
optional elapsed?: number;

Simulated seconds since init. Defaults to frame * dt.

events? ​
ts
optional events?: readonly GameEvent[];

Host-side events.

frame? ​
ts
optional frame?: number | bigint;

Fixed-step counter. Accepts a number for convenience.

input? ​
ts
optional input?: MutableInputState;

Input state to reuse; a fresh neutral one is built when absent.

players? ​
ts
optional players?: readonly PlayerInput[];

Every player's input in a room, ascending by id. Empty by default: one player.


LogLine ​

One recorded env.log call.

Properties ​

level ​
ts
level: LogLevel;
msg ​
ts
msg: string;

MockAsset ​

A manifest entry the mock host will resolve.

Properties ​

hasCollider? ​
ts
optional hasCollider?: boolean;

Whether a collider asset is attached. Default false.

kind? ​
ts
optional kind?: AssetKind;

Asset family. Default 'data'.

name ​
ts
name: string;

Manifest string id.

ready? ​
ts
optional ready?: boolean;

Whether the bytes are resident. Default true.

rig? ​
ts
optional rig?: string;

Rig backend for character assets.

tags? ​
ts
optional tags?: readonly string[];

Tags, verbatim.


MockHost ​

A mock host, plus the recordings a test asserts on.

Extends ​

Properties ​

log_ ​
ts
readonly log_: LogLine[];

Every env.log call, in order.

rayCalls ​
ts
readonly rayCalls: object;

How many raycast calls the guest made.

batch ​
ts
batch: number;
overlap ​
ts
overlap: number;
raycast ​
ts
raycast: number;

Methods ​

describe() ​
ts
describe(id): AssetDesc | null | undefined;

Metadata for a handle, or nullish when the handle is unknown.

Parameters ​
ParameterType
idnumber
Returns ​

AssetDesc | null | undefined

Inherited from ​

HostApi.describe

handleOf() ​
ts
handleOf(name): number;

The handle a name resolves to, minting one when needed.

Parameters ​
ParameterType
namestring
Returns ​

number

log() ​
ts
log(level, msg): void;

Route a message to the host logger.

Parameters ​
ParameterType
levelLogLevel
msgstring
Returns ​

void

Inherited from ​

HostApi.log

nowMs() ​
ts
nowMs(): number;

Monotonic milliseconds since engine start. Never feed this to simulation.

Returns ​

number

Inherited from ​

HostApi.nowMs

overlapSphere() ​
ts
overlapSphere(
   center, 
   radius, 
   filter, 
   maxResults
): readonly OverlapHit[];

Bodies overlapping a sphere, nearest first.

Parameters ​
ParameterType
centerVec3
radiusnumber
filterQueryFilter
maxResultsnumber
Returns ​

readonly OverlapHit[]

Inherited from ​

HostApi.overlapSphere

raycast() ​
ts
raycast(
   origin, 
   direction, 
   maxDistance, 
   filter
): RayHit | null | undefined;

Closest hit along a ray, or nullish on a miss.

Parameters ​
ParameterType
originVec3
directionVec3
maxDistancenumber
filterQueryFilter
Returns ​

RayHit | null | undefined

Inherited from ​

HostApi.raycast

raycastBatch() ​
ts
raycastBatch(rays): readonly (RayHit | null | undefined)[];

One round trip for many rays; result index i matches rays[i].

Parameters ​
ParameterType
raysreadonly RayQuery[]
Returns ​

readonly (RayHit | null | undefined)[]

Inherited from ​

HostApi.raycastBatch

reset() ​
ts
reset(): void;

Forget every recording.

Returns ​

void

resolveId() ​
ts
resolveId(name): number | null | undefined;

Manifest string id to handle, or nullish when the manifest has no entry.

Parameters ​
ParameterType
namestring
Returns ​

number | null | undefined

Inherited from ​

HostApi.resolveId

seed() ​
ts
seed(): number;

The deterministic run seed, as a number (already Number()-coerced).

Returns ​

number

Inherited from ​

HostApi.seed

warnings() ​
ts
warnings(): LogLine[];

Lines at 'warn' or 'error'.

Returns ​

LogLine[]


MockHostOptions ​

How to build a mock host.

Properties ​

assets? ​
ts
optional assets?: readonly (string | MockAsset)[];

Manifest entries: the assets this game is allowed to name.

With the default MockHostOptions.strictAssets this is the whole manifest, and a name that is not here resolves to nothing, exactly as a missing entry in assets.json would.

nowMs? ​
ts
optional nowMs?: () => number;

Milliseconds env.nowMs() returns; defaults to a 0.1 ms counter.

Returns ​

number

overlapSphere? ​
ts
optional overlapSphere?: (center, radius, filter, maxResults) => readonly OverlapHit[];

Scripted sphere overlap. Default: no hits.

Parameters ​
ParameterType
centerVec3
radiusnumber
filterQueryFilter
maxResultsnumber
Returns ​

readonly OverlapHit[]

raycast? ​
ts
optional raycast?: (origin, direction, maxDistance, filter) => RayHit | null;

Scripted raycast. Return null for a miss. Default: always a miss.

Parameters ​
ParameterType
originVec3
directionVec3
maxDistancenumber
filterQueryFilter
Returns ​

RayHit | null

seed? ​
ts
optional seed?: number;

The value env.seed() returns. Default 0x5eed1234.

strictAssets? ​
ts
optional strictAssets?: boolean;

Refuse to resolve names that are not in assets. Default true.

Minting a synthetic handle for any name a game asks for hides the most common asset bug there is — a typo, or an id that was never added to assets.json — behind a test that passes. Set it to false for a test that genuinely does not care which assets exist.


MutableInputState ​

A mutable, host-shaped input state.

Extends ​

Properties ​

focused ​
ts
focused: boolean;
Overrides ​

InputState.focused

gamepads ​
ts
gamepads: GamepadState[];
Overrides ​

InputState.gamepads

keys ​
ts
keys: object;
down ​
ts
down: Uint32Array;
pressed ​
ts
pressed: Uint32Array;
released ​
ts
released: Uint32Array;
Overrides ​

InputState.keys

mods ​
ts
mods: InputMods;
Overrides ​

InputState.mods

mouse ​
ts
mouse: MouseState;
Overrides ​

InputState.mouse


PlayerLanes ​

One step's room lanes: frame-input.players and the room's events.

Properties ​

events ​
ts
events: GameEvent[];

The joins, leaves and messages queued since the last step, in call order.

players ​
ts
players: PlayerInput[];

The joined seats, ascending by id.


PlayerView ​

What one joined player is shown on one frame, beyond the shared world.

Properties ​

camera ​
ts
camera: string | undefined;

Their own camera (set-player-camera) as canonical JSON, on the frames it was set.

entity ​
ts
entity: number;

The entity this player controls, from the latest set-player-entity; 0 for none.

hud ​
ts
hud: string | undefined;

Their own HUD JSON (set-player-hud), only on the frames it changed.

player ​
ts
player: number;
sends ​
ts
sends: ReceivedSend[];

The sends addressed to them or to everyone, in command order.


ReceivedSend ​

One send a player receives on a frame.

Properties ​

broadcast ​
ts
broadcast: boolean;

True for a send with no to: everyone got it.

name ​
ts
name: string;
payload ​
ts
payload: string;
reliable ​
ts
reliable: boolean;

SimulateOptions ​

How to run a simulation.

Properties ​

dt? ​
ts
optional dt?: number;

Fixed timestep in seconds. Default 1 / 60.

frames ​
ts
frames: number;

How many frames to run.

keepOutputs? ​
ts
optional keepOutputs?: boolean;

Keep every frame-output. Off by default: outputs are reused objects.

onOutput? ​
ts
optional onOutput?: (frame, output) => void;

Called with each frame-output right after its tick, before the input's edges clear.

Parameters ​
ParameterType
framenumber
outputFrameOutput
Returns ​

void

script? ​
ts
optional script?: FrameScript;

Per-frame overrides. Mutate input (the same state object is reused and its edges cleared between frames) or return fields to merge. A returned input replaces it for that frame (the caller then owns its edges).


SimulatePlayersOptions ​

How to run a room simulation.

Properties ​

dt? ​
ts
optional dt?: number;

Fixed timestep in seconds. Default 1 / 60.

frames ​
ts
frames: number;

How many frames to run.

keepOutputs? ​
ts
optional keepOutputs?: boolean;

Keep every frame-output.

players? ​
ts
optional players?: number | PlayersInput;

The seats: a count (default DEFAULT_ROOM_SEATS, 8, the SDK's default room) or a tape to reuse.

script? ​
ts
optional script?: PlayersScript;

Per-frame joins, leaves, messages and input.


SimulatePlayersResult ​

What a room simulation produced.

Extends ​

Properties ​

commandTags ​
ts
commandTags: string[][];

Commands seen per frame, by tag.

Inherited from ​

SimulateResult.commandTags

hash ​
ts
hash: number;

Combined hash of every frame.

Inherited from ​

SimulateResult.hash

hashes ​
ts
hashes: number[];

One hashFrameOutput per frame, in order.

Inherited from ​

SimulateResult.hashes

hud ​
ts
hud: object[];

Every HUD payload that crossed, with the frame it crossed on.

frame ​
ts
frame: number;
json ​
ts
json: string;
Inherited from ​

SimulateResult.hud

outputs ​
ts
outputs: FrameOutput[];

Outputs, when keepOutputs is set.

Inherited from ​

SimulateResult.outputs

players ​
ts
players: PlayersInput;

The tape, as it stands after the last frame.

transformRows ​
ts
transformRows: number[];

Transform row counts per frame.

Inherited from ​

SimulateResult.transformRows

views ​
ts
views: PlayerView[][];

views[frame]: one entry per seat joined on that frame, ascending.


SimulateResult ​

What a simulation produced.

Extended by ​

Properties ​

commandTags ​
ts
commandTags: string[][];

Commands seen per frame, by tag.

hash ​
ts
hash: number;

Combined hash of every frame.

hashes ​
ts
hashes: number[];

One hashFrameOutput per frame, in order.

hud ​
ts
hud: object[];

Every HUD payload that crossed, with the frame it crossed on.

frame ​
ts
frame: number;
json ​
ts
json: string;
outputs ​
ts
outputs: FrameOutput[];

Outputs, when keepOutputs is set.

transformRows ​
ts
transformRows: number[];

Transform row counts per frame.


Tickable ​

Anything with the guest's per-frame entry point.

Methods ​

tick() ​
ts
tick(input): FrameOutput;
Parameters ​
ParameterType
inputHostFrameInput
Returns ​

FrameOutput

Type Aliases ​

FrameScript ​

ts
type FrameScript = (frame, input) => FrameInputOverrides | void;

A per-frame script. Mutate input in place, and optionally return fields to merge into the frame.

void in the return union is deliberate: a script that only mutates input should not have to write return undefined.

Parameters ​

ParameterType
framenumber
inputMutableInputState

Returns ​

FrameInputOverrides | void


PlayersScript ​

ts
type PlayersScript = (frame, players) => FrameInputOverrides | void;

A per-frame script over the seats. Join, leave, message and press on players, or return fields to merge into the frame; returned events follow the tape's.

There is no separate input lane to press: as in a room, the frame's own input is seat 0's (players.input(0)) while seat 0 is joined, and neutral otherwise.

Parameters ​

ParameterType
framenumber
playersPlayersInput

Returns ​

FrameInputOverrides | void

Variables ​

PACKAGE ​

ts
const PACKAGE: "@gameable/test-harness";

Package identity marker.

Example ​

ts
import { PACKAGE } from 'gameable/test';

console.log(PACKAGE); // 'gameable/test'

Functions ​

createFrameInput() ​

ts
function createFrameInput(overrides?): HostFrameInput;

Build one frame-input, in host-side shapes.

Parameters ​

ParameterTypeDescription
overridesFrameInputOverridesAnything to change from the neutral frame.

Returns ​

HostFrameInput

A fresh frame input.

Example ​

ts
import { createFrameInput } from 'gameable/test';

const input = createFrameInput({ frame: 0 });

createGameConfig() ​

ts
function createGameConfig(overrides?): HostGameConfig;

Build a game-config, in host-side shapes.

Parameters ​

ParameterTypeDescription
overridesPartial<HostGameConfig>Anything to change from the defaults.

Returns ​

HostGameConfig

A fresh config.

Example ​

ts
import { createGameConfig } from 'gameable/test';

sandbox.init(createGameConfig({ seed: 7n }));

createInputState() ​

ts
function createInputState(): MutableInputState;

Build a neutral input state: nothing held, nothing moving, canvas focused.

Returns ​

MutableInputState

A fresh mutable input state.

Example ​

ts
import { createInputState, press } from 'gameable/test';

const state = createInputState();
press(state, 'W');

createMockHost() ​

ts
function createMockHost(options?): MockHost;

Create a scripted host.

Parameters ​

ParameterTypeDescription
optionsMockHostOptionsSeed, scripted queries and the manifest.

Returns ​

MockHost

A HostApi with recordings attached.

Example ​

ts
import { createMockHost } from 'gameable/test';

const host = createMockHost({
  seed: 42,
  assets: ['env.arena'],
  raycast: (origin) => ({
    body: 1,
    entity: 1,
    point: origin,
    normal: { x: 0, y: 1, z: 0 },
    distance: 2,
  }),
});

createPlayersInput() ​

ts
function createPlayersInput(count): PlayersInput;

Make a multi-player tape with count seats, all empty.

Parameters ​

ParameterTypeDescription
countnumberHow many seats; ids are 0..count-1.

Returns ​

PlayersInput

The tape.

Example ​

ts
import { createFrameInput, createPlayersInput } from 'gameable/test';

const tape = createPlayersInput(3);
tape.join(0);
tape.join(1);
const out = guest.tick(createFrameInput({ frame: 0, ...tape.frame() }));
tape.observe(out.commands);
tape.endFrame();

endFrame() ​

ts
function endFrame(state): void;

Clear the per-frame edges, keeping held keys held.

Call this between frames, exactly as gameable/input does.

Parameters ​

ParameterTypeDescription
stateMutableInputStateThe input state to mutate.

Returns ​

void

Nothing.

Example ​

ts
import { endFrame } from 'gameable/test';

endFrame(state);

hashCommands() ​

ts
function hashCommands(commands): number;

Hash a command list.

Commands are JSON, not floats: their payloads are structural, and a textual difference is exactly what a parity test wants to see.

Parameters ​

ParameterTypeDescription
commandsreadonly Command[]The frame-output.commands list.

Returns ​

number

A 32-bit hash.

Example ​

ts
import { hashCommands } from 'gameable/test';

expect(hashCommands(out.commands)).toMatchInlineSnapshot();

hashFrameOutput() ​

ts
function hashFrameOutput(out): number;

Hash a whole frame-output: transforms, commands, local commands, camera and HUD.

Local commands fold in only when there are some, so a single-player frame hashes exactly as it did before local-commands existed.

Parameters ​

ParameterTypeDescription
outFrameOutputThe output to hash.

Returns ​

number

A 32-bit hash.

Example ​

ts
import { hashFrameOutput } from 'gameable/test';

expect(hashFrameOutput(direct)).toBe(hashFrameOutput(wasm));

hashString() ​

ts
function hashString(hash, text): number;

Fold a string into a running FNV-1a hash.

Parameters ​

ParameterTypeDescription
hashnumberThe running hash.
textstringThe string.

Returns ​

number

The new hash.


hashTransforms() ​

ts
function hashTransforms(transforms): number;

Hash a packed transform buffer.

Parameters ​

ParameterTypeDescription
transformsArrayLike<number>The frame-output.transforms list, stride 12.

Returns ​

number

A 32-bit hash.

Example ​

ts
import { hashTransforms } from 'gameable/test';

expect(hashTransforms(a.transforms)).toBe(hashTransforms(b.transforms));

hashU32() ​

ts
function hashU32(hash, word): number;

Fold a 32-bit word into a running FNV-1a hash, little-endian.

Parameters ​

ParameterTypeDescription
hashnumberThe running hash.
wordnumberThe word.

Returns ​

number

The new hash.


packBodies() ​

ts
function packBodies(rows): Float32Array;

Build a packed bodies row set, stride 15.

Parameters ​

ParameterTypeDescription
rowsreadonly object[]One entry per live body.

Returns ​

Float32Array

A Float32Array sorted ascending by body id.

Example ​

ts
import { packBodies } from 'gameable/test';

const bodies = packBodies([{ body: 1, position: [0, 1, 0] }]);

press() ​

ts
function press(state, key): void;

Hold a key down and record the press edge.

Parameters ​

ParameterTypeDescription
stateMutableInputStateThe input state to mutate.
keystringA key name: 'KeyW', 'W', 'Shift'.

Returns ​

void

Nothing.

Example ​

ts
import { createInputState, press } from 'gameable/test';

const state = createInputState();
press(state, 'Space');

pressMouse() ​

ts
function pressMouse(state, button): void;

Press a mouse button.

Parameters ​

ParameterTypeDescription
stateMutableInputStateThe input state to mutate.
buttonnumberA bit mask; 1 left, 2 right, 4 middle.

Returns ​

void

Nothing.

Example ​

ts
import { pressMouse } from 'gameable/test';

pressMouse(state, 1);

release() ​

ts
function release(state, key): void;

Release a key and record the release edge.

Parameters ​

ParameterTypeDescription
stateMutableInputStateThe input state to mutate.
keystringA key name.

Returns ​

void

Nothing.

Example ​

ts
import { release } from 'gameable/test';

release(state, 'Space');

releaseMouse() ​

ts
function releaseMouse(state, button): void;

Release a mouse button.

Parameters ​

ParameterTypeDescription
stateMutableInputStateThe input state to mutate.
buttonnumberA bit mask.

Returns ​

void

Nothing.

Example ​

ts
import { releaseMouse } from 'gameable/test';

releaseMouse(state, 1);

simulate() ​

ts
function simulate(guest, options): SimulateResult;

Run a guest for frames fixed steps.

Parameters ​

ParameterTypeDescription
guestTickableAnything with a tick, including a Sandbox.
optionsSimulateOptionsFrame count and the per-frame script.

Returns ​

SimulateResult

Hashes, command tags, HUD payloads and row counts.

Example ​

ts
import { simulate, press } from 'gameable/test';

const result = simulate(sandbox, {
  frames: 300,
  script: (frame, input) => {
    if (frame === 10) press(input, 'W');
  },
});

simulatePlayers() ​

ts
function simulatePlayers(guest, options): SimulatePlayersResult;

Run a room's authority for frames fixed steps from a multi-player tape.

Each step is built the way a room builds it: frame-input.players from the joined seats, and frame-input.input aliased to seat 0's input while seat 0 is joined (a neutral input otherwise), so ctx.input and ctx.players.get(0).input agree.

Parameters ​

ParameterTypeDescription
guestTickableAnything with a tick, including a Sandbox, booted as an authority.
optionsSimulatePlayersOptionsFrame count, seats and the per-frame script.

Returns ​

SimulatePlayersResult

simulate's hashes, tags and HUD, plus every player's view per frame.

Example ​

ts
import { press, simulatePlayers } from 'gameable/test';

const result = simulatePlayers(sandbox, {
  frames: 200,
  players: 2,
  script: (frame, players) => {
    if (frame === 0) players.join(0);
    if (frame === 10) players.join(1);
    if (frame === 12) press(players.input(1), 'W');
    if (frame === 150) players.leave(1);
  },
});
console.log(result.views[100].map((v) => v.entity));

stableJson() ​

ts
function stableJson(value): string;

JSON with object keys sorted, so two structurally identical payloads hash the same whatever order their fields were assigned in.

Parameters ​

ParameterTypeDescription
valueunknownAnything JSON-serialisable.

Returns ​

string

The canonical rendering.