Skip to content

Scenarios

Dave Walker edited this page Oct 12, 2026 · 2 revisions

A scenario is a UTF-8 JSON file describing the starting body position, food, circular obstacles, hazards, arm parameters, reach targets and signalling/conflict policies. The Python application and web explorer accept the same version-1 format; older files without the new optional fields remain valid. No additional dependencies are needed.

Loading and playing scenarios

You can supply a scenario when launching the desktop application, or load one after it has started. Both routes validate the file, replace the scene with its starting conditions and leave playback paused.

Option 1: Pass a scenario on the command line

From the project directory, load the included example:

./scripts/run.sh scenarios/two-targets.json

With the virtual environment activated, either entry point also accepts a file:

octopus-controller scenarios/two-targets.json
python -m octopus_controller scenarios/two-targets.json

Paths are relative to the directory from which you launch the application, unless you supply an absolute path. Quote a path containing spaces.

Option 2: Load a scenario inside the application

Start without a filename:

./scripts/run.sh

With the virtual environment activated, you can instead run octopus-controller or python -m octopus_controller. The desktop opens an empty, paused scene with eight idle arms.

  1. Click Load in the Scenario card, or press Ctrl+O (Command+O on macOS)
  2. Select a .json file in the file picker, then click Open or press Enter. To browse into a folder, select it and use Open or Enter first. The picker initially uses the project's scenarios directory when available
  3. Alternatively, click the path field and enter a file or directory path. Relative paths are resolved from the folder currently shown in the picker
  4. After loading, check the scenario name in the card and window title. The body is centred and the arms fitted into view; playback remains paused
  5. Press Space to begin

The same Load control lets you switch scenarios without closing the application. Loading replaces the current scene, so use Save first if you want to retain an edited setup. Cancel or Esc closes the picker without loading. A file that fails validation displays an error and leaves the current scene intact.

Playback and restart

Control Action
Space Play or pause.
N Advance one simulation tick while paused.
V Cycle playback through 1×, 2×, 4× and 8×.
D Restart the loaded setup and leave it paused.
R Reset only the selected arm's pose.

Selection, target dragging, object editing and parameter controls remain available. Faster playback runs more unchanged simulation ticks; single-step still advances exactly one tick at any speed.

Restart restores the initial objects (including hazards), targets, policies, parameters and arm poses. It clears elapsed time, grips, recoil episodes, reports, conflict reservations and histories. It uses the validated setup held in memory: to pick up external JSON edits, load the file again, rather than merely restarting it.

Loading in the web explorer

Choose a bundled scenario from Starting conditions, or use Load a scenario file to select a custom JSON file. Loaded scenarios start paused. Use Play, Step, Restart and Playback speed beneath the scenario controls. Reload a custom file to pick up external edits; Restart restores the already-loaded starting conditions.

Example

{
  "version": 1,
  "name": "Two arms reaching for food",
  "body": [400, 460],
  "defaults": {
    "segment_count": 20,
    "segment_length": 12,
    "maximum_bend_degrees": 60,
    "turning_speed_degrees": 90
  },
  "food": [
    { "id": "meal-a", "position": [400, 240], "radius": 12 },
    { "id": "meal-b", "position": [620, 460], "radius": 12 }
  ],
  "obstacles": [
    { "position": [520, 570], "radius": 25 }
  ],
  "arms": [
    { "number": 1, "target": { "food": "meal-a" } },
    {
      "number": 3,
      "parameters": { "segment_count": 24 },
      "target": { "food": "meal-b" }
    },
    { "number": 5, "target": { "position": [400, 650] } }
  ]
}

JSON conventions

The file must contain one JSON object, encoded as UTF-8. Field names are case-sensitive and must match the names below exactly. Unknown fields and duplicate JSON keys are rejected at every level. Comments and trailing commas are not supported.

