FacilityVR
← Back to Documentation

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 like server_rack, crah_unit, pdu_floor.
  • <signType> — sign key like fire_extinguisher, emergency_exit. The Sign type picker shows label + ISO code; the stored value is the key.
  • weight is omitted in most recipes because it only matters under passMode = weighted.

How rules combine

A scenario step has a list of rules and one of three pass modes:

passModeStep passes when…Use when
all (default)every rule in the step passesEvery 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) ≥ thresholdSome 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.”

FieldValue
Typeitem_count
Equipment typeserver_rack
Space(any)
Min6
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: 0 is a real value — it means “place none.” Pair it with min: 0 (or omit min) so the schema doesn’t reject min > 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 (vs staging).”

{ "type": "item_count", "ruleId": "r1",
  "filter": { "equipmentType": "server_rack", "tag": "production" },
  "min": 3 }

Tags live on the equipment placement’s properties.tags array. 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 within maxDistance metres of at least one target.
  • none_within — no subject may be within minDistance metres 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.”

FieldValue
Typespatial
Subject filterequipmentType server_rack
Target filterequipmentType crah_unit
ModeAll 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.”

FieldValue
Typecoverage
Subject filterequipmentType aisle_marker
Target filterequipmentType 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 coverage over spatial / all_within when 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.”

FieldValue
Typespacing
FilterequipmentType 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.”

FieldValue
Typeconnectivity
Endpoint AequipmentType pdu_floor
Endpoint BequipmentType server_rack
Connector stylecable
ModeEvery A linked to every B
Tolerance0.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.”

FieldValue
Typesequence
Connector stylecable
Modeany
Expected order(5 filter rows)
Tolerance0.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. The each mode is stricter than any and will fail if any terminal item is unreached — useful as a final assessment, but any is 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 & equipmentitem_count rules for required equipment per room.
  • Step 2: Safety signageitem_count rules for signs per space.
  • Step 3: Power & cablingconnectivity and sequence rules.
  • Step 4: Spacing & ergonomicsspacing, 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:

MessageWhat it meansFix
Found 3 server_rack, expected at least 6.item_count min not metPlace more racks
Found 7 server_rack, expected 4–4.item_count exact match failedRemove extras
Found 6 sign "fire_extinguisher" in <space>, expected at least 1.item_count signType matchOK — passing
2 server_rack subject(s) not within 5 m of any crah_unit.spatial all_within failureMove racks closer to a CRAH
1 pair of server_rack within 0.8 m (min 1.2 m).spacing failureSpread them apart
Rule misconfigured: min (6) exceeds max (0). …Author bug — fix the ruleEdit 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 = all first. Switch to weighted only if you genuinely need partial credit.
  • Set a label on each rule. It’s what trainees see in the per-step breakdown.
  • Use tag filters when you need to distinguish two placements of the same equipment type (e.g. production vs staging racks).

For the broader Produce-mode lifecycle (forking the layout, save vs submit, lock-on-submit, course-step integration), see scenario-authoring-guide.md.