Skip to content

ALife

Spatial artificial life: grid cellular automata (life-like, cyclic and Wolfram's elementary rules), continuous systems (reaction-diffusion, Turing patterns, Lenia), agent populations (boids, Physarum, diffusion-limited aggregation) and structural growth (L-systems, space colonization), plus the measurement layer every one of them makes its claims through.

17 source file(s), 938 public symbol(s), 851 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.

alife.kt

Spatial artificial life for the Field stdlib — the continuous / grid / agent half of ALife, the counterpart to the graph-rewriting half already here (growing.kt, dynamic_graph_systems.kt, interactionnets.kt, booleannetworks.kt) and to the constraint-based generator in wfc.kt.

Six files:

  • alife.kt (this one) — the substrate: IntGrid, the grid↔Img bridges, and the measurement layer. Everything else in the family makes its claims through the functions at the bottom of this file.
  • alife_ca.kt — discrete cellular automata: LifeRule parsed from B3/S23, outer-totalistic and cyclic multi-state CA, Wolfram's elementary 1-D rules rendered as a space-time Img, and a 3-D life-like CA that marches out to a TriMesh.
  • alife_continuous.kt — Gray–Scott reaction–diffusion, Turing patterns, and Lenia (continuous CA: ring-kernel convolution plus a growth mapping).
  • alife_field.kt — the inline ToHTML views for IntGrid, GrayScottState and LeniaRun, kept separate so the five system files name nothing from Field's visualizer.
  • alife_agents.kt — boids flocking, a Physarum/slime-mould deposit–sense–move population over a trail Img, and diffusion-limited aggregation.
  • alife_lsystem.kt — a bracketed/parametric L-system with a turtle that emits afield.geometry.path.FLines, and space-colonization branching.

Design choices, all deliberate:

  • Built on Img, not on a private raster. Every continuous system is an Img -> Img step, so a reaction–diffusion field pipelines straight into gaussianBlur, colormap, connectedComponents, isoContourFLines and asHeightSdf with no glue. Nothing here reimplements convolution.
  • A step is a value, not a side effect. step() returns the next state; you keep the frames you want. The one exception is the mutable agent populations, where copying 50 000 agents per frame would be the whole cost.
  • Deterministic from a seed. Every stochastic system takes a seed: Long and uses its own kotlin.random.Random, so a figure in the docs is reproducible and a regression is a diff.
  • No external dependency. Pure Kotlin over FloatArray/IntArray.
  • Measured, not asserted. The functions under "Measurement" exist because "it produced pixels" is not evidence. A flock is a population whose polarization is high and whose nearestNeighborDistances have a floor; a DLA cluster is one whose massRadiusDimension is ~1.7; a Gray–Scott spot regime is one whose blobShapes are round. Each of those is a number a control arm gets wrong.

Out of scope: GPU implementations (that is Field 4 / StackSL / RectSL — these are CPU reference implementations you can read), and any system needing a trained model.

src/jvmMain/kotlin/afield/simulation/agents/alife.kt — read the source

Used in: Spatial artificial life

Top-level functions

Declaration Summary
fun IntGrid.toImg(states: Int? = null): Img Render this grid as a 1-channel Img with state s mapped to s / max(1, states - 1), so a two-state grid becomes a clean 0/1 mask and a 12-state cyclic CA becomes a ramp ready for Img.colormap.
fun IntGrid.toImg(palette: List<Triple<Float, Float, Float>>): Img Render this grid as RGB through an explicit palette, one Triple(r, g, b) per state — the exact way to show a multi-state automaton, where a grayscale ramp implies an ordering between states that may not exist. States beyond the palette wrap around it.
fun Img.toIntGrid(threshold: Float = 0.5f, wrap: Boolean = true): IntGrid Threshold a 1-channel Img into a two-state IntGrid — the way a reaction–diffusion field, a Physarum trail or an imported picture becomes CA seed material.
fun cyclicPalette(states: Int): List<Triple<Float, Float, Float>> A dark-background categorical palette sized for multi-state automata: state 0 is near-black background, and the rest sweep the hue circle at fixed saturation/value so no two adjacent states look alike. Built from hsvToRgb's maths directly so it needs no Img round trip.
fun translationBetween(a: IntGrid, b: IntGrid): Pair<Int, Int>? The rigid translation from a to b as (dx, dy), or null if b is not a translate of a.
fun IntGrid.liveCentroid(): Vec2? Area-weighted centroid of the non-zero cells, or null when the grid is empty. Fine for reporting; prefer translationBetween when the claim is that a pattern moved rigidly.
fun polarization(vx: FloatArray, vy: FloatArray): Double The polarization (a.k.a. alignment) order parameter of a set of headings: \|Σ v̂\| / N, the length of the mean unit velocity.
fun randomPolarizationBaseline(n: Int): Double The expected polarization of n independent uniformly-random headings — the null hypothesis to compare a measured value against.
fun nearestNeighborDistances(xs: FloatArray, ys: FloatArray, wrapWidth: Double? = null, wrapHeight: Double? = null): DoubleArray Distance from every point to its nearest other point, in index order — the second half of "this is a flock": a flock has a floor under this distribution, because separation keeps agents apart, while a cohesion-only population collapses to a point and a random cloud has no floor at all.
fun quantileOf(values: DoubleArray, p: Double): Double The p-quantile of values (0 = min, 0.5 = median, 1 = max), by nearest-rank on a sorted copy.
fun Img.blobShapes(minArea: Int = 4, connectivity: Int = 8): List<BlobShape> Per-blob BlobShapes of a binary mask — the statistic that tells spots from stripes.
fun Img.radialAutocorrelation(maxLag: Int = 32, directions: Int = 24, stride: Int = 2, bilinear: Boolean = false): DoubleArray Direction-averaged spatial autocorrelation of a 1-channel image at integer lags 0..maxLag, normalised so index 0 is 1.
fun Img.correlationLength(maxLag: Int = 32, directions: Int = 24, stride: Int = 2, bilinear: Boolean = false, level: Double = 1.0 / Math.E): Double The correlation length of a pattern: the lag at which radialAutocorrelation first falls to level, linearly interpolated, or maxLag if it never does.
fun Img.topShare(fraction: Double = 0.05): Double The share of an image's total mass carried by its brightest fraction of pixels — a concentration measure (the Lorenz-curve reading behind a Gini coefficient).
fun massRadiusDimension(points: List<Vec2>, center: Vec2, rMin: Double = 4.0, rMax: Double = 64.0, samples: Int = 16, minCount: Int = 0): MassRadiusFit Fractal dimension by mass–radius scaling: fit N(r) ∝ r^D for the number of occupied points within radius r of center, over rMin..rMax, by least squares in log–log space.
fun Img.totalMass(): Double Total of every sample, as a Double — the conserved quantity a pure-diffusion step must preserve.
fun IntGrid.stateEntropyBits(states: Int? = null): Double Shannon entropy of the state histogram of a grid, in bits — 0 when every cell agrees and log2(states) when the states are equally common. Used to say "the initial soup was near-maximal and the settled pattern is not", quantitatively.
fun smoothNoiseImg(width: Int, height: Int, cells: Int = 8, seed: Long = 1L): Img A smooth pseudo-random scalar field in 0..1 — value noise at cells cells across, smoothstep-faded (t*t*(3-2t)) between lattice values rather than linearly interpolated.
fun discMaskImg(width: Int, height: Int, radius: Double): Img A filled disc mask of radius centred in a width x height image — the calibration object the gallery measures blobShapes and massRadiusDimension against, so both statistics are shown to read the value theory says rather than assumed to.

IntGrid

class IntGrid(val width: Int, val height: Int, val cells: IntArray, val wrap: Boolean = true) : afield.ui.visualization.ToHTML

A rectangular lattice of integer cell states — the substrate every discrete automaton in the family steps. Row-major, cells[y * width + x], origin top-left (matching Img).

Declaration Summary
val width: Int Cells across, >= 1.
val height: Int Cells down, >= 1.
val cells: IntArray State per cell, cells[y * width + x].
val wrap: Boolean true for a torus (out-of-range reads wrap), false for a bounded board whose outside counts as state 0. A glider crossing a torus seam reappears; the R-pentomino's 1103-generation history needs room rather than a wrap.
val size: Int Number of cells.
fun at(x: Int, y: Int): Int State at (x, y); throws if out of bounds. See get for the border-resolving read.
operator fun get(x: Int, y: Int): Int State at (x, y), resolving out-of-range coordinates through wrap: wrapped on a torus, otherwise 0. This is the read every neighbourhood count goes through.
operator fun set(x: Int, y: Int, v: Int) Write v at (x, y), mutating in place; out-of-range writes are ignored on a bounded grid.
fun copy(): IntGrid An independent copy.
fun blank(): IntGrid A same-shape grid of zeros.
fun count(state: Int): Int How many cells hold state.
fun population(): Int How many cells are non-zero — "population", for a two-state automaton.
fun maxState(): Int The largest state present, so a palette can be sized without being told.
fun sameCellsAs(other: IntGrid): Boolean true when other has the same shape and exactly the same states.
fun mooreCount(x: Int, y: Int, state: Int = 1): Int How many of the eight surrounding cells hold state (the Moore neighbourhood, excluding the centre) — the count Conway's Life and every other life-like rule is a function of.
fun vonNeumannCount(x: Int, y: Int, state: Int = 1): Int How many of the four edge-adjacent cells hold state (the von Neumann neighbourhood).
fun neighborhoodCount(x: Int, y: Int, state: Int, radius: Int, vonNeumann: Boolean = false): Int How many cells within Chebyshev distance radius (excluding the centre) hold state — the generalised Moore neighbourhood that "Larger than Life" rules and cyclicStep read.
fun translated(dx: Int, dy: Int): IntGrid Copy this grid shifted by (dx, dy). On a torus nothing is lost; on a bounded grid whatever leaves is dropped. Used to state "the pattern moved by exactly (1, 1)" as an equality rather than as a claim about centroids — see translationBetween.
fun livePoints(): List<Vec2> Coordinates of every non-zero cell, row-major.
fun liveBounds(): IntArray? Inclusive bounding box of the non-zero cells as (minX, minY, maxX, maxY), or null when the grid is empty. Bounding-box displacement is how a translating pattern is measured without centroid rounding coming into it.
override fun toString(): String
override fun toHTML(): String Inline view for the value visualizer: the lattice at one pixel per cell, plus population, border mode and state entropy. The body lives in alife_field.kt (alifeGridHtml) so that this file keeps its "integer lattices and nothing else" footprint.

BlobShape

data class BlobShape(val label: Int, val area: Int, val crackPerimeter: Int, val isoperimetricRatio: Double)

Shape statistics of one connected blob: pixel area, the crack perimeter (the number of unit cell edges separating the blob from anything else, including the image border), and the dimensionless isoperimetricRatio.

Declaration Summary
val label: Int The blob's label in the ComponentLabelling it came from.
val area: Int Foreground pixel count.
val crackPerimeter: Int Unit edges on the blob's boundary.
val isoperimetricRatio: Double P² / (4πA) with the crack perimeter corrected by π/4; see blobShapes for why that factor is there and what the resulting scale means.

MassRadiusFit

data class MassRadiusFit(val dimension: Double, val points: List<Pair<Double, Double>>, val rSquared: Double)

Result of a mass–radius fractal-dimension fit.

Declaration Summary
val dimension: Double The fitted slope of log N(r) against log r — the mass dimension.
val points: List<Pair<Double, Double>> The (log r, log N) pairs that were fitted, so a caller can plot the fit and see whether it really is a line rather than trusting a single number.
val rSquared: Double Coefficient of determination of the fit; a genuine scaling regime is > 0.99, and a low value means "this object has no fractal dimension over this range", not "the dimension is uncertain".

alife_agents.kt

Agent-based spatial ALife: boids flocking, a Physarum (slime-mould) deposit–sense–move population over a trail Img, and diffusion-limited aggregation.

See alife.kt for the family overview.

These are the three canonical demonstrations that global order can have no global rule: a flock's alignment, a slime mould's transport network and a dendrite's fractal dimension are all properties nothing in the code mentions. Which is exactly why each one here ships with a control arm — a run of the same code with one interaction switched off — because "emergence" is otherwise the easiest thing in computing to claim and the hardest to check. See polarization, Img.topShare and massRadiusDimension for the three numbers those controls fail.

Unlike the rest of the family these types are mutable: step() advances the population in place. Copying 50 000 agents per frame would be the entire cost of a Physarum run, and the systems are simulations rather than values.

src/jvmMain/kotlin/afield/simulation/agents/alife_agents.kt — read the source

Used in: Spatial artificial life

Top-level functions

Declaration Summary
fun BoidFlock.step(p: BoidParams = BoidParams()) One boid step, mutating the flock in place: accumulate the three steering terms from every neighbour within BoidParams.perceptionRadius (toroidal metric), cap the total force, integrate, wrap.
fun BoidFlock.stepTimes(p: BoidParams = BoidParams(), steps: Int) step applied steps times.
fun BoidFlock.toFLines(length: Double = 4.0, z: Number = 0): List<afield.geometry.path.FLine> Draw the flock as one short arrow afield.geometry.path.FLine per agent — a heading is the thing worth seeing, and a dot per boid shows position while hiding the very thing being claimed.
fun BoidFlock.toImg(scale: Double = 1.0, tail: Double = 3.0): Img Render the flock into an Img of the given size: each agent splats a small anti-aliased dot, and a short tail behind it shows its heading.
fun PhysarumSwarm.step(p: PhysarumParams, rng: kotlin.random.Random): PhysarumSwarm One Physarum step: every agent deposits, samples its three sensors, turns toward the strongest, and moves; then the trail diffuses and decays.
fun PhysarumSwarm.stepTimes(p: PhysarumParams, steps: Int, seed: Long = 1L): PhysarumSwarm step applied steps times with one kotlin.random.Random seeded from seed.
fun dlaCluster(width: Int, height: Int, particles: Int = 4000, seed: Long = 1L, stickiness: Double = 1.0, maxWalkSteps: Long = 200_000_000L): Aggregate Grow a diffusion-limited aggregation cluster: a seed at the centre, then particles released far away that random-walk until they touch the cluster and stick.
fun edenCluster(width: Int, height: Int, particles: Int = 4000, seed: Long = 1L): Aggregate Eden-model (ballistic) growth: repeatedly pick a random empty cell adjacent to the cluster and fill it. Same seed, same neighbourhood, same output type as dlaCluster — the only difference is that a growth site is chosen uniformly instead of by a diffusing walker.
fun sprinkleCluster(width: Int, height: Int, particles: Int, radius: Double, seed: Long = 1L): Aggregate A uniform random sprinkle of particles cells inside a disc of the same radius as a comparable cluster — the second control for Aggregate.fractalDimension, and the one that catches a dimension estimator fooled by a bounded support.
fun Aggregate.arrivalOrderImg(): Img Colour an aggregate by arrival order: the seed at one end of the ramp, the last particle to stick at the other.

BoidParams

data class BoidParams(val perceptionRadius: Double = 12.0, val separationRadius: Double = 4.5, val separationWeight: Double = 1.6, val alignmentWeight: Double = 1.0, val cohesionWeight: Double = 0.9, val maxSpeed: Double = 1.4, val minSpeedFraction: Double = 0.75, val maxForce: Double = 0.06)

Reynolds' three boid steering weights plus the numbers that bound the motion.

Declaration Summary
val perceptionRadius: Double How far an agent sees for alignment and cohesion.
val separationRadius: Double The (smaller) radius inside which it actively pushes away.
val separationWeight: Double Strength of the push away from close neighbours.
val alignmentWeight: Double Strength of the pull toward the mean neighbour heading — set this to 0 and the flock stops being a flock, which is the control arm the polarization claim is made against.
val cohesionWeight: Double Strength of the pull toward the neighbour centroid.
val maxSpeed: Double Speed cap (agents fly at close to this).
val minSpeedFraction: Double Floor on speed as a fraction of maxSpeed.
val maxForce: Double Per-step cap on the steering acceleration; without it a boid in a dense cluster receives an unbounded separation impulse and leaves at implausible speed.

BoidFlock

class BoidFlock(val width: Double, val height: Double, val x: FloatArray, val y: FloatArray, val vx: FloatArray, val vy: FloatArray)

A population of boids on a torus of width x height, stored as parallel arrays.

Declaration Summary
val width: Double Domain width.
val height: Double Domain height.
val x: FloatArray Agent x positions.
val y: FloatArray Agent y positions.
val vx: FloatArray Agent x velocities.
val vy: FloatArray Agent y velocities.
val count: Int How many agents.
fun order(): Double The flock's current polarization — the alignment order parameter.
fun neighborDistances(): DoubleArray Nearest-neighbour distances under the toroidal metric. See nearestNeighborDistances.
fun meanSpeed(): Double Mean speed — a sanity readout, since a flock whose agents have stalled is not flocking.
fun copy(): BoidFlock An independent copy, so a run can be forked to compare two parameter sets from one initial state.
override fun toString(): String

PhysarumParams

data class PhysarumParams(val sensorAngle: Double = PI / 4.0, val sensorDistance: Double = 9.0, val turnAngle: Double = PI / 4.0, val stepSize: Double = 1.0, val deposit: Double = 5.0, val decay: Double = 0.92, val blurRadius: Int = 1, val exclusive: Boolean = true, val sensing: Boolean = true)

Jones' Physarum transport-network model parameters — the "deposit, sense, move" agent whose population reproduces the transport networks Physarum polycephalum grows.

Declaration Summary
val sensorAngle: Double Angle between the centre sensor and each side sensor, in radians.
val sensorDistance: Double How far ahead the sensors look, in cells.
val turnAngle: Double How far an agent rotates when a side sensor wins, in radians.
val stepSize: Double How far it moves per step, in cells.
val deposit: Double How much trail it leaves per step.
val decay: Double Fraction of the trail that survives each step (0.9 = 10 % evaporates).
val blurRadius: Int Box-blur radius applied to the trail each step, which is the diffusion term.
val exclusive: Boolean Jones' one-agent-per-cell rule: an agent may only move into a cell no other agent occupies, and otherwise stays put and picks a new random heading.
val sensing: Boolean When false the agent turns at random instead of following the trail — the control arm. Everything else is identical, so a difference in Img.topShare between the two runs is attributable to the sensing and to nothing else.

PhysarumSwarm

class PhysarumSwarm(val trail: Img, val x: FloatArray, val y: FloatArray, val heading: FloatArray)

A Physarum population over a trail field.

Declaration Summary
val trail: Img The deposit field, mutated each step; 1-channel, unbounded above.
val x: FloatArray Agent x positions in cells.
val y: FloatArray Agent y positions in cells.
val heading: FloatArray Agent headings in radians.
val count: Int How many agents.
val width: Int Trail field width in cells.
val height: Int Trail field height in cells.
override fun toString(): String

Aggregate

class Aggregate(val mask: Img, val points: List<Vec2>, val center: Vec2, val maxRadius: Double, val walkSteps: Long)

A grown aggregate.

Declaration Summary
val mask: Img 1-channel binary occupancy of the cluster.
val points: List<Vec2> The occupied cells, in the order they stuck — so growth order is available, not only the final shape.
val center: Vec2 The seed location, which is the origin every radial measurement is taken from.
val maxRadius: Double The furthest occupied cell from center, in cells.
val walkSteps: Long How many random-walk steps were taken in total (the cost, and the thing a naive implementation makes quadratic).
fun fractalDimension(rMin: Double = 4.0, rMaxFraction: Double = 0.7): MassRadiusFit massRadiusDimension of this aggregate, fitted over the range its own size supports.
override fun toString(): String

alife_animate.kt

One shared way to animate the alife*.kt family — because every system in it is a time evolution, and a montage of frozen generations is the one view that hides the thing they are about.

The whole file is Box.animateEvolution plus four small "show a prefix of something" helpers. It sits apart from the five simulation files for the same reason alife_field.kt does: those files name nothing from Field's box runtime, and this one is entirely about it (Box, Frames, display).

Pacing is the craft, and it is the reason this is a shared helper rather than a copied loop. The naive animation — one simulation step per displayed frame — is wrong for almost every system here, and wrong in two opposite directions:

  • Gray–Scott needs about 6000 explicit-Euler steps to reach its named regime. One step per frame at 20 fps is five minutes of a clip in which nothing happens.
  • Conway's glider moves one cell per four generations. Forty steps per frame is a strobe.

So Box.animateEvolution takes the whole evolution — totalSteps — and the seconds it should occupy, and derives the steps per displayed frame. Live playback uses monotonic time. Frame-synchronous recording installs an AnimationClock whose time is exactly frameIndex / fps, so the same seed produces the same ordered simulation states even when browser painting or encoding is slow. Capture performance changes how long the build takes, never which states enter the movie.

A linear step schedule is not a neutral choice, and for some of these systems it is the wrong one. Reaction–diffusion and Lenia are log-time processes: the visible change from step 60 to step 600 is about the same as from step 600 to step 6000. Spend equal steps on equal wall-clock and the clip front-loads — measured as the mean absolute luma difference between the extracted frames of the Gray–Scott clip on alife_examples.md, a linear schedule gave 42.09 from the first frame to the middle and 3.98 from the middle to the last: the reader watches it finish and then waits. So Box.animateEvolution takes a ramp, and the schedule it drives is geometric rather than linear. See that parameter for the arithmetic.

The step counter belongs in the frame. Every call site here labels its panels with the step number, which is not decoration: it is how "did this clip actually reach the regime?" is answered by looking at the last frame of the recorded mp4 rather than by trusting the schedule. A clip whose final label reads step 2100 / 6000 is a failed clip, and it says so on its face.

The range policy from alife_field.kt applies with more force here, not less. A colour map whose range is taken from the frame makes an animation of a growing structure appear to stand still, and makes a saturating field indistinguishable from a dying one. So every call site passes explicit lo/hi to Img.colormap. Auto-ranging is a per-figure mistake and a per-frame lie.

src/jvmMain/kotlin/afield/simulation/agents/alife_animate.kt — read the source

Used in: Spatial artificial life

Top-level functions

Declaration Summary
fun <S> Box.animateEvolution(start: S, totalSteps: Int, seconds: Double = 7.0, columns: Int = 1, cellSide: Int = 420, channel: String = "main", framesTarget: Int = 140, ramp: Double = 1.0, step: (S) -> S, panels: (S, Int) -> List<Pair<String, Img>>): Frames Animate a stateful system in a box: step it toward a schedule, draw it, repeat, forever.
fun Img.rowsRevealed(rows: Int): Img A copy of this image with every row after rows set to zero — the space-time diagram of a 1-D automaton as it would look at generation rows.
fun rasterizeFLinesFramed(lines: List<FLine>, reference: List<FLine>, width: Int, height: Int, margin: Int = 4): Img rasterizeFLines, but with the framing taken from reference rather than from lines.
fun flinePrefix(lines: List<FLine>, fraction: Double): List<FLine> The first fraction of a turtle's path, in the order the turtle walked it — so an L-system can be animated by drawing, which is what a turtle does, rather than by cutting between derivation depths.
fun Aggregate.arrivalPrefixImg(upTo: Int): Img The first upTo particles of this aggregate, coloured by arrival order on the full cluster's scale.

alife_bml.kt

The Biham–Middleton–Levine traffic model on the GPU: a torus of cells, each empty or holding one car, advanced by a single full-screen fragment pass into a ping-pong texture.

The rule

There are two kinds of car. Eastbound cars (red) only ever move one cell to the right, southbound cars (blue) only ever move one cell down, and the two take turns: on even steps every eastbound car whose right-hand cell is empty moves into it, all at once; on odd steps the southbound cars do the same downwards. The grid wraps in both directions. Nothing else happens, and no car is ever created or destroyed.

What makes the model famous is that its only parameter, the fraction of cells holding a car, has a sharp transition: at low density the cars sort themselves into diagonal bands that flow freely, at high density a single jam spanning the grid forms and nothing moves. On a 512 × 512 torus, 0.20 reaches a moving fraction of 0.998 and 0.45 reaches 0 by step 1750, while 0.32 is near the transition and decays slowly from flow toward a jam.

One texel is one cell

ShaderBmlSystem holds the state in an RGBA8 ping-pong target: red at 1 is an eastbound car, blue at 1 a southbound one, black an empty cell. Each frame the pass reads the previous frame's texture and writes this one, so one frame is one half-step, and a full step (both colours) takes two frames. An 8-bit target is enough because the values are only ever 0 or 1.

The update is a pure gather. A cell becomes empty if it holds a car of the moving colour and the cell ahead is empty, becomes that car if it is empty and the cell behind holds one, and otherwise keeps what it had. Each cell decides from three texels of the previous frame, so the whole grid advances in parallel with no conflicts and conserves the car count exactly.

The first frame after construction or ShaderBmlSystem.reset ignores the previous texture and scatters cars instead, each cell independently holding a car with probability ShaderBmlSettings.density, eastbound with probability ShaderBmlSettings.eastboundFraction.

Getting one on screen

installInto registers the simulation as a pre-target of the render list and blits it to the window. As with ShaderDlaSystem, constructing it inside a box seeds that box's fragment tab with the update rule, so the rule arrives as source you can edit. The CPU-side alife family does not have this model; nothing here reads the grid back.

src/jvmMain/kotlin/afield/simulation/agents/alife_bml.kt — read the source

ShaderBmlSettings

data class ShaderBmlSettings(var name: String = "bml", var width: Int = 512, var height: Int = 512, var density: Float = 0.32f, var eastboundFraction: Float = 0.5f, var running: Boolean = true, var randomSeed: Int = 1, var probe: String? = null)

Everything ShaderBmlSystem reads. Most are bound as lambdas, so changing one while it runs takes effect on the next frame; density, eastboundFraction and randomSeed are only read by the scatter, so they take effect at the next ShaderBmlSystem.reset.

Declaration Summary
var name: String Prefix for the module, mesh and pre-target names. Two systems in one renderer need two names.
var width: Int Width of the torus in cells, which is also the texture width in pixels.
var height: Int Height of the torus in cells. Must be positive, as must width.
var density: Float The fraction of cells that hold a car. On a 512 × 512 torus 0.20 flows freely, 0.45 locks, and the default sits near the transition between them.
var eastboundFraction: Float The fraction of cars that are eastbound (red); the rest are southbound (blue).
var running: Boolean false holds the grid as it is; true advances it one half-step per frame.
var randomSeed: Int Seed for the scatter; ShaderBmlSystem.reset can override it.
var probe: String? A GLSL expression to hand to fieldProbe(...) once per moving car per half-step, for example "float(p.x + p.y)"; p is the cell the car is leaving. null (the default) leaves the rule uninstrumented. Setting it adds the include and the call to the rule and writes the result into the box's fragment tab, replacing what was there; turn the probe on with fieldProbeHere(). Read once, in the constructor.

ShaderBmlSystem

class ShaderBmlSystem(val renderer: Renderer = fromContext(), configure: ShaderBmlSettings.() -> Unit = {}) : Target, HasImageView, HasImageViewArray

A shader-driven BML traffic model: one fragment pass into a ping-pong target, half a step per frame. Build one, call installInto, and it runs. The file overview above describes the rule and the encoding.

Declaration Summary
val renderer: Renderer The renderer to build the target and module on; by default the one this box is running in.
val settings The settings this system was built with, after configure ran, and the handle for changing it live.
var step How many half-steps have been recorded since the last reset. Even values move the eastbound cars, odd values the southbound ones, so step / 2 is the model's time.
val simulation The ping-pong pair holding the grid.
val stepModule: ModulePointer The full-screen pass that applies the rule. This is the module a surrounding box adopts.
override val imageView: ImageView The grid as a texture: red is an eastbound car, blue a southbound one.
override val imageViewArray: List<HasImageView> The same single colour attachment, in the list form the array-sampling path asks for.
override fun render(command: CommandBuffer, index: Int) Runs one half-step. Called by whatever the system is registered with; see installInto.
fun reset(seed: Int = settings.randomSeed) Scatter a fresh grid on the next frame from ShaderBmlSettings.density and ShaderBmlSettings.eastboundFraction, and restart step from 0. Passing no seed reuses ShaderBmlSettings.randomSeed.
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewName: String = "${settings.name}-preview", previewViewport: Rect? = null): ModulePointer? Wire the simulation into a render target as a pre-target and, by default, blit it to the window. Returns the preview module, or null with withPreview = false.
fun blitToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer Draw the grid to a target without registering the simulation with it. viewport is where it lands and sourceRect which part of the grid is taken; a small sourceRect over a large viewport is how to see individual cars.