Numbers must be JSON numbers, not quoted strings or booleans. All coordinates, lengths, radii and speeds must be finite; NaN, infinity and numbers too large to represent as finite floating-point values are rejected. Integer fields require integer notation: use 20, not 20.0.

Optional fields should be omitted to select their defaults. Explicit null is not a substitute for omission. Empty objects are allowed for defaults, parameters, escalation and conflicts; empty arrays are allowed for food, obstacles, hazards and arms. Policy objects are partial overrides: omitted members keep their defaults, and the resulting complete policy must be valid.

Top-level fields

Field JSON type Required Accepted values / meaning Default when omitted
version Integer Yes Exactly 1 None
name String No Contains at least one non-whitespace character; displayed in the window title "Untitled scenario"
body Array of two numbers No Fixed body centre [x, y]; both coordinates finite [400, 460]
defaults Object No Any of the four arm parameters listed below Built-in arm defaults
food Array of objects No Food circles, as described below []
obstacles Array of objects No Obstacle circles, as described below []
hazards Array of objects No Traversable hazard circles with severity and sensing range []
escalation Object No Shared reporting, withdrawal, exposure and clearance thresholds Built-in escalation policy
conflicts Object No Critical broadcast threshold and coordinate claim tolerance Built-in conflict policy
arms Array of objects No Up to eight entries with distinct arm numbers []; all eight arms idle

The smallest valid scenario is:

{ "version": 1 }

This creates eight idle arms at the default body position with no food, obstacles or hazards.

Coordinates and units

Every body or position value is an array of exactly two numbers: [x, y]. Coordinates, segment lengths and object radii are measured in world pixels. Camera fitting can scale and translate their on-screen appearance without changing saved values. X increases rightwards and Y increases downwards. Fractional and negative coordinates are accepted.

Coordinates are not restricted to the viewport. The desktop camera centres the body and fits the arms’ full reach; it does not fit every distant object or target. Keep the intended action near the body and within the arms’ reach for a readable demonstration. Moving the camera does not change sensing distances or collision rules.

Arms retain the standard radial attachment points around the body and gently curved initial poses. The format does not set individual attachment points or joint angles.

Food entries

Each object in food accepts only these fields:

Field JSON type Required Accepted values / range Default when omitted
position Array of two numbers Yes [x, y], both finite None
radius Number No Finite and strictly greater than 0 pixels; no explicit upper bound 14
id String No Contains at least one non-whitespace character; unique among food IDs No named reference

Food without an id can still be contacted and captured. IDs are case-sensitive and compared exactly; whitespace is not trimmed. For example, "meal-a" and "Meal-A" are different IDs.

{
  "version": 1,
  "food": [
    { "id": "meal-a", "position": [400, 240], "radius": 12 },
    { "position": [300, 350] }
  ]
}

Obstacle entries

Each object in obstacles accepts only these fields:

Field JSON type Required Accepted values / range Default when omitted
position Array of two numbers Yes [x, y], both finite None
radius Number No Finite and strictly greater than 0 pixels; no explicit upper bound 14

Obstacles are circles. They do not accept an id, shape, width or height. Their collection determines their type, so neither food nor obstacles accept a kind field.

Hazard entries

Add a top-level hazards array. Each entry accepts only:

Field JSON type Required Accepted values Default when omitted
position Array of two numbers Yes Finite [x, y] None
radius Number No Finite and strictly positive 14
severity Number No Finite, greater than 0 and at most 1 0.5
detection_range Number No Finite and non-negative, in world pixels 24

Hazards do not accept id, kind, a name or a shape. Their numeric runtime identities are assigned by the loader. They cannot be referenced using a food target. Food and obstacles do not accept hazard-only fields such as severity.

A hazard is traversable and produces avoidance; an obstacle is a solid collision constraint. Detection tests the full segment against radius + detection_range + 3 (the 3-pixel sensing strip). A zero detection range is valid and still includes that strip. Exposure means strict centreline penetration of the actual radius, not merely detection.

