Scenario Build Rules Cookbok
A cookbook containing recipes for better scenarios
Practical recipes for the six rule primitives that grade Produce-mode scenarios in FacilityVR. Pair this with scenario-authoring-guide.md for the broader Produce-mode lifecycle.
Every recipe is shown twice: as you’d configure it in the Build Rules editor (the form on the scenario edit page), and as the JSON shape the server stores. You don’t have to write the JSON by hand — the editor produces it — but the JSON makes the rule’s exact semantics unambiguous.
Conventions used below:
<spaceId>— copy from the layout’s space dropdown in the editor.<equipmentType>— type key likeserver_rack,crah_unit,pdu_floor.<signType>— sign key likefire_extinguisher,emergency_exit. The Sign type picker shows label + ISO code; the stored value is the key.weightis omitted in most recipes because it only matters underpassMode = weighted.
How rules combine
A scenario step has a list of rules and one of three pass modes:
passMode | Step passes when… | Use when |
|---|---|---|
all (default) | every rule in the step passes | Every requirement is mandatory (“place 6 signs and wire every rack”) |
count (with passThreshold: N) | at least N rules pass | ”Demonstrate any 3 of these 5 safety requirements” |
weighted (with passThreshold: 0..1) | sum(passed weights) / sum(all weights) ≥ threshold | Some requirements matter more than others |
Aim for one logical requirement per rule. Two rules with passMode = all is clearer than one rule trying to express both.
item_count — count placements against min / max
Counts how many placements match a filter and asserts a min, max, or both.
Recipe 1.1 — At least N items of a type
“Place at least 6 server racks somewhere in the facility.”
| Field | Value |
|---|---|
| Type | item_count |
| Equipment type | server_rack |
| Space | (any) |
| Min | 6 |
| Max | (blank) |
{ "type": "item_count", "ruleId": "r1",
"filter": { "equipmentType": "server_rack" },
"min": 6 }
Recipe 1.2 — Exactly N (use min == max)
“Place exactly 4 PDUs — no more, no less.”
{ "type": "item_count", "ruleId": "r1",
"filter": { "equipmentType": "pdu_floor" },
"min": 4, "max": 4 }
Recipe 1.3 — At most N (“don’t overdo it”)
“No more than 2 cooling units per server room — over-cooling wastes energy.”
One rule per room (or use the recipe pattern under “One rule per space” below):
{ "type": "item_count", "ruleId": "r1",
"filter": { "equipmentType": "crah_unit", "spaceId": "<room-A>" },
"max": 2 }
Recipe 1.4 — Forbidden in a space (max: 0)
“No naked flames in the chemical store.”
{ "type": "item_count", "ruleId": "r1",
"filter": { "equipmentType": "gas_burner", "spaceId": "<chem-store>" },
"max": 0 }
Tip:
max: 0is a real value — it means “place none.” Pair it withmin: 0(or omit min) so the schema doesn’t rejectmin > max.
Recipe 1.5 — Specific safety sign in a specific space
“Each equipment room must have a fire extinguisher sign and an emergency exit sign.”
One rule per (room × required sign):
{ "type": "item_count", "ruleId": "r1",
"filter": { "decorationType": "sign", "signType": "fire_extinguisher", "spaceId": "<room-A>" },
"min": 1, "label": "Fire extinguisher sign — Room A" }
{ "type": "item_count", "ruleId": "r2",
"filter": { "decorationType": "sign", "signType": "emergency_exit", "spaceId": "<room-A>" },
"min": 1, "label": "Emergency exit sign — Room A" }
Repeat for each room. Set passMode = all. The Sign type picker only appears when decoration type is sign.
Recipe 1.6 — Tagged equipment
“At least 3 racks tagged
production(vsstaging).”
{ "type": "item_count", "ruleId": "r1",
"filter": { "equipmentType": "server_rack", "tag": "production" },
"min": 3 }
Tags live on the equipment placement’s
properties.tagsarray. Set them in the layout editor’s equipment side panel.
spatial — distance check between two filtered groups
Two modes:
all_within— every subject must be withinmaxDistancemetres of at least one target.none_within— no subject may be withinminDistancemetres of any target.
Recipe 2.1 — Every subject near a target (all_within)
“Every server rack must be within 5 m of a cooling unit.”
| Field | Value |
|---|---|
| Type | spatial |
| Subject filter | equipmentType server_rack |
| Target filter | equipmentType crah_unit |
| Mode | All subjects within max |
| Max distance (m) | 5 |
{ "type": "spatial", "ruleId": "r1",
"subjectFilter": { "equipmentType": "server_rack" },
"targetFilter": { "equipmentType": "crah_unit" },
"mode": "all_within", "maxDistance": 5 }
If even one rack has no cooler within 5 m, the rule fails and the offending rack(s) get pulsing-red highlight rings in the editor.
Recipe 2.2 — No subject near a hazard (none_within)
“No server rack within 1.5 m of a wet pipe.”
{ "type": "spatial", "ruleId": "r1",
"subjectFilter": { "equipmentType": "server_rack" },
"targetFilter": { "equipmentType": "wet_pipe" },
"mode": "none_within", "minDistance": 1.5 }
Recipe 2.3 — Sign placed near specific equipment
“An emergency-stop sign within 2 m of every press.”
{ "type": "spatial", "ruleId": "r1",
"subjectFilter": { "equipmentType": "press" },
"targetFilter": { "decorationType": "sign", "signType": "emergency_stop" },
"mode": "all_within", "maxDistance": 2 }
coverage — every subject covered by at least one nearby target
A common spatial special case promoted to its own primitive for readability. Equivalent to spatial + all_within, but reads as “is everything covered?”
Recipe 3.1 — Smoke detector overhead per aisle
“Every aisle position has a smoke detector within 4 m.”
| Field | Value |
|---|---|
| Type | coverage |
| Subject filter | equipmentType aisle_marker |
| Target filter | equipmentType smoke_detector |
| Max distance (m) | 4 |
{ "type": "coverage", "ruleId": "r1",
"subjectFilter": { "equipmentType": "aisle_marker" },
"targetFilter": { "equipmentType": "smoke_detector" },
"maxDistance": 4 }
Recipe 3.2 — Fire extinguisher coverage by room
“Every workstation within 10 m of a fire extinguisher.”
{ "type": "coverage", "ruleId": "r1",
"subjectFilter": { "equipmentType": "workstation" },
"targetFilter": { "equipmentType": "fire_extinguisher" },
"maxDistance": 10 }
Choose
coverageoverspatial / all_withinwhen the rule’s intent is every X is served by some Y. The two evaluate the same; the wording in the editor is friendlier.
spacing — minimum pairwise distance between matching items
Recipe 4.1 — Minimum aisle spacing
“No two racks closer than 1.2 m to each other.”
| Field | Value |
|---|---|
| Type | spacing |
| Filter | equipmentType server_rack |
| Min distance (m) | 1.2 |
{ "type": "spacing", "ruleId": "r1",
"filter": { "equipmentType": "server_rack" },
"minDistance": 1.2 }
If any pair is closer than 1.2 m, the rule fails and both offending racks light up.
Recipe 4.2 — Bollard spacing in a yard
“Yard bollards spaced at least 2 m apart.”
{ "type": "spacing", "ruleId": "r1",
"filter": { "equipmentType": "bollard", "spaceId": "<yard>" },
"minDistance": 2 }
connectivity — are A and B linked by connectors?
Looks for connector decorations of a given style (cable, pipe, conduit, …) that touch one item from group A and one from group B. The tolerance is how close (m) a connector vertex must be to count as touching an item — default 0.5 m.
Two modes:
each_to_each— every A must be linked to every B.any_path— at least one connector links the groups (smoke test).
Recipe 5.1 — Every PDU cabled to every rack
“Each rack PDU must be cabled to each rack.”
| Field | Value |
|---|---|
| Type | connectivity |
| Endpoint A | equipmentType pdu_floor |
| Endpoint B | equipmentType server_rack |
| Connector style | cable |
| Mode | Every A linked to every B |
| Tolerance | 0.5 |
{ "type": "connectivity", "ruleId": "r1",
"endpointAFilter": { "equipmentType": "pdu_floor" },
"endpointBFilter": { "equipmentType": "server_rack" },
"connectorStyle": "cable",
"mode": "each_to_each",
"tolerance": 0.5 }
Recipe 5.2 — Any-path smoke test
“Confirm at least one cable connects the switchboard to a rack.”
{ "type": "connectivity", "ruleId": "r1",
"endpointAFilter": { "equipmentType": "switchboard" },
"endpointBFilter": { "equipmentType": "server_rack" },
"connectorStyle": "cable",
"mode": "any_path" }
Recipe 5.3 — Pipe network coverage
“Every pump connected to the manifold via piping.”
{ "type": "connectivity", "ruleId": "r1",
"endpointAFilter": { "equipmentType": "pump" },
"endpointBFilter": { "equipmentType": "manifold" },
"connectorStyle": "pipe",
"mode": "each_to_each",
"tolerance": 0.3 }
Tip: tighten tolerance (e.g. 0.2 m) for precision pipework. Loosen it (e.g. 0.8 m) for cable runs where the trainee may end the cable a bit short of the chassis.
sequence — connector path walks expected items in order
Use when the order of equipment along a single connector run matters: power flow, pipework, signal chains.
Two modes:
any— at least one connector path satisfies the order. Use for “demonstrate flow exists.”each— every equipment item matching the terminal step is reached by a path that walks the full sequence. Use for “every endpoint must be supplied.”
Recipe 6.1 — Power flow (any path)
“Demonstrate power flow: junction box → switchboard → UPS → PDU → rack PDU.”
| Field | Value |
|---|---|
| Type | sequence |
| Connector style | cable |
| Mode | any |
| Expected order | (5 filter rows) |
| Tolerance | 0.5 |
{ "type": "sequence", "ruleId": "r1",
"connectorStyle": "cable",
"mode": "any",
"tolerance": 0.5,
"expectedOrder": [
{ "equipmentType": "junction_box" },
{ "equipmentType": "switchboard" },
{ "equipmentType": "ups" },
{ "equipmentType": "pdu_floor" },
{ "equipmentType": "rack_pdu" }
] }
Recipe 6.2 — Power flow (each path) — every rack PDU supplied
“Every rack PDU in the facility must be reachable from a junction box via the full power chain.”
Same shape as 6.1 but with mode: "each" — every rack_pdu must be touched by some sequence-matching path.
Recipe 6.3 — Pipework chain
“Pump → strainer → control valve → discharge header — pipework runs in that order.”
{ "type": "sequence", "ruleId": "r1",
"connectorStyle": "pipe",
"mode": "any",
"tolerance": 0.3,
"expectedOrder": [
{ "equipmentType": "pump" },
{ "equipmentType": "strainer" },
{ "equipmentType": "control_valve" },
{ "equipmentType": "discharge_header" }
] }
A sequence rule needs at least one filter row in
expectedOrder. Theeachmode is stricter thananyand will fail if any terminal item is unreached — useful as a final assessment, butanyis friendlier for early practice.
Composition recipes
Real exercises typically use multiple rules. Some patterns:
Recipe C.1 — One rule per space
“Each of 4 server rooms needs at least 6 racks.”
Add one item_count rule per room (with spaceId set), set passMode = all. Use label on each rule so the per-step breakdown reads cleanly:
[
{ "type": "item_count", "ruleId": "r-roomA",
"filter": { "equipmentType": "server_rack", "spaceId": "<room-A>" },
"min": 6, "label": "Room A — racks" },
{ "type": "item_count", "ruleId": "r-roomB",
"filter": { "equipmentType": "server_rack", "spaceId": "<room-B>" },
"min": 6, "label": "Room B — racks" },
{ "type": "item_count", "ruleId": "r-roomC",
"filter": { "equipmentType": "server_rack", "spaceId": "<room-C>" },
"min": 6, "label": "Room C — racks" },
{ "type": "item_count", "ruleId": "r-roomD",
"filter": { "equipmentType": "server_rack", "spaceId": "<room-D>" },
"min": 6, "label": "Room D — racks" }
]
Recipe C.2 — Mandatory + nice-to-have (weighted)
“Mandatory: 6 racks, fire-extinguisher signs everywhere. Nice-to-have: smoke-detector coverage, room labels.”
Use passMode = weighted with passThreshold = 0.7. Heavy weights on the mandatory rules:
{
"passMode": "weighted",
"passThreshold": 0.7,
"rules": [
{ "type": "item_count", "ruleId": "r1", "weight": 3, "label": "Min racks",
"filter": { "equipmentType": "server_rack" }, "min": 6 },
{ "type": "item_count", "ruleId": "r2", "weight": 3, "label": "Fire extinguisher signs",
"filter": { "decorationType": "sign", "signType": "fire_extinguisher" }, "min": 4 },
{ "type": "coverage", "ruleId": "r3", "weight": 1, "label": "Smoke detector coverage",
"subjectFilter": { "equipmentType": "aisle_marker" },
"targetFilter": { "equipmentType": "smoke_detector" },
"maxDistance": 4 },
{ "type": "item_count", "ruleId": "r4", "weight": 1, "label": "Room labels",
"filter": { "decorationType": "label" }, "min": 4 }
]
}
Recipe C.3 — “Any 3 of 5” demonstration
“Demonstrate any 3 of these safety practices.”
passMode = count, passThreshold = 3:
{ "passMode": "count", "passThreshold": 3, "rules": [ /* 5 rules */ ] }
Recipe C.4 — Layered grading across steps
Don’t try to cram every requirement into one step. Split:
- Step 1: Layout & equipment —
item_countrules for required equipment per room. - Step 2: Safety signage —
item_countrules for signs per space. - Step 3: Power & cabling —
connectivityandsequencerules. - Step 4: Spacing & ergonomics —
spacing,spatial / none_within.
Each step gets its own pass/fail in the trainee’s left-side panel, so they can fix issues incrementally instead of staring at one giant rule list.
Reading the editor’s failure messages
When a step fails, the trainee sees per-rule lines like:
| Message | What it means | Fix |
|---|---|---|
Found 3 server_rack, expected at least 6. | item_count min not met | Place more racks |
Found 7 server_rack, expected 4–4. | item_count exact match failed | Remove extras |
Found 6 sign "fire_extinguisher" in <space>, expected at least 1. | item_count signType match | OK — passing |
2 server_rack subject(s) not within 5 m of any crah_unit. | spatial all_within failure | Move racks closer to a CRAH |
1 pair of server_rack within 0.8 m (min 1.2 m). | spacing failure | Spread them apart |
Rule misconfigured: min (6) exceeds max (0). … | Author bug — fix the rule | Edit the scenario |
Failing items glow red in 3D with a bobbing arrow above them so the trainee can find offenders without scanning the whole layout.
When in doubt
- Start with
item_count. It covers ~70% of authoring needs. - Use one rule per logical requirement, even if you have to add several rules. Easier to author, easier to grade, easier for the trainee to debug.
- Prefer
passMode = allfirst. Switch toweightedonly if you genuinely need partial credit. - Set a
labelon each rule. It’s what trainees see in the per-step breakdown. - Use
tagfilters when you need to distinguish two placements of the same equipment type (e.g.productionvsstagingracks).
For the broader Produce-mode lifecycle (forking the layout, save vs submit, lock-on-submit, course-step integration), see scenario-authoring-guide.md.