alife_continuous.kt

Continuous spatial ALife: reaction–diffusion (Gray–Scott / Turing) and Lenia, all shaped as Img -> Img steps so they compose with the imaging family.

See alife.kt for the family overview.

Every system here is a field, not a lattice of labels, so the measurements that matter are different from the discrete half: not "is this pattern periodic" but "what shape are its features" (Img.blobShapes) and "what length scale did it choose" (Img.correlationLength). The one exception, and the reason the two halves live in the same family, is leniaFromLifeRule: Lenia in its discrete limit is a life-like CA, and that gives this file's float convolution and alife_ca.kt's integer neighbour counting a claim they must agree on exactly.

src/jvmMain/kotlin/afield/simulation/agents/alife_continuous.kt — read the source

Used in: Spatial artificial life

Top-level functions

Declaration Summary
fun Img.diffuse(rate: Double = 0.2, border: Border = Border.WRAP): Img One explicit-Euler diffusion step: u + rate * Lap(u), on a torus by default.
fun Img.diffuseTimes(steps: Int, rate: Double = 0.2, border: Border = Border.WRAP): Img diffuse applied steps times.
fun grayScottSeed(width: Int, height: Int, patches: Int = 5, patchSize: Int = 12, seed: Long = 1L): GrayScottState Start a Gray–Scott run: u = 1 everywhere, v = 0 except inside a few square seed patches of random noise.
fun GrayScottState.step(p: GrayScottParams): GrayScottState One Gray–Scott timestep, returning a new GrayScottState.
fun GrayScottState.stepTimes(p: GrayScottParams, steps: Int): GrayScottState step applied steps times, returning only the final state.
fun GrayScottState.filmstrip(p: GrayScottParams, steps: Int, every: Int): List<GrayScottState> Every every-th state from this one out to steps, inclusive of both ends — a filmstrip of the run without stepping it twice.
fun Img.morphology(level: Float? = null, minArea: Int = 4): MorphologyReport Measure a 1-channel field's morphology at threshold — the single call every regime claim in this family is made through.
fun turingPattern(width: Int, height: Int, scale: Double = 1.0, steps: Int = 4000, rho: Double = 0.010, seed: Long = 1L): Img A Turing-instability field in the simplest form that shows the essential fact: two species, one diffusing fast and inhibiting, one diffusing slowly and activating, produce a pattern whose wavelength is set by the diffusion ratio and not by the initial condition.
fun Img.leniaStep(rule: LeniaRule): Img One Lenia step over a 1-channel field, toroidal. Returns a new Img.
fun Img.leniaTimes(rule: LeniaRule, steps: Int): Img leniaStep applied steps times.
fun leniaFromLifeRule(rule: LifeRule): LeniaRule A LeniaRule that reproduces the two-state life-like rule rule exactly — Lenia's discrete limit, and the cross-check that ties this file to alife_ca.kt.
fun leniaCreatures(json: String): List<LeniaCreature> Every 2-D creature in a Lenia catalogue in the format of animals.json from github.com/Chakazul/Lenia: a JSON array of entries with code, name, params and cells. Entries without params (the catalogue's section headings) are skipped.
fun leniaRuleFromCatalogue(radius: Int, t: Double, mu: Double, sigma: Double, shellPeaks: DoubleArray = doubleArrayOf(1.0), kernelCore: Int = 1, growthKind: Int = 1): LeniaRule A LeniaRule with the kernel and growth mapping of Chan's reference implementation, as named by a catalogue entry's kn and gn.
fun leniaCellsFromRle(rle: String): Img Decode a 2-D Lenia catalogue cell string into an Img: Chan's run-length encoding, in which . is 0, A..X are 1..24, two-letter pA..yO are 25..255, both scaled by 1/255, a leading count repeats the next value, $ ends a row (a count before it adds empty rows) and ! ends the pattern. Short rows are padded with zeros.
fun leniaRun(initial: Img, rule: LeniaRule, steps: Int = 200, deathMass: Double = 0.005, saturationMass: Double = 0.80, sampleEvery: Int = 10): LeniaRun Run a Lenia rule from initial for steps and classify the outcome.
fun fieldCentroid(f: Img): Pair<Double, Double> Mass-weighted centroid of a field, as (x, y) in cells. (0, 0) for an empty field.
fun leniaSeed(width: Int, height: Int, rule: LeniaRule, blobRadii: Double = 1.4, seed: Long = 1L): Img A Lenia seed: a smooth radial blob of noise of radius rule.radius * blobRadii, centred, on an otherwise empty field.
fun leniaMuSweep(width: Int, height: Int, mus: DoubleArray, steps: Int = 200, seed: Long = 1L, makeRule: (Double) -> LeniaRule = { LeniaPresets.ring(it) }): List<LeniaRun> Sweep mu and report the LeniaPhase and mass at each value — the measured phase diagram of a Lenia rule along its most consequential axis.
fun fieldMaxAbsDiff(a: Img, b: Img): Double Largest absolute per-cell difference between two same-shape fields: an L-infinity norm, and not a sum. This is the residual a claim of "these two implementations agree exactly" asserts to be zero, and a per-cell tolerance compares against it directly. A total would be larger by up to the pixel count, so read the name carefully before choosing a tolerance.

Top-level values

Declaration Summary
val REACTION_DIFFUSION_LAPLACIAN: Kernel2D The 9-point Laplacian with the weights the reaction–diffusion literature uses: 0.05 on the diagonals, 0.2 on the edges, -1 at the centre.
val GRAY_SCOTT_SPOTS: GrayScottParams F = 0.030, k = 0.062 — isolated round spots that neither grow nor merge.
val GRAY_SCOTT_WORMS: GrayScottParams F = 0.037, k = 0.060 — elongated worms that wander and branch into a labyrinth.
val GRAY_SCOTT_MAZE: GrayScottParams F = 0.029, k = 0.057 — a space-filling maze of connected corridors.
val GRAY_SCOTT_MITOSIS: GrayScottParams F = 0.0367, k = 0.0649 — the dividing/sparse-spot regime.
val GRAY_SCOTT_HOLES: GrayScottParams F = 0.039, k = 0.058 — the inverse morphology: round holes in a filled field.

GrayScottState

class GrayScottState(val u: Img, val v: Img) : afield.ui.visualization.ToHTML

A Gray–Scott reaction–diffusion state: two coupled fields u (the substrate) and v (the autocatalyst), both Imgs of the same shape.

Declaration Summary
val u: Img Substrate field, 1-channel; starts at 1 everywhere.
val v: Img Autocatalyst field, 1-channel; starts at 0 except where you seed it.
val width: Int Field width in cells.
val height: Int Field height in cells.
override fun toString(): String
override fun toHTML(): String Inline view: both reagent fields on fixed 0..1 scales plus the morphology reading. Body in alife_field.kt (alifeGrayScottHtml). The scales are fixed because Img's own auto-normalising thumbnail turns an unseeded v = 0 control arm into a rich texture.

GrayScottParams

data class GrayScottParams(val feed: Double, val kill: Double, val du: Double = 0.16, val dv: Double = 0.08, val dt: Double = 1.0)

Gray–Scott parameters.

Declaration Summary
val feed: Double F, the rate fresh substrate is supplied at.
val kill: Double k, the extra rate the autocatalyst is removed at.
val du: Double Substrate diffusion coefficient.
val dv: Double Autocatalyst diffusion coefficient (must be smaller than du for a Turing instability).
val dt: Double Timestep.

MorphologyReport

data class MorphologyReport(val coverage: Double, val blobCount: Int, val meanArea: Double, val medianIsoperimetric: Double, val correlationLength: Double, val largestShare: Double)

What the morphology of a reaction–diffusion field measures as — the numbers a claim about a regime has to be made out of, rather than a look at a thumbnail.

Declaration Summary
val coverage: Double Fraction of the field above the threshold.
val blobCount: Int How many connected features there are.
val meanArea: Double Mean feature area in cells.
val medianIsoperimetric: Double Median P²/4πA over the features: ~1 for round spots, large for worms.
val correlationLength: Double The length scale from Img.correlationLength.
val largestShare: Double Share of the covered area held by the single largest feature — the number that separates "many separate spots" from "one connected labyrinth".
override fun toString(): String

LeniaRule

class LeniaRule(val radius: Int = 12, val dt: Double = 0.1, val mu: Double = 0.15, val sigma: Double = 0.017, val shellPeaks: DoubleArray = doubleArrayOf(1.0), val shellWidth: Double = 0.15, val growth: (Float) -> Float = { u -> val d = u - mu (2.0 * exp(-d * d / (2.0 * sigma * sigma)) - 1.0).toFloat() }, val kernelTaps: LeniaTaps? = null)

A Lenia rule: a continuous cellular automaton. One field A in 0..1, one convolution kernel K, one growth mapping G, and the update

Declaration Summary
val radius: Int R, kernel radius in cells.
val dt: Double 1/T in the usual notation — the integration step. Smaller is smoother and slower.
val mu: Double Growth mapping centre: the neighbourhood mean that produces maximum growth.
val sigma: Double Growth mapping width.
val shellPeaks: DoubleArray Relative weights of the concentric kernel shells.
val shellWidth: Double Gaussian cross-section width of each shell, as a fraction of the shell spacing.
val growth: (Float) -> Float The growth mapping G, defaulting to the standard bell 2*exp(-(u-mu)²/(2σ²)) - 1; overridden by leniaFromLifeRule to recover a discrete rule.
val kernelTaps: LeniaTaps?
val taps: LeniaTaps The kernel as a sparse tap list: (dx, dy, weight) for every tap whose weight is worth evaluating, weights summing to 1 — kernelTaps when one was supplied, otherwise built from the shell parameters.
fun kernelImg(): Img The kernel rendered as an Img for inspection — the picture that explains the rule.
fun growthCurve(samples: Int = 256): DoubleArray The growth mapping sampled over 0..1, for plotting it beside the kernel.
override fun toString(): String

LeniaTaps

class LeniaTaps(val dx: IntArray, val dy: IntArray, val weight: FloatArray)

A Lenia kernel as parallel arrays of offsets and weights.

Declaration Summary
val dx: IntArray X offsets.
val dy: IntArray Y offsets.
val weight: FloatArray Normalised weights, summing to 1.
val size: Int Number of taps.

LeniaPresets

object LeniaPresets

Named continuous-Lenia parameter sets, and the one published lifeform this family ships.

Declaration Summary
fun orbium(): LeniaCreature Orbium unicaudatus, the best-known Lenia lifeform: a single crescent-shaped creature, about twenty cells across, that glides in a straight line and keeps its shape. Rule R = 13, T = 10, mu = 0.15, sigma = 0.015, with the polynomial kernel and growth mapping its catalogue entry names.
fun ring(mu: Double, sigma: Double = 0.017, radius: Int = 12, dt: Double = 0.1): LeniaRule The reference rule shape — one ring, R = 12, T = 10 — with mu left for a sweep.
fun threeShell(mu: Double, sigma: Double = 0.025, radius: Int = 12, dt: Double = 0.1): LeniaRule A three-shell kernel, the shape Lenia's richer lifeforms use.

LeniaCreature

class LeniaCreature(val code: String, val name: String, val rule: LeniaRule, val cells: Img)

A named Lenia lifeform: its rule and the initial cells that make it, as published.

Declaration Summary
val code: String The catalogue's short code (O2u for Orbium).
val name: String The catalogue's name.
val rule: LeniaRule
val cells: Img The creature's own array, values in 0..1, at the size the rule's radius expects.
fun seed(width: Int, height: Int, cx: Int = width / 2, cy: Int = height / 2): Img cells placed with its centre at (cx, cy) on an otherwise empty width × height field.
fun shuffled(seed: Long = 1L): LeniaCreature The same cell values in a random arrangement, under the same rule: the control for "the shape is the creature". It has the creature's mass and its histogram of values, and nothing else.
override fun toString(): String

LeniaPhase

enum class LeniaPhase

Which of the three things a Lenia run can do.

Constants: DEATH, BOUNDED, SATURATION.

LeniaRun

class LeniaRun(val phase: LeniaPhase, val finalMass: Double, val massHistory: DoubleArray, val blobCount: Int, val centroidDrift: Double, val field: Img) : afield.ui.visualization.ToHTML

The outcome of a Lenia run, as measured rather than as looked at.

Declaration Summary
val phase: LeniaPhase Which of LeniaPhase the run ended in.
val finalMass: Double Mean field value at the end (0 = empty, 1 = saturated).
val massHistory: DoubleArray Mean field value at each recorded step, so the shape of the approach is inspectable and not only its endpoint.
val blobCount: Int Connected features at the end.
val centroidDrift: Double Distance the field's centre of mass moved over the last half of the run, in cells; a stationary pattern is ~0 and a travelling soliton is proportional to the run length.
val field: Img The final field.
override fun toString(): String
override fun toHTML(): String Inline view: the final field on a fixed 0..1 scale, the measured phase, and the mass history as a sparkline. Body in alife_field.kt (alifeLeniaRunHtml). A saturated field is constant at 1 and an auto-ranged colour map draws it black, i.e. identical to death — so this view is the one place in the family where the range policy changes the result rather than tidying it.

alife_dla.kt

Diffusion-limited aggregation on the GPU: a buffer of random walkers stepped, stuck and drawn by a single vertex shader, with the growing cluster held in a texture rather than in any Kotlin data structure.

The CPU version of the same process is dlaCluster in alife_agents.kt, and the two answer different questions. dlaCluster returns points you can measure, with massRadiusDimension and a control arm to measure them against. What is here returns an image, at a walker count the CPU version cannot reach, and nothing reads it back. Measure DLA with alife_agents.kt; watch one grow at window resolution with this.

The state is three channels of one texture

ShaderDlaSystem renders into a single RGBA16F ping-pong target, and every part of the simulation except the walkers themselves lives in its channels:

  • red is solidity, and a pixel counts as part of the cluster once red reaches solidThreshold;
  • green marks where the walkers are this frame, and nothing carries it forward;
  • blue is age, rising while a pixel is solid and falling back when it stops being, which is what a colour ramp reads if you want tips to look younger than the interior.

Two draws per frame, and no compute pass

The target is cleared at the start of every frame, so a full-screen preserve pass runs first and rebuilds the carried state by sampling the previous frame's image: red decays, blue grows or decays, and the seed disc is held solid. The particle pass then draws one additively blended point per walker. Both read the previous frame rather than the one being built, and they run in the order they were added to the target.

All of the simulation is in that second pass's vertex shader. It reads its walker from an addressable buffer through a push-constant pointer, probes the four neighbours of its pixel in the previous frame's red channel, and either sticks where it stands or takes one axis-aligned random step; then it writes the walker back. Drawing the point and advancing the simulation are the same draw call.

A stuck walker lasts one frame

A walker that sticks emits one red splat and is respawned at a uniformly random position on the next frame, so particleCount bounds how many walkers are in flight rather than how large the cluster can get. The cluster is only ever the red channel, which is also why it is mortal: red loses solidDecayPerFrame every frame whether or not anything is still attaching to it, so at the defaults a pixel that took one splat and nothing since falls back under the threshold about 500 frames later.

Getting one on screen

installInto registers the simulation as a pre-target of the render list and blits the result to the window. Constructing the system inside a box also seeds that box's shader properties from the particle pass, so the walk arrives as source you can edit rather than as a closed component; the diffusion-limited aggregation section of afield_four_graphics.md is the worked version of that, and says why it carries no figure.

src/jvmMain/kotlin/afield/simulation/agents/alife_dla.kt — read the source

ShaderDlaSettings

data class ShaderDlaSettings(var name: String = "dla", var width: Int = 1024, var height: Int = 1024, var particleCount: Int = 65_536, var pointSize: Float = 1f, var walkStepPx: Float = 1f, var sampleStepPx: Float = 1f, var solidThreshold: Float = 0.5f, var walkerGreen: Float = 0.12f, var stuckRed: Float = 1f, var stuckBlue: Float = 0.35f, var seedPositionPx: Vector2f? = null, var seedRadiusPx: Float = 1.5f, var solidDecayPerFrame: Float = 0.001f, var ageDecayPerFrame: Float = 0.004f, var ageGrowPerFrame: Float = 0.01f, var seedBlue: Float = 0.15f, var format: Int = VK_FORMAT_R16G16B16A16_SFLOAT, var clearColor: Vec4 = vec(0, 0, 0, 1), var randomSeed: Long = 1L)

Everything ShaderDlaSystem reads, from texture size down to the two colours a walker writes.

Declaration Summary
var name: String Prefix for everything this system registers by name: its two modules, their meshes, the particle buffer, the barrier, and the entries it makes in a render target's pre-target and module maps. Two systems in one renderer need two names.
var width: Int Width of the simulation texture in pixels, which is also the coordinate system the walkers step in: positions are pixels, not normalised.
var height: Int Height of the simulation texture in pixels. Must be positive, as must width.
var particleCount: Int How many walkers are in flight at once, one 16-byte record each. Because a stuck walker is respawned immediately this is a growth rate rather than a bound on the cluster, and it trades off against width and height: too few walkers for the area and the cluster takes thousands of frames to become visible, too many and the whole field sticks in the first few.
var pointSize: Float gl_PointSize for the drawn walkers and splats. 1 draws a single pixel each.
var walkStepPx: Float How far a walker moves each frame, along one of the four axis directions.
var sampleStepPx: Float How far from a walker the four neighbour probes are taken, rounded and floored to one pixel. Raising it lets walkers stick before they touch, which thickens the branches.
var solidThreshold: Float The red value at which a pixel counts as solid. Both the neighbour probe and the age channel ask this question, so it is one threshold for sticking and for colouring.
var walkerGreen: Float The green a walker adds to the pixel it is standing on. Nothing reads green back, and the preserve pass does not carry it forward, so this is a live view of the diffusing cloud.
var stuckRed: Float The red a walker adds at the moment it sticks. This is the only thing that grows the cluster, and it is added to what is already there, so a pixel several walkers have found reads above 1 and takes proportionally longer to decay away.
var stuckBlue: Float The blue added alongside stuckRed, so a new branch tip starts with some age rather than none.
var seedPositionPx: Vector2f? Centre of the disc the preserve pass holds solid, in pixels; null puts it at the middle of the texture. Something has to be solid before anything can stick to it, so this is where the cluster starts. Resolved once in the constructor, unlike the rest of these.
var seedRadiusPx: Float Radius of that disc in pixels.
var solidDecayPerFrame: Float How much red every pixel outside the seed disc loses per frame. At 0 the cluster is permanent; at the default of 0.001 a branch fades once walkers stop reinforcing it, which is what keeps a long-running field from filling in.
var ageDecayPerFrame: Float How much blue a pixel below solidThreshold loses per frame.
var ageGrowPerFrame: Float How much blue a solid pixel gains per frame, up to 1. At the default that is 100 frames to full age.
var seedBlue: Float The blue the seed disc is held at, so the oldest part of the cluster reads as old from frame one.
var format: Int Colour format of the ping-pong target. A float format is doing work here: solidDecayPerFrame at its default is smaller than one step of an 8-bit channel, so an 8-bit target would round each frame's decay away and nothing would ever fade.
var clearColor: Vec4 Clear value for the target's colour attachment. The preserve pass covers every pixel, so this is what the first frame starts from rather than a background that stays visible behind the cluster.
var randomSeed: Long Seed for the initial scatter of walkers, handed to ShaderDlaSystem.resetParticles at construction.

ShaderDlaSystem

class ShaderDlaSystem(val renderer: Renderer = fromContext(), configure: ShaderDlaSettings.() -> Unit = {}) : Target, HasImageView, HasImageViewArray

A shader-driven DLA system: two draws into a ping-pong target, with the walkers in an addressable buffer and the cluster in the target's red channel.

Declaration Summary
val renderer: Renderer The renderer to build the target, modules and buffer on; by default the one this box is running in.
val settings The settings this system was built with, after configure ran. Most of them are read fresh from here every frame, so this is the handle for changing the simulation while it runs.
val particleBuffer: FrameBufferGroup The walkers: position, RNG state and a stuck flag, 16 bytes each, host-visible and addressable so the vertex shader can reach them through a push-constant pointer. It is one allocation shared by all frames in flight rather than one per frame, which is what makes the walk persist and what the buffer barrier in front of the particle pass is for.
val simulation The ping-pong target holding the cluster. Both passes render into it and both sample the frame before.
val preserveModule: ModulePointer The full-screen pass that carries the cluster forward: it samples the previous frame, decays red and blue, and holds the seed disc solid. Without it the per-frame clear would erase everything the walkers have built.
val particleModule: ModulePointer The point-list pass that both advances and draws the walkers. This is the module a surrounding box adopts, so its GLSL is what appears in the box's shader tabs.
val particleMesh: Mesh One point per walker, and where the settings reach the shader: every push. constant the walk reads is bound here as a lambda over settings.
override val imageView: ImageView The cluster as a texture, so a system can be sampled by any other module the way a loaded image would be. Red is solidity, blue is age, and green is whatever the walkers did this frame.
override val imageViewArray: List<HasImageView> The same single colour attachment, in the list form the array-sampling path asks for.
override fun render(command: CommandBuffer, index: Int) Runs both passes for one frame. Called by whatever the system was registered with rather than by hand; installInto is the usual way to arrange that.
fun resetParticles(seed: Long = lastRandomSeed) Scatter the walkers uniformly again, all of them unstuck. The cluster is in the texture and is untouched by this, so it restarts the diffusing half of the process against the structure already grown. Called once at construction with ShaderDlaSettings.randomSeed; passing no seed reuses the last one.
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewName: String = "${settings.name}-preview", previewViewport: Rect? = null): ModulePointer? Wire the simulation into a render target and, by default, show it. The system is added as a pre-target, so both of its passes run before that target draws its own contents, and the result is then blitted to the window.
fun blitToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false): ModulePointer Draw the cluster to a target without registering the simulation with it, for a second view of a system installInto is already running, or for placing the picture by hand.