This complete file places a mild hazard near the initial right-hand arm:

{
  "version": 1,
  "name": "Mild hazard beside Arm 3",
  "hazards": [
    { "position": [550, 480], "radius": 10, "severity": 0.25, "detection_range": 12 }
  ],
  "arms": [
    { "number": 3, "target": { "position": [620, 460] } }
  ]
}

Recoil has no scenario field of its own. New encounters at severity 0.4 or above trigger local recoil for 0.25 + 0.9 × severity simulation seconds. Lower severities use local avoidance, including outward steering if already penetrating. Changing the reporting policy below does not change this local recoil threshold or duration.

Escalation settings

The optional top-level escalation object applies to every arm:

Field JSON type Default Constraint and meaning
report_severity Number 0.4 Minimum severity for a central report
withdrawal_severity Number 0.75 Minimum severity for an immediate withdrawal request
exposure_seconds Number 2.0 Strictly positive continuous exposure before a reportable hazard escalates
clear_seconds Number 0.6 Strictly positive time outside sensing range before a report clears

All values must be finite, with 0 < report_severity < withdrawal_severity <= 1. Exposure and clearance use simulation seconds, so playback speed changes wall-clock duration rather than these thresholds.

Below the report threshold, danger stays local even after prolonged exposure. At or above the report threshold, the arm sends an informational report. It requests withdrawal when severity reaches the withdrawal threshold or continuous exposure reaches exposure_seconds. Exposure resets when penetration stops. An outstanding report clears only after continuous absence from sensing range for clear_seconds; lowering severity alone does not clear it.

For a quicker sustained-exposure demonstration, override just the exposure interval:

{
  "version": 1,
  "escalation": { "exposure_seconds": 0.75 },
  "hazards": [
    { "position": [550, 480], "radius": 14, "severity": 0.55, "detection_range": 14 }
  ],
  "arms": [
    { "number": 3, "target": { "position": [620, 460] } }
  ]
}

This configures earlier escalation if penetration remains continuous; the layout does not force the arm to stay exposed. Use hazard-escalation.json or reef-forage-extended.json as starting points for an established demonstration.

Conflict settings

The optional top-level conflicts object accepts:

Field JSON type Default Constraint and meaning
critical_severity Number 0.95 Finite, greater than 0 and at most 1; critical threshold for all-arm withdrawal
target_tolerance Number 2.0 Finite and non-negative; world-pixel distance for matching coordinate claims

All-arm withdrawal requires an outstanding priority-100 report at or above critical_severity. Lowering this setting does not cause unreported hazards or informational reports to trigger a broadcast. For immediate severity-based broadcasts in a custom policy, ensure the hazard also meets withdrawal_severity.

The priorities themselves are fixed: withdrawal 100, local avoidance 80, grip 60, assigned reach 50, discovery 20 and idle 0. There are no JSON fields for custom priority weights, named conflict scripts, forced winners or fairness timeouts.

To introduce competition, assign the same food to more than one arm:

{
  "version": 1,
  "name": "Shared food with a critical hazard",
  "food": [
    { "id": "shared-meal", "position": [200, 460] }
  ],
  "hazards": [
    { "position": [550, 480], "radius": 8, "severity": 1.0, "detection_range": 8 }
  ],
  "escalation": {
    "report_severity": 0.4,
    "withdrawal_severity": 0.75,
    "exposure_seconds": 2.0,
    "clear_seconds": 0.6
  },
  "conflicts": { "critical_severity": 0.95, "target_tolerance": 2 },
  "arms": [
    { "number": 7, "target": { "food": "shared-meal" } },
    { "number": 8, "target": { "food": "shared-meal" } }
  ]
}

When eligible, the existing owner wins, then the eligible incumbent, then the lowest arm number. Other claimants yield while retaining their assignments. Safety can override that contest, including withdrawal of idle arms when critical danger is reported. Remove the hazard from this example to isolate shared-target arbitration.

