Skip to content

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.


A complete domain file defines six core blocks, plus optional machines and dynamics:

BlockPurposeRequired
bodyAgent state, sensors, actuators, and machinesYes
worldTopology, entities, queries, world machines, and data importsYes
perceptionCompute sensor values from agent/world state and spatial queriesYes
actionInterpret actuator outputs through physics and state updatesYes
fitnessGates, metrics, and weighted scoringYes
evolveEvolution parameters (population, generations, mutation rates)Yes
dynamicsPer-tick state cascade rules (fatigue, hunger, etc.)Optional

The evolve block ties everything together by referencing the other blocks by name.


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

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 - route for 1D track-based simulations, grid(W, H) for 2D cell-based simulations
  • Entity interactions - on_cross fires when the agent passes an entity’s position; on_enter fires when the agent is within a threshold distance and below a speed limit; on_pass fires when the agent passes without entering
  • Records - record statements inside handlers emit structured events that fitness can iterate over with per-record metrics

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).


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.


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 alive for death-based zeroing, or gate <name> = <expr> for conditional multipliers (e.g. gate completion = agent.position / world.length)
  • Metric types - simple expressions for final-state values; per record for averaging over interaction events; per tick for running accumulations
  • Verb weights - maximize adds to fitness, penalize subtracts, reward adds raw values. Weight magnitudes control relative optimization pressure

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 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
}

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
}
}

Terminal window
-- Check for errors before running
quale check rail-driver.quale
-- Run the experiment
quale evolve rail-driver.quale
-- Inspect the evolved brain
quale inspect checkpoints/signalcompliance/checkpoint_gen500.quale-ckpt --pathways

All 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.