alife_evolve.kt

The search half of this family: rule sets as genomes, the measurements in alife.kt as fitness, and PopulationSearch as the engine that connects them.

Until this file existed every figure on the ALife page was "run a fixed rule and measure it". The measuring vocabulary was already here and was already good — stateEntropyBits, massRadiusDimension, blobShapes, translationBetween, LeniaRun.centroidDrift — and each of those is a fitness function with nobody calling it. This file supplies the missing halves: a GenomeOps for three families (LifeRuleGenome, LeniaGenome, LSystemGenome) and an Objective for each, assembled out of statistics that were already in the file next door.

There is no search code here. Every driver comes from PopulationSearch, which is also what FunctionEvolve.evolve runs on. That is the point: one engine, several genomes, rather than a third private optimizer.

Every objective in this file evaluates every candidate from the same fixed initial condition. A Lenia rule is seeded from leniaSeed(..., seed = initialSeed), a CA rule from the same randomSoup, an L-system from the same axiom. A search that re-randomised the initial condition per candidate would be selecting on the luck of the draw, and the resulting "improvement" would be mostly the search rediscovering that some soups are easier than others. Where robustness across initial conditions is the question, caEdgeOfChaosObjective takes several soups and reports one Evaluation.cases entry each, which is what makes Selection.LEXICASE meaningful here.

src/jvmMain/kotlin/afield/simulation/agents/alife_evolve.kt — read the source

Used in: Spatial artificial life, Agent transcript gallery, Braitenberg vehicles with memory

Top-level functions

Declaration Summary
fun randomSoup(width: Int, height: Int, density: Double = 0.35, seed: Long = 1L, states: Int = 2, wrap: Boolean = true): IntGrid An i.i.d. random grid — the initial condition every CA fitness in this file starts from.
fun lifeRuleSignature(rule: LifeRule): String B…/S… plus the neighbourhood, unique per distinct rule — the memoisation key for a rule table.
fun caRun(soup: IntGrid, rule: LifeRule, warmup: Int = 30, scored: Int = 40): CaRunSummary Run rule from soup and summarise it.
fun caEdgeOfChaosObjective(soups: List<IntGrid>, targetDensity: Double = 0.20, bandWidth: Double = 0.12, warmup: Int = 30, scored: Int = 40): Objective<LifeRule> "Alive but not saturated, and still changing" — the edge-of-chaos objective, as a number.
fun leniaSolitonObjective(width: Int = 64, height: Int = 64, steps: Int = 120, initialSeed: Long = 1L, blobRadii: Double = 1.4, driftScale: Double = 12.0, blobBonus: Double = 0.5): Objective<LeniaRule> "Stays alive, stays localised, and travels" — the soliton objective.
fun balanceBrackets(s: String): String Close every bracket left open and drop every close with no matching open.
fun lSystemDrawing(system: LSystem, generations: Int = 4, params: TurtleParams = TurtleParams(angle = Math.PI / 7.0, scaleOnBranch = true), side: Int = 128, minSegments: Int = 24, maxSymbols: Int = 60_000): LSystemDrawing Derive system, draw it, rasterise it and measure it.
fun lSystemDimensionObjective(targetDimension: Double = 1.70, generations: Int = 4, params: TurtleParams = TurtleParams(angle = Math.PI / 7.0, scaleOnBranch = true), side: Int = 128, straightnessWeight: Double = 0.25): Objective<LSystem> "Draw something with this fractal dimension" — the L-system objective.
fun vehicleForageObjective(lights: List<Light>, params: VehicleParams = VehicleParams(neuron = NeuronParams(bias = -1.0)), count: Int = 30, steps: Int = 3000, scored: Int = 1000, initialSeed: Long = 1L): Objective<DoubleArray> "Hold the light": every vehicle in a population of count is given the candidate wiring, the population is run for steps steps among lights under params from the same initialSeed, and the fitness is the mean light at the bodies over the last scored steps, minus speedCost times the mean speed over the same steps when params carries one.

Top-level values

Declaration Summary
const val EXTINCT_SCORE: Double What an extinct run scores: strictly below any survivor, and the same number everywhere.

LifeRuleGenome

class LifeRuleGenome(val radius: Int = 1, val vonNeumann: Boolean = false, val birthDensity: Double = 0.25, val surviveDensity: Double = 0.4) : GenomeOps<LifeRule>

A life-like LifeRule as a genome: the birth and survival sets, over a fixed neighbourhood.

Declaration Summary
val radius: Int
val vonNeumann: Boolean
val birthDensity: Double Probability a given count is in the birth set of a freshly drawn genome.
val surviveDensity: Double Probability a given count is in the survival set of a freshly drawn genome.
val maxCount: Int Highest neighbour count the fixed neighbourhood can produce.
val birthCounts: IntRange Counts a birth bit may be set at: 1..maxCount, excluding 0. See the class KDoc.
val surviveCounts: IntRange Counts a survival bit may be set at: 0..maxCount.
override fun random(rng: Random): LifeRule
override val mutations: List<Variation<LifeRule>>
override fun crossover(a: LifeRule, b: LifeRule, rng: Random): LifeRule Uniform crossover: every bit of the table comes from one parent or the other, independently.
override fun signature(g: LifeRule): String

CaRunSummary

class CaRunSummary(val density: Double, val activity: Double, val entropyBits: Double, val extinct: Boolean, val finalGrid: IntGrid)

What one CA rule did to one soup: where its density settled and how much it was still changing.

Declaration Summary
val density: Double Fraction of live cells at the end.
val activity: Double Mean fraction of cells that changed state per step, over the scored window.
val entropyBits: Double IntGrid.stateEntropyBits of the final grid.
val extinct: Boolean true if the grid emptied.
val finalGrid: IntGrid The grid the run ended on, so a figure can show what was selected.
override fun toString(): String

LeniaGenome

class LeniaGenome(val radius: Int = 8, val dt: Double = 0.1, val shells: Int = 3, val muRange: ClosedFloatingPointRange<Double> = 0.05..0.40, val sigmaRange: ClosedFloatingPointRange<Double> = 0.006..0.070) : GenomeOps<LeniaRule>

A LeniaRule as a genome: mu, sigma and the kernel's shell weights, over a fixed radius and time step.

Declaration Summary
val radius: Int
val dt: Double
val shells: Int
val muRange: ClosedFloatingPointRange<Double>
val sigmaRange: ClosedFloatingPointRange<Double>
override fun random(rng: Random): LeniaRule
override val mutations: List<Variation<LeniaRule>>
override fun crossover(a: LeniaRule, b: LeniaRule, rng: Random): LeniaRule
override fun signature(g: LeniaRule): String Rounded to five decimals. Two Lenia rules whose parameters agree to that precision produce the same kernel taps to well inside float precision, so calling them the same phenotype is a statement about the simulation and not a convenience.

LSystemGenome

class LSystemGenome(val axiom: String = "F", val symbols: String = "F", val alphabet: String = "FF+-[]", val minLength: Int = 3, val maxLength: Int = 14) : GenomeOps<LSystem>

An LSystem as a genome: one production per rewritten symbol, over a fixed turtle alphabet.

Declaration Summary
val axiom: String
val symbols: String
val alphabet: String The characters a replacement may be built from.
val minLength: Int
val maxLength: Int
override fun random(rng: Random): LSystem
override val mutations: List<Variation<LSystem>>
override fun crossover(a: LSystem, b: LSystem, rng: Random): LSystem Single-point crossover per production: a prefix from one parent, a suffix from the other.
override fun signature(g: LSystem): String

LSystemDrawing

class LSystemDrawing(val dimension: Double, val rSquared: Double, val totalLength: Double, val branches: Int, val segments: Int, val raster: Img, val drawable: Boolean)

What one L-system grammar drew: the branching structure and the two numbers it is scored on.

