Action
The action block interprets brain actuator outputs and translates them into physical changes in the simulation. It runs at step 4 of the tick loop, after the brain fires. This is where throttle becomes acceleration, braking force becomes deceleration, and movement sweeps trigger entity interactions.
The action block is the inverse of perception: perception turns the world into brain inputs, action turns brain outputs into world effects. Together they form the agent-world interface, completely defined in .quale.
action OperatorActions { let dt = world.tick
-- Emergency stop overrides everything when actuator.emergency_stop > 0.8 { agent.accel = -5.0 agent.stress += 0.05 }
-- Normal braking when actuator.brake > 0.3 and actuator.emergency_stop <= 0.8 { agent.accel = -2.0 }
-- Acceleration when actuator.accelerate > 0.3 and actuator.brake <= 0.3 and actuator.emergency_stop <= 0.8 { agent.accel = 1.5 }
-- Coasting (no inputs) when actuator.accelerate <= 0.3 and actuator.brake <= 0.3 and actuator.emergency_stop <= 0.8 { agent.accel = -0.1 agent.idle_ticks += 1 }
-- Physics integration agent.speed = max(0, agent.speed + agent.accel * dt)
-- Speed limit enforcement let zone = speed_zone_at(agent.position) let limit_ms = zone / 3.6 when agent.speed > limit_ms { agent.stress += 0.01 * dt }
-- Position update agent.position += agent.speed * dt / 1000.0
-- End of route check when agent.position >= world.length { agent.reached_end = true agent.alive = false }
agent.elapsed += dt agent.ticks_alive += 1}action SecurityResponse { let dt = world.tick
-- The brain decides allow/throttle/block for each connection -- Actions are applied via entity on_cross handlers above
-- Track activity agent.ticks_alive += 1
-- Move along the traffic stream (constant scan rate) agent.position += 0.001 * dt
-- End of stream when agent.position >= world.length { agent.alive = false }}action ForagerActions { -- Track activity agent.ticks_alive += 1
-- Check if any movement actuator fired let moved = actuator.move_n > 0.5 or actuator.move_e > 0.5 or actuator.move_s > 0.5 or actuator.move_w > 0.5
when not moved { agent.idle_ticks += 1 }}Reading Actuator Outputs
Section titled “Reading Actuator Outputs”Brain actuator values are read via actuator.<name>. The values are raw floats from the brain’s output nodes. Thresholds declared in the body block are available for comparison but not automatically applied - the action block decides how to interpret each output.
-- Human Factors: compare against body-declared thresholdswhen actuator.emergency_stop > 0.8 { ... }when actuator.brake > 0.3 { ... }when actuator.accelerate > 0.3 { ... }
-- Network Security: read block decisionwhen actuator.block > 0.5 { ... }
-- Survival: read directional actuator sub-nodeslet moved = actuator.move_n > 0.5 or actuator.move_e > 0.5 or actuator.move_s > 0.5 or actuator.move_w > 0.5All actuator.* references must match actuator declarations in the body block. Directional actuators expand to per-direction sub-nodes (e.g., actuator.move_n, actuator.move_e, actuator.move_s, actuator.move_w).
Physics
Section titled “Physics”Physics runs when the agent is active. Typical structure: emergency handling, braking, throttle, coasting, then movement.
Route Physics (Human Factors)
Section titled “Route Physics (Human Factors)”The Human Factors demo shows a full physics pipeline with prioritized actuator interpretation:
-- Emergency stop overrides everythingwhen actuator.emergency_stop > 0.8 { agent.accel = -5.0 agent.stress += 0.05}
-- Normal brakingwhen actuator.brake > 0.3 and actuator.emergency_stop <= 0.8 { agent.accel = -2.0}
-- Accelerationwhen actuator.accelerate > 0.3 and actuator.brake <= 0.3 and actuator.emergency_stop <= 0.8 { agent.accel = 1.5}
-- Coasting (no inputs)when actuator.accelerate <= 0.3 and actuator.brake <= 0.3 and actuator.emergency_stop <= 0.8 { agent.accel = -0.1 agent.idle_ticks += 1}
-- Physics integrationagent.speed = max(0, agent.speed + agent.accel * dt)
-- Position updateagent.position += agent.speed * dt / 1000.0Constant-Rate Scanning (Network Security)
Section titled “Constant-Rate Scanning (Network Security)”The Network Security demo advances at a fixed rate - the brain does not control movement. All decision-making happens through entity on_cross handlers.
-- Move along the traffic stream (constant scan rate)agent.position += 0.001 * dtGrid Movement (Survival)
Section titled “Grid Movement (Survival)”The Survival demo does not have explicit movement logic in the action block. Grid movement for directional actuators is handled by the engine. The action block only tracks whether the agent moved:
let moved = actuator.move_n > 0.5 or actuator.move_e > 0.5 or actuator.move_s > 0.5 or actuator.move_w > 0.5
when not moved { agent.idle_ticks += 1}Entity Interaction Dispatch
Section titled “Entity Interaction Dispatch”During the position update (agent.position += ...), the engine detects entities whose positions fall within the sweep from previous position to new position. For each crossed entity, the appropriate handler fires:
on_cross- always fires when the entity is crossedon_enter- fires if within threshold AND below max_speedon_pass- fires if the entity is crossed buton_enterconditions are not met
Inside interaction handlers, agent.position is set to the entity’s position (the crossing point), not the pre-sweep or post-sweep position.
Let Bindings
Section titled “Let Bindings”Local variables reduce repetition and improve readability. They are stack-allocated temporaries scoped to the action block. See the let keyword reference for full documentation.
-- Human Factors: bind the tick durationlet dt = world.tick
-- Human Factors: query and compute derived valueslet zone = speed_zone_at(agent.position)let limit_ms = zone / 3.6
-- Survival: compound boolean expressionlet moved = actuator.move_n > 0.5 or actuator.move_e > 0.5 or actuator.move_s > 0.5 or actuator.move_w > 0.5When Blocks
Section titled “When Blocks”Multiple when blocks at the same nesting level are evaluated independently - they do NOT form an if-else chain. Both when actuator.emergency_stop > 0.8 and when actuator.brake > 0.3 can fire in the same tick if both conditions are true.
The Human Factors demo prevents overlapping actuator effects by adding explicit exclusion conditions:
-- Emergency stop overrides everythingwhen actuator.emergency_stop > 0.8 { agent.accel = -5.0 agent.stress += 0.05}
-- Normal braking (only when NOT emergency stopping)when actuator.brake > 0.3 and actuator.emergency_stop <= 0.8 { agent.accel = -2.0}
-- Acceleration (only when NOT braking or emergency stopping)when actuator.accelerate > 0.3 and actuator.brake <= 0.3 and actuator.emergency_stop <= 0.8 { agent.accel = 1.5}For mutually exclusive branches, use when / else when / else:
when aspect >= 0.9 { agent.stress -= 0.02} else when aspect >= 0.7 { agent.stress += 0.02} else when aspect >= 0.4 { agent.stress += 0.1} else { agent.stress += 0.3}Rule of thumb: Use standalone when blocks for independent checks. Use when/else when/else for mutually exclusive conditions.
Termination Checks
Section titled “Termination Checks”Action blocks typically include end-of-simulation checks that kill the agent when the scenario’s spatial boundary is reached:
-- Human Factorswhen agent.position >= world.length { agent.reached_end = true agent.alive = false}
-- Network Securitywhen agent.position >= world.length { agent.alive = false}Expression Reference
Section titled “Expression Reference”The action block has access to the full expression language. See the expression reference for the complete operator and function listing.
| Feature | Syntax | Description |
|---|---|---|
| Local binding | let x = expr | Stack-allocated temporary |
| Actuator read | actuator.X | Read brain output value |
| Agent state | agent.X | Read/write agent state |
| World state | world.X | Read world constants and state |
| Spatial query | nearest_ahead(type, pos) | Query entities ahead |
| Conditional | when cond { effects } | Guarded block |
| Single-line conditional | when cond: effect | Guarded single effect |
| Exclusive chain | when/else when/else | Mutually exclusive branches |
| Ternary | cond ? a : b | Inline conditional |
| Match | match { when cond: val ... } | Piecewise function |
| Math built-ins | min, max, abs | Standard math |
| Logical operators | and, or, not | Boolean logic |
Validation Rules
Section titled “Validation Rules”- Every
actuator.Xreference must match anactuator Xdeclaration in the body block - All
agent.*writes must matchstatedeclarations in the body block - Spatial queries must match
querydeclarations in the world block - World state references (
world.X) are read-only in action
Action Block Statement Reference
Section titled “Action Block Statement Reference”All statement types available in the action block.
| Statement | Syntax | Description |
|---|---|---|
let | let x = expr | Local variable binding, scoped to the action block |
when (block) | when cond { stmts } | Independent conditional guard |
when (single-line) | when cond: stmt | Single-line conditional |
when/else when/else | when cond { } else when cond { } else { } | Mutually exclusive branches |
Assignment (=) | agent.X = expr | Direct assignment to agent state |
Assignment (+=) | agent.X += expr | Add and assign |
Assignment (-=) | agent.X -= expr | Subtract and assign |
Assignment (*=) | agent.X *= expr | Multiply and assign |
Assignment (/=) | agent.X /= expr | Divide and assign |
record | record type { fields } | Emit a typed event record |
Data Available in Action
Section titled “Data Available in Action”| Namespace | Access | Description |
|---|---|---|
agent.* | Read/Write | All agent state declared in the body block |
world.* | Read-only | World constants and state (world.tick, world.length, world.max_speed) |
actuator.* | Read-only | Brain output values from the current tick |
| Spatial queries | Read-only | nearest_ahead(), speed_zone_at(), etc. |
let bindings | Read-only | Local variables declared in the same action block |
When Block Semantics
Section titled “When Block Semantics”Independent when blocks vs exclusive chains:
-- INDEPENDENT: both can fire in the same tickwhen actuator.brake > 0.3 { agent.accel = -2.0}when agent.speed > limit_ms { agent.stress += 0.01 * dt}
-- EXCLUSIVE: only the first matching branch fireswhen actuator.emergency_stop > 0.8 { agent.accel = -5.0} else when actuator.brake > 0.3 { agent.accel = -2.0} else { agent.accel = -0.1}Entity Interaction Dispatch
Section titled “Entity Interaction Dispatch”When agent.position changes in the action block, the engine detects entity crossings and fires handlers. This happens during the position update, not after the action block completes.
| Handler | Condition | Fires when |
|---|---|---|
on_cross | Position sweep crosses entity | Always on crossing |
on_enter | Within threshold AND below max_speed | Controlled stop |
on_pass | Position sweep crosses entity but on_enter not met | Missed stop |