When both claims identify food, identity determines whether they compete. Otherwise, destinations within target_tolerance can compete, including a coordinate target beside a named-food target. A tolerance of 0 requires identical coordinates for that comparison. Matching uses the first target in a group as its anchor; it does not merge a chain of increasingly distant targets.

Arm entries

Each object in arms accepts only these fields:

Field JSON type Required Accepted values / meaning Default when omitted
number Integer Yes 1–8 inclusive; each number may appear once None
parameters Object No Any of the four arm parameters listed below Inherit shared defaults
focus_food String No Existing food ID; requires a target and must agree with any named-food target No extra food focus
target Object No Exactly one position or food field Arm remains idle

Numbering starts at the top and proceeds clockwise, matching the keyboard controls:

Arm Attachment direction
1 Up
2 Upper right
3 Right
4 Lower right
5 Down
6 Lower left
7 Left
8 Upper left

The order of entries does not matter. Unlisted arms still exist and receive the shared defaults, but remain idle. A listed arm without a target also remains idle; this allows its parameters to be configured without starting a reach.

Arm parameters and ranges

These same names are accepted in both top-level defaults and each arm's parameters object. All are optional.

Parameter JSON type Units Accepted range Built-in default
segment_count Integer Segments 2–100 inclusive 20
segment_length Number Pixels per segment Finite and strictly greater than 0; no explicit upper bound 12
maximum_bend_degrees Number Degrees Strictly greater than 0 and at most 180 60
turning_speed_degrees Number Degrees per second per joint Finite and strictly greater than 0; no explicit upper bound Approximately 85.9436692696 (1.5 radians/second)

maximum_bend_degrees limits a joint's relative bend in either direction. turning_speed_degrees limits how quickly each joint turns; it is not a tip speed in pixels per second. Zero speed is invalid: omit the arm's target to leave it idle, or pause the simulation.

Degree values are converted internally to radians. Use the degree-suffixed names above in files; maximum_bend and turning_speed are not accepted file fields. Values must remain positive and finite after conversion to the model's floating-point units; extremely tiny degree values that underflow to zero are rejected.

Parameter precedence, for each individual field, is:

  1. The built-in default.
  2. A replacement in top-level defaults, if supplied.
  3. A replacement in the arm's own parameters, if supplied.

For example, the complete example above gives every arm 20 segments except Arm 3, which has 24. All arms inherit a segment length of 12, a bend limit of 60 degrees and a turning speed of 90 degrees/second. Overriding one parameter does not discard the others.

The file ranges are wider than some interactive keyboard controls. Keyboard edits retain their existing limits: 2–40 segments, lengths of 6–30 pixels, bends of 5–180 degrees, and turning speeds of 0.25–4 radians/second (approximately 14.32–229.18 degrees/second). Editing a file-supplied value outside a keyboard range can clamp it back into that range.

Target entries

If target is present, it must contain exactly one of the following fields:

Field JSON type Accepted values Behaviour
position Array of two numbers [x, y], both finite Assign a fixed tip destination
food String Exact ID of an existing entry in food Assign its initial centre and retain its identity for capture filtering and arbitration

Coordinate target:

{
  "version": 1,
  "arms": [
    { "number": 1, "target": { "position": [400, 240] } }
  ]
}

Named food target:

{
  "version": 1,
  "food": [
    { "id": "meal-a", "position": [400, 240] }
  ],
  "arms": [
    { "number": 1, "target": { "food": "meal-a" } }
  ]
}

A food reference resolves its destination at load time and retains food identity. It does not track food moved later or grant immediate ownership. It participates in resource arbitration, and the arm ignores incidental food that conflicts with that explicit assignment. Contact and grasp dwell are still needed for capture. Several arms may reference the same food; eligible losers yield. A coordinate-only target without focus_food remains opportunistic and may capture food encountered along the way.