Declaration Summary
val dimension: Double Mass–radius dimension of the drawn points, from massRadiusDimension.
val rSquared: Double How straight the log–log fit was. Below ~0.95 the object has no scaling regime over this range and dimension is a slope through a curve, which is not a dimension.
val totalLength: Double flineTotalLength of the drawing.
val branches: Int How many separate drawn runs the turtle produced.
val segments: Int
val raster: Img The drawing, rasterised, ready for a contact sheet.
val drawable: Boolean false when the grammar drew too little to measure; every number above is then meaningless and the objective says so instead of fitting three points.
override fun toString(): String

VehicleWiringGenome

class VehicleWiringGenome(val maxWeight: Double = 3.0) : GenomeOps<DoubleArray>

A Braitenberg vehicle's four sensor→motor weights as a genome, laid out as VehicleWorld.w lays out one vehicle (w[2m + s], sensor s to motor m), each within ±maxWeight.

Declaration Summary
override fun random(rng: Random): DoubleArray
override val mutations: List<Variation<DoubleArray>>
override fun crossover(a: DoubleArray, b: DoubleArray, rng: Random): DoubleArray
override fun signature(g: DoubleArray): String Rounded to two decimals: weights closer than that drive indistinguishably over a fitness run.

alife_field.kt

Inline ToHTML views for the alife*.kt family — what a box draws when you evaluate an IntGrid or a GrayScottState, with nothing to set up.

alife.kt and alife_continuous.kt name only the three entry points here (alifeGridHtml, alifeGrayScottHtml, alifeLeniaRunHtml), each from a one-line toHTML override, and inherit every pixel of their view from them.

The range policy:

  • Every scale is named in the caption, with both endpoints spelled out in the units of the thing being shown.
  • Domains are fixed by meaning, not by the frame. An IntGrid's states run 0 .. maxState; a Gray–Scott v runs 0 .. 1, the range the reaction is defined on, not min .. max of this particular field. Auto-range a saturated Lenia field — constant at 1 — and it maps to the bottom of the ramp: solid black, identical to a field that has died.
  • One pixel per cell, never resampled. The <img> is scaled by an integer factor with image-rendering:pixelated, so cell coordinates and pixel coordinates stay the same thing and a reader can count. A filtered upscale destroys exactly the local structure a CA is about.
  • A flat field says so. If every cell holds the same value the caption reports it, because a uniform panel is otherwise indistinguishable from a broken one.

AlifeGallery's inline-view figure decodes the base64 PNG back out of the emitted HTML and asserts the colour of individual pixels against the value the caption claims for them.

src/jvmMain/kotlin/afield/simulation/agents/alife_field.kt — read the source

Top-level functions

Declaration Summary
fun alifeGridHtml(grid: IntGrid): String The body of IntGrid.toHTML: the lattice itself, one pixel per cell, plus the two readings that say whether it is doing anything — population and state entropy.
fun alifeGrayScottHtml(state: GrayScottState): String The body of GrayScottState.toHTML: both reagent fields side by side on fixed 0 .. 1 scales, plus the morphology reading that the docs page's regime claims are actually made through.
fun alifeLeniaRunHtml(run: LeniaRun): String The body of LeniaRun.toHTML: the final field on a fixed 0 .. 1 scale, the measured phase, and the mass history as a sparkline.

alife_fire.kt

Fire spreading through a forest on the GPU: a fixed scatter of trees, fire that you light by drawing into a texture, and four kinds of fire, one per colour channel, that compete for the same trees.

The rule

Each texel is a tree or bare ground, decided once by a hash of its position and ShaderForestFireSettings.randomSeed, so the forest is the same every run with the same seed. A tree is ready, burning or recovering, and which it is lives in one number per kind of fire: 1 the frame it catches, then falling by that kind's ShaderForestFireSettings.refractoryDecayPerFrame each frame. Above ShaderForestFireSettings.activeFireThreshold it is burning; below ShaderForestFireSettings.readyThreshold it can catch again; in between it is recovering and cannot. A ready tree catches when a burning neighbour's fire, times ShaderForestFireSettings.spreadGain, reaches ShaderForestFireSettings.ignitionThreshold and a random draw against ShaderForestFireSettings.spreadProbability lets it across, or when the ignition texture at that texel reaches the threshold. If several kinds could take it, the strongest wins, and catching one kind sets the other three to zero.

Because trees recover, this is an excitable medium rather than a fire that leaves ash: a front passes, the forest behind it recovers, and a second front can follow the first. A front broken by bare ground curls at the break, and steady ignition makes a train of rings.

What is in each texture

ShaderForestFireSystem.state holds the four fire levels in RGBA; it is what imageView returns. ShaderForestFireSystem.fireInput is where ignition is drawn, RGBA one kind per channel, carried forward and faded by ShaderForestFireSettings.ignitionDecayPerFrame. ShaderForestFireSystem.ink is a colour that burning trees pass on to their neighbours, so that what you draw into it travels with the fire. The preview colours each kind red, green, blue or magenta, tinted by the ink.

Lighting it

Nothing burns until something is lit. Draw into ShaderForestFireSystem.fireInput with a module built on ShaderForestFireSystem.fireInputParameters, which blends additively: a fragment writing vec4(1, 0, 0, 0) lights the red kind there. As with ShaderDlaSystem, constructing the system inside a box seeds that box's fragment tab with the spreading rule, so the rule arrives as source you can edit.

src/jvmMain/kotlin/afield/simulation/agents/alife_fire.kt — read the source

FireChannels

data class FireChannels(val r: Float = 0f, val g: Float = 0f, val b: Float = 0f, val a: Float = 0f)

Four numbers, one per kind of fire, red to alpha.

Declaration Summary
val r: Float Kind 0, drawn red.
val g: Float Kind 1, drawn green.
val b: Float Kind 2, drawn blue.
val a: Float Kind 3, drawn magenta.
operator fun times(scale: Float) All four scaled.
fun toVec4(): Vec4 As a vec4, for a push constant.

ShaderForestFireSettings

data class ShaderForestFireSettings(var name: String = "forest-fire", var width: Int = 1024, var height: Int = 1024, var format: Int = VK_FORMAT_R16G16B16A16_SFLOAT, var clearColor: Vec4 = vec(0, 0, 0, 0), var treeDensity: Float = 0.72f, var treeScalePx: Float = 3f, var treeJitter: Float = 0.28f, var spreadStepPx: Float = 1f, var spreadGain: Float = 1.02f, var spreadProbability: Float = 0.5f, var paintGain: Float = 1.0f, var ignitionThreshold: Float = 0.33f, var activeFireThreshold: Float = 0.82f, var readyThreshold: Float = 0.03f, var refractoryDecayPerFrame: FireChannels = FireChannels(0.010f, 0.008f, 0.006f, 0.004f), var ignitionDecayPerFrame: FireChannels = FireChannels(0.002f, 0.002f, 0.002f, 0.002f), var inkDecayPerFrame: FireChannels = FireChannels(0.004f, 0.004f, 0.004f, 0.004f), var inkSpreadGain: Float = 0.92f, var randomSeed: Long = 1L)

Everything ShaderForestFireSystem reads. All but the sizes and format are bound as lambdas, so changing one while it runs takes effect on the next frame; changing the forest itself (treeDensity, treeScalePx, treeJitter, randomSeed) regrows it in place.

Declaration Summary
var name: String Prefix for module, mesh and pre-target names. Two systems in one renderer need two names.
var width: Int Forest width in texels.
var height: Int Forest height in texels.
var format: Int Format of all three textures.
var clearColor: Vec4 Clear colour of every texture. Transparent black, because alpha is not opacity here: it is the fourth kind of fire, and an opaque clear would light all of it at once.
var treeDensity: Float The fraction of texels that are trees. Below about 0.4 fire stops crossing the gaps.
var treeScalePx: Float Size in texels of the coarse cells the forest is hashed on, so trees grow in clumps.
var treeJitter: Float How much per-texel noise is mixed into the clumps, 0 to 1.
var spreadStepPx: Float How far, in texels, a tree looks for burning neighbours.
var spreadGain: Float Gain on a burning neighbour's fire before it is compared with ignitionThreshold.
var spreadProbability: Float The chance, each frame, that one burning neighbour lights a ready tree. At 1 every front moves one texel a frame in all eight directions and comes out square; below 1 it moves by chance, slower on average, and rounds itself into a ragged circle.
var paintGain: Float Gain on the ignition texture before it is compared with ignitionThreshold.
var ignitionThreshold: Float The signal at or above which a ready tree catches.
var activeFireThreshold: Float The fire level at or above which a tree is burning.
var readyThreshold: Float The fire level at or below which a tree can catch again.
var refractoryDecayPerFrame: FireChannels How fast each kind's fire level falls per frame. With the default thresholds red burns for 18 frames and is ready again after 97; alpha takes two and a half times as long.
var ignitionDecayPerFrame: FireChannels How fast the ignition texture fades per frame, per kind.
var inkDecayPerFrame: FireChannels How fast the ink fades per frame, per channel.
var inkSpreadGain: Float The fraction of a burning neighbour's ink a burning tree takes on.
var randomSeed: Long Seed for the forest.

ShaderForestFireSystem

class ShaderForestFireSystem(val renderer: Renderer = fromContext(), configure: ShaderForestFireSettings.() -> Unit = {}) : Target, HasImageView, HasImageViewArray

A forest-fire excitable medium: ignition, fire state and ink, each a full-screen pass into its own ping-pong target. Build one, call installInto, then light it with a module from fireInputParameters. The file overview above describes the rule and the encoding.

Declaration Summary
val renderer: Renderer The renderer to build the targets and modules on; by default the one this box is running in.
val settings The settings this system was built with, after configure ran, and the handle for changing it live.
val fireInput Where ignition is drawn, one kind of fire per channel. Add modules to it with addFireInput.
val state The four fire levels per tree. This is what imageView returns.
val ink Colour carried from tree to tree by the fire. Draw into it with addInk.
val fireInputPreserveModule: ModulePointer The pass that carries fireInput forward a frame and fades it.
val stateUpdateModule: ModulePointer The spreading rule. The box adopts this one.
val inkPreserveModule: ModulePointer The pass that carries ink forward a frame and fades it.
val inkPropagateModule: ModulePointer The pass that hands ink from burning trees to their burning neighbours. Always runs last on ink.
override val imageView: ImageView The fire levels, so a system can be sampled by another module the way a loaded image would be.
override val imageViewArray: List<HasImageView> The same texture, in the list form the array-sampling path asks for.
override fun render(command: CommandBuffer, index: Int) Runs ignition, the spreading rule and the ink, in that order. Called by whatever the system is registered with; see installInto.
fun clearFireInput() Erase all drawn ignition on the next frame.
fun clearState() Put every tree out and make it ready on the next frame.
fun clearInk() Erase the ink on the next frame.
fun reset() clearFireInput, clearState and clearInk together.
fun addFireInput(name: String, module: ModulePointer) Add a module that draws into fireInput; it runs after the carry-forward pass, every frame.
fun addFireInput(name: String, module: Module) addFireInput for a module that has not been given a pointer yet.
fun addInk(name: String, module: ModulePointer) Add a module that draws into ink; it runs before the propagate pass, so the fire carries what it draws.
fun addInk(name: String, module: Module) addInk for a module that has not been given a pointer yet.
fun fireInputParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters Parameters for a module that draws into fireInput: additive blending, so writing vec4(1, 0, 0, 0) adds red fire and a negative value erases it (the target is floating point), and the previous ignition available as sourceTex. Hand the result to moduleHere(...) in a box, then to addFireInput.
fun inkParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters Parameters for a module that draws into ink, additive, with the previous ink as sourceTex.
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewName: String = "${settings.name}-preview", previewViewport: Rect? = null): ModulePointer? Wire the simulation into a render target as a pre-target and, by default, show it with blitToScreen. Returns the preview module, or null with withPreview = false.
fun blitToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null): ModulePointer Draw the forest to a target: each kind of fire in its colour, bright while burning and dim while recovering, tinted by the ink. viewport is where it lands.
fun blitFireInputToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-fire-input-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer Draw the raw ignition texture.
fun blitInkToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-ink-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer Draw the raw ink texture.

alife_lbm.kt

A two-dimensional fluid on the GPU by the lattice Boltzmann method, with a control texture you draw obstacles, forces and dye sources into, and a dye field that the flow carries.

The method

Every texel is a lattice site holding nine numbers, the D2Q9 populations: how much fluid at that site is moving with each of the nine velocities (0,0), the four axis neighbours and the four diagonals. A step is two moves. Collision relaxes each site's populations a fraction ShaderLatticeBoltzmannSettings.relaxation of the way toward the equilibrium for that site's own density and velocity (the BGK operator); streaming then moves each population one site along its direction. A population that would stream out of a solid site is reflected back where it came from instead, which is the no-slip wall. Density and velocity are never stored as such; they are sums over the nine populations. The viscosity follows from the relaxation, (1/relaxation - 0.5) / 3 in lattice units, so values near 2 are runny and turbulent and values near 1 are syrupy.

The method is only accurate while the fluid is slow against the lattice, below about 0.1 sites per step; ShaderLatticeBoltzmannSettings.maxVelocity clamps it so that a strong push degrades the physics rather than the numbers.

What is in each texture

ShaderLatticeBoltzmannSystem.fluid holds the populations in three RGBA32F attachments, written by one pass: attachment 0 holds directions 0–3, attachment 1 directions 4–7, and attachment 2 holds direction 8 in red, then density, velocity x and velocity y, so attachment 2 is also the readable velocity field. ShaderLatticeBoltzmannSystem.controls holds, per texel, red obstacle (solid at or above ShaderLatticeBoltzmannSettings.solidThreshold), green and blue a velocity to steer toward, and alpha a dye source. ShaderLatticeBoltzmannSystem.dye is colour advected by the velocity and fed by the dye source, by speed and by vorticity.

Every frame runs the controls (carried forward, then whatever you have added), then ShaderLatticeBoltzmannSettings.stepsPerFrame fluid steps, then the dye.

Driving it

A fresh system is still water: nothing moves until something pushes. There are two ways. Set ShaderLatticeBoltzmannSettings.baseVelocity for a wind everywhere, which with ShaderLatticeBoltzmannSettings.wrapX makes a wind tunnel; or draw into the controls texture with a module built on ShaderLatticeBoltzmannSystem.controlsParameters, which blends additively, so a fragment that writes vec4(1, 0, 0, 0) marks a texel solid and vec4(0, 0.2, 0, 1) pushes it right and releases dye there. The controls texture is carried from frame to frame, so a module that draws every frame accumulates; ShaderLatticeBoltzmannSettings.obstacleDecayPerFrame and its siblings let old marks fade.

As with ShaderDlaSystem, constructing it inside a box seeds that box's fragment tab with the collision-and-streaming rule, so the physics arrives as source you can edit.

src/jvmMain/kotlin/afield/simulation/agents/alife_lbm.kt — read the source

Used in: Field 4 Graphics

FluidVisualizationMode

enum class FluidVisualizationMode

What ShaderLatticeBoltzmannSystem.blitToScreen draws.

Constants: COMPOSITE, DYE, VELOCITY, SPEED, DENSITY, CURL, CONTROLS.

ShaderLatticeBoltzmannSettings

data class ShaderLatticeBoltzmannSettings(var name: String = "lbm", var width: Int = 384, var height: Int = 384, var format: Int = VK_FORMAT_R16G16B16A16_SFLOAT, var populationFormat: Int = VK_FORMAT_R32G32B32A32_SFLOAT, var clearColor: Vec4 = vec(0, 0, 0, 1), var stepsPerFrame: Int = 4, var relaxation: Float = 1.32f, var baseDensity: Float = 1f, var baseVelocity: Vector2f = Vector2f(0.0f, 0.0f), var initialNoise: Float = 0.002f, var driveBlend: Float = 0.002f, var controlVelocityScale: Float = 0.35f, var maxVelocity: Float = 0.28f, var wrapX: Boolean = true, var wrapY: Boolean = true, var borderSolidPx: Float = 0f, var solidThreshold: Float = 0.5f, var obstacleDecayPerFrame: Float = 0f, var directionDecayPerFrame: Float = 0f, var dyeSourceDecayPerFrame: Float = 0f, var controlDirectionLimit: Float = 1f, var dyeDecayPerFrame: Float = 0.012f, var dyeAdvectionScale: Float = 1f, var controlDyeGain: Float = 1.15f, var speedDyeGain: Float = 0.02f, var curlDyeGain: Float = 0.14f, var dyeLimit: Float = 1.5f, var speedVisualizationGain: Float = 6f, var curlVisualizationGain: Float = 42f, var densityVisualizationGain: Float = 4f, var compositeDyeExposure: Float = 0.55f, var compositeVelocityGain: Float = 0.26f, var compositeCurlGain: Float = 0.42f, var previewMode: FluidVisualizationMode = FluidVisualizationMode.COMPOSITE)

Everything ShaderLatticeBoltzmannSystem reads. All but the sizes and formats are bound as lambdas, so changing one while it runs takes effect on the next frame.

Declaration Summary
var name: String Prefix for module, mesh and pre-target names. Two systems in one renderer need two names.
var width: Int Lattice width in sites, which is also the texture width in pixels.
var height: Int Lattice height in sites.
var format: Int Format of the controls and dye textures.
var populationFormat: Int Format of the three population attachments. 32-bit, because the populations are small numbers whose differences carry the velocity, and half precision rounds slow flows away.
var clearColor: Vec4 Clear colour of every texture on the first frame.
var stepsPerFrame: Int Lattice steps per frame. The fluid moves at most a fraction of a site per step, so at one step a frame a flow takes thousands of frames to cross the lattice; this is the speed of the film, not of the physics. The dye is advected once per frame, the whole distance.
var relaxation: Float The BGK relaxation rate ω, in (0, 2): how far each collision moves the populations toward equilibrium. Viscosity is (1/ω - 0.5) / 3, so larger is thinner and less stable.
var baseDensity: Float The density the fluid starts at and that solid sites are held at.
var baseVelocity: Vector2f A wind applied everywhere, in sites per step; the fluid starts moving at this velocity.
var initialNoise: Float Random velocity, in sites per step, added to every site when the fluid starts or is cleared. A symmetric setup computed on a GPU stays exactly symmetric, so without it a wake that ought to shed vortices can sit still for ever; the default is small enough not to show.
var driveBlend: Float How far each site's velocity is pulled toward baseVelocity each step, 0 to 1. This is what keeps the wind blowing against the drag of obstacles, and also what erases a wake: a disturbance fades over about 1 / driveBlend steps, so small values let wakes travel far.
var controlVelocityScale: Float Scale from the controls texture's green and blue to a velocity added each step. A push held in place keeps accelerating the fluid there until maxVelocity stops it.
var maxVelocity: Float The speed no site may exceed, in sites per step. Above about 0.3 the method becomes unstable.
var wrapX: Boolean Whether the lattice wraps left to right; otherwise the edges clamp.
var wrapY: Boolean Whether the lattice wraps top to bottom.
var borderSolidPx: Float A solid frame this many pixels thick around the edge; 0 for none.
var solidThreshold: Float The controls' red value at or above which a site is solid.
var obstacleDecayPerFrame: Float How much each obstacle mark fades per frame.
var directionDecayPerFrame: Float How much each steering mark fades per frame, toward zero from either side.
var dyeSourceDecayPerFrame: Float How much each dye-source mark fades per frame.
var controlDirectionLimit: Float The largest steering value the controls texture keeps, either sign, so accumulated pushes saturate.
var dyeDecayPerFrame: Float The fraction of the dye that fades each frame.
var dyeAdvectionScale: Float How far, in multiples of the velocity times stepsPerFrame, the dye is carried each frame.
var controlDyeGain: Float Dye released per frame by a full dye-source mark.
var speedDyeGain: Float Dye released by speed above the wind.
var curlDyeGain: Float Dye released by vorticity, which is what makes vortices draw themselves.
var dyeLimit: Float The brightest the dye may get.
var speedVisualizationGain: Float Gain from speed to brightness in the speed and velocity views.
var curlVisualizationGain: Float Gain from vorticity to saturation in the curl view.
var densityVisualizationGain: Float Gain from density deviation to brightness in the density view.
var compositeDyeExposure: Float Exposure of the dye in the composite view.
var compositeVelocityGain: Float Weight of the velocity colour in the composite view.
var compositeCurlGain: Float Weight of the vorticity colour in the composite view.
var previewMode: FluidVisualizationMode What ShaderLatticeBoltzmannSystem.blitToScreen shows when it is not given a mode.

