---
title: "Playtest verification loop proving gameplay executed evidence"
description: "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"
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/knowledge/playbooks/playtest-verification-loop/
---

# 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.
