@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
| Parameter | Type | Description |
|---|---|---|
count | number | How many seats; ids are 0..count-1. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
seat | number | A 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
The lanes for createFrameInput.
input()
ts
input(seat): MutableInputState;Parameters
| Parameter | Type | Description |
|---|---|---|
seat | number | A seat id. |
Returns
That seat's input, the same object every step; mutate it with press and friends.
isJoined()
ts
isJoined(seat): boolean;Parameters
| Parameter | Type | Description |
|---|---|---|
seat | number | A 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
| Parameter | Type | Description |
|---|---|---|
seat | number | The seat id. |
name | string | The display name; default p<seat>. |
data? | string | The 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
| Parameter | Type | Default value | Description |
|---|---|---|---|
seat | number | undefined | The seat id. |
reason | string | 'left' | Why; default left. |
Returns
void
message()
ts
message(
seat,
name,
payload?
): void;Queue a game message from a player.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
seat | number | undefined | The sender. |
name | string | undefined | The message name. |
payload | string | '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
| Parameter | Type | Description |
|---|---|---|
commands | readonly 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
| Parameter | Type | Description |
|---|---|---|
event | GameEvent | The 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
| Parameter | Type |
|---|---|
id | number |
Returns
AssetDesc | null | undefined
Inherited from
handleOf()
ts
handleOf(name): number;The handle a name resolves to, minting one when needed.
Parameters
| Parameter | Type |
|---|---|
name | string |
Returns
number
log()
ts
log(level, msg): void;Route a message to the host logger.
Parameters
| Parameter | Type |
|---|---|
level | LogLevel |
msg | string |
Returns
void
Inherited from
nowMs()
ts
nowMs(): number;Monotonic milliseconds since engine start. Never feed this to simulation.
Returns
number
Inherited from
overlapSphere()
ts
overlapSphere(
center,
radius,
filter,
maxResults
): readonly OverlapHit[];Bodies overlapping a sphere, nearest first.
Parameters
| Parameter | Type |
|---|---|
center | Vec3 |
radius | number |
filter | QueryFilter |
maxResults | number |
Returns
readonly OverlapHit[]
Inherited from
raycast()
ts
raycast(
origin,
direction,
maxDistance,
filter
): RayHit | null | undefined;Closest hit along a ray, or nullish on a miss.
Parameters
| Parameter | Type |
|---|---|
origin | Vec3 |
direction | Vec3 |
maxDistance | number |
filter | QueryFilter |
Returns
RayHit | null | undefined
Inherited from
raycastBatch()
ts
raycastBatch(rays): readonly (RayHit | null | undefined)[];One round trip for many rays; result index i matches rays[i].
Parameters
| Parameter | Type |
|---|---|
rays | readonly RayQuery[] |
Returns
readonly (RayHit | null | undefined)[]
Inherited from
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
| Parameter | Type |
|---|---|
name | string |
Returns
number | null | undefined
Inherited from
seed()
ts
seed(): number;The deterministic run seed, as a number (already Number()-coerced).
Returns
number
Inherited from
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
| Parameter | Type |
|---|---|
center | Vec3 |
radius | number |
filter | QueryFilter |
maxResults | number |
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
| Parameter | Type |
|---|---|
origin | Vec3 |
direction | Vec3 |
maxDistance | number |
filter | QueryFilter |
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
gamepads
ts
gamepads: GamepadState[];Overrides
keys
ts
keys: object;down
ts
down: Uint32Array;pressed
ts
pressed: Uint32Array;released
ts
released: Uint32Array;Overrides
mods
ts
mods: InputMods;Overrides
mouse
ts
mouse: MouseState;Overrides
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
| Parameter | Type |
|---|---|
frame | number |
output | FrameOutput |
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
hash
ts
hash: number;Combined hash of every frame.
Inherited from
hashes
ts
hashes: number[];One hashFrameOutput per frame, in order.
Inherited from
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
outputs
ts
outputs: FrameOutput[];Outputs, when keepOutputs is set.
Inherited from
players
ts
players: PlayersInput;The tape, as it stands after the last frame.
transformRows
ts
transformRows: number[];Transform row counts per frame.
Inherited from
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
| Parameter | Type |
|---|---|
input | HostFrameInput |
Returns
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
| Parameter | Type |
|---|---|
frame | number |
input | MutableInputState |
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
| Parameter | Type |
|---|---|
frame | number |
players | PlayersInput |
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
| Parameter | Type | Description |
|---|---|---|
overrides | FrameInputOverrides | Anything to change from the neutral frame. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
overrides | Partial<HostGameConfig> | Anything to change from the defaults. |
Returns
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
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
| Parameter | Type | Description |
|---|---|---|
options | MockHostOptions | Seed, scripted queries and the manifest. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
count | number | How many seats; ids are 0..count-1. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
state | MutableInputState | The 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
| Parameter | Type | Description |
|---|---|---|
commands | readonly 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
| Parameter | Type | Description |
|---|---|---|
out | FrameOutput | The 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
| Parameter | Type | Description |
|---|---|---|
hash | number | The running hash. |
text | string | The string. |
Returns
number
The new hash.
hashTransforms()
ts
function hashTransforms(transforms): number;Hash a packed transform buffer.
Parameters
| Parameter | Type | Description |
|---|---|---|
transforms | ArrayLike<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
| Parameter | Type | Description |
|---|---|---|
hash | number | The running hash. |
word | number | The word. |
Returns
number
The new hash.
packBodies()
ts
function packBodies(rows): Float32Array;Build a packed bodies row set, stride 15.
Parameters
| Parameter | Type | Description |
|---|---|---|
rows | readonly 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
| Parameter | Type | Description |
|---|---|---|
state | MutableInputState | The input state to mutate. |
key | string | A 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
| Parameter | Type | Description |
|---|---|---|
state | MutableInputState | The input state to mutate. |
button | number | A 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
| Parameter | Type | Description |
|---|---|---|
state | MutableInputState | The input state to mutate. |
key | string | A 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
| Parameter | Type | Description |
|---|---|---|
state | MutableInputState | The input state to mutate. |
button | number | A 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
| Parameter | Type | Description |
|---|---|---|
guest | Tickable | Anything with a tick, including a Sandbox. |
options | SimulateOptions | Frame count and the per-frame script. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
guest | Tickable | Anything with a tick, including a Sandbox, booted as an authority. |
options | SimulatePlayersOptions | Frame count, seats and the per-frame script. |
Returns
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
| Parameter | Type | Description |
|---|---|---|
value | unknown | Anything JSON-serialisable. |
Returns
string
The canonical rendering.