Gessa Docs
Recipes

Recipe

Playtest verification loop proving gameplay executed evidence

  1. 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
engine v1.0.234since world-build-operation-catalog.v1, runtime.v1Copy for LLM

Use this for

the evidence step; playing what you built; possessing an entity and executing an action sequence; gathering before and after proof that a change actually works

Not for

a durable mutation (the proof room is discarded); a purely visual render check (use the renderer viewport capture)

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)

  1. 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.
  2. 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.
  3. A MOVE STEP WALKS ITS WHOLE ticks BUDGET; SIZE IT TO THE GAP. A held move re-injects on every authoritative tick, so a single move step keeps walking for all of its ticks (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, so ticks 20 walks about 8 m). A bigger ticks walks strictly farther: the SAME held step with ticks 10 and ticks 60 travels roughly 4 m then 24 m (measured on the FP-template arena). The real trap is UNDERSIZING it - a move with too few ticks barely 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. READ possessedDelta.distance and possessedDelta.moved in the outcome: they report exactly how far the possessed avatar actually travelled. Give the move enough ticks to cover the gap (chaining several move steps also accumulates) BEFORE you judge whether the pickup fired.
  4. 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 physics launch) 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 straight move whose 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 is possessedDelta showing a large VERTICAL jump or an end position far outside the arena bounds. Before planning the move, read the before scene digest (or project_get_graph_snapshot) for props tagged interactive / jump_pad / with collision:"trigger" colliders that sit between the spawn and the target, and either route the move around 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.
  5. 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_waiting together with an EMPTY outcomes set (no despawn, no score) even though possessedDelta shows the avatar walked onto the pickup. This is NOT a short walk or a broken collider. script.dispatch_waiting carries 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 enough ticks to 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

  1. 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.
  2. Call world_playtest_scenario with game_id, possessed_entity_id, an optional world_id, and a steps array. Each step has a kind of move, look, jump, interact, or press, with optional action_key, value, phase, ticks, and label.
  3. Drive the exact actions your loop scores on: for a collector, move the player onto the pickups and let the trigger fire; for an interaction, add an interact or press step on the target. A held move step walks for its whole ticks budget (walk trap 3), so reach a pickup with a single move step given enough ticks to 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 the ticks budget to cover the gap.
  4. Read the outcome: compare the before and after scene digests, check the per step applied or failed results and diagnostics, and read possessedDelta.distance to confirm the avatar actually reached the target. If the distance is a fraction of the gap, raise the ticks budget (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.
Was this helpful?Report an issueContact support

On this page