ShaderLatticeBoltzmannSystem

class ShaderLatticeBoltzmannSystem(val renderer: Renderer = fromContext(), configure: ShaderLatticeBoltzmannSettings.() -> Unit = {}) : Target, HasImageView, HasImageViewArray

A D2Q9 lattice Boltzmann fluid: controls, one fluid step and dye advection, each a full-screen pass into its own ping-pong target. Build one, call installInto, then push it, with ShaderLatticeBoltzmannSettings.baseVelocity or a module from controlsParameters. The file overview above describes the method and what each texture holds.

Declaration Summary
val renderer: Renderer The renderer to build the targets and modules on; by default the one this box is running in.
val settings The settings this system was built with, after configure ran, and the handle for changing it live.
val controls The control field: red obstacle, green and blue steering velocity, alpha dye source. Draw into it with modules from controlsParameters, registered with addControls.
val fluid The populations, as three attachments of one ping-pong target; attachment 2 is (f8, density, velocity.x, velocity.y), so fluid.latestImageViewFor(2) is the velocity field.
val dye Colour carried by the flow. This is what imageView returns.
val controlsPreserveModule: ModulePointer The pass that carries controls forward a frame and fades it.
val fluidModule: ModulePointer One collision-and-streaming step, written to all three fluid attachments. The box adopts this one.
val dyeUpdateModule: ModulePointer The pass that carries dye along the velocity, fades it and adds new dye.
override val imageView: ImageView The dye, so a system can be sampled by another module the way a loaded image would be.
override val imageViewArray: List<HasImageView> The same dye texture, in the list form the array-sampling path asks for.
override fun render(command: CommandBuffer, index: Int) Runs controls, ShaderLatticeBoltzmannSettings.stepsPerFrame fluid steps and the dye, in that order. Called by whatever the system is registered with; see installInto.
fun clearFluid() Put the fluid back to ShaderLatticeBoltzmannSettings.baseVelocity, plus ShaderLatticeBoltzmannSettings.initialNoise, on the next frame.
fun clearControls() Erase every obstacle, push and dye source on the next frame.
fun clearDye() Erase the dye on the next frame.
fun reset() clearFluid, clearControls and clearDye together.
fun addControls(name: String, module: ModulePointer) Add a module that draws into controls; it runs after the carry-forward pass, every frame.
fun addControls(name: String, module: Module) addControls for a module that has not been given a pointer yet.
fun addDye(name: String, module: ModulePointer) Add a module that draws into dye; it runs before the built-in update so the flow carries what it draws.
fun addDye(name: String, module: Module) addDye for a module that has not been given a pointer yet.
fun controlsParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters Parameters for a module that draws into controls: additive blending, so what a fragment writes is added to what is there, and the previous controls available as sourceTex. Hand the result to moduleHere(...) in a box, then to addControls.
fun dyeParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters Parameters for a module that draws into dye, additive, with the previous dye as sourceTex.
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewName: String = "${settings.name}-preview", previewViewport: Rect? = null): ModulePointer? Wire the simulation into a render target as a pre-target and, by default, show it with blitToScreen. Returns the preview module, or null with withPreview = false.
fun blitToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null, mode: FluidVisualizationMode? = null): ModulePointer Draw the fluid to a target, as mode or, by default, as ShaderLatticeBoltzmannSettings.previewMode read every frame, so changing that setting switches the view live. viewport is where it lands; call this again with another name and viewport for a second view side by side.
fun blitDyeToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-dye-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer Draw the raw dye texture, without the composite's tone mapping.
fun blitControlsToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-controls-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer Draw the raw controls texture: red obstacle, green and blue steering, alpha not shown.

alife_lsystem.kt

Structural spatial ALife: a bracketed, parametric, optionally stochastic L-system with a turtle that emits FLines, plus space-colonization branching.

See alife.kt for the family overview.

L-systems are the odd one out in this family and belong in it for a precise reason: they are the one generative system here whose output can be checked exactly and combinatorially rather than statistically. Lindenmayer's original algae system has Fibonacci-length derivations; the Koch snowflake has exactly 3 * 4^n forward symbols after n rewrites (the quadratic Koch island this file also ships, LSYSTEM_KOCH_QUADRATIC, has 5^n — check which one you are asserting about); the Koch snowflake's turtle path closes to a measurable area. Those are equalities, not distributions, so they are the sharpest evidence in the whole family — and they exercise the same turtle that draws the pretty trees.

src/jvmMain/kotlin/afield/simulation/agents/alife_lsystem.kt — read the source

Used in: Spatial artificial life

Top-level functions

Declaration Summary
fun lSystem(axiom: String, vararg rules: Pair<Char, String>): LSystem Build an LSystem from an axiom and symbol to replacement pairs — the terse form.
fun turtleDraw(s: String, p: TurtleParams = TurtleParams(), startX: Double = 0.0, startY: Double = 0.0, startHeading: Double = -PI / 2.0, rng: kotlin.random.Random = kotlin.random.Random(0)): TurtleResult Walk an L-system string with a turtle and return the FLines it drew.
fun drawLSystem(system: LSystem, generations: Int, p: TurtleParams = TurtleParams(), seed: Long = 1L, startX: Double = 0.0, startY: Double = 0.0, startHeading: Double = -PI / 2.0): TurtleResult Derive system for generations and draw it in one call — the ordinary entry point.
fun turtleClosureError(r: TurtleResult, step: Double): Double How far a turtle path fails to close, as a fraction of one step — a scale-free version of TurtleResult.closureError.
fun flineBounds(lines: List<FLine>): DoubleArray? Axis-aligned bounds of every point in lines as (minX, minY, maxX, maxY), or null when there is nothing drawn.
fun flineTotalLength(lines: List<FLine>): Double Total drawn length of lines — the sum of the straight-line distances between consecutive nodes within each line.
fun flineSignedArea(lines: List<FLine>): Double Signed area enclosed by the polygon through every node of lines, by the shoelace formula. Meaningful only for a single closed curve, which is exactly the case it is used for: a Koch snowflake's area is a known multiple of its generating triangle's, so it is a second independent check on the turtle.
fun rasterizeFLines(lines: List<FLine>, width: Int, height: Int, margin: Int = 4): Img Rasterise lines into a 1-channel Img, scaled and centred to fit with a margin-pixel border — the bridge from turtle geometry back into the raster half of the family, so an L-system can be measured with Img.blobShapes, skeletonised, or fed to a CA as a seed.
fun spaceColonization(attractors: List<Vec2>, rootX: Double, rootY: Double, rootHeading: Double = -PI / 2.0, influenceRadius: Double = 24.0, killRadius: Double = 6.0, stepSize: Double = 4.0, maxIterations: Int = 400): ColonizationTree Space colonization (Runions et al.): grow a tree toward a cloud of attractors.
fun discAttractors(count: Int, centerX: Double, centerY: Double, radius: Double, seed: Long = 1L): List<Vec2> A cloud of attractors filling a disc — the simplest colonisation target, and the one that makes the "the form comes from the space" claim easiest to see against a leaf-shaped or ring-shaped cloud.
fun annulusAttractors(count: Int, centerX: Double, centerY: Double, innerRadius: Double, outerRadius: Double, seed: Long = 1L): List<Vec2> A cloud of attractors filling an annulus — a shape a tree cannot fill by growing straight, so the branching really has to follow the space. The clearest demonstration that space colonization is not simply "grow upward with noise".

Top-level values

Declaration Summary
val LSYSTEM_ALGAE: LSystem Lindenmayer's original algae system: A -> AB, B -> A, from the axiom A.
val LSYSTEM_KOCH_QUADRATIC: LSystem The Koch curve (quadratic island form): F -> F+F-F-F+F, drawn with 90° turns.
val LSYSTEM_KOCH_SNOWFLAKE: LSystem The Koch snowflake: axiom F--F--F with F -> F+F--F+F, drawn with 60° turns.
val LSYSTEM_SIERPINSKI_ARROWHEAD: LSystem The Sierpiński arrowhead curve: A -> B-A-B, B -> A+B+A, 60° turns, drawn from A.
val LSYSTEM_DRAGON: LSystem The dragon curve: X -> X+YF+, Y -> -FX-Y, 90° turns, axiom FX.
val LSYSTEM_BINARY_TREE: LSystem A bracketed binary tree: F -> F[+F]F[-F]F, axiom F, drawn with a ~25° branch angle.
val LSYSTEM_BUSH: LSystem Prusinkiewicz's bushy plant: X -> F[+X][-X]FX, F -> FF, axiom X, ~25° turns.
val LSYSTEM_STOCHASTIC_PLANT: LSystem A stochastic plant: the same symbol X has three competing productions, so two derivations with different seeds are different plants of the same species.

LProduction

data class LProduction(val symbol: Char, val replacement: String, val weight: Double = 1.0)

One production of an LSystem: replace symbol with replacement, with relative weight when several productions compete for the same symbol.

Declaration Summary
val symbol: Char The single character this production rewrites.
val replacement: String What it becomes.
val weight: Double Relative probability among the productions sharing symbol; equal weights make a uniform choice, and a single production per symbol makes the system deterministic.

LSystem

class LSystem(val axiom: String, val productions: List<LProduction>)

A Lindenmayer system: an axiom plus productions, rewritten in parallel.

Declaration Summary
val axiom: String The generation-0 string.
val productions: List<LProduction> The rewrite rules.
val deterministic: Boolean true when no symbol has more than one production, so derivation needs no randomness.
fun rewrite(s: String, rng: kotlin.random.Random = kotlin.random.Random(0)): String One parallel rewrite of s.
fun derive(generations: Int, rng: kotlin.random.Random = kotlin.random.Random(0), maxLength: Int = 4_000_000): String Derive generations rewrites from the axiom, returning the final string.
fun derivations(generations: Int, rng: kotlin.random.Random = kotlin.random.Random(0)): List<String> Every generation from the axiom to generations inclusive, so the growth itself is inspectable.
override fun toString(): String

TurtleParams

data class TurtleParams(val angle: Double = PI / 8.0, val step: Double = 1.0, val stepScale: Double = 0.62, val widthScale: Double = 0.7, val scaleOnBranch: Boolean = false, val angleJitter: Double = 0.0, val drawSymbols: String = "FG")

Turtle-graphics parameters for interpreting an L-system string.

