Writing Domain Files
A Quale domain is a complete simulation expressed in one or more .quale files. The GenericDomain compiles everything to bytecode - there is no Go code to write. This guide walks through the structure of a domain file and the decisions involved in building one.
What a Domain File Contains
Section titled “What a Domain File Contains”A complete domain file defines six core blocks, plus optional machines and dynamics:
| Block | Purpose | Required |
|---|---|---|
body | Agent state, sensors, actuators, and machines | Yes |
world | Topology, entities, queries, world machines, and data imports | Yes |
perception | Compute sensor values from agent/world state and spatial queries | Yes |
action | Interpret actuator outputs through physics and state updates | Yes |
fitness | Gates, metrics, and weighted scoring | Yes |
evolve | Evolution parameters (population, generations, mutation rates) | Yes |
dynamics | Per-tick state cascade rules (fatigue, hunger, etc.) | Optional |
The evolve block ties everything together by referencing the other blocks by name.
Step 1: Define the Body
Section titled “Step 1: Define the Body”The body declares the agent’s state variables, sensors (brain inputs), and actuators (brain outputs). State variables are the agent’s mutable properties - things like speed, position, health, or counters.
body Driver { -- Physical state state speed: m/s = 0 state position: km = 0
-- Lifecycle state alive: bool = true
-- Sensors: what the brain sees sensor current_speed: internal(0..1) sensor signal_aspect: internal(0..1)
-- Actuators: what the brain controls actuator throttle: trigger(threshold: 0.3) actuator brake: trigger(threshold: 0.3)}Key decisions:
- State granularity - every value you want to read in perception, modify in action, or score in fitness needs to be a state variable
- Sensor normalization - sensors feed brain input nodes, which work best with values in 0..1. The perception block handles the normalization math
- Actuator types -
trigger(threshold: N)fires when the brain output exceeds N;directional(threshold: N, directions: 4)expands into multiple output nodes
Step 2: Define the World
Section titled “Step 2: Define the World”The world declares the simulation’s spatial system, entity types, spatial queries, and optionally imports entity data from CSV.
world BeenleighLine { topology: route length: 24.87 km max_speed: 100 km/h tick: 0.05 s
-- World-scope state state preceding_train.position: km = 3.0
-- Entity types entity signal { properties { position: km, aspect: 0..1 } on_cross { record signal_grade { aspect, grade } agent.signals_handled += 1 } }
entity station { properties { position: km, dwell: seconds, departure: seconds } on_enter(threshold: 0.16 km, max_speed: 1.5 m/s) { agent.stations_stopped += 1 agent.dwelling = true } on_pass { agent.stations_missed += 1 } }
-- Spatial queries query nearest_ahead(entity_type, position) -> distance, index, properties query speed_zone_at(position) -> limit
-- Import entity positions from CSV import entities from "beenleigh-route.csv"}Key decisions:
- Topology type -
routefor 1D track-based simulations,grid(W, H)for 2D cell-based simulations - Entity interactions -
on_crossfires when the agent passes an entity’s position;on_enterfires when the agent is within a threshold distance and below a speed limit;on_passfires when the agent passes without entering - Records -
recordstatements inside handlers emit structured events that fitness can iterate over with per-record metrics
Step 3: Define Perception
Section titled “Step 3: Define Perception”The perception block computes sensor values from agent state, world state, and spatial queries. It runs at tick step 2, before the brain fires.
perception DriverPerception { let sig = nearest_ahead(signal, agent.position) let max_speed_ms = world.max_speed / 3.6
sensor signal_aspect = sig.aspect sensor current_speed = min(1.0, agent.speed / max_speed_ms)}Perception is where you control what the brain sees. Normalize raw values to 0..1, compute derived features (like braking distance), and apply cognitive effects (like attention degradation from fatigue).
Step 4: Define Action
Section titled “Step 4: Define Action”The action block interprets brain actuator outputs and applies them through physics and game logic. It runs at tick step 4, after the brain fires.
action DriverAction { let throttle = actuator.throttle let brake = actuator.brake let dt = world.tick
-- Physics when throttle > 0.3 { agent.speed = min(target, agent.speed + accel * dt) } when brake > 0.3 { agent.speed = max(0, agent.speed - brake * decel * dt) }
-- Movement (entity crossings detected during position sweep) agent.position += agent.speed * dt / 1000.0}The action block is where entity crossing detection happens. When the action program updates agent.position, the runtime detects which entities the agent crossed and fires their on_cross/on_enter/on_pass handlers.
Step 5: Define Fitness
Section titled “Step 5: Define Fitness”The fitness block defines how evolution scores each agent. Gates enforce hard constraints, metrics compute numeric values, and verbs weight them into a final score.
fitness DrivingAssessment { -- Gates: must be alive to score gate alive
-- Simple metrics: expression against final state metric route_completion = agent.position / world.length
-- Per-record metrics: iterate over emitted records metric signal_response { per record signal_grade: grade aggregate: avg }
-- Weighted combination maximize route_completion: 100.0 maximize signal_response: 30.0 penalize engine.complexity: 0.001}Key decisions:
- Gates - use
gate alivefor death-based zeroing, orgate <name> = <expr>for conditional multipliers (e.g.gate completion = agent.position / world.length) - Metric types - simple expressions for final-state values;
per recordfor averaging over interaction events;per tickfor running accumulations - Verb weights -
maximizeadds to fitness,penalizesubtracts,rewardadds raw values. Weight magnitudes control relative optimization pressure
Step 6: Optional Blocks
Section titled “Step 6: Optional Blocks”Dynamics
Section titled “Dynamics”The dynamics block defines per-tick state cascade rules that run independently of the brain. Use this for autonomous processes like fatigue accumulation, hunger, or environmental effects.
dynamics DriverPhysiology { per_tick { fatigue += 0.00015 boredom += fatigue * 0.0005 } rules { if stress > 0.8: fatigue += 0.001 } death { if health <= 0 } clamp 0..1}State Machines
Section titled “State Machines”State machines can be declared in either the body (agent scope) or world (world scope). Agent machines run at tick step 5; world machines run at tick step 1.
-- Inside body block:machine AWS { scope: agent initial: idle let sig = nearest_ahead(signal, agent.position) state idle {} state alert { on_enter { agent.aws_alert = 1.0 } } transition idle -> alert: when sig.distance <= 0.09 and sig.aspect < 0.9 transition alert -> acknowledged: when actuator.acknowledge_aws > 0.5}Step 7: Wire It Together
Section titled “Step 7: Wire It Together”The evolve block references all the other blocks by name and sets evolution parameters:
evolve SignalCompliance { body: Driver world: BeenleighLine fitness: DrivingAssessment dynamics: DriverPhysiology perception: DriverPerception action: DriverAction
population: 300 generations: 2000 scenarios: 5 ticks: 66000 seed: 42 agents: 1
mutation { weight_shift: 0.8 add_connection: 0.15 add_node: 0.03 }
speciation { threshold: 3.0 target_species: 15 stagnation: 25 generations }
checkpoint { every: 1 generations }}Validating and Running
Section titled “Validating and Running”-- Check for errors before runningquale check rail-driver.quale
-- Run the experimentquale evolve rail-driver.quale
-- Inspect the evolved brainquale inspect checkpoints/signalcompliance/checkpoint_gen500.quale-ckpt --pathwaysAll validation happens at compile time. If quale check passes, the domain is fully defined and ready to evolve. There is no separate domain registration or Go code to write.