Test gameplay with playtests
Write a playtest scenario that drives the running game and checks what happened, then run it against the browser or a native build.
On this page
Run a scenario
A scenario is a JSON file that lists input steps and the results to check. Generated projects keep
them in playtests/. Run one against your dev server:
npx @threenative/playtest playtests/movement.playtest.json \
--url http://127.0.0.1:5173 \
--server-command "npm run dev"--server-command starts the server before the run. If your target uses WebGPU, pass the Chromium
flags with --browser-arg, once per flag:
npx @threenative/playtest playtests/movement.playtest.json \
--url http://127.0.0.1:5173 \
--browser-arg --enable-unsafe-webgpu \
--browser-arg --enable-features=Vulkanpnpm test in a generated project builds the web target and runs every playtests/*.playtest.json.
If the project has no playtest setup, run npx @threenative/playtest init. It creates a config, a
smoke scenario and an adapter example without changing your source.
Write a scenario
This trimmed scenario from the minimal template holds the right arrow for 60 ticks, waits, then
checks that the HUD changed and nothing logged an error:
{
"name": "play",
"target": "web",
"schemaVersion": 1,
"warmupFrames": 60,
"steps": [
{ "kind": "input", "press": "ArrowRight", "holdTicks": 60, "release": true },
{ "kind": "wait", "waitTicks": 600, "release": true }
],
"assert": {
"diagnostics": { "noConsoleErrors": true, "runtimeReady": true },
"components": [{ "entity": "hud", "component": "glyphs", "changed": true }]
}
}Assert on what the player sees: position, state, camera, visibility. These checks survive a rewrite of the code behind them.
Connect the game
Checks on entities, cameras, movement and visibility need a bridge inside the game. Generated
projects install it with the playtest() plugin:
import { playtest } from "@threenative/core/playtest";
const game = defineGame({
plugins: [rapier(), playtest()],
// ...
});Without the bridge, these assertions fail with TN_PLAYTEST_BRIDGE_MISSING. Input, screenshot,
DOM, console, network and trace checks work without it.
Read the result
A run passes only when at least one assertion ran against an observation that arrived. The runner fails a missing entity, an absent resource, an empty effect log and a scenario with no assertions. It rejects wrong-typed assertion values when it loads the scenario.
Check the setup
npx @threenative/playtest doctor --text
npx @threenative/playtest doctor --url http://127.0.0.1:5173 --textThe first command checks that this machine can run a playtest. With --url, doctor also reports
what the running game exposes.
Test a native build
The same scenario format runs with --target browser, --target desktop or --target android.
For desktop, pass the packaged executable:
npx @threenative/playtest playtests/device-smoke.playtest.json \
--target desktop \
--executable .threenative/build/ThreeNativeThe runner hands the native host a temporary mailbox, so the game source stays unchanged. Run release checks against the extracted or installed build. Native runtime covers packaging.
Network, DOM and visual-metric assertions are browser-only. On a device target they fail with
TN_PLAYTEST_UNSUPPORTED_ON_TARGET and exit code 2. Keep shared gameplay checks in one scenario
and put touch or window checks in target-specific ones.