Declaration Summary
val angle: Double Turn per +/-, in radians.
val step: Double Initial forward distance.
val stepScale: Double Factor applied by " and, when scaleOnBranch, on every [.
val widthScale: Double Factor applied by ! and, when scaleOnBranch, on every [.
val scaleOnBranch: Boolean When true, entering a branch shortens and thins it automatically, which is what makes an unadorned F -> F[+F]F[-F]F look like a plant instead of a wire diagram.
val angleJitter: Double Random fraction of angle added to each turn; 0 is exactly reproducible.
val drawSymbols: String Which characters mean "move forward drawing".

TurtleResult

class TurtleResult(val lines: List<FLine>, val widths: List<Double>, val startX: Double, val startY: Double, val finalX: Double, val finalY: Double, val drawnSegments: Int, val maxDepth: Int)

The result of interpreting an L-system string with a turtle.

Declaration Summary
val lines: List<FLine> One FLine per unbroken drawn run — a branch, in plant terms. Bracketed side branches and pen-up jumps each start a new line, so the list is the branching structure.
val widths: List<Double> The line width in force for each entry of lines, parallel to it, so a renderer can taper a tree without re-walking the string.
val startX: Double Where the pen started, x.
val startY: Double Where the pen started, y.
val finalX: Double Where the pen ended up, x.
val finalY: Double Where the pen ended up, y.
val drawnSegments: Int How many forward-drawing moves were made.
val maxDepth: Int Deepest bracket nesting reached.
val closureError: Double Distance from the pen's start to its end — 0 for a closed curve. This is the geometric equality a closed L-system (the Koch snowflake) is checked against; see turtleClosureError.
override fun toString(): String

ColonizationTree

class ColonizationTree(val x: DoubleArray, val y: DoubleArray, val parent: IntArray, val consumedAttractors: Int, val remainingAttractors: List<Vec2>)

A branching structure grown by space colonization.

Declaration Summary
val x: DoubleArray Node x coordinates.
val y: DoubleArray Node y coordinates.
val parent: IntArray Parent index per node, -1 for the root.
val consumedAttractors: Int How many attractors were reached and removed.
val remainingAttractors: List<Vec2> Attractors the structure never got close enough to.
val nodeCount: Int How many nodes.
val edgeCount: Int How many parent→child edges — exactly nodeCount - 1 for a tree, which is worth checking.
fun isTree(): Boolean true when the structure really is a tree: exactly one root, nodeCount - 1 edges, every non-root parent index valid and smaller than its child (so there can be no cycle).
fun toFLines(z: Number = 0): List<FLine> One FLine per parent→child edge.
fun depths(): IntArray Depth of every node from the root, in edges — the "order" of a branch, which is what a renderer needs to taper a tree.
override fun toString(): String

alife_physarum_gpu.kt

Jones' Physarum on the GPU, built on the TrailField stigmergy field in alife_trail.kt.

ShaderPhysarumSystem keeps a million agents (by default) in an addressable buffer and steps them in one vertex-shader draw per frame: each agent samples the previous frame's trail at three sensors, turns toward the strongest, steps, writes itself back and deposits one additive point. Up to four species share the field, one per channel, each attracted by its own trail and repelled by the others. sensing = false is the random-turn control arm, so Img.topShare of the trail read back with TrailField.read separates a network from a random walk, as it does for the CPU PhysarumSwarm.

Unlike the CPU model there is no one-agent-per-cell exclusion; at about one agent per pixel the density does that job.

src/jvmMain/kotlin/afield/simulation/agents/alife_physarum_gpu.kt — read the source

ShaderPhysarumSettings

data class ShaderPhysarumSettings(var name: String = "physarum", var width: Int = 1024, var height: Int = 1024, var agentCount: Int = 1_000_000, var speciesCount: Int = 1, var sensorAngle: Float = (PI / 4).toFloat(), var sensorDistance: Float = 9f, var turnAngle: Float = (PI / 4).toFloat(), var stepPx: Float = 1f, var deposit: Float = 1f, var repulsion: Float = 1f, var jitter: Float = 0f, var sensing: Boolean = true, var pointSize: Float = 1f, var spawnRadiusFraction: Float = 0f, var exposure: Float = 0.08f, var randomSeed: Long = 1L)

Everything ShaderPhysarumSystem reads. The defaults are the CPU PhysarumParams defaults, so the two can be compared: 45° sensors 9 pixels ahead, 45° turns, one pixel a step, a radius-one blur and decay = 0.92 (the trail's half-life of 8.3 frames).

Declaration Summary
var name: String Prefix for the modules and buffer this system registers; its trail field is named "<name>-trail".
var width: Int Field width in pixels.
var height: Int Field height in pixels.
var agentCount: Int How many agents, 16 bytes each. A million at 1024x1024 is about one per pixel.
var speciesCount: Int How many species, 1 to 4. Agent i is species i % speciesCount and deposits into channel species of the trail; it is attracted by its own channel and repelled by the others by repulsion.
var sensorAngle: Float Angle between the centre sensor and each side sensor, radians.
var sensorDistance: Float How far ahead the sensors look, pixels.
var turnAngle: Float How far an agent turns when a side sensor wins, radians.
var stepPx: Float Step length per frame, pixels.
var deposit: Float Trail added per agent per frame. Only ratios matter to the agents; this sets the brightness.
var repulsion: Float How strongly another species' trail counts against a sensor, relative to an agent's own.
var jitter: Float Random extra turn per frame, radians, uniformly in ±jitter.
var sensing: Boolean false turns at random instead of following the trail: the control arm, as in PhysarumParams.sensing.
var pointSize: Float Point size for the deposit splat.
var spawnRadiusFraction: Float Agents start in a disc of this fraction of the smaller side, headings random; 0 or less scatters them uniformly over the field instead.
var exposure: Float Exposure of the default preview tone map; see TrailField.previewInto.
var randomSeed: Long Seed for the initial placement, handed to ShaderPhysarumSystem.resetAgents at construction.

ShaderPhysarumSystem

class ShaderPhysarumSystem(val renderer: Renderer = fromContext(), configure: ShaderPhysarumSettings.() -> Unit = {}) : Target, HasImageView, HasImageViewArray

Jones' Physarum on the GPU: up to millions of agents in an addressable buffer that sense, turn, move and deposit into a TrailField, one vertex-shader invocation per agent per frame.

Declaration Summary
val renderer: Renderer The renderer to build on; by default the one this box is running in.
val settings The settings after configure; change them while it runs.
val trail: TrailField The trail the agents write and read. Its own settings (half-life, diffusion, wind) are live too.
val agentBuffer: FrameBufferGroup Agents: position (px), heading (radians), RNG state; 16 bytes each.
val agentModule: ModulePointer The point-list pass that steps and deposits every agent.
val agentMesh: Mesh One point per agent; the push. constants of the agent shader are bound here.
override val imageView: ImageView
override val imageViewArray: List<HasImageView>
override fun render(command: CommandBuffer, index: Int)
fun resetAgents(seed: Long = lastSeed) Place every agent again (see ShaderPhysarumSettings.spawnRadiusFraction); the trail is untouched. Call TrailField.clear too for a fresh start.
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewViewport: Rect? = null, palette: List<Vec4> = TrailField.DEFAULT_PALETTE): ModulePointer? Register the system so it steps once a frame and, by default, draw it tone-mapped (species coloured by TrailField.DEFAULT_PALETTE unless palette says otherwise). Returns the preview module, or null with withPreview = false.

alife_script.kt

Per-agent scripted behaviour for ALife: a coroutine each agent runs as its own little program, on a fixed-step virtual clock, composing into weighted steering drives.

See alife.kt for the family overview, and alife_agents.kt for the family this completes.

Why this exists

Everything in alife_agents.kt is a global rule. A flock is BoidFlock.step(p: BoidParams) — one function, one parameter block, applied identically to every agent on every tick. That is the right shape for the three canonical emergence demonstrations and it is the wrong shape for the moment you want this agent to spend two seconds approaching, half a second feeding and then go somewhere else. A global rule has no place to keep "where am I up to". You end up encoding the agent's position in its own program as an integer state field and a when, which is the standard way this is done and is also why it is never done twice the same way.

A coroutine has a place to keep it: the continuation. So a behaviour here reads as the sequence it is —

forever {
    phase("travel") { moveUntil({ near(site, 6.0) }, maxSeconds = 8.0) { seek(site); separate() } }
    phase("feed")   { rest(0.4) }
}

— and the scheduler, not the author, remembers that this agent is 1.3 seconds into its approach.

Why the scheduler counts ticks

The obvious way to build this is on kotlinx.coroutines with delay and a real dispatcher. That gives you something you can watch and nothing you can assert, because the interleaving depends on the machine. This scheduler is mine and it counts ticks:

  • wait(seconds) rounds up to a whole number of steps, ceil(seconds / fixedStep), minimum one. Wake-ups are compared as Long tick numbers, so there is no floating-point boundary to be on the wrong side of, and wait(0.5) at 60 Hz is exactly 30 steps, every time.
  • Resumptions run from a FIFO queue I own, ordered by (wakeTick, sequence) with a unique monotonic sequence number, which is a total order. Two behaviours that wake on the same tick resume in the order they went to sleep.
  • Drives are held in a LinkedHashMap keyed by a monotonic Long, so they are evaluated in the order they were registered. (Keying a motor map by Any() — the natural thing, since the key is only a handle — makes the evaluation order depend on identity hash codes, i.e. on the run.)
  • Every drive reads a pre-step snapshot of every agent, so the result does not depend on which agent is integrated first.
  • Randomness is per agent, from Random(seed, id), never from a shared stream, so adding an agent does not reshuffle the others.

The consequence is ScriptWorld.state: run a population twice from one seed and the DoubleArray is equal bit for bit. That is asserted, not asserted-about — see AlifeScriptGallery.

Composing, not duplicating

The steering verbs in BehaviourSteering are the Reynolds vocabulary already in this family: BehaviourSteering.separate, BehaviourSteering.align and BehaviourSteering.cohere are the three boid terms, so a scripted population can be a flock when its script says to be one, and ScriptWorld.asBoidFlock hands the state to polarization, nearestNeighborDistances and the rest of alife.kt's measures rather than growing a second set. Trails come out as FLines (ScriptWorld.trailFLines) because geometry is FLine's job; nothing here rasterises anything except where a contact sheet needs pixels.

What it deliberately is not

Not a behaviour tree and not a blackboard. Both are ways of giving a stateless evaluator somewhere to put the state, and the whole argument above is that a coroutine already has somewhere. There is no reactive re-evaluation either: a behaviour that wants to abandon what it is doing says so, with BehaviourScope.moveUntil or BehaviourScope.timeout or by cancelling a BehaviourJob.

src/jvmMain/kotlin/afield/simulation/agents/alife_script.kt — read the source

Top-level functions

Declaration Summary
fun ScriptWorld.toImg(scale: Double = 2.0, trail: Float = 0.45f, minDot: Double = 2.5, marks: List<Pair<Double, Double>> = emptyList(), markRadius: Double = 6.0): Img The world as an RGB raster: walls, every agent's trail so far, the agents themselves, and any marks (goals, waypoints) as rings — the frame a Box.animateEvolution clip of a scripted population draws, in the way BoidFlock.toImg is the frame of a flock.

BehaviourCancelled

class BehaviourCancelled internal constructor(val reason: String) : RuntimeException(reason)

Thrown into a behaviour at its next suspension point when its BehaviourJob is cancelled — by BehaviourJob.cancel, by its parent being cancelled, or by the agent dying.

ScriptEvent

data class ScriptEvent(val tick: Long, val actorId: Int, val kind: String, val detail: String)

Something a behaviour did, stamped with the tick it happened on — the run's audit trail.

Declaration Summary
val tick: Long The step it was recorded on; 0 means "before the first step".
val actorId: Int The agent it belongs to, or -1 for a world-level event.
val kind: String A short stable tag — phase, phase-end, log, died, failed.
val detail: String The phase name, message or exception text.
override fun toString(): String

ScriptDrive

fun interface ScriptDrive

A steering ingredient: given an agent and what it can sense, the velocity it would like to have.

Declaration Summary
fun steer(self: ScriptActor, sense: ScriptSense): Vector2d The desired velocity for self, in world units per second.

WeightedDrive

data class WeightedDrive(val drive: ScriptDrive, val weight: Double)

One ScriptDrive and the weight it carries in the blend.

ScriptSense

class ScriptSense internal constructor(val world: ScriptWorld, val self: ScriptActor, val speed: Double)

What an agent can see this step — always the pre-step snapshot, never the half-integrated present.

Declaration Summary
val world: ScriptWorld
val self: ScriptActor
val speed: Double The speed the currently-evaluating behaviour asked for — scale desired velocities by this.
val position: Vector2d self's position as at the start of this step.
val velocity: Vector2d self's velocity as at the start of this step.
fun neighbours(range: Double): List<ScriptActor> Live agents other than self within range of it, under the world's metric, in agent-id order.
fun delta(x: Double, y: Double): Vector2d Offset from self to (x, y), taking the shorter way round if the world wraps.
fun canSee(other: ScriptActor): Boolean Whether nothing blocks the straight line from self to other — both read off the pre-step snapshot, so it agrees with the positions every other query in this class reports.
fun canSee(x: Double, y: Double): Boolean Whether nothing blocks the straight line from self to (x, y).
fun visibleNeighbours(range: Double): List<ScriptActor> Live agents other than self within range that self can also see — neighbours with the wall between them removed.

ScriptTarget

fun interface ScriptTarget

Somewhere an agent might steer toward — resolved fresh every step, so a target that moves is followed and a target that has gone (a dead agent, a cleared goal) simply stops pulling.

Declaration Summary
fun resolve(world: ScriptWorld, self: ScriptActor): Vector2d? The position to aim at, or null if there is none this step.

ScriptActor

class ScriptActor internal constructor(val world: ScriptWorld, val id: Int, x: Double, y: Double, val maxSpeed: Double, val radius: Double, val steerResponse: Double, val tag: String, val wallResponse: WallResponse = WallResponse.SLIDE, val restitution: Double = 0.8)

One scripted agent: a position, a velocity, a motor set its behaviours write into, and a name for what it is currently doing.

Declaration Summary
val world: ScriptWorld
val id: Int
val maxSpeed: Double Hard cap on speed, whatever a behaviour asks for.
val radius: Double
val steerResponse: Double How much of the gap between current and desired velocity is closed per step, in 0..1. 1 is instant turning (correct for a projectile, wrong for anything with inertia); the default 0.25 gives a visible arc without the overshoot a spring would.
val tag: String
val wallResponse: WallResponse What happens when this agent's swept path meets one of the world's obstacles: one of WallResponse's three, SLIDE by default. Irrelevant in a world with no obstacles, where the integrator does not even look.
val restitution: Double How much of the normal velocity survives a WallResponse BOUNCE, in 0..1. Ignored by the other two responses.
var alive: Boolean False once ScriptWorld has killed it; a dead agent is no longer integrated or sensed.
var phase: String? The innermost BehaviourScope.phase currently running, or null between phases.
val rng: Random This agent's own random stream, seeded from the world seed and the agent id. Per-agent rather than shared, so inserting an agent does not reshuffle every other agent's future.
var wallContacts: Int How many times this agent's swept path has been stopped, deflected or reflected by a wall.
val position: Vector2d A fresh copy of the current position.
val velocity: Vector2d A fresh copy of the current velocity, in world units per second.
val speed: Double Current speed.
val driveCount: Int How many drives are currently registered across all active behaviours — a load readout.
fun distanceTo(px: Double, py: Double): Double Distance to (px, py) under the world's metric.
fun distanceTo(p: Vector2d): Double Distance to p under the world's metric.
fun placeAt(px: Double, py: Double) Move this agent immediately, ignoring its velocity — teleporting, so use it for placement and respawns, not for motion. The trail records the jump as a jump.
override fun toString(): String

BehaviourJob

class BehaviourJob internal constructor(internal val world: ScriptWorld, val actor: ScriptActor, internal val parent: BehaviourJob?)

A running behaviour, and the handle that cancels it.

Declaration Summary
val actor: ScriptActor
var cancelled: Boolean True once cancel has been called, or a parent was cancelled.
var done: Boolean True once the behaviour has returned, thrown, or finished unwinding a cancellation.
val isActive: Boolean Still worth waiting for.
fun cancel(reason: String = "cancelled") Cancel this behaviour and everything it started. Idempotent.
override fun toString(): String

ScriptWorld

class ScriptWorld(val width: Double = 200.0, val height: Double = 200.0, val fixedStep: Double = 1.0 / 60.0, val seed: Long = 1L, val wrap: Boolean = true, val maxParallelPerActor: Int = 8, val maxResumptionsPerStep: Int = 200_000, val recordTrails: Boolean = true, val maxRouteSearchesPerStep: Int = 8, val maxRouteExpansions: Int = 20_000, val maxNavCells: Int = 40_000)

A fixed-step arena of scripted agents: the clock, the scheduler, the population and the physics.

Declaration Summary
val width: Double
val height: Double
val fixedStep: Double Seconds per tick. Also the quantum every BehaviourScope.wait is rounded up to, so it sets the time resolution of every script in the world.
val seed: Long
val wrap: Boolean True for a torus (the metric BoidFlock uses, and what asBoidFlock assumes); false to clamp at the walls, which stops agents but does not make them avoid them — that is BehaviourSteering.avoidEdges' job.
val maxParallelPerActor: Int The cap BehaviourScope.parallel checks. A behaviour that launches a sub-behaviour inside a forever and never cancels it is the standard way to fill a scheduler with garbage, and it presents as a gradual slowdown rather than an error. This turns it into an error.
val maxResumptionsPerStep: Int How many behaviour resumptions one step may take before the world declares a livelock. Catches the two shapes that otherwise hang: a while (true) with no wait, and a wait short enough to round to zero (it cannot — wait has a one-tick floor — but a hand-rolled forever over an empty body would still spin).
val recordTrails: Boolean Whether to accumulate per-agent trails for trailFLines. On by default because the trail is the picture; turn it off for long runs where only the final state matters.
val maxRouteSearchesPerStep: Int How many BehaviourSteering.route drives may run a fresh A* in one step. A grid search is the one thing in this file that is not O(1) per agent, and a population that all repaths on the same tick is the frame that drops. Agents over the budget keep steering to the route they already have and try again next step; the budget is spent in agent-id order, so which agents get it is reproducible.
val maxRouteExpansions: Int Cells one A* may expand before giving up. A search that hits this returns no route rather than a bad one, and the drive falls back to a direct seek.
val maxNavCells: Int The cap on a NavGrid's cell count. Exceeding it enlarges the cells and sets NavGrid.coarse rather than allocating a grid proportional to the arena.
var tick: Long Steps taken. 0 before the first step.
val time: Double Simulation seconds elapsed: exactly tick * fixedStep, never a wall-clock reading.
val actors: List<ScriptActor> The population, in spawn order, alive and dead.
val living: List<ScriptActor> Live agents, in spawn order.
val events: List<ScriptEvent> Everything the behaviours recorded, in order. See ScriptEvent.
val failures: List<String> Drives that threw or returned non-finite vectors, and behaviours that threw.
var lastStepResumptions: Int How many behaviour resumptions the last step ran — a load readout, and a livelock warning.
val wallField: ObstacleField The world's walls. See alife_walls.kt for what they are and why they are segments.
var routeSearches: Long Total A* searches run since the world began — the cost readout for a routed population.
var routeSearchesDeferred: Long Searches refused this run because maxRouteSearchesPerStep was already spent.
val obstacles: List<ScriptObstacle> The walls, in the order they were added.
fun addObstacle(o: ScriptObstacle): ScriptObstacle Add o to the world.
fun addWall(x0: Double, y0: Double, x1: Double, y1: Double, name: String = "wall"): ScriptObstacle A single straight wall from (x0, y0) to (x1, y1).
fun addPolyline(vararg points: Double, name: String = "polyline"): ScriptObstacle An open run of points, x, y, x, y, … — a corridor side, a curved barrier.
fun addPolygon(vararg points: Double, name: String = "polygon"): ScriptObstacle A closed outline, x, y, x, y, …. Its interior is unreachable, not solid — see the file KDoc.
fun addRect(x: Double, y: Double, w: Double, h: Double, name: String = "rect"): ScriptObstacle An axis-aligned box with its lower-left corner at (x, y).
fun addDisc(cx: Double, cy: Double, radius: Double, segments: Int = 24, name: String = "disc"): ScriptObstacle A round pillar, approximated by segments sides.
fun obstacleFLines(z: Number = 0): List<FLine> Every wall's outline, for drawing the world the agents are in.
fun navGrid(inflate: Double): NavGrid The navigation grid for a circle of inflate, built on first ask and cached.
fun findRoute(sx: Double, sy: Double, gx: Double, gy: Double, inflate: Double): DoubleArray? A route for a circle of inflate from (sx, sy) to (gx, gy) as x, y, x, y, …, or null if there is no way through.
fun lineOfSight(x0: Double, y0: Double, x1: Double, y1: Double): Boolean Whether a straight sight line from (x0, y0) to (x1, y1) is unobstructed.
fun canSee(a: ScriptActor, b: ScriptActor): Boolean Whether a can see b — both on snapshot positions, so it agrees with what drives read.
fun spawn(x: Double, y: Double, maxSpeed: Double = 20.0, radius: Double = 1.0, steerResponse: Double = 0.25, vx: Double = 0.0, vy: Double = 0.0, tag: String = "", wallResponse: WallResponse = WallResponse.SLIDE, restitution: Double = 0.8, program: suspend BehaviourScope.() -> Unit): ScriptActor Add an agent at (x, y) running program.
fun kill(a: ScriptActor) Kill a: cancel its behaviours (so their finally blocks empty its motor set), stop integrating it, and take it out of everyone's neighbour queries.
fun neighbours(self: ScriptActor, range: Double): List<ScriptActor> Live agents other than self within range, in agent-id order, on snapshot positions.
fun deltaX(from: Double, to: Double): Double Signed x offset from from to to, the shorter way round if the world wraps.
fun deltaY(from: Double, to: Double): Double Signed y offset from from to to, the shorter way round if the world wraps.
fun start() Run every spawned program up to its first suspension point, at tick 0. Idempotent.
fun step() Advance one tick: wake what is due, run every behaviour to its next suspension point, then integrate.
fun run(steps: Int) step applied steps times.
fun state(): DoubleArray The whole population's state as one array — x, y, vx, vy per agent, in agent-id order.
fun stateSignature(): String A short hex digest of state over the raw bits of every coordinate — for printing, where a 4N-element array will not fit.
fun eventSignature(): String A digest of the event log — the symbolic witness beside the numeric one.
fun asBoidFlock(): BoidFlock The live population as a BoidFlock, so alife.kt's measures — polarization, nearestNeighborDistances, BoidFlock.meanSpeed — apply to a scripted run unchanged.
fun trailFLines(stride: Int = 1, z: Number = 0): List<FLine> One FLine per agent, its trail so far, split wherever it crossed a wrap seam.
fun totalPathLength(): Double Total distance every agent travelled, summed — the "did anything happen at all" readout.
override fun toString(): String

ALifeBehaviour

annotation class ALifeBehaviour

Marks the two scripting receivers, so a steering block cannot accidentally call a suspending behaviour verb on the enclosing scope.

BehaviourScope

class BehaviourScope internal constructor(val world: ScriptWorld, val self: ScriptActor, internal val job: BehaviourJob)

The verbs an agent's program is written in: sequencing, waiting on simulation time, repetition, bounded sub-behaviours, named phases, and motion.

Declaration Summary
val world: ScriptWorld
val self: ScriptActor
val time: Double Simulation seconds since the run began.
val tick: Long Steps taken since the run began.
val position: Vector2d A fresh copy of this agent's current position.
val velocity: Vector2d A fresh copy of this agent's current velocity.
val phase: String? The innermost phase currently running, or null.
val rng: Random This agent's own random stream. Per agent, so drawing from it cannot disturb anyone else.
suspend fun wait(seconds: Double) Spend seconds of simulation time, rounded up to a whole number of steps.
suspend fun waitTicks(ticks: Int) Spend exactly ticks steps. The primitive wait is expressed in.
suspend fun waitUntil(maxSeconds: Double = 30.0, predicate: () -> Boolean): Boolean Wait until predicate holds, testing it once per step, giving up after maxSeconds.
suspend fun forever(block: suspend BehaviourScope.() -> Unit) Repeat block for as long as this behaviour is active and the agent is alive.
suspend fun every(seconds: Double, block: suspend BehaviourScope.() -> Unit) Run block, then wait seconds, forever. The period is the gap, not the cycle length.
suspend fun repeatTimes(times: Int, block: suspend BehaviourScope.(Int) -> Unit) Run block times times, or until cancelled.
suspend fun parallel(block: suspend BehaviourScope.() -> Unit): BehaviourJob Start block as a sub-behaviour of this one and carry on immediately.
suspend fun timeout(seconds: Double, block: suspend BehaviourScope.() -> Unit): Boolean Run block with at most seconds to finish in. Returns true if it finished, false if it was cut off.
suspend fun phase(name: String, block: suspend BehaviourScope.() -> Unit) Name what the agent is doing for the duration of block, and log it.
fun log(message: String) Append a note to ScriptWorld.events on this tick.
suspend fun moveFor(seconds: Double, speed: Double = self.maxSpeed, block: BehaviourSteering.() -> Unit) Steer by block's drives for seconds, at speed, then stop steering.
suspend fun moveUntil(maxSeconds: Double = 10.0, speed: Double = self.maxSpeed, until: () -> Boolean, block: BehaviourSteering.() -> Unit): Boolean Steer by block's drives until until holds or maxSeconds runs out. Returns true if until became true.
suspend fun coast(seconds: Double) Keep the velocity the agent already has, for seconds — ballistic motion.
suspend fun rest(seconds: Double) Stop dead and stay stopped for seconds.
fun die() Kill this agent: it stops being integrated and stops being sensed, and its behaviours are cancelled.
fun point(p: Vector2d): ScriptTarget A fixed point. Copied on the way in, so a later mutation of p cannot move the target.
fun point(x: Double, y: Double): ScriptTarget A fixed point.
fun actor(other: ScriptActor): ScriptTarget Another agent — resolving to null once it is dead, so the pull simply stops.
fun nearest(range: Double = max(world.width, world.height), accept: (ScriptActor) -> Boolean = { true }): ScriptTarget The nearest live agent within range satisfying accept, re-chosen every step.
fun centroid(accept: (ScriptActor) -> Boolean = { true }): ScriptTarget The centroid of the live agents satisfying accept, on snapshot positions.
fun near(t: ScriptTarget, distance: Double): Boolean True when this agent is within distance of t's current position.
fun near(x: Double, y: Double, distance: Double): Boolean True when this agent is within distance of (x, y).

BehaviourSteering

class BehaviourSteering internal constructor(private val scope: BehaviourScope)

The ingredients a BehaviourScope.moveFor blends: each verb adds one weighted ScriptDrive.

Declaration Summary
val world: ScriptWorld The world being steered in.
val self: ScriptActor The agent being steered.
val rng: Random This agent's own random stream — for a choice made once, when the behaviour starts.
val time: Double Simulation seconds at the moment the behaviour started, i.e. when this block ran.
fun drive(weight: Double = 1.0, drive: ScriptDrive) Add a custom drive. The escape hatch every one of the verbs below is built on.
fun keepVelocity(weight: Double = 1.0) Keep the velocity the agent already has. See BehaviourScope.coast.
fun stop(weight: Double = 1.0) Hold still — a desired velocity of zero, which the agent decays toward.
fun direction(heading: Vector2d, weight: Double = 1.0) Travel along a fixed heading, normalised here so its length does not become a second gain.
fun heading(radians: Double, weight: Double = 1.0) Travel along the heading radians measures anticlockwise from +x.
fun seek(t: ScriptTarget, weight: Double = 1.0) Head straight for t at full speed.
fun arrive(t: ScriptTarget, slowRadius: Double = 12.0, stopRadius: Double = 0.5, weight: Double = 1.0) Head for t, easing off inside slowRadius and stopping at stopRadius.
fun flee(t: ScriptTarget, weight: Double = 1.0) Head directly away from t.
fun orbit(t: ScriptTarget, distance: Double = 20.0, clockwise: Boolean = true, weight: Double = 1.0) Circle t at distance, blending a tangential component with a radial correction.
fun wander(turn: Double = 2.0, weight: Double = 1.0) Drift, turning by at most turn radians per second, from this agent's random stream.
fun separate(range: Double = 6.0, weight: Double = 0.8) Push away from live agents within range, weighted by how close they are — Reynolds' separation, the same term BoidParams.separationWeight scales inside BoidFlock.step.
fun align(range: Double = 15.0, weight: Double = 1.0) Match the mean heading of live agents within range — Reynolds' alignment term.
fun cohere(range: Double = 15.0, weight: Double = 0.7) Steer toward the centroid of live agents within range — Reynolds' cohesion term.
fun avoidEdges(margin: Double = 15.0, weight: Double = 1.0) Turn away from the arena edges within margin — for a world with wrap = false, where ScriptWorld.confine otherwise pins an agent against a wall with its velocity zeroed.
fun avoidWalls(clearance: Double = 6.0, weight: Double = 1.2) Push away from any wall within clearance of the agent's surface, weighted by how close it is — the wall counterpart of separate, and written in the same shape for the same reason.
fun cling(distance: Double = 1.0, clockwise: Boolean = true, range: Double = 25.0, weight: Double = 1.0) Follow the nearest wall, holding distance between the agent's surface and it — wall following, the same blend of a tangential term and a radial correction that orbit is.
fun route(t: ScriptTarget, clearance: Double = 0.5, repathDistance: Double = 8.0, stallSeconds: Double = 0.75, waypointRadius: Double = -1.0, weight: Double = 1.0) Head for t along a route: a radius-aware A* through the world's walls, smoothed, and followed waypoint by waypoint.
fun gradient(field: ScalarField, towards: Boolean = true, probe: Double = 2.0, weight: Double = 1.0) Climb (or descend) a ScalarField — chemotaxis, phototaxis, thermotaxis, whichever the field means.
fun point(p: Vector2d): ScriptTarget A fixed point. See BehaviourScope.point.
fun point(x: Double, y: Double): ScriptTarget A fixed point. See BehaviourScope.point.
fun actor(other: ScriptActor): ScriptTarget Another agent, resolving to nothing once it is dead. See BehaviourScope.actor.
fun nearest(range: Double = max(world.width, world.height), accept: (ScriptActor) -> Boolean = { true }): ScriptTarget The nearest live agent within range satisfying accept. See BehaviourScope.nearest.
fun centroid(accept: (ScriptActor) -> Boolean = { true }): ScriptTarget The centroid of the live agents satisfying accept. See BehaviourScope.centroid.

alife_trail.kt

A stigmergy field on the GPU: a four-channel trail texture that agents write into and read from, and that diffuses, decays and drifts on its own between their visits.

This is the medium of every "leave a mark, follow the marks" system: Physarum's chemoattractant, ant pheromone, the dust in a particle system that remembers where the crowd went. TrailField owns only the medium. Whatever writes into it is a deposit module you add with addDeposit, built on depositParameters; ShaderPhysarumSystem is one such writer, and its agent shader is the worked example of reading the field back.

One frame

The field is an RGBA16F PingPongRenderToTexture. Each frame, first, a full-screen evolve pass rebuilds the field from the previous frame: it samples the old trail at p - wind (bilinear, so sub-pixel wind drifts smoothly), mixes in the 3x3 mean by TrailFieldSettings.diffusion, and multiplies each channel by its decay. Then every deposit module draws, additively, into the same target, reading the previous frame through trailTex. The four channels are independent trails (four species, or one trail plus three other kinds of mark); each has its own half-life.

The field is toroidal: sampling and diffusion wrap at the edges.

Reading it from a shader

#include "trail/sample" gives trailAt(trailTex, px) (bilinear, wrapping, pixel coordinates with pixel centres at +0.5), trailGradient(trailTex, px, weights, h), and trailClip(px, dims) for placing a deposit point at a pixel. The same source is TrailShaders.SAMPLE for code that builds shader strings itself.

Reading it from Kotlin

read queues a copy of the raw floats and completes with an Img (four channels, unclamped); channel(c) of that is what Img.topShare takes.

src/jvmMain/kotlin/afield/simulation/agents/alife_trail.kt — read the source

TrailFieldSettings

data class TrailFieldSettings(var name: String = "trail", var width: Int = 1024, var height: Int = 1024, var halfLifeFrames: Vec4 = vec(8.3f, 8.3f, 8.3f, 8.3f), var diffusion: Float = 1f, var windPx: Vector2f = Vector2f(0f, 0f), var format: Int = VK_FORMAT_R16G16B16A16_SFLOAT)

Everything TrailField reads: size, per-channel half-lives, diffusion, wind and format. All but the size and format are read every frame.

Declaration Summary
var name: String Prefix for the modules and render-list entries this field registers. Two fields in one renderer need two names.
var width: Int Width of the field in pixels; agents built on it step in these pixels.
var height: Int Height of the field in pixels.
var halfLifeFrames: Vec4 Per-channel half-life in frames: after this many frames with nothing deposited, a channel holds half of what it had. Negative holds a channel for ever; zero clears it every frame. The CPU Physarum's decay = 0.92 per step is a half-life of about 8.3 frames.
var diffusion: Float How much of each pixel is replaced by its 3x3 neighbourhood mean per frame, from 0 (no diffusion) to 1 (a full box blur of radius one, the CPU Physarum's blurRadius = 1).
var windPx: Vector2f Drift of the whole field in pixels per frame, applied by sampling upstream. Zero by default.
var format: Int Colour format. A float format is needed: the trail is unbounded above and decays below 8-bit steps.

TrailShaders

object TrailShaders

The packaged GLSL behind TrailField, reachable from any shader as #include "trail/<name>".

Declaration Summary
val SAMPLE: String trail/sample: wrapping bilinear sampling, a gradient, and pixel-to-clip placement. Expects nothing to be declared beforehand; every function takes the sampler as an argument.
fun has(name: String): Boolean Whether trail/<name> names one of these sources, with or without .glsl.
fun source(name: String): String The source of trail/<name>.

TrailField

class TrailField(val renderer: Renderer = fromContext(), configure: TrailFieldSettings.() -> Unit = {}) : Target, HasImageView, HasImageViewArray

A GPU stigmergy field: a toroidal RGBA16F trail that diffuses, decays per channel and drifts, plus the plumbing for modules that deposit into it and shaders that sense it. See TrailFieldSettings for the overview.

Declaration Summary
val renderer: Renderer The renderer to build on; by default the one this box is running in.
val settings The settings, read every frame, so changing one takes effect on the next frame.
val target The ping-pong target holding the trail. The evolve pass and every deposit module render into it.
val sampler One sampler for every read of the trail; filtering is done in GLSL, so nothing depends on its mode.
val evolveModule: ModulePointer The full-screen pass that carries the trail from one frame to the next: wind, diffusion, decay.
fun decayFactors(): Vec4 Per-channel multiplicative decay per frame, from TrailFieldSettings.halfLifeFrames.
fun depositParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters Parameters for a module that deposits into this field: rendering into target, additively, with the previous frame of the trail bound to any sampler named trailTex. Build the module on these, then hand it to addDeposit.
fun addDeposit(name: String, module: ModulePointer) Add a deposit module; deposits draw after the evolve pass, in the order they were added.
fun removeDeposit(name: String) Remove a deposit module added with addDeposit.
fun clear() Empty every channel. Takes effect over the next PingPongRenderToTexture.frameCount frames, so no history slot keeps old trail.
fun previousTrailResolver(): (Introspected.Texture) -> Pair<HasImageViewArray, TextureSampler>? Resolver binding the trail's previous frame to a sampler named trailTex (and its latest to trailLatestTex), for modules built outside depositParameters that read the field.
override val imageView: ImageView
override val imageViewArray: List<HasImageView>
override fun render(command: CommandBuffer, index: Int)
fun installInto(renderTarget: RenderTarget = renderer.renderList) Register this field as a pre-target of renderTarget, so it steps once per frame.
fun previewInto(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null, exposure: () -> Float = { 0.15f }, palette: List<Vec4> = DEFAULT_PALETTE): ModulePointer Draw the trail to target with an exposure tone map: channel c contributes palette[c](solvers.md#ceresexpression-kt--c) * (1 - exp(-exposure * trail.c)). Registers nothing else, so it can be called for a second view.
fun read(): CompletableFuture<Img> Read the trail's raw floats back after its next frame: an Img of width x height x 4, top row first as the field stores it, unclamped. Completes once that frame's GPU work finishes.

alife_vehicle_brains.kt

A larger Braitenberg vehicle genome, and an equal-budget comparison of the searches that look for one.

VehicleWiringGenome is four numbers. A VehicleBrainGenome adds what the vehicle model can express besides its wires: where the sensors point, how sensitive they are, a bias on each motor, and VehicleBrainGenome.hidden recurrent hidden neurons between the sensors and the motors. With no hidden neurons it is 8 numbers; with 8 it is 112, with 16 it is 344. compareVehicleSearch runs random sampling, a hill climber and the (µ+λ) evolver of PopulationSearch at the same evaluation budget over many seeds and reports distributions, which is how braitenberg_vehicles.md §10 asks whether evolution beats random sampling and at what genome size the answer changes.

src/jvmMain/kotlin/afield/simulation/agents/alife_vehicle_brains.kt — read the source

Used in: Braitenberg vehicles with memory

Top-level functions

Declaration Summary
fun compareVehicleSearch(task: VehicleBrainTask, hidden: Int, budget: Int = 300, seeds: List<Long> = (1L..12L).toList(), mu: Int = 6, lambda: Int = 12, crossoverRate: Double = 0.3, immigrantRate: Double = 0.0, patience: Int = 30): VehicleSearchComparison Random sampling, hill climbing and evolution on task with a hidden-neuron VehicleBrainGenome, each for exactly budget objective calls at every seed in seeds, through compareSearch. Deterministic given its arguments. Evolution is (µ=mu + λ=lambda) with tournament 2, crossoverRate and immigrantRate (the fraction of children that are fresh random draws); the hill climber restarts after patience non-improving steps. For a fourth arm, or another genome, call compareSearch directly.

VehicleBrain

class VehicleBrain(val hidden: Int, val sensorAngle: Double, val sensorGain: Double, val motorBias: DoubleArray, val direct: DoubleArray, val inHidden: DoubleArray, val hiddenBias: DoubleArray, val outHidden: DoubleArray, val recurrent: DoubleArray)

A decoded vehicle controller. Every gene is in -1..1; decode scales them.

Declaration Summary
val hidden: Int Hidden neurons.
val sensorAngle: Double Sensor half-angle in radians, 0.1..1.5.
val sensorGain: Double Multiplier on what the sensors read, e^-1..e^1.5.
val motorBias: DoubleArray Bias added to each motor neuron's input.
val direct: DoubleArray Direct sensor→motor weights, [2m + s].
val inHidden: DoubleArray Sensor→hidden weights, [2j + s].
val hiddenBias: DoubleArray Hidden biases.
val outHidden: DoubleArray Hidden→motor weights, [2j + m].
val recurrent: DoubleArray Hidden→hidden weights, [H * to + from].

VehicleBrainWorld

class VehicleBrainWorld(val brain: VehicleBrain, lights: List<Light>, val count: Int, seed: Long = 1L, val width: Double = 240.0, val height: Double = 240.0, val params: VehicleParams = VehicleParams())

A population of VehicleBrain vehicles on the same torus, kinematics and lights as VehicleWorld.

Declaration Summary
val brain: VehicleBrain The controller every vehicle runs.
val count: Int How many vehicles.
val width: Double Domain width.
val height: Double Domain height.
val params: VehicleParams Kinematics; only the body fields (sensorDistance, baseSpeed, motorSpeed, axle) are read.
var lights: List<Light> Lights at their current positions.
val x: DoubleArray Body x.
val y: DoubleArray Body y.
val heading: DoubleArray Headings.
val speed: DoubleArray Forward speed on the last step.
fun light(px: Double, py: Double): Double Brightness at a point, summed over lights on the torus.
fun nearestLightDistance(i: Int): Double Distance from vehicle i to the nearest light.
fun meanLight(): Double Mean light at the bodies.
fun meanSpeed(): Double Mean forward speed.
fun step() Advance every vehicle one step.
fun stepTimes(n: Int) step n times.
fun trails(steps: Int, scale: Double = 1.2): Img Trails of steps steps from here, drawn over the light field as it stands at the end, at scale pixels per unit. Moving lights leave a faint track of their own.

VehicleBrainGenome

class VehicleBrainGenome(val hidden: Int = 0) : GenomeOps<DoubleArray>

A VehicleBrain as a GenomeOps: VehicleBrain.genes``(hidden) numbers in -1..1, drawn uniformly.

Declaration Summary
val size: Int Genes per genome.
override fun random(rng: Random): DoubleArray
override val mutations: List<Variation<DoubleArray>>
override fun crossover(a: DoubleArray, b: DoubleArray, rng: Random): DoubleArray
override fun signature(g: DoubleArray): String

VehicleBrainTask

class VehicleBrainTask(val name: String, val lights: List<Light>, val score: (VehicleBrainWorld) -> Double, val count: Int = 10, val steps: Int = 1500, val scored: Int = 500, val initialSeed: Long = 1L)

A scoring task for a VehicleBrain: count vehicles from initialSeed among lights for steps steps, scored over the last scored by score, which sees the world after each step and returns that step's mean per-vehicle score.

Declaration Summary
val name: String Short name for tables.
val lights: List<Light> Lights at the start of every run.
val score: (VehicleBrainWorld) -> Double Per-step score, averaged over the scored window.
val count: Int Vehicles per evaluation.
val steps: Int Steps per evaluation.
val scored: Int Steps at the end that are scored.
val initialSeed: Long Start positions.
fun world(genes: DoubleArray, hidden: Int, seed: Long = initialSeed, vehicles: Int = count): VehicleBrainWorld A fresh world for genes decoded with hidden hidden neurons.
fun objective(hidden: Int): Objective<DoubleArray> The task as an Objective for a hidden-neuron genome. Behaviour is [meanLight, meanSpeed].

VehicleSearchDriver

enum class VehicleSearchDriver(val arm: String)

The three searches compareVehicleSearch runs, by their SearchArm names.

Constants: RANDOM, HILL_CLIMB, EVOLVE.

Declaration Summary
val arm: String The SearchArm.name this driver runs as.

VehicleSearchCell

class VehicleSearchCell(val driver: VehicleSearchDriver, val seed: Long, val run: SearchRun<DoubleArray>)

One driver's result for one seed.

Declaration Summary
val driver: VehicleSearchDriver Which driver.
val seed: Long The search seed.
val run: SearchRun<DoubleArray> The run, with its per-evaluation SearchRun.trace.

VehicleSearchComparison

class VehicleSearchComparison(val task: String, val hidden: Int, val genes: Int, val general: SearchComparison<DoubleArray>)

compareVehicleSearch's result: every driver at every seed, all at budget evaluations. The statistics and the plot are general's, the SearchComparison any genome gets.

Declaration Summary
val task: String The task.
val hidden: Int Hidden neurons.
val genes: Int Genes.
val general: SearchComparison<DoubleArray> The generic comparison this wraps.
val budget: Int Evaluations each run was allowed.
val cells: List<VehicleSearchCell> Every run.
fun of(d: VehicleSearchDriver): List<VehicleSearchCell> The runs of one driver, in seed order.
fun best(d: VehicleSearchDriver): DoubleArray Best fitness of each seed's run of d.
fun wins(a: VehicleSearchDriver, b: VehicleSearchDriver): Int Seeds at which a ended strictly above b.
fun champion(d: VehicleSearchDriver): DoubleArray The best genome any run of d found.
fun table(): String One line per driver: median, IQR, min, max of the per-seed bests, and wins against random.
fun perSeed(): String Per-seed bests, one row per seed.
fun curve(d: VehicleSearchDriver): Array<DoubleArray> Median best-so-far of d after each evaluation, with its quartiles: [q25, median, q75] rows.
fun medianRun(d: VehicleSearchDriver): SearchRun<DoubleArray> The run of d whose best fitness is the median over seeds (the lower middle for an even count).
fun fitnessRange(): ClosedFloatingPointRange<Double> The fitness range plot uses when none is given: every arm's quartile band.
fun plot(width: Int = 300, height: Int = 300, yRange: ClosedFloatingPointRange<Double> = fitnessRange()): Img Best-so-far fitness against evaluations: each driver's median as a line, its interquartile range as a band, and its final median written at the right. Random is grey, hill climbing blue, evolution orange. Pass the same yRange to two plots to put them on one scale.
override fun toString(): String table.

alife_walls.kt

Walls for alife_script.kt: obstacles, swept circle collision, and radius-aware routing around them.

See alife_script.kt for the scripting layer this completes, and alife.kt for the family overview.

Why this exists

ScriptWorld shipped as a bare rectangle. The only spatial thing an agent could say was BehaviourSteering.avoidEdges, which pushes away from the four sides of the arena; everything else a scripted agent could steer around was another agent. That rules out the whole category of behaviour that is about the environment — a corridor, a room with a door, a barrier you have to go round rather than through — and it rules it out silently, because an agent pressing into a wall that does not exist just flies on.

Three representations

Obstacles are a flat soup of line segments (ScriptObstacle, ObstacleField) and not polygons-as-solids. The only predicate collision needs is "is this swept circle crossing a wall", which a segment answers directly; making obstacles solid would mean a point-in-polygon test, a winding rule, and an answer for what two overlapping solids mean. A soup accepts an open wall, a concave room, a fence with a gap in it and two shapes that cross, all without a special case. The price is stated rather than hidden: there is no "inside", so nothing stops an agent that starts inside a closed shape from staying there — ScriptWorld.spawn pushes it out to the nearest surface instead of teleporting it somewhere arbitrary.

Collision is swept, not discrete (ObstacleField.sweep). A discrete test asks whether the agent's new position overlaps a wall, which is false for any agent whose per-step displacement is larger than the wall is thick — and a wall here is a segment, so it is infinitely thin and a discrete test can miss it at any speed at all. The swept test solves for the first fraction of the step at which the moving circle touches, so a projectile at 600 units/s stops at the wall it would otherwise have crossed in one tick. AlifeWallsGallery's tunnelling figure runs both.

Navigation is a uniform occupancy grid inflated by the agent's radius, searched with A*, then string-pulled against the exact segment test (NavGrid). The obvious alternative is a visibility graph, which is exact and gives shorter paths from a much smaller search. It was rejected on construction, not on search: a visibility graph over a segment soup needs its corners offset outward by the agent radius, and "the offset corner" is not defined where two segments cross, nearly touch, or are collinear — which is precisely the geometry that appears when a user draws a wall twice or butts two rectangles together. A grid has no corners to reason about: it is built by one distance query per cell per segment, radius-awareness is a single inflation number, and the one thing it gets wrong — paths that staircase along cell centres — is removed afterwards by NavGrid.stringPull, which uses the same exact swept test the physics does. A coarse grid can still over-block a narrow gap; NavGrid.coarse says when that happened rather than leaving the caller to wonder why there was no route.

Determinism

The same property the rest of alife_script.kt depends on, and the same discipline. Segments are held in insertion order in flat DoubleArrays. A's open set is ordered by (f, cellIndex), a total* order, so two cells with equal estimated cost expand in a fixed order rather than in whichever order a heap happened to leave them. Neighbours are visited through one fixed eight-element offset table. The nav-grid cache is a LinkedHashMap keyed by the quantised inflation radius. The per-step route budget is consumed in agent-id order, because drives are evaluated in agent-id order. Nothing here reads a clock or a hash set.

src/jvmMain/kotlin/afield/simulation/agents/alife_walls.kt — read the source

Used in: Spatial artificial life

Top-level functions

Declaration Summary
fun closestPointOnSegment(px: Double, py: Double, ax: Double, ay: Double, bx: Double, by: Double, out: DoubleArray) The closest point of the segment a–b to (px, py), written into out as x, y.
fun segmentDistance(px: Double, py: Double, qx: Double, qy: Double, rx: Double, ry: Double, sx: Double, sy: Double): Double The shortest distance between the two closed segments p–q and r–s. Zero if they cross.
fun segmentsCross(px: Double, py: Double, qx: Double, qy: Double, rx: Double, ry: Double, sx: Double, sy: Double): Boolean Whether the two closed segments p–q and r–s cross, including touching at a point.
fun sweepSegment(px: Double, py: Double, dx: Double, dy: Double, r: Double, ax: Double, ay: Double, bx: Double, by: Double, out: DoubleArray): Boolean The earliest touch of a circle of r centred at (px, py) moving by (dx, dy) against the one segment a–b, written into out as t, normalX, normalY. Returns false when it misses.
fun pointSourceField(vararg sources: Double, falloff: Double = 20.0): ScalarField A field that is the sum of inverse-square-ish point sources — the standard "some lamps in a room".

Top-level values

Declaration Summary
const val WALL_SKIN: Double How far outside a wall a contact is resolved to.
const val PUSH_OUT_PASSES: Int How many times ObstacleField.pushOut will try to find non-overlapping ground.
const val PROGRESS_EPSILON: Double How much closer to its waypoint BehaviourSteering.route has to get to count as having moved.
const val WALL_RESPONSE_ITERATIONS: Int How many wall responses one integration step may resolve.

WallResponse

enum class WallResponse

What an agent does when its swept path meets a wall.

Constants: SLIDE, BOUNCE, STOP.

ScriptObstacle

class ScriptObstacle(val name: String, points: DoubleArray, val closed: Boolean)

A wall: a run of points, open (a polyline) or closed (a polygon outline).

Declaration Summary
val points: DoubleArray x0, y0, x1, y1, …. A defensive copy; mutating what you passed in cannot move a wall.
val vertexCount: Int How many points the outline has.
val segmentCount: Int How many segments it contributes: one fewer than the vertices, or the same if closed.
fun outline(z: Number = 0): FLine The outline as an FLine, for drawing the world the agents are actually in.
override fun toString(): String

WallHit

class WallHit internal constructor(val t: Double, val nx: Double, val ny: Double, val obstacle: ScriptObstacle, val segment: Int)

One swept contact: how far along the displacement it happened, and which way the wall faces there.

Declaration Summary
val t: Double
val nx: Double
val ny: Double
val obstacle: ScriptObstacle
val segment: Int
override fun toString(): String

ObstacleField

class ObstacleField

Every wall in a world, indexed as one flat array of segments — the thing the physics and the navigation both ask.

Declaration Summary
val obstacles: List<ScriptObstacle> The walls, in the order they were added.
val segmentCount: Int How many segments all the walls come to.
val isEmpty: Boolean True when there is nothing to collide with — the fast path the integrator takes.
fun add(o: ScriptObstacle) Add o. Invalidates the packed index and any NavGrid built from this field.
fun segments(): DoubleArray The packed segments, ax, ay, bx, by per segment, in insertion order.
fun ownerOf(i: Int): ScriptObstacle The obstacle that owns segment i of segments.
fun sweep(px: Double, py: Double, dx: Double, dy: Double, radius: Double): WallHit? The first wall a circle of radius at (px, py) touches while moving by (dx, dy), or null if it touches none.
fun nearestInto(px: Double, py: Double, range: Double, out: DoubleArray): Boolean The nearest wall point to (px, py) within range, written into out as distance, wallX, wallY, normalX, normalY — the normal pointing from the wall toward the query. Returns false and leaves out alone when nothing is in range.
fun distanceTo(px: Double, py: Double): Double The distance from (px, py) to the nearest wall, or Double.MAX_VALUE if there are none.
fun clearPath(x0: Double, y0: Double, x1: Double, y1: Double, radius: Double): Boolean Whether a circle of radius could stand anywhere along the straight line from (x0, y0) to (x1, y1) — the test NavGrid.stringPull shortens a grid path with, and the one BehaviourSteering.route decides it has passed a waypoint on.
fun lineOfSight(x0: Double, y0: Double, x1: Double, y1: Double): Boolean Whether the open segment from (x0, y0) to (x1, y1) crosses any wall — line of sight for a point, with no radius.
fun pushOut(px: Double, py: Double, radius: Double, out: DoubleArray): Boolean Move (px, py) to the nearest position where a circle of radius is not overlapping a wall, writing x, y into out. Returns true if it had to move.
override fun toString(): String

NavGrid

class NavGrid internal constructor(val width: Double, val height: Double, val cols: Int, val rows: Int, val cellSize: Double, val inflate: Double, val coarse: Boolean, private val blocked: BooleanArray)

A radius-aware occupancy grid over a world's walls, and the A* that searches it.

Declaration Summary
val width: Double
val height: Double
val cols: Int
val rows: Int
val cellSize: Double
val inflate: Double
val coarse: Boolean
val blockedCells: Int How many of the grid's cells a circle of inflate cannot stand in.
val cellCount: Int Total cells.
fun isBlocked(c: Int, r: Int): Boolean Whether the cell at column c, row r is unusable (out of range counts as blocked).
fun colOf(x: Double): Int The column containing world x x, clamped into the grid.
fun rowOf(y: Double): Int The row containing world y y, clamped into the grid.
fun centreX(c: Int): Double The world x of the centre of column c.
fun centreY(r: Int): Double The world y of the centre of row r.
fun nearestFree(x: Double, y: Double, searchCells: Int = 24): Int The nearest free cell to (x, y) within searchCells rings, as a packed row * cols + col, or -1.
fun route(sx: Double, sy: Double, gx: Double, gy: Double, maxExpansions: Int = 20_000): IntArray? A* from (sx, sy) to (gx, gy), returning the cell indices of the path including both ends, or null if there is no route inside maxExpansions.
fun stringPull(path: IntArray, field: ObstacleField, startX: Double, startY: Double, goalX: Double, goalY: Double): DoubleArray Turn a cell path into a short list of world waypoints, dropping every one a circle of inflate could have skipped.
fun blockedFLines(z: Number = 0): List<FLine> The blocked cells as one FLine per cell outline — for looking at what the router saw.
override fun toString(): String

ScalarField

fun interface ScalarField

A scalar quantity spread over the arena — light, a chemical, temperature — that an agent can sense and climb with BehaviourSteering.gradient.

Declaration Summary
fun at(x: Double, y: Double): Double The value at (x, y).

alife_ca.kt

Discrete cellular automata: life-like and outer-totalistic rules on a 2-D lattice, cyclic multi-state CA, Wolfram's elementary 1-D rules as a space-time raster, and a 3-D life-like CA that marches out to a TriMesh.

See alife.kt for the family overview and for IntGrid, the lattice everything here steps.

The design commitment worth stating: a rule is a value (LifeRule), separate from the grid it is applied to. That is what lets the gallery run the same glider under B3/S23 and under a deliberately-wrong neighbour rule and show that only one of them translates — and what lets alife_continuous.kt's Lenia stepper be checked against this file's discrete stepper in the limit where the two must agree.

src/jvmMain/kotlin/afield/simulation/automata/alife_ca.kt — read the source

Used in: Spatial artificial life, Three-dimensional Life, counted and traced

Top-level functions

Declaration Summary
fun lifeRule(notation: String, radius: Int = 1, vonNeumann: Boolean = false): LifeRule Parse a life-like rule from the standard B3/S23 notation (or the equivalent 23/3 "survive/birth" form, or S23/B3), returning a LifeRule.
fun IntGrid.step(rule: LifeRule): IntGrid One synchronous generation of rule over this grid, returned as a new IntGrid. The border is resolved by the grid's own IntGrid.wrap flag: a torus, or a bounded board whose outside is dead.
fun IntGrid.step(rule: CyclicRule): IntGrid One synchronous generation of a CyclicRule.
fun IntGrid.stepTimes(rule: LifeRule, generations: Int): IntGrid step applied generations times, returning only the final grid.
fun IntGrid.stepTimes(rule: CyclicRule, generations: Int): IntGrid step applied generations times under a CyclicRule.
fun IntGrid.history(rule: LifeRule, generations: Int): List<IntGrid> Every generation from this one to generations, inclusive of both ends — generations + 1 grids. Keeping the history is what lets a caller detect a cycle, or render a filmstrip, without stepping twice.
fun IntGrid.runUntilCycle(rule: LifeRule, maxGenerations: Int = 2000): CycleReport Step until the grid repeats a state it has already been in, or until maxGenerations.
fun IntGrid.placedOn(width: Int, height: Int, x: Int, y: Int, wrap: Boolean = true): IntGrid Place pattern with its top-left corner at (x, y) on a fresh width x height grid — the standard way to give a small named pattern room to run.
fun IntGrid.centeredOn(width: Int, height: Int, wrap: Boolean = true): IntGrid placedOn with the pattern centred.
fun cyclicAdvanceFraction(before: IntGrid, after: IntGrid): Double The fraction of cells that changed state between before and after — for a CyclicRule, the fraction that advanced, since the only legal change is s -> s + 1.
fun elementaryStep(row: IntArray, rule: Int, wrap: Boolean = true): IntArray One generation of Wolfram's elementary rule rule (0..255) applied to a 1-D row of 0/1 cells.
fun elementaryCa(rule: Int, width: Int = 201, generations: Int = 100, initial: IntArray? = null, wrap: Boolean = true): IntGrid Run elementary rule rule for generations steps and return the whole space-time diagram as an IntGrid: one row per generation, time increasing downward. grid.toImg() is then the classic picture, and grid.toImg().upscaleNearest(k) is the classic picture at a size you can see.
fun binomialParity(n: Int, k: Int): Int C(n, k) mod 2 — the parity of a binomial coefficient, by Kummer's theorem: it is 1 exactly when k's binary digits are a subset of n's, i.e. n and k == k.
fun sierpinskiReference(width: Int, generations: Int): IntGrid The exact space-time diagram of rule 90 from a single central seed, computed from binomialParity rather than by stepping — the reference the CA must reproduce cell for cell.
fun IntGrid.centerColumn(): IntArray The centre column of an elementary rule's space-time diagram, as a bit sequence.
fun longestRunLength(bits: IntArray): Int The longest run of identical values in bits — a crude but real structure detector for a bit sequence.
fun lifeRule3D(notation: String): LifeRule3D Parse a 3-D rule from B…/S… where the counts are comma-separated (they run to 26, so the digit-per-count convention of 2-D notation cannot work).
fun IntVolume.step(rule: LifeRule3D): IntVolume One synchronous generation of a LifeRule3D.
fun IntVolume.stepTimes(rule: LifeRule3D, generations: Int): IntVolume step applied generations times under a LifeRule3D.
fun IntVolume.asDensityField(smoothRadius: Int = 1, iso: Double = 0.5): ScalarField3D Adapt this volume to a ScalarField3D so mesh_sdf.kt's marchingCubes can extract its surface — the bridge from "a 3-D automaton ran" to "here is a mesh you can export, boolean and render".
fun IntVolume.marchBounds(): AxisAlignedBoundingBox3D The march window for asDensityField: the volume's own voxel coordinates, grown by one voxel on every side so the surface of a blob touching the volume edge still closes.
fun IntVolume.toMesh(resolution: Int = 96, smoothRadius: Int = 1, iso: Double = 0.5): TriMesh Surface mesh of this volume's filled voxels, via asDensityField and marchingCubes. Watertight by construction (the marcher shares vertices across cubes), which the gallery checks with TriMesh.isWatertight rather than assuming.
fun IntVolume.slice(z: Int): IntGrid A z-slice of this volume as an IntGrid — the cheap way to look at a 3-D automaton without a renderer, and how the gallery shows a volume beside the mesh marched from it.
fun IntVolume.depthProjection(): Img The volume's occupancy summed along z and normalised to 0..1 — a "density projection", which shows the internal structure a surface mesh hides.

Top-level values

Declaration Summary
val LIFE: LifeRule Conway's Life, B3/S23 — the rule every claim in this family's discrete half is checked against.
val HIGH_LIFE: LifeRule HighLife, B36/S23: Life plus birth on 6, which gives it a small self-replicating pattern.
val SEEDS: LifeRule Seeds, B2/S: nothing survives, so every generation is entirely new. Explosive and never static.
val DAY_AND_NIGHT: LifeRule Day & Night, B3678/S34678: self-complementary, so patterns and their photographic negatives behave alike.
val DIAMOEBA: LifeRule Diamoeba, B35678/S5678: large amoeba-like blobs with ragged, constantly-renegotiated borders.
val MAZE: LifeRule "Maze", B3/S12345: Life-like birth with very tolerant survival, which grows corridor mazes.
val CYCLIC_LABYRINTH: CyclicRule 14 states, threshold 1, Moore radius 1 — Griffeath's original cyclic CA parameters.
val CYCLIC_EXCITABLE: CyclicRule Greenberg–Hastings-like excitable medium: 6 states, threshold 3, radius 2 — fatter, slower fronts.
val LIFE3D_CLOUDS: LifeRule3D "Clouds 1", B13,14,17,18,19/S13,…,26 — a 3-D rule that grows soft, rounded, cloud-like solids.
val LIFE3D_CRYSTAL: LifeRule3D B4/S4 — a sparse 3-D rule whose survivors form thin, crystalline, coral-like shells.
val LIFE3D_BLOB: LifeRule3D B5/S4,5 — the 3-D rule that gives compact blobs with a corrugated surface.

LifeRule

class LifeRule(val birth: Set<Int>, val survive: Set<Int>, val radius: Int = 1, val vonNeumann: Boolean = false, val notation: String = "B${birth.sorted().joinToString("")}/S${survive.sorted().joinToString("")}")

An outer-totalistic two-state rule: a cell's next state depends only on its own state and on how many of its neighbours are alive, not on which.

Declaration Summary
val birth: Set<Int> Neighbour counts at which a dead cell becomes alive.
val survive: Set<Int> Neighbour counts at which a live cell stays alive.
val radius: Int Chebyshev (or, with vonNeumann, Manhattan) neighbourhood radius; 1 is the classic 8-cell Moore neighbourhood, larger radii give the "Larger than Life" family.
val vonNeumann: Boolean true counts only the diamond \|dx\| + \|dy\| <= radius instead of the square.
val notation: String The B…/S… string this rule was parsed from, or a reconstruction of it.
val neighborhoodSize: Int How many cells the neighbourhood contains, excluding the centre.
fun next(state: Int, neighbors: Int): Int The next state of a cell currently in state with neighbors live neighbours.
override fun toString(): String

CycleReport

class CycleReport(val period: Int, val firstSeenAt: Int, val generations: Int, val finalState: IntGrid, val population: IntArray)

Result of running an automaton until it repeats.

Declaration Summary
val period: Int The cycle length, or 0 if none was found within the step budget.
val firstSeenAt: Int The generation the repeated state was first reached (so period == 1 and firstSeenAt == 0 means the seed was already a still life).
val generations: Int How many generations were actually stepped.
val finalState: IntGrid The grid at generations.
val population: IntArray The population at each generation, 0..generations.
val settled: Boolean true when a repeat was found within the budget.
override fun toString(): String

LifePatterns

object LifePatterns

Named Life patterns, written as ASCII so they can be read rather than decoded.

Declaration Summary
val glider: IntGrid The glider: the smallest spaceship, translating (1, 1) every 4 generations.
val block: IntGrid The block: the smallest still life; a step must leave it bit-identical.
val beehive: IntGrid The beehive, a six-cell still life — a second still life, so the block is not a lucky special case.
val blinker: IntGrid The blinker: period 2, so it is the control that a "still life" check must fail on.
val toad: IntGrid The toad: period 2, and unlike the blinker it is not a subset of a line.
val rPentomino: IntGrid The R-pentomino: the classic long-lived methuselah — 1103 generations to a population of 116.
val acorn: IntGrid The acorn: a 7-cell methuselah, 5206 generations to a population of 633.
val lightweightSpaceship: IntGrid The lightweight spaceship: translates (2, 0) every 4 generations — orthogonal, not diagonal.

CyclicRule

class CyclicRule(val states: Int, val threshold: Int = 1, val radius: Int = 1, val vonNeumann: Boolean = false)

A cyclic cellular automaton rule: a cell in state s advances to (s + 1) mod states as soon as at least threshold of its neighbours are already in state (s + 1) mod states; otherwise it waits.

Declaration Summary
val states: Int Cycle length, >= 2.
val threshold: Int How many neighbours in the successor state are needed to advance.
val radius: Int Neighbourhood radius.
val vonNeumann: Boolean Use the diamond neighbourhood rather than the square.
override fun toString(): String

IntVolume

class IntVolume(val width: Int, val height: Int, val depth: Int, val cells: IntArray, val wrap: Boolean = false)

A cubic lattice of integer cell states — the 3-D counterpart of IntGrid, stored cells[(z * height + y) * width + x].

Declaration Summary
val width: Int Cells across, >= 1.
val height: Int Cells down, >= 1.
val depth: Int Cells deep, >= 1.
val cells: IntArray State per voxel.
val wrap: Boolean true for a 3-torus; otherwise outside counts as state 0.
val size: Int Number of voxels.
fun index(x: Int, y: Int, z: Int): Int Flat index of (x, y, z). No bounds checking; see get.
operator fun get(x: Int, y: Int, z: Int): Int State at (x, y, z), resolving out-of-range coordinates through wrap (otherwise 0).
operator fun set(x: Int, y: Int, z: Int, v: Int) Write v at (x, y, z) if it is inside; out-of-range writes on a bounded volume are ignored.
fun blank(): IntVolume A same-shape volume of zeros.
fun population(): Int How many voxels are non-zero.
fun mooreCount(x: Int, y: Int, z: Int): Int How many of the 26 surrounding voxels are non-zero.
override fun toString(): String

LifeRule3D

class LifeRule3D(val birth: Set<Int>, val survive: Set<Int>)

A 3-D outer-totalistic rule over the 26-voxel Moore neighbourhood — the direct generalisation of LifeRule. Notation is the same B…/S…, with counts up to 26, so B5/S45 is written LifeRule3D(setOf(5), setOf(4, 5)).

Declaration Summary
val birth: Set<Int> Neighbour counts at which an empty voxel fills.
val survive: Set<Int> Neighbour counts at which a filled voxel stays filled.
val notation: String The B…/S… string for this rule.
override fun toString(): String