An empty target, null, an unknown food ID, or a target containing both food and position is invalid. Omit target entirely for an idle arm.

Fixed destination with explicit food focus

Use arm-level focus_food when the desired coordinate should remain fixed independently of the food's current location, while preserving the identity used for conflict resolution and capture filtering:

{
  "version": 1,
  "food": [
    { "id": "meal-a", "position": [420, 250] }
  ],
  "arms": [
    {
      "number": 1,
      "target": { "position": [400, 240] },
      "focus_food": "meal-a"
    }
  ]
}

focus_food belongs beside target, not inside it. It must be a string naming existing food and cannot appear without a target. If used with target.food, the two IDs must match; normally target.food alone is sufficient. The destination need not equal the focused food's centre, but an unreachable or misplaced destination may prevent capture.

Saving may generate this form to preserve a retained destination after food has moved. It does not preload a grip or turn the target into automatic object tracking.

Saving and restarting the new elements

Saving includes hazard positions, radii, severities and detection ranges, plus both policy objects. Named-food focus is preserved using exported food IDs and focus_food where needed. Exported food names can differ from the original file's labels.

Saved files describe starting conditions, not a continuation checkpoint. They do not preserve hazard exposure, recoil timers, reports, reservations, joint poses, grips, capture history or conflict history. Currently held food is exported as unheld food at its current position. A carrying destination can be exported, but loading it creates an ordinary unladen assignment rather than a preloaded carry.

To demonstrate hold-versus-withdrawal, arrange for the arm to capture food before detecting serious danger, or interactively introduce a hazard after capture. food-vs-danger.json and reef-forage-extended.json provide examples. A critical hazard detected immediately can interrupt reaching before any grip exists.

Validation and movement

The entire file is checked before a window opens. Invalid input reports the filename and a useful error, for example Arm 3 references unknown food 'meal-c'.

Loaded targets use the existing distributed movement, obstacle avoidance and contact-based grasping rules. The loader does not promise that targets are reachable or that an obstacle layout admits a route. Multiple arms can target the same food; conflict arbitration selects eligible claimants before movement, and capture reservations prevent double ownership. This format stores starting conditions, not an in-progress simulation or a sequence of future commands.

Common validation errors

Invalid input Reason / correction
"version": 2 Only version 1 is supported
"segment_count": 20.0 Use the integer 20
"segment_length": "12" Use a number, not a string
"maximum_bend_degrees": 181 Maximum is 180 degrees
"turning_speed_degrees": 0 Speed must be strictly positive
"position": [400] Supply exactly two coordinates
"radius": -5 Radius must be strictly positive
Two entries with "number": 3 Each arm number may appear only once
Two food entries with the same id Food IDs must be unique
"target": {} Supply one target field, or omit target
"target": { "food": "missing" } Define food with that exact ID
Hazard "severity": 0 or 1.1 Use a finite value greater than 0 and at most 1
Hazard "detection_range": -1 Use a finite non-negative value
Hazard "id": "danger" Hazard IDs are assigned internally; omit this field
"escalation": null Omit the object for defaults, or use {}
report_severity equal to or above withdrawal_severity The report threshold must be strictly lower after defaults are applied
"exposure_seconds": 0 or "clear_seconds": 0 Both intervals must be strictly positive
"critical_severity": 0 Use a finite value greater than 0 and at most 1
"target_tolerance": -1 Use a finite non-negative distance
focus_food without a target or with an unknown ID Supply a target and an existing food ID
Different IDs in focus_food and target.food They must identify the same food
"recoil_seconds": 1 No such scenario field; recoil timing is a local model rule
"segment_lenght": 12 Unknown field; use segment_length

The loader validates the file structure and parameter values, not the physical feasibility of the experiment. Overlapping obstacles, obstacles intersecting initial arm poses, or targets inside obstacles may be accepted as a file but prevent movement. The format has no fields for autoplay, custom arm counts, object tracking, command sequences or saved simulation progress.