Playtest verification loop proving gameplay executed evidence
- MOVE VALUES ARE {x, y}, NEVER {x, z}. A vector2d move value is
{"x": <strafe>, "y": <forward>}and y drives world z. A value spelled{x, z}is REJECTED (value_type_mismatch, reason expected_x_y) and the step contributes ZERO displacement - the player stands still while the step reads as processed. Nine straight proofs were voided this way on the arena benchmark. Keep every vector unit magni
Use this for
Not for
Pairs with: Game loop assembly entities scripts HUD win shippable loop, Collectibles pickups scoring and win condition, World build document authoring compile apply loop, Spawn points checkpoints and respawn flow
Playtest verification loop proving gameplay with executed evidence
The five walk traps that void a proof (the first four measured live, the fifth proven from the runtime datastore-read substrate)
- MOVE VALUES ARE {x, y}, NEVER {x, z}. A vector2d move value is
{"x": <strafe>, "y": <forward>}and y drives world z. A value spelled{x, z}is REJECTED (value_type_mismatch, reason expected_x_y) and the step contributes ZERO displacement - the player stands still while the step reads as processed. Nine straight proofs were voided this way on the arena benchmark. Keep every vector unit magnitude; to reach a diagonal target chain two axis-aligned walks. - READ rejectCode ON EVERY STEP. A step whose rejectCode is set did NOT happen; a proof whose steps were rejected proves nothing. Combined with the rule below (an unchanged after-state is not proof of gameplay), a valid positive proof shows applied steps AND a changed after-state.
- A MOVE STEP WALKS ITS WHOLE
ticksBUDGET; SIZE IT TO THE GAP. A held move re-injects on every authoritative tick, so a single move step keeps walking for all of itsticks(roughly the player's speed divided by the tick rate, times ticks; the proof room runs at 20 Hz, so a CharacterMovementComponent maxSpeed of 8 covers about 0.4 m per tick, soticks20 walks about 8 m). A biggertickswalks strictly farther: the SAME held step withticks10 andticks60 travels roughly 4 m then 24 m (measured on the FP-template arena). The real trap is UNDERSIZING it - a move with too fewticksbarely leaves the start, so a coin one meter away reads as never touched when the avatar simply did not walk far enough, not because the collider is broken. READpossessedDelta.distanceandpossessedDelta.movedin the outcome: they report exactly how far the possessed avatar actually travelled. Give the move enoughticksto cover the gap (chaining several move steps also accumulates) BEFORE you judge whether the pickup fired. - AN INTERACTIVE STARTER PROP IN THE WALK PATH EJECTS THE AVATAR. The first-person template ships live gameplay props on the starter floor - a jump-pad (a trigger volume near
{x: 2, z: -1.5}whose handler issues a physicslaunch) and an auto-door - and building your own arena ON TOP of the template does NOT remove them; they stay live under whatever you place. A straightmovewhose path crosses the jump-pad's trigger footprint fires the launch, flings the avatar up and out of the arena, and the pickup one meter ahead reads as never reached - not because the collider or the coin is broken, but because the avatar left the floor mid-walk. The tell ispossessedDeltashowing a large VERTICAL jump or an end position far outside the arena bounds. Before planning the move, read the before scene digest (orproject_get_graph_snapshot) for props taggedinteractive/jump_pad/ withcollision:"trigger"colliders that sit between the spawn and the target, and either route themovearound them (chain axis-aligned walks that skirt the trigger footprint) or, if you are building your own gameplay over the starter content, remove or disable the starter jump-pad and other interactive starter props first so your walk path is clear. - A PICKUP GATED BEHIND A DATASTORE READ NEVER RESOLVES IN THE PROOF ROOM. If your coin or pickup handler READS A PERSISTENT DATASTORE before it despawns the entity or adds to the score, the proof room parks the handler and the outcome never appears. The tell is a diagnostic of kind
script.dispatch_waitingtogether with an EMPTY outcomes set (no despawn, no score) even thoughpossessedDeltashows the avatar walked onto the pickup. This is NOT a short walk or a broken collider.script.dispatch_waitingcarries a single meaning: the runtime queued an ASYNCHRONOUS datastore read and suspended the handler until that read resolves. The discardable proof room does not run the authority worker that services datastore reads and never delivers the read result back into the tick loop, so the read never completes and NO number of extra or trailing ticks will ever flush it. Running idle "settle" ticks after the walk only re-parks the handler tick after tick (proven in server/tests/playtestDatastoreReadDispatchBoundary.test.ts), so do not expect a longer budget or a trailing wait to expose the pickup. To PROVE a pickup here, author the despawn and the score as DIRECT IN-ROOM effects the handler applies inside the tick loop (despawn the coin and add to the score in the handler body itself), not gated behind a datastore read. If the pickup genuinely needs a persistent datastore (for example a cross-session score), its EXECUTED proof belongs on the full runtime service that runs the authority worker, not the discardable proof room, the same substrate boundary the two-client replication check already carries. One timing corollary that applies to EVERY trigger pickup, datastore or not: a trigger handler dispatches one tick AFTER contact (the script step runs before the collision step in the tick pipeline), so give the move enoughticksto make contact AND run at least one further tick, and never end the walk exactly on the coin.
Reading a target-policy rejection
An interact (or any world_target action) can be REJECTED by the action's targetPolicy even when the avatar is right on the target. When a step reports rejectCode target_policy_failed, the outcome diagnostic now carries the exact reason and the fix data: the human reason (for example "Input action target entity lacks an allowed tag") plus the structured details folded into the message (actionKey, the allowedTags the target must carry, and targetEntityId, or a maxDistance or line-of-sight limit for the other target-policy classes). Do NOT read target_policy_failed as a broken engine or a bad step; it is the policy telling you the target did not satisfy the action's targetPolicy.
Two fixes, both authorable: TAG THE TARGET with one of the action's allowedTags (the diagnostic prints the exact tag set), or WIDEN THE POLICY on the action so the target the loop aims at qualifies (add the target's tag to allowedTags, relax entityRequired, or raise maxDistance). Re-run world_playtest_scenario and confirm the interact step now reports applied. An interact action whose allowedTags is ["interactable"] fired at an untagged prop rejects until the prop carries interactable or the policy admits the prop's tag.
When to use
Before you accept a build or a change as done. Do not declare a loop finished on the strength of the writes landing; PLAY it and read the evidence. world_playtest_scenario is the evidence step.
What it does
It boots an isolated proof room for the world, possesses an entity, executes a typed action sequence with per step tick budgets, and returns a revision stamped outcome: before and after scene digests, per step applied or failed, and diagnostics. The proof room is discarded and leaves no project state, so it is safe to run repeatedly.
The recipe
- Ensure a possessable player exists. The scenario possesses
possessed_entity_id; without a real controllable player entity the run has nothing to drive. A missing possessable player, not the tool, is the usual cause of a useless playtest. - Call
world_playtest_scenariowithgame_id,possessed_entity_id, an optionalworld_id, and astepsarray. Each step has akindofmove,look,jump,interact, orpress, with optionalaction_key,value,phase,ticks, andlabel. - Drive the exact actions your loop scores on: for a collector,
movethe player onto the pickups and let the trigger fire; for an interaction, add aninteractorpressstep on the target. A heldmovestep walks for its wholeticksbudget (walk trap 3), so reach a pickup with a singlemovestep given enoughticksto cover the gap - about two to three ticks per meter on the FP-template rig - rather than guessing. Read the coin or target position from the scene first and size theticksbudget to cover the gap. - Read the outcome: compare the before and after scene digests, check the per step applied or failed results and diagnostics, and read
possessedDelta.distanceto confirm the avatar actually reached the target. If the distance is a fraction of the gap, raise theticksbudget (or add more move steps) and re run before concluding anything about the pickup or the collider.
The proof rule
An after state that is identical to the before state is NOT proof of gameplay. A real collect loop shows the pickup entities GONE and the score field RAISED in the after digest. If the digest did not change, the loop did not run: the tag or collider is missing, the player did not reach the target, or the handler never fired. Do not report a build as verified on an unchanged digest.
Pitfalls
- The proof room is a throwaway; do not treat a playtest as a way to persist state.
- A step that reports failed is signal, not noise; read its diagnostic and fix the cause before re running.
- Possess the entity that actually carries the input and movement components, not a static prop.
Verify
- The after scene digest differs from the before digest in exactly the way the loop intends (items consumed, score raised, phase advanced).
- Per step results are applied, not failed, for the actions that should succeed.