Skip to main content

Arena validation

A saved arena can be structurally correct and still be unusable. A door can name a real wall and sit four units away from it. A region can be walled off with no doorway, so the demonstration agent never enters it and the run quietly measures less than the researcher intended. Neither fault stops the configuration loading, and neither is visible in the 3D view without looking for it.

backend/validation.py checks arena geometry on the server before a configuration is stored.

Two validators, on purpose

There are two validation layers and they do not overlap.

LayerRuns inCovers
ExperimentValidatorGodot, before load_experimentStructure, types, required fields, material references, id uniqueness
validation.pyPython, before a save is storedGeometry: placement, attachment, boundaries, reachability

The split is deliberate. Two validators checking the same rules drift apart, and the failure mode is the worst kind: the API accepts a configuration the simulator then rejects, so the error surfaces far from its cause. When adding a check, decide which layer owns it first.

What is checked

Errors block a save.

  • A door further than two units from the wall it names
  • A door wider than its wall is long
  • A region or wall extending past the arena boundary
  • A start position inside a wall
  • open and state disagreeing on the same door
  • A bridge structure naming a region that does not exist

Warnings are recorded but do not block a save.

  • A region that cannot be reached from the start position
  • A region where a wall cuts off most of its open floor
  • An elevated region with no elevation set, or the reverse
  • A stimulus positioned outside the arena
  • Use of a region id the movement code matches literally

Reachability

The obvious implementation builds a graph from doors and walks it. That is wrong here. Two regions that simply share an edge are connected even with no door between them, and a door graph reports them as sealed.

Instead the floor is rasterised at half-unit resolution. A cell is blocked if it falls inside an enabled inner wall, unless it also falls within an open doorway on that wall. A flood fill runs from the configured start position, and any region with no reached cells is unreachable. Rotated walls are handled using the same local-space transform StatisticalAgentPlugin uses for collision, so the check and the movement code agree about where a wall is.

Running this against the MCSF preset found that the bridge region could not be entered from the start position. wall_inner_east has no doorway and wall_bridge_south blocks the route from the exit slope. That region is the one the paradigm is built around: elevated, wire mesh, risk semantic type, with bridge_light aimed at it.

Door state

Arena3D decides how to draw a door from state, falling back to default_state. StatisticalAgentPlugin decides whether the agent can pass from open. Nothing keeps the two in step when a configuration is loaded.

A door written as state: "closed" with open: true therefore renders shut and lets the agent walk through it. The validator reports the mismatch, and the arena editor writes state, default_state and open together from one control so the fault cannot originate there.

Where it runs

validate_experiment in backend/app.py calls it, so geometry is checked on every experiment save, on project creation, and on every preset the server offers through /api/templates. A preset that fails is dropped from the template catalogue rather than offered and then failing at load.

It can also be run directly, and exits non-zero on errors, so it works in CI:

python backend/validation.py # the default preset
python backend/validation.py path/to/file.json # any configuration

Reserved region names

StatisticalAgentPlugin._resolve_bridge matches the region ids bridge, bridge_slope_entry, bridge_slope_exit, corridor_a and corridor_c literally. A custom arena cannot have working bridge behaviour unless it reuses those exact names, and a region a researcher happens to name bridge inherits crossing restrictions it was never given deliberately.

The validator warns when a reserved name is used, and the editor shows the same warning while drawing. Removing the coupling needs a change in the plugin, reading semantic_type or an explicit field rather than matching ids.

AI Attribution: This page was drafted with AI assistance from the implemented backend/validation.py source. The team should review it against the code.