YAML-driven scenario loader, runner, verifier, and game API. Extracted from Crossing::Scenario, made operation-agnostic via the boundary registry.
The same YAML file serves three purposes:
name: "parameter extraction from path"
description: |
The route pattern /node/:slug has one dynamic segment.
When the request path is /node/sprout-api, :slug captures "sprout-api".
level:
world: 1
number: 2
hint: "Dynamic segments start with : and capture the path value"
operation: route_match
input:
method: GET
path: /node/sprout-api
routes:
- pattern: /node/:slug
layout: node-view
middleware: [fetch-node, fetch-edges]
expected:
matched: true
route:
layout: node-view
middleware: [fetch-node, fetch-edges]
params:
slug: sprout-api
blanks:
slug_value:
path: params.slug
answer: sprout-api
layout:
path: route.layout
answer: node-view
hint: The layout declared on the matching route
# All scenarios in a directory (recursive, sorted)
scenarios = Wanderland::Scenario.load_all("spec/scenarios/route-match/")
# Filter by operation
walks = Wanderland::Scenario.load_by_operation("spec/scenarios/", :route_match)
# Filter by world (game progression)
world_1 = Wanderland::Scenario.load_by_world("spec/scenarios/", 1)
# From a hash (for API/programmatic use)
scenario = Wanderland::Scenario.from_hash(data, source: "inline")
scenario.run
# → calls Wanderland::Boundary.execute(scenario.operation, scenario.input)
# → stores result in scenario.actual
failures = scenario.verify
# → ShapeMatcher compares scenario.actual against scenario.expected
# → returns array of failure messages (empty = pass)
scenario.passed? # boolean
scenario.blank_paths # ["slug_value", "layout"]
scenario.blank_info("layout") # { path: "route.layout", hint: "The layout..." }
# Check one answer
result = scenario.check_answer("slug_value", "sprout-api")
# { blank: "slug_value", path: "params.slug", user: "sprout-api",
# expected: "sprout-api", actual: "sprout-api",
# user_correct: true, engine_correct: true }
# Check all answers with scoring
result = scenario.check_all_answers({
"slug_value" => "sprout-api",
"layout" => "node-view"
})
# { results: [...], score: { correct: 2, total: 2, percentage: 100.0 } }
scenario.actual # full result hash from boundary
scenario.expected # full expected hash from YAML
scenario.actual_at("params.slug") # dot-path dig into actual
scenario.expected_at("route.layout") # dot-path dig into expected
Deep comparison with matchers. Expected is a shape — actual must fit through it.
Exact values: strings, numbers, booleans compared with == (symbol/string normalized).
Hash subset: expected keys must exist in actual. Extra keys in actual are allowed.
Matchers (special keys in expected hashes):
| Matcher | What it checks |
|---|---|
count: N |
Array/hash has exactly N elements |
gte: N / lte: N / gt: N / lt: N |
Numeric comparison |
matches: pattern |
String matches regex |
contains: value |
Array contains value |
includes: [...] |
Array includes all values |
excludes: [...] |
Array excludes all values |
first: shape / last: shape |
First/last element matches |
any: shape |
At least one element matches |
keys: [...] |
Hash has exactly these keys |
has_key: key |
Hash has this key |
not: shape |
Value does NOT match |
empty: true/false |
Is empty? |