Skip to content

Creatures.kt

src/jvmMain/kotlin/afield/simulation/creatures/Creatures.kt · 3248 lines

@file:Suppress("MemberVisibilityCanBePrivate")

Evolving 2D creatures: a genetic algorithm over bodies of points and springs whose every point is a foot with a cyclic contact program, and whose joints are angle motors following cyclic target curves.

CreatureEvolution.evolve (or CreatureEvolution.evolveFlow) breeds a population under an EvolutionConfig; CreatureEvolution.simulate scores one CreatureGenome; a CreatureEvaluation plays back as FLines (CreatureEvaluation.frameLines) with a per-foot score (CreatureEvaluation.notationLines).

The world is not in here. A ContactModel says what a foot receives from its surroundings, and CreatureSimulationConfig.fitness says what counts as doing well, so a water strider, a crawler on a floor and a swimmer differ only in box code. The evolving_water_bugs starting point writes two of those contact models.

One thing to know before writing a CreatureSimulationConfig.fitness: during selection it is handed an evaluation with no frames. Evolution scores its whole population with recording off and re-simulates only each generation's best under the configured recording, so a score read out of CreatureEvaluation.frames is zero for every creature that is actually being chosen between. Score from the measured totals instead — CreatureEvaluation.sustainedTravel, CreatureEvaluation.displacement and the means beside them.

package afield.simulation.creatures

import afield.simulation.physics.SimdVerlet
import afield.runtime.lifecycle.generator2
import afield.geometry.path.FLine
import afield.graphics.render.color
import afield.graphics.render.pointSize
import afield.graphics.render.pointed
import afield.graphics.render.thickness
import afield.math.linalg.vec
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import kotlin.math.*
import kotlin.random.Random

private const val MIN_REST_EDGE_LENGTH = 1e-4f
private const val JOINT_TARGET_KNOT_COUNT = 4
private const val MAX_JOINT_TARGET_SIGNAL = 0.9f
private const val MIN_SPRING_STIFFNESS = 3.5f
private const val DEFAULT_SPRING_STIFFNESS = 6.0f
private const val MAX_SPRING_STIFFNESS = 12.0f
private const val DEFAULT_MAX_ACTUATION_BUDGET_PER_CYCLE = 4.5f
private const val DEFAULT_ACTUATION_EFFORT_PENALTY = 0.05f
private const val DEFAULT_JOINT_TARGET_ANGLE_SCALE = 1.0f
private const val DEFAULT_JOINT_MOTOR_KP = 1.0f
private const val DEFAULT_JOINT_MOTOR_KD = 0.15f
private const val DEFAULT_JOINT_TARGET_FILTER_RATE = 8.5f
private const val DEFAULT_MAX_JOINT_IMPULSE_PER_STEP = 0.02f
private const val DEFAULT_TARGET_SLEW_PENALTY = 0.04f
private const val DEFAULT_MAX_MEAN_STRETCH_ERROR = 2.0f
private const val DEFAULT_MAX_PEAK_STRETCH_ERROR = 3.0f
private const val DEFAULT_MAX_CENTER_SPEED = 4.0f

data class Float2(val x: Float, val y: Float)

A 2D point or vector of Floats, with the arithmetic the creature model needs. Every operation returns a new value; nothing here mutates.

perpDot is the 2D cross product, positive when the argument turns counter-clockwise from the receiver, which is how edge crossings and lateral drift are measured. normalizedOr returns its fallback rather than a non-finite vector when the length is near zero.

operator fun plus(other: Float2) = Float2(x + other.x, y + other.y)

operator fun minus(other: Float2) = Float2(x - other.x, y - other.y)

operator fun times(scale: Float) = Float2(x * scale, y * scale)

operator fun div(scale: Float) = Float2(x / scale, y / scale)

fun dot(other: Float2): Float = x * other.x + y * other.y

fun perpDot(other: Float2): Float = x * other.y - y * other.x

fun length(): Float = sqrt(x * x + y * y)

fun lengthSquared(): Float = x * x + y * y

fun normalizedOr(fallback: Float2 = Float2(1f, 0f)): Float2

    fun normalizedOr(fallback: Float2 = Float2(1f, 0f)): Float2 {
        val len = length()
        return if (len <= 1e-6f) fallback else this / len
    }

fun perpendicular(): Float2 = Float2(-y, x)

fun lerp(other: Float2, t: Float): Float2 = this * (1f - t) + other * t

data class EdgeGene(val a: Int, val b: Int)

One spring of the body, as the indices of the two points it joins. Its rest length is whatever the distance between those points is in CreatureGenome.points, so the rest shape and the springs are the same piece of genome.

normalized puts the lower index first, which is how CreatureGenome.normalized can recognise the same edge written either way around and drop the duplicate.

fun normalized(): EdgeGene

    fun normalized(): EdgeGene =
        if (a <= b) this else EdgeGene(b, a)
}

data class ContactProgram

One foot's gait, as a window on the body's phase clock: the foot is down over duty of each cycle, centred on phase, pressing with strength. Every point of a creature carries one of these, and CreatureGenome.feet is parallel to CreatureGenome.points.

smooth is the fraction of the window's half-width spent ramping in and out, as a smoothstep, so near 0.02 the foot snaps down and near 0.95 it swells and fades. sample reads the window at a phase and returns a value between zero and strength; a program with no duty or no strength returns zero everywhere, which is how a point that never touches down is written.

data class ContactProgram(
    val phase: Float,
    val duty: Float = 0.35f,
    val smooth: Float = 0.2f,
    val strength: Float = 1f
) {

fun normalized(): ContactProgram

    fun normalized(): ContactProgram = copy(
        phase = wrap01(phase),
        duty = duty.coerceIn(0f, 0.95f),
        smooth = smooth.coerceIn(0.02f, 0.95f),
        strength = strength.coerceIn(0f, 1f)
    )

fun sample(currentPhase: Float): Float

    fun sample(currentPhase: Float): Float {
        val c = normalized()
        if (c.duty <= 1e-4f || c.strength <= 1e-4f) return 0f
        return c.strength * smoothGate(currentPhase, c.phase, c.duty, c.smooth)
    }
}

data class JointProgram

An angle motor at pivot, pulling the angle between its two neighbours a and b toward a target that cycles with the phase clock. A joint is only valid where two actual edges meet at its pivot.

The target curve is four knots read as a wrapped Catmull-Rom spline, so a joint's entire gait is four numbers. A knot is a signal clamped to ±0.9, which the run turns into an angle by scaling it with CreatureSimulationConfig.jointTargetAngleScale and adding it to the joint's rest angle, meaning the angle between its two arms in the genome's rest shape rather than zero. normalized orders a and b and negates the targets when it has to swap them, so the two ways of writing one joint drive the same motion; shifted rotates the knots, moving the joint in phase without changing the shape of its curve.

data class JointProgram(
    val pivot: Int,
    val a: Int,
    val b: Int,
    val targets: List<Float> = List(JOINT_TARGET_KNOT_COUNT) { 0f }
) {

fun normalized(): JointProgram

    fun normalized(): JointProgram {
        val normalizedTargets = normalizedJointTargets(targets)
        return if (a <= b) {
            copy(targets = normalizedTargets)
        } else {
            JointProgram(
                pivot = pivot,
                a = b,
                b = a,
                targets = normalizedTargets.map { -it }
            )
        }
    }

fun sample(currentPhase: Float): Float

    fun sample(currentPhase: Float): Float {
        val c = normalized()
        if (c.targets.all { abs(it) <= 1e-4f }) return 0f
        return cyclicSplineSample(c.targets, currentPhase)
    }

fun shifted(steps: Int): JointProgram

    fun shifted(steps: Int): JointProgram =
        copy(targets = rotateValues(targets, steps))

fun meanAbsSignal(sampleCount: Int = 24): Float

    fun meanAbsSignal(sampleCount: Int = 24): Float {
        if (sampleCount <= 0) return 0f
        var sum = 0f
        repeat(sampleCount) { index ->
            sum += abs(sample(index / sampleCount.toFloat()))
        }
        return sum / sampleCount.toFloat()
    }

internal fun key(): JointKey

    internal fun key(): JointKey {
        val normalized = normalized()
        return JointKey(normalized.pivot, normalized.a, normalized.b)
    }
}

data class CreatureGenome

Everything evolution can change about one creature: its rest shape as points and edges, its gait as feet and joints, and the handful of physical gains below them that decide how heavy it is, how stiff its springs are and how hard its feet can hold.

Two methods police it, and they are not the same check. normalized is total: it orders and de-duplicates the edges and joints, resizes feet to one program per point, and clamps every gain into the range the solver tolerates. validationIssues can still find faults normalized cannot repair, and returns them as sentences meant to be read; CreatureEvolution.simulate refuses a genome for which it returns anything.

Two of those faults constrain what a body can be rather than how it is written, and so matter to anyone assembling a genome by hand. A point may carry at most four edges, since SimdVerlet holds four constraint slots per particle; and no two edges may cross, which keeps a body from being threaded through itself.

data class CreatureGenome(
    val points: List<Float2>,
    val edges: List<EdgeGene>,
    val feet: List<ContactProgram>,
    val joints: List<JointProgram>,
    val clockPeriod: Float = 1f,
    val massPerLength: Float = 1.1f,
    val basePointMass: Float = 0.3f,
    val springStiffness: Float = DEFAULT_SPRING_STIFFNESS,
    val damping: Float = 0.968f,
    val freeDrag: Float = 12f,
    val footDrag: Float = 180f,
    val footStickiness: Float = 640f,
    val jointTorqueScale: Float = 58f
) {

fun normalized(): CreatureGenome

    fun normalized(): CreatureGenome {
        val edgeSet = LinkedHashSet<EdgeGene>()
        edges.map { it.normalized() }
            .filter { it.a != it.b }
            .forEach(edgeSet::add)

        val pointCount = points.size
        val normalizedFeet = MutableList(pointCount) { index ->
            feet.getOrNull(index)?.normalized() ?: ContactProgram(phase = 0f, strength = 0f)
        }

        return copy(
            edges = edgeSet.sortedWith(compareBy<EdgeGene> { it.a }.thenBy { it.b }),
            feet = normalizedFeet,
            joints = joints.map { it.normalized() }
                .distinctBy { it.key() }
                .sortedWith(
                    compareBy<JointProgram> { it.pivot }
                        .thenBy { it.a }
                        .thenBy { it.b }
                ),
            clockPeriod = clockPeriod.coerceIn(0.2f, 4f),
            massPerLength = massPerLength.coerceIn(0.05f, 10f),
            basePointMass = basePointMass.coerceIn(0.05f, 10f),
            springStiffness = springStiffness.coerceIn(MIN_SPRING_STIFFNESS, MAX_SPRING_STIFFNESS),
            damping = damping.coerceIn(0.7f, 0.9995f),
            freeDrag = freeDrag.coerceIn(0f, 400f),
            footDrag = footDrag.coerceIn(0f, 4000f),
            footStickiness = footStickiness.coerceIn(0f, 4000f),
            jointTorqueScale = jointTorqueScale.coerceIn(0f, 220f)
        )
    }

fun validationIssues(): List<String>

body — 61 lines, from line 250
    fun validationIssues(): List<String> {
        val g = normalized()
        val issues = mutableListOf<String>()
        val pointCount = g.points.size

        if (pointCount < 2) issues += "creatures need at least 2 points"
        if (g.edges.isEmpty()) issues += "creatures need at least one edge"
        if (g.feet.size != pointCount) issues += "feet program count must match point count"

        g.points.forEachIndexed { index, point ->
            if (!point.x.isFinite() || !point.y.isFinite()) {
                issues += "point $index has non-finite coordinates"
            }
        }

        val degrees = IntArray(pointCount)
        val edgeSet = HashSet<Long>()
        for (edge in g.edges) {
            if (edge.a !in 0 until pointCount || edge.b !in 0 until pointCount) {
                issues += "edge $edge points outside valid range"
                continue
            }
            if (edge.a == edge.b) issues += "edge $edge is self-connected"
            val key = edgeKey(edge.a, edge.b)
            if (!edgeSet.add(key)) issues += "duplicate edge $edge"
            degrees[edge.a]++
            degrees[edge.b]++

            val length = distance(g.points[edge.a], g.points[edge.b])
            if (length <= MIN_REST_EDGE_LENGTH) issues += "edge $edge has near-zero rest length"
        }

        degrees.forEachIndexed { index, degree ->
            if (degree > 4) issues += "point $index has degree $degree, but SimdVerlet DirectedConstraints4 needs degree <= 4"
        }

        edgeCrossings(g.points, g.edges).forEach { crossing ->
            val first = g.edges[crossing.firstIndex].normalized()
            val second = g.edges[crossing.secondIndex].normalized()
            issues += "edge $first crosses edge $second"
        }

        if (issues.isEmpty() && !isConnected(pointCount, g.edges)) {
            issues += "creature graph must stay connected"
        }

        val adjacency = buildAdjacency(pointCount, g.edges)
        val validJointKeys = candidateJointKeys(adjacency).toHashSet()
        g.joints.forEach { joint ->
            if (joint.pivot !in 0 until pointCount || joint.a !in 0 until pointCount || joint.b !in 0 until pointCount) {
                issues += "joint $joint points outside valid range"
            } else if (joint.a == joint.b || joint.a == joint.pivot || joint.b == joint.pivot) {
                issues += "joint $joint must connect two distinct neighbors around a pivot"
            } else if (joint.key() !in validJointKeys) {
                issues += "joint $joint does not refer to two actual edges meeting at its pivot"
            }
        }

        return issues
    }
}

data class CreatureSimulationConfig

How one creature is run and scored: for how long, in what surroundings, against what measure of doing well, and inside what limits.

The clock is shared between this and the genome. CreatureGenome.clockPeriod sets how long one gait cycle lasts and stepsPerCycle sets how finely it is divided, so the physics step is one divided by the other. A run lasts warmupCycles plus measuredCycles of them, and measurement starts at the end of the warm-up, which keeps a creature's initial settle out of its score.

A run ends early and scores explodedFitness when a coordinate stops being finite or leaves maxAbsCoordinate, or when centre speed, mean stretch or peak stretch pass maxCenterSpeed, maxMeanStretchError or maxPeakStretchError. These are stability limits, not taste: a body that passes one is being torn apart by its own springs, and whatever it scored on the way is an artefact of the solver rather than a gait.

data class CreatureSimulationConfig(
    val warmupCycles: Int = 1,
    val measuredCycles: Int = 5,
    val stepsPerCycle: Int = 120,
    val solverIterations: Int = 16,
    val gravity: Float = 0f,
    val targetDirection: Float2 = Float2(1f, 0f),
    val recordFrames: Boolean = true,
    val recordEvery: Int = 4,
    val maxAbsCoordinate: Float = 200f,
    val fitnessObjective: FitnessObjective = FitnessObjective.DIRECTIONAL_PROGRESS,
    val maxActuationBudgetPerCycle: Float = DEFAULT_MAX_ACTUATION_BUDGET_PER_CYCLE,
    val actuationEffortPenalty: Float = DEFAULT_ACTUATION_EFFORT_PENALTY,
    val jointTargetAngleScale: Float = DEFAULT_JOINT_TARGET_ANGLE_SCALE,
    val jointMotorKp: Float = DEFAULT_JOINT_MOTOR_KP,
    val jointMotorKd: Float = DEFAULT_JOINT_MOTOR_KD,
    val jointTargetFilterRate: Float = DEFAULT_JOINT_TARGET_FILTER_RATE,
    val maxJointImpulsePerStep: Float = DEFAULT_MAX_JOINT_IMPULSE_PER_STEP,
    val targetSlewPenalty: Float = DEFAULT_TARGET_SLEW_PENALTY,
    val maxMeanStretchError: Float = DEFAULT_MAX_MEAN_STRETCH_ERROR,
    val maxPeakStretchError: Float = DEFAULT_MAX_PEAK_STRETCH_ERROR,
    val maxCenterSpeed: Float = DEFAULT_MAX_CENTER_SPEED,
    /** How a foot meets the world: the creature's environment. See [ContactModel]. */
    val contact: ContactModel = ContactModel.Drag,
    /**
     * Replaces [fitnessObjective] when set. Called with the finished evaluation (its `fitness` still holds
     * the built-in objective's score); a creature that exploded scores [explodedFitness] without calling it.
     */
    val fitness: ((CreatureEvaluation) -> Float)? = null,
    /**
     * The score of a creature whose simulation blew up or broke a stability limit (and of a non-finite custom
     * fitness). Keep it below anything a living creature can score under your [fitness].
     */
    val explodedFitness: Float = -1000f
)

data class FootStep

What one foot is doing on one physics step: its program's contact strength in 0..1 for the current phase, where it is, how fast it moves, and the genome (for evolved gains such as CreatureGenome.footStickiness, CreatureGenome.footDrag and CreatureGenome.freeDrag).

data class FootStep(
    val contact: Float,
    val position: Float2,
    val velocity: Float2,
    val genome: CreatureGenome
)

interface ContactModel

The environment a creature moves in, as the force each foot receives. Every point of the body is a foot (most of them evolve programs that never touch down), and each gets its own Foot from foot, so a foot can remember where it planted.

The evolution itself knows nothing about water, ground or air: a water strider, a crawler on a floor, or something swimming in syrup differ only in this interface and in CreatureSimulationConfig.gravity.

fun foot(index: Int): Foot

fun place(points: List<Float2>): List<Float2> = points

Where the body starts, given the genome's centred rest shape. A floor model lifts it onto the floor.

interface Foot

    interface Foot {

fun force(step: FootStep): Float2

The force (acceleration) this foot receives this step.

val grip: Float

How firmly the foot is gripping, 0..1; above 0.35 playback draws it as down.

val anchor: Float2? get() = null

The point it is held to, drawn as a cross in playback, or null.

object Drag : ContactModel

A viscous medium with nothing to push against except CreatureGenome.freeDrag: no foot ever grips.

override fun foot(index: Int)

        override fun foot(index: Int) = object : Foot {
            override val grip = 0f
            override fun force(step: FootStep) = step.velocity * -step.genome.freeDrag
        }
    }
}

enum class FitnessObjective

Which built-in measure CreatureSimulationConfig.fitnessObjective scores with, when CreatureSimulationConfig.fitness has not replaced it.

DIRECTIONAL_PROGRESS rewards distance along CreatureSimulationConfig.targetDirection and charges for drift across it, so it asks a creature to go somewhere in particular. NET_CENTROID_DISPLACEMENT rewards distance from where measurement began in any direction, which suits a creature with no heading to keep. Both then subtract the body's mean stretch error and the actuation and slew penalties, so neither will pay for a gait that works by shaking itself apart.

enum class FitnessObjective {
    DIRECTIONAL_PROGRESS,
    NET_CENTROID_DISPLACEMENT
}

data class CreatureSnapshot

One recorded physics step: where every point was, which feet were holding, and what each joint was doing. A run keeps one of these every CreatureSimulationConfig.recordEvery steps, and they are what CreatureEvaluation's playback draws.

down is a foot whose grip, as its ContactModel reported it that step, passed 0.35, the same threshold the playback lines use to draw it as planted. anchors holds a position for each foot that is held to a point and null for the rest, so a contact model that only ever pushes fills it with nulls.

data class CreatureSnapshot(
    val step: Int,
    val phase: Float,
    val center: Float2,
    val points: List<Float2>,
    val down: List<Boolean>,
    val anchors: List<Float2?>,
    val jointTorques: List<Float>,
    val jointTargets: List<Float>
)

data class CreatureEvaluation

What one run of one creature came to: the score it earned, how far it actually travelled, what the gait cost it, and the frames of it that were recorded.

Every measurement is reported beside the score rather than folded into it, so a custom CreatureSimulationConfig.fitness can weigh them differently without re-running anything. forwardTravel and lateralDrift are the components of displacement along and across the target direction; forwardProgress adds up only the forward part of each step, so it stays high for a creature that goes back and forth; and meanStretchError, meanActuationEffort and meanTargetSlew are what the built-in objectives charge for.

frameLines draws one frame flat, frameLines3d draws it as a strider on a water surface, and notationLines draws the gait itself as a score with one row per foot. playback and its siblings are those as endless sequences, one frame at a time. All of them return an empty list when frames is empty, which is how an evaluation produced during selection arrives.

data class CreatureEvaluation(
    val genome: CreatureGenome,
    val fitness: Float,
    val netCentroidMotion: Float,
    /** Where the centroid ended up relative to where measurement began. */
    val displacement: Float2,
    val forwardTravel: Float,
    val forwardProgress: Float,
    val lateralDrift: Float,
    val meanStretchError: Float,
    val meanActuationEffort: Float,
    val programmedActuationBudgetPerCycle: Float,
    val meanTargetSlew: Float,
    val meanSpeed: Float,
    val frames: List<CreatureSnapshot>,
    val exploded: Boolean,
    /** How far the centroid moved in the first half of measurement, and in the second. */
    val firstHalfDisplacement: Float2 = Float2(0f, 0f),
    val secondHalfDisplacement: Float2 = Float2(0f, 0f)
) {

fun sustainedTravel(direction: Float2 = Float2(1f, 0f)): Float

The smaller of the two half-run distances along direction, so a creature that topples once and then lies still scores about zero, and only one that keeps going scores well.

    fun sustainedTravel(direction: Float2 = Float2(1f, 0f)): Float {
        val d = direction.normalizedOr()
        return min(firstHalfDisplacement.dot(d), secondHalfDisplacement.dot(d))
    }

fun summary(): String

    fun summary(): String =
        "fitness=${"%.3f".format(fitness)} net=${"%.3f".format(netCentroidMotion)} forward=${"%.3f".format(forwardTravel)} " +
            "progress=${"%.3f".format(forwardProgress)} lateral=${"%.3f".format(lateralDrift)} " +
            "stretch=${"%.3f".format(meanStretchError)} effort=${"%.3f".format(meanActuationEffort)} " +
            "budget=${"%.3f".format(programmedActuationBudgetPerCycle)} slew=${"%.3f".format(meanTargetSlew)} " +
            "speed=${"%.3f".format(meanSpeed)} " +
            "frames=${frames.size} exploded=$exploded"

fun frameLines3d

Frame index as 3D FLines: x and y are the surface, z is height. Show them with three { lines(...); camera(upAxis = ThreeUpAxis.Z) } so the viewer's grid is the water.

A planted foot sits on the surface (height 0) and every other point is lift above it. Each edge arcs upward by arc times its length, so legs bow like a strider's. On the surface: the body's shadow, a drop line from every point, and a ring where each planted foot is holding.

body — 79 lines, from line 505
    fun frameLines3d(
        index: Int,
        lift: Float = 0.2f,
        arc: Float = 0.06f,
        alignNetMotionToPositiveX: Boolean = true
    ): List<FLine> {
        if (frames.isEmpty()) return emptyList()
        val frame = frames[index.coerceIn(0, frames.lastIndex)]
        val alignment = motionAlignmentTransform(alignNetMotionToPositiveX)
        val ground = frame.points.map(alignment::transform)
        val height = frame.down.map { if (it) 0f else lift }

        fun project(p: Float2, z: Float) = vec(p.x, p.y, z)

        val shadow = FLine().apply {
            color = vec(0.35, 0.5, 0.6, 0.35)
            thickness = 1.2
        }
        val drops = FLine().apply {
            color = vec(0.6, 0.7, 0.8, 0.3)
            thickness = 0.6
        }
        val rings = FLine().apply {
            color = vec(1.0, 0.55, 0.25, 0.8)
            thickness = 1.2
        }
        val legs = FLine().apply {
            color = vec(0.72, 0.87, 0.88, 0.95)
            thickness = 2.0
        }
        for (edge in genome.edges) {
            val a = ground[edge.a]
            val b = ground[edge.b]
            shadow.moveTo(project(a, 0f))
            shadow.lineTo(project(b, 0f))

            // A cubic whose controls sit a third of the way along, raised so the curve peaks arc * length above
            // the straight line between its ends (a cubic reaches 3/4 of its control offset).
            val za = height[edge.a]
            val zb = height[edge.b]
            val rise = arc * distance(a, b) * 4f / 3f
            legs.moveTo(project(a, za))
            legs.cubicTo(
                project(a.lerp(b, 1f / 3f), (za + (zb - za) / 3f) + rise),
                project(a.lerp(b, 2f / 3f), (za + (zb - za) * 2f / 3f) + rise),
                project(b, zb)
            )
        }
        ground.forEachIndexed { i, p ->
            drops.moveTo(project(p, 0f))
            drops.lineTo(project(p, height[i]))
        }
        frame.anchors.forEach { anchor ->
            anchor ?: return@forEach
            val at = alignment.transform(anchor)
            val r = 0.12f
            for (k in 0..16) {
                val t = k / 16f * 2f * PI.toFloat()
                val p = project(Float2(at.x + r * cos(t), at.y + r * sin(t)), 0f)
                if (k == 0) rings.moveTo(p) else rings.lineTo(p)
            }
        }

        val upPoints = FLine().apply {
            pointed = true
            pointSize = 1.4
            color = vec(0.31, 0.61, 0.93, 1.0)
        }
        val downPoints = FLine().apply {
            pointed = true
            pointSize = 2.2
            color = vec(0.98, 0.69, 0.22, 1.0)
        }
        ground.forEachIndexed { i, p ->
            (if (frame.down[i]) downPoints else upPoints).moveTo(project(p, height[i]))
        }

        return listOf(shadow, drops, rings, legs, upPoints, downPoints)
    }

fun frameLines

body — 68 lines, from line 585
    fun frameLines(
        index: Int,
        trailLength: Int = 40,
        alignNetMotionToPositiveX: Boolean = true
    ): List<FLine> {
        if (frames.isEmpty()) return emptyList()
        val clampedIndex = index.coerceIn(0, frames.lastIndex)
        val frame = frames[clampedIndex]
        val alignment = motionAlignmentTransform(alignNetMotionToPositiveX)
        val points = frame.points.map { alignment.transform(it) }
        val anchors = frame.anchors.map { anchor -> anchor?.let(alignment::transform) }

        val edgeLine = FLine().apply {
            color = vec(0.72, 0.87, 0.88, 0.9)
            thickness = 2.25
        }
        for (edge in genome.edges) {
            val a = points[edge.a]
            val b = points[edge.b]
            edgeLine.moveTo(vec(a.x, a.y, 0))
            edgeLine.lineTo(vec(b.x, b.y, 0))
        }

        // Two point clouds rather than per-node colours: feet that are down, and everything else.
        // Sizes suit showPlane, where a dot's radius is 3 * pointSize pixels.
        val upPoints = FLine().apply {
            pointed = true
            pointSize = 1.4
            color = vec(0.31, 0.61, 0.93, 1.0)
        }
        val downPoints = FLine().apply {
            pointed = true
            pointSize = 2.2
            color = vec(0.98, 0.69, 0.22, 1.0)
        }
        points.forEachIndexed { pointIndex, point ->
            val cloud = if (frame.down[pointIndex]) downPoints else upPoints
            cloud.moveTo(vec(point.x, point.y, 0))
        }

        val anchorMarks = FLine().apply {
            color = vec(1.0, 0.42, 0.18, 0.75)
            thickness = 1.5
        }
        anchors.forEach { anchor ->
            anchor ?: return@forEach
            val r = 0.08f
            anchorMarks.moveTo(vec(anchor.x - r, anchor.y - r, 0))
            anchorMarks.lineTo(vec(anchor.x + r, anchor.y + r, 0))
            anchorMarks.moveTo(vec(anchor.x - r, anchor.y + r, 0))
            anchorMarks.lineTo(vec(anchor.x + r, anchor.y - r, 0))
        }

        val trail = FLine().apply {
            color = vec(0.93, 0.93, 0.55, 0.85)
            thickness = 1.6
        }
        val start = max(0, clampedIndex - trailLength + 1)
        val centers = frames.subList(start, clampedIndex + 1).map { alignment.transform(it.center) }
        if (centers.isNotEmpty()) {
            trail.moveTo(vec(centers.first().x, centers.first().y, 0))
            for (i in 1 until centers.size) {
                trail.lineTo(vec(centers[i].x, centers[i].y, 0))
            }
        }

        return listOf(edgeLine, upPoints, downPoints, anchorMarks, trail)
    }

fun playback

    fun playback(
        loop: Boolean = true,
        trailLength: Int = 40,
        alignNetMotionToPositiveX: Boolean = true
    ): Sequence<List<FLine>> = sequence {
        if (frames.isEmpty()) return@sequence
        do {
            for (i in frames.indices) {
                yield(frameLines(i, trailLength, alignNetMotionToPositiveX))
            }
        } while (loop)
    }

fun notationLines

body — 93 lines, from line 667
    fun notationLines(
        index: Int,
        scoreWidth: Float = 6f,
        rowHeight: Float = 0.55f,
        origin: Float2? = null
    ): List<FLine> {
        if (frames.isEmpty()) return emptyList()
        val clampedIndex = index.coerceIn(0, frames.lastIndex)
        val frame = frames[clampedIndex]
        val resolvedOrigin = resolveNotationOrigin(scoreWidth, rowHeight, origin)
        val left = resolvedOrigin.x
        val right = resolvedOrigin.x + scoreWidth
        val phaseX = left + wrap01(frame.phase) * scoreWidth

        fun rowY(pointIndex: Int): Float = resolvedOrigin.y - pointIndex * rowHeight

        val baseline = FLine().apply {
            color = vec(0.42, 0.46, 0.54, 0.55)
            thickness = 0.8
        }
        val footDown = FLine().apply {
            color = vec(0.98, 0.69, 0.22, 0.9)
            thickness = 3.2
        }
        val footTransitions = FLine().apply {
            color = vec(0.95, 0.95, 0.95, 0.75)
            thickness = 1.1
        }
        val pivotMarkers = FLine().apply {
            pointed = true
            pointSize = 1.2
        }

        genome.points.indices.forEach { pointIndex ->
            val y = rowY(pointIndex)
            baseline.moveTo(vec(left, y, 0))
            baseline.lineTo(vec(right, y, 0))

            notationSegments(genome.feet[pointIndex]).forEach { segment ->
                val x0 = left + segment.start * scoreWidth
                val x1 = left + segment.end * scoreWidth
                footDown.moveTo(vec(x0, y, 0))
                footDown.lineTo(vec(x1, y, 0))

                val tick = rowHeight * 0.28f
                footTransitions.moveTo(vec(x0, y - tick, 0))
                footTransitions.lineTo(vec(x0, y + tick, 0))
                footTransitions.moveTo(vec(x1, y - tick, 0))
                footTransitions.lineTo(vec(x1, y + tick, 0))
            }
        }

        val torqueCurves = mutableListOf<FLine>()
        genome.joints.forEachIndexed { jointIndex, joint ->
            val torque = frame.jointTorques.getOrElse(jointIndex) { 0f }
            val magnitude = (abs(torque) / genome.jointTorqueScale.coerceAtLeast(1e-4f)).coerceIn(0f, 1f)
            if (magnitude <= 1e-4f) return@forEachIndexed

            val bow = (0.14f + magnitude * scoreWidth * 0.14f) * if (torque >= 0f) 1f else -1f
            val color =
                if (torque >= 0f) vec(0.92, 0.54, 0.31, 0.82)
                else vec(0.41, 0.72, 0.95, 0.82)
            val curve = FLine().apply {
                this.color = color
                thickness = 0.7 + magnitude * 1.9
            }
            addNotationBow(curve, phaseX, rowY(joint.pivot), rowY(joint.a), bow)
            addNotationBow(curve, phaseX, rowY(joint.pivot), rowY(joint.b), bow)
            torqueCurves += curve

            pivotMarkers.moveTo(vec(phaseX, rowY(joint.pivot), 0))
            pivotMarkers.here.color = color
            pivotMarkers.here.pointSize = 0.9 + magnitude * 0.6
        }

        val playhead = FLine().apply {
            color = vec(0.98, 0.98, 0.98, 0.9)
            thickness = 1.4
        }
        val top = resolvedOrigin.y + rowHeight * 0.45f
        val bottom = rowY(genome.points.lastIndex.coerceAtLeast(0)) - rowHeight * 0.45f
        playhead.moveTo(vec(phaseX, top, 0))
        playhead.lineTo(vec(phaseX, bottom, 0))

        return buildList {
            add(baseline)
            add(footDown)
            add(footTransitions)
            addAll(torqueCurves)
            add(pivotMarkers)
            add(playhead)
        }
    }

fun notationPlayback

    fun notationPlayback(
        loop: Boolean = true,
        scoreWidth: Float = 6f,
        rowHeight: Float = 0.55f,
        origin: Float2? = null
    ): Sequence<List<FLine>> = sequence {
        if (frames.isEmpty()) return@sequence
        do {
            for (i in frames.indices) {
                yield(notationLines(i, scoreWidth, rowHeight, origin))
            }
        } while (loop)
    }

fun playbackWithNotation

body — 72 lines, from line 775
    fun playbackWithNotation(
        loop: Boolean = true,
        trailLength: Int = 40,
        alignNetMotionToPositiveX: Boolean = true,
        scoreWidth: Float = 6f,
        rowHeight: Float = 0.55f,
        notationOrigin: Float2? = null
    ): Sequence<List<FLine>> = sequence {
        if (frames.isEmpty()) return@sequence
        do {
            for (i in frames.indices) {
                yield(
                    frameLines(i, trailLength, alignNetMotionToPositiveX) +
                        notationLines(i, scoreWidth, rowHeight, notationOrigin)
                )
            }
        } while (loop)
    }

    private fun motionAlignmentTransform(enabled: Boolean): MotionAlignmentTransform {
        if (!enabled || frames.size < 2) return MotionAlignmentTransform.identity()
        val pivot = frames.first().center
        val displacement = frames.last().center - pivot
        if (displacement.lengthSquared() <= 1e-8f) return MotionAlignmentTransform.identity()
        val rotation = -atan2(displacement.y, displacement.x)
        return MotionAlignmentTransform(pivot = pivot, cos = cos(rotation), sin = kotlin.math.sin(rotation))
    }

    private fun resolveNotationOrigin(scoreWidth: Float, rowHeight: Float, origin: Float2?): Float2 {
        origin ?: run {
            val verticalExtent = genome.points.lastIndex.coerceAtLeast(0) * rowHeight
            return Float2(
                x = -scoreWidth * 0.5f,
                y = max(2.2f, verticalExtent * 0.5f + 1.5f)
            )
        }
        return origin
    }

    private fun addNotationBow(line: FLine, x: Float, y0: Float, y1: Float, bow: Float) {
        if (abs(y1 - y0) <= 1e-6f) return
        val controlX = x + bow
        val c1Y = lerp(y0, y1, 0.33f)
        val c2Y = lerp(y0, y1, 0.66f)
        line.moveTo(vec(x, y0, 0))
        line.cubicTo(
            vec(controlX, c1Y, 0),
            vec(controlX, c2Y, 0),
            vec(x, y1, 0)
        )
    }
}

private data class MotionAlignmentTransform(
    val pivot: Float2,
    val cos: Float,
    val sin: Float
) {
    fun transform(point: Float2): Float2 {
        val translated = point - pivot
        return Float2(
            translated.x * cos - translated.y * sin,
            translated.x * sin + translated.y * cos
        ) + pivot
    }

    companion object {
        fun identity(): MotionAlignmentTransform = MotionAlignmentTransform(Float2(0f, 0f), 1f, 0f)
    }
}

private data class PhaseInterval(val start: Float, val end: Float)

data class EvolutionConfig

The genetic algorithm's settings: how big a population, for how many generations, how parents are chosen, how hard children are mutated, and the simulation each of them is scored by.

Selection is a tournament of tournamentSize drawn with replacement, with the fittest eliteCount copied forward untouched. The rest of each generation is bred from tournament winners, crossed over with probability crossoverRate and then mutated between mutationSteps times.

Fresh random genomes are added every generation as well, and the two knobs for them are both floors rather than a count and an override: whichever of immigrantsPerGeneration and immigrantFraction of the population is larger wins. The stagnation knobs raise both that fraction and the mutation range once stagnationGenerations have passed without the champion improving, which with the default 80 against the default 12 generations cannot happen; it is a setting for the long runs a box leaves going, and lowering it is how you reach it.

data class EvolutionConfig(
    val populationSize: Int = 28,
    val generations: Int = 12,
    val eliteCount: Int = 4,
    val tournamentSize: Int = 4,
    val crossoverRate: Float = 0.35f,
    val mutationSteps: IntRange = 2..5,
    val immigrantsPerGeneration: Int = 1,
    val immigrantFraction: Float = 0.08f,
    val stagnationGenerations: Int = 80,
    val stagnationMutationBoost: IntRange = 2..4,
    val stagnationImmigrantFraction: Float = 0.3f,
    val minPoints: Int = 4,
    val maxPoints: Int = 9,
    val targetEdgeCount: Int? = null,
    val targetJointActivations: Int? = null,
    val alwaysUpFootFraction: Float = 0f,
    val cyclingFootFraction: Float = 0f,
    val simulation: CreatureSimulationConfig = CreatureSimulationConfig(),
    val randomSeed: Int = 1
)

data class EvolutionGeneration

The fitness spread of one finished generation, over the genomes that survived repair. A genome no repair could make valid is dropped rather than scored, so these are computed over a population that can be smaller than EvolutionConfig.populationSize.

EvolutionResult.history is a list of these, one per generation, which is what a convergence plot is drawn from.

data class EvolutionGeneration(
    val generation: Int,
    val bestFitness: Float,
    val meanFitness: Float,
    val medianFitness: Float
)

data class EvolutionResult

What a finished evolution returns: the best creature it ever scored, and a history entry per generation.

best is re-simulated under the full EvolutionConfig.simulation, so it carries frames and can be played back even though the population around it was scored without them. A run stopped by its EvolutionController before any generation completed has no champion to report, and answers with the first genome of its starting population instead.

data class EvolutionResult(
    val best: CreatureEvaluation,
    val history: List<EvolutionGeneration>
)

data class EvolutionUpdate

One generation, as it finishes: handed to CreatureEvolution.evolve's callback and emitted by CreatureEvolution.evolveFlow. This is what a box watches to draw an evolution as it happens.

generationBest is this generation's fittest creature and champion is the best of every generation so far, so the two are the same object until the run stops improving and then diverge. generationsSinceImprovement counts that gap and stagnating is it having passed EvolutionConfig.stagnationGenerations. Both of these evaluations are re-simulated under the full EvolutionConfig.simulation, so unlike the population they were chosen from they can be played back.

data class EvolutionUpdate(
    val generation: EvolutionGeneration,
    val generationBest: CreatureEvaluation,
    val champion: CreatureEvaluation,
    val generationsSinceImprovement: Int,
    val stagnating: Boolean
)

class EvolutionController

A stop button for a running evolution, safe to call from a thread other than the one evolving.

Evolution checks it between genomes, between generations and between children, so a stop takes effect within about one creature's simulation rather than at the end of the generation. It is a clean finish and not a cancellation: the run returns the best creature it has found so far, with the history it has accumulated, exactly as if that had been its last generation. reset clears the flag so the same controller can start another run.

class EvolutionController {
    @Volatile
    private var stopRequested: Boolean = false

fun requestStop()

Asks the evolution reading this controller to finish early and return what it has.

    fun requestStop() {
        stopRequested = true
    }

fun reset()

Clears a previous requestStop, so this controller can be handed to another run.

    fun reset() {
        stopRequested = false
    }

fun isStopRequested(): Boolean = stopRequested

Whether requestStop has been called and not yet reset.

internal data class JointKey(val pivot: Int, val a: Int, val b: Int)

private data class EdgeRest(val a: Int, val b: Int, val restLength: Float)
private data class EdgeCrossing(val firstIndex: Int, val secondIndex: Int, val point: Float2)
private data class EdgeConnectionCandidate(val a: Int, val b: Int, val distance: Float)
private data class RuntimeClock(var phase: Float = 0f, var step: Int = 0)
private data class ScoredGenome(val genome: CreatureGenome, val evaluation: CreatureEvaluation)
private data class JointMotorPose(
    val restAngle: Float,
    val currentAngle: Float,
    val angleVelocity: Float,
    val aLen2: Float,
    val bLen2: Float,
    val aInvMass: Float,
    val bInvMass: Float,
    val pivotInvMass: Float,
    val aCoeffX: Float,
    val aCoeffY: Float,
    val bCoeffX: Float,
    val bCoeffY: Float,
    val pivotCoeffX: Float,
    val pivotCoeffY: Float
)

object CreatureEvolution

The genetic algorithm itself: where a creature comes from, how one is scored, and how a population is bred.

randomGenome grows a body by attaching points one at a time, which is how both the starting population and every later immigrant is made. simulate repairs and validates a single genome and runs it, returning a CreatureEvaluation. evolve runs a whole evolution and blocks until it is done, evolveSuspend is the same thing for a coroutine, and evolveFlow is the shape a box usually wants: a kotlinx.coroutines.flow.Flow of EvolutionUpdate it can collect and draw from as the generations arrive.

Mutation and repair are not public. A genome mutates by a few small edits drawn from a set of sixteen, three of which are weighted to come up twice as often, and whatever comes out is put back through repair before it is scored. What the next generation breeds from is therefore the repaired creature that earned the score, not the child as it was written.

object CreatureEvolution {

fun randomGenome

A random starting body: points attached one at a time to a parent with a spare edge slot, then given feet, joints and gains drawn from the ranges that tend to produce a creature that moves.

The result is already normalised, repaired and inside its actuation budget, so it is valid to simulate as it stands.

The two foot fractions shape the gait before evolution ever sees it, and they are not symmetric. alwaysUpFootFraction silences that share of the body outright, taking the feet that were pressing least. cyclingFootFraction is a floor instead of a share: if fewer than that many feet already both land and lift within a cycle, the strongest of the remaining feet are rewritten until enough do, and a body that already clears the floor is left alone.

body — 99 lines, from line 1012
    fun randomGenome(
        random: Random = Random.Default,
        minPoints: Int = 4,
        maxPoints: Int = 9,
        targetEdgeCount: Int? = null,
        targetJointActivations: Int? = null,
        alwaysUpFootFraction: Float = 0f,
        cyclingFootFraction: Float = 0f,
        maxActuationBudgetPerCycle: Float = DEFAULT_MAX_ACTUATION_BUDGET_PER_CYCLE
    ): CreatureGenome {
        require(minPoints >= 2) { "minPoints must be >= 2" }
        require(maxPoints >= minPoints) { "maxPoints must be >= minPoints" }

        val pointCount = random.nextInt(minPoints, maxPoints + 1)
        val points = mutableListOf<Float2>()
        val edges = mutableListOf<EdgeGene>()
        val degrees = mutableListOf<Int>()

        points += Float2(0f, 0f)
        degrees += 0

        for (i in 1 until pointCount) {
            val possibleParents = degrees.indices.filter { degrees[it] < 4 }
            val parent = possibleParents.random(random)
            val base = points[parent]
            val angle = random.nextFloat() * (2f * PI.toFloat())
            val length = randomFloat(random, 0.65f, 1.35f)
            val offset = Float2(cos(angle) * length, kotlin.math.sin(angle) * length)
            points += base + offset + Float2(
                gaussian(random, 0.08f),
                gaussian(random, 0.08f)
            )
            edges += EdgeGene(parent, i)
            degrees[parent]++
            degrees += 1
        }

        val maxPossibleEdges = maxReachableEdgeCount(pointCount)
        val desiredEdgeCount = targetEdgeCount
            ?.coerceIn(pointCount - 1, maxPossibleEdges)
            ?: (edges.size + random.nextInt(0, min(3, pointCount / 2) + 1))

        while (edges.size < desiredEdgeCount) {
            val candidates = mutableListOf<Pair<Int, Int>>()
            for (a in 0 until pointCount) {
                if (degrees[a] >= 4) continue
                for (b in a + 1 until pointCount) {
                    if (degrees[b] >= 4) continue
                    if (edges.any { it.normalized() == EdgeGene(a, b) }) continue
                    val distance = distance(points[a], points[b])
                    if (distance in 0.4f..2.4f && !wouldCrossExistingEdges(points, edges, EdgeGene(a, b))) {
                        candidates += a to b
                    }
                }
            }
            if (candidates.isEmpty()) break
            val (a, b) = candidates.random(random)
            edges += EdgeGene(a, b)
            degrees[a]++
            degrees[b]++
        }

        val centeredPoints = centerPoints(points)
        val feet = MutableList(pointCount) { ContactProgram(phase = 0f, strength = 0f) }
        val sortedByY = centeredPoints.indices.sortedBy { centeredPoints[it].y }
        val footCount = min(max(2, pointCount / 3), pointCount)
        val footPhaseStride = 1f / footCount.toFloat()
        for (footIndex in 0 until footCount) {
            val pointIndex = sortedByY[footIndex]
            feet[pointIndex] = ContactProgram(
                phase = wrap01(footIndex * footPhaseStride + gaussian(random, 0.05f)),
                duty = randomFloat(random, 0.18f, 0.52f),
                smooth = randomFloat(random, 0.12f, 0.45f),
                strength = randomFloat(random, 0.65f, 1f)
            )
        }

        val seedGenome = CreatureGenome(
            points = centeredPoints,
            edges = edges,
            feet = feet,
            joints = emptyList(),
            clockPeriod = randomFloat(random, 0.7f, 1.45f),
            massPerLength = randomFloat(random, 0.6f, 1.8f),
            basePointMass = randomFloat(random, 0.2f, 0.7f),
            springStiffness = randomFloat(random, 4.8f, 8.2f),
            damping = randomFloat(random, 0.93f, 0.985f),
            freeDrag = randomFloat(random, 4f, 28f),
            footDrag = randomFloat(random, 70f, 240f),
            footStickiness = randomFloat(random, 280f, 980f),
            jointTorqueScale = randomFloat(random, 18f, 72f)
        ).normalized()
            .repairCrossingEdges()
            .repairAlwaysUpFeet(alwaysUpFootFraction)
            .repairCyclingFeet(cyclingFootFraction, random)

        return seedGenome.withJointPopulation(random, targetJointActivations)
            .repairActuationBudget(maxActuationBudgetPerCycle)
    }

fun simulate

Runs one creature and scores it.

The genome is repaired first, and the CreatureEvaluation reports the repaired genome rather than the one passed in, so a creature can always be rebuilt from what came back. A genome that repair cannot make valid throws, with CreatureGenome.validationIssues' sentences as the message; inside an evolution that genome is dropped instead, and the run carries on.

    fun simulate(
        genome: CreatureGenome,
        config: CreatureSimulationConfig = CreatureSimulationConfig()
    ): CreatureEvaluation {
        val sanitized = genome.normalized()
            .repairNearZeroEdges()
            .repairConnectivity()
            .repairCrossingEdges()
            .repairNearZeroEdges()
            .repairConnectivity()
            .repairCrossingEdges()
            .pruneInvalidJoints()
            .repairActuationBudget(config.maxActuationBudgetPerCycle)
            .normalized()
        val issues = sanitized.validationIssues()
        require(issues.isEmpty()) {
            "invalid creature genome:\n" + issues.joinToString(separator = "\n") { " - $it" }
        }
        return CreatureRuntime(sanitized, config).run()
    }

fun evolve

Runs a whole evolution on the calling thread and returns the champion. onGeneration sees every generation as it finishes; controller stops it early from another thread.

    fun evolve(
        config: EvolutionConfig = EvolutionConfig(),
        controller: EvolutionController? = null,
        onGeneration: (EvolutionUpdate) -> Unit = {}
    ): EvolutionResult =
        runBlocking {
            evolveSuspend(config, controller) { onGeneration(it) }
        }

suspend fun evolveSuspend

evolve for a coroutine: the same run, suspending instead of blocking, with a suspending onGeneration.

It suspends at every point it checks controller, so cancelling the coroutine stops the evolution there rather than at the end of the generation.

    suspend fun evolveSuspend(
        config: EvolutionConfig = EvolutionConfig(),
        controller: EvolutionController? = null,
        onGeneration: suspend (EvolutionUpdate) -> Unit = {}
    ): EvolutionResult =
        evolveInternal(
            config = config,
            controller = controller,
            onGeneration = onGeneration
        )

fun evolveFlow

The evolution as a cold kotlinx.coroutines.flow.Flow of EvolutionUpdate, one per generation, which is usually what a box wants: collect it and redraw the champion as the generations arrive.

Nothing runs until the flow is collected, and the work happens in the collector's context. Unlike the other two entry points this one makes its own EvolutionController by default; keep a reference to it if you want a stop button, or cancel the collecting coroutine instead.

body — 2068 lines, from line 1180
    fun evolveFlow(
        config: EvolutionConfig = EvolutionConfig(),
        controller: EvolutionController = EvolutionController()
    ): Flow<EvolutionUpdate> =
        flow {
            evolveInternal(
                config = config,
                controller = controller,
                onGeneration = { emit(it) }
            )
        }

    private suspend fun evolveInternal(
        config: EvolutionConfig,
        controller: EvolutionController?,
        onGeneration: suspend (EvolutionUpdate) -> Unit
    ): EvolutionResult {
        require(config.populationSize > 0) { "populationSize must be > 0" }
        require(config.generations > 0) { "generations must be > 0" }
        require(config.eliteCount in 0..config.populationSize) { "eliteCount must be between 0 and populationSize" }
        require(config.tournamentSize > 0) { "tournamentSize must be > 0" }
        require(config.immigrantsPerGeneration >= 0) { "immigrantsPerGeneration must be >= 0" }
        require(config.immigrantFraction in 0f..1f) { "immigrantFraction must be between 0 and 1" }
        require(config.stagnationGenerations >= 0) { "stagnationGenerations must be >= 0" }
        require(config.stagnationMutationBoost.first >= 0 && config.stagnationMutationBoost.last >= config.stagnationMutationBoost.first) {
            "stagnationMutationBoost must be a non-negative IntRange"
        }
        require(config.stagnationImmigrantFraction in 0f..1f) { "stagnationImmigrantFraction must be between 0 and 1" }
        require(config.minPoints >= 2 && config.maxPoints >= config.minPoints) { "point range is invalid" }
        require(config.alwaysUpFootFraction in 0f..1f) { "alwaysUpFootFraction must be between 0 and 1" }
        require(config.cyclingFootFraction in 0f..1f) { "cyclingFootFraction must be between 0 and 1" }
        require(config.alwaysUpFootFraction + config.cyclingFootFraction <= 1f + 1e-6f) {
            "alwaysUpFootFraction + cyclingFootFraction must be <= 1"
        }
        require(config.mutationSteps.first > 0 && config.mutationSteps.last >= config.mutationSteps.first) {
            "mutationSteps must be a positive IntRange"
        }

        val random = Random(config.randomSeed)
        val fastSimulation = config.simulation.copy(recordFrames = false)
        var population = MutableList(config.populationSize) {
            randomGenome(
                random = random,
                minPoints = config.minPoints,
                maxPoints = config.maxPoints,
                targetEdgeCount = config.targetEdgeCount,
                targetJointActivations = config.targetJointActivations,
                alwaysUpFootFraction = config.alwaysUpFootFraction,
                cyclingFootFraction = config.cyclingFootFraction,
                maxActuationBudgetPerCycle = config.simulation.maxActuationBudgetPerCycle
            )
        }

        val history = mutableListOf<EvolutionGeneration>()
        var champion: CreatureEvaluation? = null
        fun result() = EvolutionResult(best = champion ?: simulate(population.first(), config.simulation), history = history)
        var generationsSinceImprovement = 0

        repeat(config.generations) { generation ->
            val scored = mutableListOf<ScoredGenome>()
            for (genome in population) {
                if (!evolutionCheckpoint(controller)) {
                    return result()
                }
                // Score the repaired genome, and breed from that: it is the creature that earned the score.
                // A genome repair cannot make valid is dropped rather than ending the run.
                val evaluation = runCatching { simulate(genome, fastSimulation) }.getOrNull() ?: continue
                scored += ScoredGenome(evaluation.genome, evaluation)
            }
            scored.sortByDescending { it.evaluation.fitness }

            if (!evolutionCheckpoint(controller)) {
                return result()
            }
            check(scored.isNotEmpty()) { "no genome in generation $generation survived repair" }
            val bestFast = scored.first()
            val bestFull = simulate(bestFast.genome, config.simulation)
            val previous = champion
            val improved = previous == null || bestFull.fitness > previous.fitness
            val currentChampion = if (improved) bestFull else previous
            champion = currentChampion
            if (improved) {
                generationsSinceImprovement = 0
            } else {
                generationsSinceImprovement++
            }

            val fitnesses = scored.map { it.evaluation.fitness }
            val generationInfo = EvolutionGeneration(
                generation = generation,
                bestFitness = fitnesses.first(),
                meanFitness = fitnesses.average().toFloat(),
                medianFitness = fitnesses[fitnesses.size / 2]
            )
            history += generationInfo
            val stagnating =
                config.stagnationGenerations > 0 &&
                    generationsSinceImprovement >= config.stagnationGenerations
            onGeneration(
                EvolutionUpdate(
                    generation = generationInfo,
                    generationBest = bestFull,
                    champion = currentChampion,
                    generationsSinceImprovement = generationsSinceImprovement,
                    stagnating = stagnating
                )
            )

            if (generation == config.generations - 1) return@repeat
            if (!evolutionCheckpoint(controller)) {
                return result()
            }

            val nextPopulation = mutableListOf<CreatureGenome>()
            scored.take(config.eliteCount).mapTo(nextPopulation) { it.genome }

            val immigrantCount = effectiveImmigrantCount(config, stagnating)
                .coerceAtMost(max(0, config.populationSize - nextPopulation.size))
            val mutationRange = effectiveMutationRange(config, stagnating)

            repeat(immigrantCount) {
                if (!evolutionCheckpoint(controller)) {
                    return result()
                }
                if (nextPopulation.size < config.populationSize) {
                    nextPopulation += randomGenome(
                        random = random,
                        minPoints = config.minPoints,
                        maxPoints = config.maxPoints,
                        targetEdgeCount = config.targetEdgeCount,
                        targetJointActivations = config.targetJointActivations,
                        alwaysUpFootFraction = config.alwaysUpFootFraction,
                        cyclingFootFraction = config.cyclingFootFraction,
                        maxActuationBudgetPerCycle = config.simulation.maxActuationBudgetPerCycle
                    )
                }
            }

            while (nextPopulation.size < config.populationSize) {
                if (!evolutionCheckpoint(controller)) {
                    return result()
                }
                val parentA = tournament(scored, config.tournamentSize, random).genome
                val parentB = tournament(scored, config.tournamentSize, random).genome
                var child =
                    if (chance(random, config.crossoverRate)) crossover(parentA, parentB, random)
                    else parentA

                val mutationCount = random.nextInt(mutationRange.first, mutationRange.last + 1)
                repeat(mutationCount) {
                    child = mutate(child, random, config)
                }

                nextPopulation += repairGenome(child, random, config)
            }

            population = nextPopulation
        }

        return result()
    }

    private fun crossover(a: CreatureGenome, b: CreatureGenome, random: Random): CreatureGenome {
        val left = a.normalized()
        val right = b.normalized()
        val base = if (random.nextBoolean()) left else right
        val other = if (base === left) right else left

        val points =
            if (left.points.size == right.points.size) {
                left.points.indices.map { index ->
                    when (random.nextInt(3)) {
                        0 -> left.points[index]
                        1 -> right.points[index]
                        else -> left.points[index].lerp(right.points[index], random.nextFloat())
                    }
                }
            } else {
                base.points
            }

        val feet =
            if (left.points.size == right.points.size) {
                left.feet.indices.map { index ->
                    when (random.nextInt(3)) {
                        0 -> left.feet[index]
                        1 -> right.feet[index]
                        else -> mixContact(left.feet[index], right.feet[index], random)
                    }
                }
            } else {
                base.feet
            }

        val mixedEdges = crossoverEdges(base, other, points, random)
        val compatibleJoints = crossoverJoints(base, other, random)
        return CreatureGenome(
            points = centerPoints(points),
            edges = mixedEdges,
            feet = feet,
            joints = compatibleJoints,
            clockPeriod = lerp(base.clockPeriod, other.clockPeriod, random.nextFloat()),
            massPerLength = lerp(base.massPerLength, other.massPerLength, random.nextFloat()),
            basePointMass = lerp(base.basePointMass, other.basePointMass, random.nextFloat()),
            springStiffness = lerp(base.springStiffness, other.springStiffness, random.nextFloat()),
            damping = lerp(base.damping, other.damping, random.nextFloat()),
            freeDrag = lerp(base.freeDrag, other.freeDrag, random.nextFloat()),
            footDrag = lerp(base.footDrag, other.footDrag, random.nextFloat()),
            footStickiness = lerp(base.footStickiness, other.footStickiness, random.nextFloat()),
            jointTorqueScale = lerp(base.jointTorqueScale, other.jointTorqueScale, random.nextFloat())
        ).normalized()
            .repairNearZeroEdges()
            .repairConnectivity()
            .repairCrossingEdges()
            .pruneInvalidJoints()
            .withJointPopulation(random)
    }

    private fun mutate(genome: CreatureGenome, random: Random, config: EvolutionConfig): CreatureGenome {
        val normalized = genome.normalized()
        val mutated =
            when (random.nextInt(19)) {
                0, 1 -> mutatePoints(normalized, random)
                2 -> relaxSkeleton(normalized, random)
                3, 4 -> mutateFeet(normalized, random)
                5 -> swapFootActivationTiming(normalized, random)
                6 -> copyFootActivation(normalized, random)
                7, 8 -> mutateJoints(normalized, random)
                9 -> swapJointActivationTiming(normalized, random)
                10 -> copyJointActivation(normalized, random)
                11 -> mutateGlobals(normalized, random)
                12 -> splitRandomEdge(normalized, random, config.maxPoints)
                13 -> addRandomEdge(normalized, random)
                14 -> addRandomPoint(normalized, random, config.maxPoints)
                15 -> splitPointIntoTwo(normalized, random, config.maxPoints)
                16 -> dropRandomPoint(normalized, random, config.minPoints)
                17 -> removeLeafPoint(normalized, random, config.minPoints)
                18 -> removeRedundantEdge(normalized, random)
                else -> normalized
            }
        return repairGenome(mutated, random, config)
    }

    private fun relaxSkeleton(genome: CreatureGenome, random: Random): CreatureGenome {
        val adjacency = buildAdjacency(genome.points.size, genome.edges)
        if (adjacency.all { it.isEmpty() }) return genome

        var points = genome.points
        val factor = randomFloat(random, 0.12f, 0.42f)
        val passes = random.nextInt(1, 4)
        repeat(passes) {
            val next = points.toMutableList()
            val indices =
                adjacency.indices
                    .filter { adjacency[it].isNotEmpty() }
                    .shuffled(random)
                    .take(max(1, adjacency.count { it.isNotEmpty() } / 2))

            indices.forEach { index ->
                val centroid = centroidOf(adjacency[index], points)
                next[index] = points[index].lerp(centroid, factor)
            }
            points = next
        }
        return genome.copy(points = centerPoints(points))
    }

    private fun mutatePoints(genome: CreatureGenome, random: Random): CreatureGenome {
        val points = genome.points.toMutableList()
        val edits = random.nextInt(1, min(3, points.size) + 1)
        repeat(edits) {
            val index = random.nextInt(points.size)
            val point = points[index]
            points[index] = Float2(
                point.x + gaussian(random, 0.18f),
                point.y + gaussian(random, 0.18f)
            )
        }
        return genome.copy(points = centerPoints(points))
    }

    private fun mutateFeet(genome: CreatureGenome, random: Random): CreatureGenome {
        val feet = genome.feet.toMutableList()
        val edits = random.nextInt(1, min(3, feet.size) + 1)
        repeat(edits) {
            val index = random.nextInt(feet.size)
            val current = feet[index]
            feet[index] = current.copy(
                phase = wrap01(current.phase + gaussian(random, 0.08f)),
                duty = (current.duty + gaussian(random, 0.07f)).coerceIn(0.05f, 0.75f),
                smooth = (current.smooth + gaussian(random, 0.07f)).coerceIn(0.05f, 0.9f),
                strength = (current.strength + gaussian(random, 0.18f)).coerceIn(0f, 1f)
            )
        }
        if (feet.size >= 2 && chance(random, 0.2f)) {
            val a = random.nextInt(feet.size)
            var b = random.nextInt(feet.size)
            if (a == b) b = (b + 1) % feet.size
            val temp = feet[a]
            feet[a] = feet[b].copy(phase = temp.phase)
            feet[b] = temp.copy(phase = feet[b].phase)
        }
        return genome.copy(feet = feet)
    }

    private fun swapFootActivationTiming(genome: CreatureGenome, random: Random): CreatureGenome {
        if (genome.feet.size < 2) return genome
        val feet = genome.feet.toMutableList()
        val a = random.nextInt(feet.size)
        var b = random.nextInt(feet.size)
        if (a == b) b = (b + 1) % feet.size

        val phase = feet[a].phase
        feet[a] = feet[a].copy(phase = feet[b].phase)
        feet[b] = feet[b].copy(phase = phase)
        return genome.copy(feet = feet)
    }

    private fun copyFootActivation(genome: CreatureGenome, random: Random): CreatureGenome {
        if (genome.feet.size < 2) return genome
        val feet = genome.feet.toMutableList()
        val source = random.nextInt(feet.size)
        var target = random.nextInt(feet.size)
        if (source == target) target = (target + 1) % feet.size

        feet[target] = feet[source].copy(
            phase = wrap01(feet[source].phase + gaussian(random, 0.03f)),
            strength = feet[source].strength.coerceIn(0f, 1f)
        )
        return genome.copy(feet = feet)
    }

    private fun mutateJoints(genome: CreatureGenome, random: Random): CreatureGenome {
        val candidates = candidateJointKeys(buildAdjacency(genome.points.size, genome.edges))
        val joints = genome.joints.toMutableList()

        if (joints.isNotEmpty() && chance(random, 0.65f)) {
            val index = random.nextInt(joints.size)
            val current = joints[index]
            joints[index] =
                when (random.nextInt(4)) {
                    0 -> current.copy(targets = mutateJointTargets(current.targets, random))
                    1 -> current.copy(targets = smoothJointTargets(current.targets, passes = random.nextInt(1, 3)))
                    2 -> current.shifted(random.nextInt(1, JOINT_TARGET_KNOT_COUNT))
                    else -> current.copy(targets = scaleJointTargets(current.targets, randomFloat(random, 0.75f, 1.2f)))
                }
        }

        if (joints.isNotEmpty() && chance(random, 0.18f)) {
            joints.removeAt(random.nextInt(joints.size))
        }

        if (candidates.isNotEmpty() && (joints.isEmpty() || chance(random, 0.3f))) {
            val occupied = joints.map { it.key() }.toHashSet()
            val available = candidates.filterNot { it in occupied }
            if (available.isNotEmpty()) {
                val key = available.random(random)
                joints += randomJoint(random, key)
            }
        }

        return genome.copy(joints = joints)
    }

    private fun swapJointActivationTiming(genome: CreatureGenome, random: Random): CreatureGenome {
        if (genome.joints.size < 2) return genome
        val joints = genome.joints.toMutableList()
        val a = random.nextInt(joints.size)
        var b = random.nextInt(joints.size)
        if (a == b) b = (b + 1) % joints.size

        val aShift = random.nextInt(1, JOINT_TARGET_KNOT_COUNT)
        val bShift = random.nextInt(1, JOINT_TARGET_KNOT_COUNT)
        joints[a] = joints[a].shifted(bShift)
        joints[b] = joints[b].shifted(aShift)
        return genome.copy(joints = joints)
    }

    private fun copyJointActivation(genome: CreatureGenome, random: Random): CreatureGenome {
        if (genome.joints.isEmpty()) return genome

        val joints = genome.joints.toMutableList()
        val sourceIndex = random.nextInt(joints.size)
        val source = joints[sourceIndex]

        if (joints.size >= 2 && chance(random, 0.65f)) {
            var targetIndex = random.nextInt(joints.size)
            if (targetIndex == sourceIndex) targetIndex = (targetIndex + 1) % joints.size
            val target = joints[targetIndex]
            joints[targetIndex] = target.copy(
                targets = mutateJointTargets(source.targets, random, edits = 1, sigma = 0.05f)
            )
            return genome.copy(joints = joints)
        }

        val occupied = joints.map { it.key() }.toHashSet()
        val available = candidateJointKeys(buildAdjacency(genome.points.size, genome.edges)).filterNot { it in occupied }
        if (available.isEmpty()) return genome

        val key = available.random(random)
        joints += JointProgram(
            pivot = key.pivot,
            a = key.a,
            b = key.b,
            targets = mutateJointTargets(source.targets, random, edits = 1, sigma = 0.05f)
        )
        return genome.copy(joints = joints)
    }

    private fun mutateGlobals(genome: CreatureGenome, random: Random): CreatureGenome =
        genome.copy(
            clockPeriod = (genome.clockPeriod + gaussian(random, 0.08f)).coerceIn(0.3f, 3f),
            massPerLength = (genome.massPerLength + gaussian(random, 0.12f)).coerceIn(0.1f, 4f),
            basePointMass = (genome.basePointMass + gaussian(random, 0.06f)).coerceIn(0.05f, 3f),
            springStiffness = (genome.springStiffness + gaussian(random, 0.18f))
                .coerceIn(4.0f, MAX_SPRING_STIFFNESS),
            damping = (genome.damping + gaussian(random, 0.01f)).coerceIn(0.8f, 0.995f),
            freeDrag = (genome.freeDrag + gaussian(random, 5f)).coerceIn(0f, 250f),
            footDrag = (genome.footDrag + gaussian(random, 40f)).coerceIn(0f, 1500f),
            footStickiness = (genome.footStickiness + gaussian(random, 90f)).coerceIn(0f, 2500f),
            jointTorqueScale = (genome.jointTorqueScale + gaussian(random, 8f)).coerceIn(5f, 180f)
        )

    private fun splitRandomEdge(genome: CreatureGenome, random: Random, maxPoints: Int): CreatureGenome {
        if (genome.points.size >= maxPoints || genome.edges.isEmpty()) return genome
        val edge = genome.edges.random(random)
        val a = genome.points[edge.a]
        val b = genome.points[edge.b]
        val midpoint = (a + b) * 0.5f
        val edgeDir = (b - a).normalizedOr(Float2(1f, 0f))
        val normal = edgeDir.perpendicular()
        val offset = normal * gaussian(random, 0.18f)
        val newPointIndex = genome.points.size
        val newPoints = genome.points + (midpoint + offset)
        val newFeet = genome.feet + mixContact(genome.feet[edge.a], genome.feet[edge.b], random).copy(
            strength = max(genome.feet[edge.a].strength, genome.feet[edge.b].strength) * 0.5f
        )
        val newEdges = genome.edges.filterNot { it.normalized() == edge.normalized() } + listOf(
            EdgeGene(edge.a, newPointIndex),
            EdgeGene(newPointIndex, edge.b)
        )
        return genome.copy(points = centerPoints(newPoints), edges = newEdges, feet = newFeet)
            .refreshJoints(random)
    }

    private fun addRandomPoint(genome: CreatureGenome, random: Random, maxPoints: Int): CreatureGenome {
        if (genome.points.size >= maxPoints) return genome

        val adjacency = buildAdjacency(genome.points.size, genome.edges)
        val degrees = degreeCounts(genome.points.size, genome.edges)
        val anchors = genome.points.indices.filter { degrees[it] < 4 }
        if (anchors.isEmpty()) return genome

        repeat(24) {
            val anchor = anchors.random(random)
            val anchorPoint = genome.points[anchor]
            val neighborhood = adjacency[anchor]
            val radial =
                if (neighborhood.isEmpty()) {
                    randomUnitVector(random)
                } else {
                    (anchorPoint - centroidOf(neighborhood, genome.points)).normalizedOr(randomUnitVector(random))
                }
            val direction =
                radial.lerp(randomUnitVector(random), randomFloat(random, 0.2f, 0.8f))
                    .normalizedOr(radial)
            val candidatePoint =
                anchorPoint +
                    direction * randomFloat(random, 0.55f, 1.2f) +
                    Float2(gaussian(random, 0.08f), gaussian(random, 0.08f))

            val newIndex = genome.points.size
            val newPoints = genome.points + candidatePoint
            val primaryEdge = EdgeGene(anchor, newIndex)
            if (wouldCrossExistingEdges(newPoints, genome.edges, primaryEdge)) return@repeat

            val newEdges = genome.edges.toMutableList()
            newEdges += primaryEdge
            val availablePartners =
                genome.points.indices
                    .filter { it != anchor && degrees[it] < 4 }
                    .sortedBy { distance(candidatePoint, genome.points[it]) }

            for (partner in availablePartners) {
                val edge = EdgeGene(partner, newIndex)
                val length = distance(candidatePoint, genome.points[partner])
                if (length !in 0.45f..2.5f) continue
                if (!wouldCrossExistingEdges(newPoints, newEdges, edge)) {
                    newEdges += edge
                    break
                }
            }

            val sourceFoot = genome.feet[anchor]
            val newFeet = genome.feet + sourceFoot.copy(
                phase = wrap01(sourceFoot.phase + gaussian(random, 0.08f)),
                duty = (sourceFoot.duty + gaussian(random, 0.04f)).coerceIn(0f, 0.85f),
                smooth = (sourceFoot.smooth + gaussian(random, 0.04f)).coerceIn(0.05f, 0.95f),
                strength = (sourceFoot.strength * randomFloat(random, 0.45f, 1f)).coerceIn(0f, 1f)
            )

            return genome.copy(
                points = centerPoints(newPoints),
                edges = newEdges,
                feet = newFeet
            ).refreshJoints(random)
        }

        return genome
    }

    private fun addRandomEdge(genome: CreatureGenome, random: Random): CreatureGenome {
        val degrees = degreeCounts(genome.points.size, genome.edges)
        val existing = genome.edges.map { it.normalized() }.toHashSet()
        val candidates = mutableListOf<EdgeGene>()
        for (a in genome.points.indices) {
            if (degrees[a] >= 4) continue
            for (b in a + 1 until genome.points.size) {
                if (degrees[b] >= 4) continue
                val edge = EdgeGene(a, b)
                if (edge in existing) continue
                val length = distance(genome.points[a], genome.points[b])
                if (length in 0.4f..2.5f && !wouldCrossExistingEdges(genome.points, genome.edges, edge)) {
                    candidates += edge
                }
            }
        }
        if (candidates.isEmpty()) return genome
        return genome.copy(edges = genome.edges + candidates.random(random)).refreshJoints(random)
    }

    private fun splitPointIntoTwo(genome: CreatureGenome, random: Random, maxPoints: Int): CreatureGenome {
        if (genome.points.size >= maxPoints) return genome

        val adjacency = buildAdjacency(genome.points.size, genome.edges)
        val candidates = adjacency.indices.filter { adjacency[it].size >= 2 }
        if (candidates.isEmpty()) return genome

        repeat(24) {
            val pointIndex = candidates.random(random)
            val neighbors = adjacency[pointIndex].shuffled(random)
            if (neighbors.size < 2) return@repeat

            val splitAt = random.nextInt(1, neighbors.size)
            val keepNeighbors = neighbors.take(splitAt)
            val movedNeighbors = neighbors.drop(splitAt)
            if (keepNeighbors.isEmpty() || movedNeighbors.isEmpty()) return@repeat

            val base = genome.points[pointIndex]
            val offsetDirection =
                (base - centroidOf(neighbors, genome.points)).perpendicular().normalizedOr(randomUnitVector(random))
            val offset = offsetDirection * randomFloat(random, 0.12f, 0.32f)
            val newIndex = genome.points.size
            val newPoints = genome.points.toMutableList().apply {
                this[pointIndex] = base + offset
                add(base - offset)
            }

            val newEdges = genome.edges.filterNot { it.a == pointIndex || it.b == pointIndex }.toMutableList()
            keepNeighbors.forEach { neighbor -> newEdges += EdgeGene(pointIndex, neighbor) }
            movedNeighbors.forEach { neighbor -> newEdges += EdgeGene(newIndex, neighbor) }
            newEdges += EdgeGene(pointIndex, newIndex)

            val sourceFoot = genome.feet[pointIndex]
            val newFeet = genome.feet + sourceFoot.copy(
                phase = wrap01(sourceFoot.phase + gaussian(random, 0.1f)),
                duty = (sourceFoot.duty + gaussian(random, 0.04f)).coerceIn(0f, 0.85f),
                smooth = (sourceFoot.smooth + gaussian(random, 0.04f)).coerceIn(0.05f, 0.95f),
                strength = (sourceFoot.strength * randomFloat(random, 0.6f, 1f)).coerceIn(0f, 1f)
            )

            return genome.copy(
                points = centerPoints(newPoints),
                edges = newEdges,
                feet = newFeet
            ).refreshJoints(random)
        }

        return genome
    }

    private fun dropRandomPoint(genome: CreatureGenome, random: Random, minPoints: Int): CreatureGenome {
        if (genome.points.size <= minPoints) return genome

        val adjacency = buildAdjacency(genome.points.size, genome.edges)
        val candidates = genome.points.indices
        val scored = candidates.map { index ->
            val removed = genome.removePoint(index)
            Triple(index, connectedComponents(removed.points.size, removed.edges).size, adjacency[index].size)
        }
        val fewestComponents = scored.minOfOrNull { it.second } ?: return genome
        val lowestDegree = scored.filter { it.second == fewestComponents }.minOfOrNull { it.third } ?: return genome
        val pool = scored.filter { it.second == fewestComponents && it.third <= lowestDegree + 1 }
        val removeIndex = pool.random(random).first

        var removed = genome.removePoint(removeIndex)
        val neighbors = adjacency[removeIndex]
        if (neighbors.size == 2) {
            val a = remapAfterRemoval(neighbors[0], removeIndex)
            val b = remapAfterRemoval(neighbors[1], removeIndex)
            val bridge = EdgeGene(a, b).normalized()
            val existing = removed.edges.map { it.normalized() }.toHashSet()
            if (a != b && bridge !in existing && distance(removed.points[a], removed.points[b]) > MIN_REST_EDGE_LENGTH) {
                removed = removed.copy(edges = removed.edges + bridge)
            }
        }
        return removed.refreshJoints(random)
    }

    private fun removeLeafPoint(genome: CreatureGenome, random: Random, minPoints: Int): CreatureGenome {
        if (genome.points.size <= minPoints) return genome
        val adjacency = buildAdjacency(genome.points.size, genome.edges)
        val leafCandidates = adjacency.indices.filter { adjacency[it].size == 1 }
        if (leafCandidates.isEmpty()) return genome
        val removeIndex = leafCandidates.random(random)
        return genome.removePoint(removeIndex).refreshJoints(random)
    }

    private fun removeRedundantEdge(genome: CreatureGenome, random: Random): CreatureGenome {
        val bridges = bridgeSet(genome.points.size, genome.edges)
        val removable = genome.edges.filterNot { it.normalized() in bridges }
        if (removable.isEmpty()) return genome
        val edge = removable.random(random)
        return genome.copy(edges = genome.edges.filterNot { it.normalized() == edge.normalized() }).refreshJoints(random)
    }

    private fun CreatureGenome.removePoint(index: Int): CreatureGenome {
        val newPoints = points.filterIndexed { pointIndex, _ -> pointIndex != index }
        val newFeet = feet.filterIndexed { pointIndex, _ -> pointIndex != index }
        fun remap(value: Int): Int = if (value < index) value else value - 1
        val newEdges = edges.mapNotNull { edge ->
            if (edge.a == index || edge.b == index) null
            else EdgeGene(remap(edge.a), remap(edge.b))
        }
        val newJoints = joints.mapNotNull { joint ->
            if (joint.pivot == index || joint.a == index || joint.b == index) null
            else JointProgram(
                pivot = remap(joint.pivot),
                a = remap(joint.a),
                b = remap(joint.b),
                targets = joint.targets
            )
        }
        return copy(points = centerPoints(newPoints), feet = newFeet, edges = newEdges, joints = newJoints)
    }

    private fun CreatureGenome.refreshJoints(random: Random, targetJointActivations: Int? = null): CreatureGenome {
        val normalized = normalized()
        val candidateSet = candidateJointKeys(buildAdjacency(normalized.points.size, normalized.edges)).toHashSet()
        val kept = normalized.joints.filter { it.key() in candidateSet }
        return normalized.copy(joints = kept).withJointPopulation(random, targetJointActivations)
    }

    private fun CreatureGenome.withJointPopulation(random: Random, targetJointActivations: Int? = null): CreatureGenome {
        val normalized = normalized()
        val candidates = candidateJointKeys(buildAdjacency(normalized.points.size, normalized.edges))
        if (candidates.isEmpty()) return normalized.copy(joints = emptyList())

        val current = normalized.joints.associateBy { it.key() }.toMutableMap()
        val desired = targetJointActivations?.coerceIn(1, candidates.size)
            ?: min(max(1, normalized.points.size - 2), min(candidates.size, normalized.points.size * 2))
        val target = if (current.isEmpty()) random.nextInt(1, desired + 1) else desired

        while (current.size < target) {
            val available = candidates.filterNot { it in current.keys }
            if (available.isEmpty()) break
            val key = available.random(random)
            current[key] = randomJoint(random, key)
        }

        return normalized.copy(
            joints = current.values
                .sortedWith(compareBy<JointProgram> { it.pivot }.thenBy { it.a }.thenBy { it.b })
        )
    }

    private fun CreatureGenome.pruneInvalidJoints(): CreatureGenome {
        val normalized = normalized()
        val candidateSet = candidateJointKeys(buildAdjacency(normalized.points.size, normalized.edges)).toHashSet()
        return normalized.copy(joints = normalized.joints.filter { it.key() in candidateSet })
    }

    private fun repairGenome(
        genome: CreatureGenome,
        random: Random,
        config: EvolutionConfig
    ): CreatureGenome {
        var repaired = genome.normalized()
        repaired = repaired.repairNearZeroEdges()
        repaired = repaired.repairConnectivity()
        repaired = repaired.repairCrossingEdges()
        repaired = repaired.repairEdgeCount(random, config.targetEdgeCount)
        repaired = repaired.repairNearZeroEdges()
        repaired = repaired.repairConnectivity()
        repaired = repaired.repairCrossingEdges()
        repaired = repaired.repairAlwaysUpFeet(config.alwaysUpFootFraction)
        repaired = repaired.repairCyclingFeet(config.cyclingFootFraction, random)
        repaired = repaired.refreshJoints(random, config.targetJointActivations)
        repaired = repaired.repairActuationBudget(config.simulation.maxActuationBudgetPerCycle)
        return repaired.normalized()
    }

    private fun CreatureGenome.repairNearZeroEdges(): CreatureGenome {
        var repaired = normalized()
        var guard = 0
        while (guard < 64) {
            val degenerate = repaired.edges.firstOrNull {
                distance(repaired.points[it.a], repaired.points[it.b]) <= MIN_REST_EDGE_LENGTH
            }?.normalized() ?: break

            val merged = repaired.tryMergeEdgeEndpoints(degenerate)
            repaired =
                when {
                    merged != null -> merged
                    else -> repaired.dropEdge(degenerate)
                }.pruneInvalidJoints().normalized()
            guard++
        }
        return repaired
    }

    private fun CreatureGenome.repairCrossingEdges(): CreatureGenome {
        if (edges.size < 2) return this

        var repaired = normalized()
        var guard = 0
        while (guard < 64) {
            val crossing = findFirstEdgeCrossing(repaired.points, repaired.edges) ?: break
            val first = repaired.edges[crossing.firstIndex].normalized()
            val second = repaired.edges[crossing.secondIndex].normalized()
            val newPointIndex = repaired.points.size
            val newPoints = repaired.points + crossing.point
            val newFeet = repaired.feet + ContactProgram(phase = 0f, duty = 0f, strength = 0f)
            val newEdges =
                repaired.edges.filterIndexed { index, _ ->
                    index != crossing.firstIndex && index != crossing.secondIndex
                } + listOf(
                    EdgeGene(first.a, newPointIndex),
                    EdgeGene(first.b, newPointIndex),
                    EdgeGene(second.a, newPointIndex),
                    EdgeGene(second.b, newPointIndex)
                )

            repaired = repaired.copy(
                points = centerPoints(newPoints),
                feet = newFeet,
                edges = newEdges
            ).normalized()
            guard++
        }
        return repaired
    }

    private fun CreatureGenome.repairConnectivity(): CreatureGenome {
        var repaired = normalized()
        var guard = 0
        while (guard < 64) {
            val components = connectedComponents(repaired.points.size, repaired.edges)
            if (components.size <= 1) break

            val degrees = degreeCounts(repaired.points.size, repaired.edges)
            val candidate = bestConnectionCandidate(repaired.points, components, degrees)
            repaired =
                if (candidate != null) {
                    repaired.copy(edges = repaired.edges + EdgeGene(candidate.a, candidate.b))
                } else {
                    val primary = components.maxWithOrNull(
                        compareBy<List<Int>>({ it.size }, { -it.minOrNull().orEmptyIndex() })
                    ) ?: break
                    repaired.keepOnlyPoints(primary.toHashSet())
                }.pruneInvalidJoints().normalized()
            guard++
        }
        return repaired
    }

    private fun CreatureGenome.tryMergeEdgeEndpoints(edge: EdgeGene): CreatureGenome? {
        if (points.size <= 2) return null

        val keep = min(edge.a, edge.b)
        val remove = max(edge.a, edge.b)
        val mergedPoint = (points[keep] + points[remove]) * 0.5f
        val mergedFoot = mergeContacts(feet[keep], feet[remove])

        fun remap(value: Int): Int =
            when {
                value == remove -> keep
                value < remove -> value
                else -> value - 1
            }

        val newPoints = points.toMutableList().apply {
            this[keep] = mergedPoint
            removeAt(remove)
        }
        val newFeet = feet.toMutableList().apply {
            this[keep] = mergedFoot
            removeAt(remove)
        }
        val newEdges = edges.mapNotNull { current ->
            val remapped = EdgeGene(remap(current.a), remap(current.b)).normalized()
            if (remapped.a == remapped.b) null else remapped
        }
        if (newEdges.isEmpty()) return null

        val newJoints = joints.mapNotNull { joint ->
            val remapped = JointProgram(
                pivot = remap(joint.pivot),
                a = remap(joint.a),
                b = remap(joint.b),
                targets = joint.targets
            ).normalized()
            if (remapped.a == remapped.b || remapped.a == remapped.pivot || remapped.b == remapped.pivot) null else remapped
        }

        val merged = copy(
            points = centerPoints(newPoints),
            feet = newFeet,
            edges = newEdges,
            joints = newJoints
        ).normalized()

        val maxDegree = degreeCounts(merged.points.size, merged.edges).maxOrNull() ?: 0
        return if (maxDegree <= 4) merged else null
    }

    private fun CreatureGenome.dropEdge(edge: EdgeGene): CreatureGenome =
        copy(edges = edges.filterNot { it.normalized() == edge.normalized() }).normalized()

    private fun CreatureGenome.keepOnlyPoints(keep: Set<Int>): CreatureGenome {
        if (keep.isEmpty() || keep.size == points.size) return this

        val ordered = keep.toList().sorted()
        val remap = IntArray(points.size) { -1 }
        ordered.forEachIndexed { newIndex, oldIndex -> remap[oldIndex] = newIndex }

        val newPoints = ordered.map { points[it] }
        val newFeet = ordered.map { feet[it] }
        val newEdges = edges.mapNotNull { edge ->
            val a = remap[edge.a]
            val b = remap[edge.b]
            if (a == -1 || b == -1) null else EdgeGene(a, b)
        }
        val newJoints = joints.mapNotNull { joint ->
            val pivot = remap[joint.pivot]
            val a = remap[joint.a]
            val b = remap[joint.b]
            if (pivot == -1 || a == -1 || b == -1) null
            else JointProgram(
                pivot = pivot,
                a = a,
                b = b,
                targets = joint.targets
            )
        }

        return copy(
            points = centerPoints(newPoints),
            feet = newFeet,
            edges = newEdges,
            joints = newJoints
        ).normalized()
    }

    private fun CreatureGenome.repairEdgeCount(random: Random, targetEdgeCount: Int?): CreatureGenome {
        if (targetEdgeCount == null) return this
        val desired = targetEdgeCount.coerceIn(points.size - 1, maxReachableEdgeCount(points.size))
        var repaired = this
        var guard = 0
        while (repaired.edges.size < desired && guard < 32) {
            val next = addRandomEdge(repaired, random)
            if (next.edges.size == repaired.edges.size) break
            repaired = next
            guard++
        }

        guard = 0
        while (repaired.edges.size > desired && guard < 32) {
            val next = removeRedundantEdge(repaired, random)
            if (next.edges.size == repaired.edges.size) break
            repaired = next
            guard++
        }
        return repaired
    }

    private fun CreatureGenome.repairAlwaysUpFeet(alwaysUpFootFraction: Float): CreatureGenome {
        val fraction = alwaysUpFootFraction.coerceIn(0f, 1f)
        if (fraction <= 1e-6f) return normalized()
        val pointCount = points.size
        val alwaysUpCount = floor(pointCount * fraction).toInt().coerceIn(0, pointCount)
        if (alwaysUpCount <= 0) return normalized()
        if (alwaysUpCount >= pointCount) {
            return copy(feet = List(pointCount) { ContactProgram(phase = 0f, duty = 0f, strength = 0f) })
        }

        val normalized = normalized()
        val ranked = normalized.feet.indices.sortedWith(
            compareByDescending<Int> { normalized.feet[it].strength }
                .thenBy { normalized.points[it].y }
        )
        val alwaysUpSet = ranked.takeLast(alwaysUpCount).toHashSet()
        val repairedFeet = normalized.feet.mapIndexed { index, foot ->
            if (index in alwaysUpSet) {
                ContactProgram(
                    phase = 0f,
                    duty = 0f,
                    smooth = foot.smooth.coerceIn(0.05f, 0.95f),
                    strength = 0f
                )
            } else {
                foot.normalized()
            }
        }
        return normalized.copy(feet = repairedFeet)
    }

    private fun CreatureGenome.repairCyclingFeet(cyclingFootFraction: Float, random: Random): CreatureGenome {
        val fraction = cyclingFootFraction.coerceIn(0f, 1f)
        val normalized = normalized()
        if (fraction <= 1e-6f) return normalized
        val pointCount = normalized.points.size
        val requiredCyclingCount = ceil(pointCount * fraction).toInt().coerceIn(0, pointCount)
        if (requiredCyclingCount <= 0) return normalized

        val feet = normalized.feet.toMutableList()
        val currentCycling = feet.indices.filter { feet[it].hasFootCycleEvent() }.toMutableSet()
        if (currentCycling.size >= requiredCyclingCount) return normalized

        val candidates =
            feet.indices
                .filterNot { it in currentCycling }
                .sortedWith(
                    compareBy<Int> { feet[it].isAlwaysUpFoot() }
                        .thenByDescending { feet[it].strength }
                        .thenBy { normalized.points[it].y }
                )

        candidates.forEach { index ->
            if (currentCycling.size >= requiredCyclingCount) return@forEach
            feet[index] = feet[index].asCyclingFoot(random)
            currentCycling += index
        }

        return normalized.copy(feet = feet)
    }

    private fun ContactProgram.isAlwaysUpFoot(): Boolean {
        val normalized = normalized()
        return normalized.strength <= 1e-4f || normalized.duty <= 1e-4f
    }

    private fun ContactProgram.hasFootCycleEvent(): Boolean {
        val normalized = normalized()
        return normalized.strength > 1e-4f && normalized.duty > 1e-4f && normalized.duty < 0.95f
    }

    private fun ContactProgram.asCyclingFoot(random: Random): ContactProgram {
        val normalized = normalized()
        val phase =
            if (normalized.hasFootCycleEvent()) {
                normalized.phase
            } else {
                random.nextFloat()
            }
        return normalized.copy(
            phase = phase,
            duty = normalized.duty.coerceIn(0.14f, 0.72f),
            smooth = normalized.smooth.coerceIn(0.05f, 0.9f),
            strength = normalized.strength.coerceIn(0.55f, 1f)
        )
    }

    private fun CreatureGenome.repairActuationBudget(maxBudgetPerCycle: Float): CreatureGenome {
        val normalized = normalized()
        val clampedBudget = maxBudgetPerCycle.coerceAtLeast(0f)
        if (normalized.joints.isEmpty() || normalized.jointTorqueScale <= 1e-6f) return normalized
        if (clampedBudget <= 1e-6f) return normalized.copy(jointTorqueScale = 0f)

        val currentBudget = normalized.programmedActuationBudgetPerCycle()
        if (currentBudget <= clampedBudget + 1e-5f) return normalized

        val scale = (clampedBudget / currentBudget).coerceIn(0f, 1f)
        return normalized.copy(jointTorqueScale = normalized.jointTorqueScale * scale)
    }

    private fun crossoverJoints(base: CreatureGenome, other: CreatureGenome, random: Random): List<JointProgram> {
        val candidateSet = candidateJointKeys(buildAdjacency(base.points.size, base.edges)).toHashSet()
        val baseMap = base.joints.filter { it.key() in candidateSet }.associateBy { it.key() }
        val otherMap = other.joints.filter { it.key() in candidateSet }.associateBy { it.key() }
        val keys = (baseMap.keys + otherMap.keys).toMutableSet()
        if (keys.isEmpty()) return emptyList()

        val joints = mutableListOf<JointProgram>()
        keys.shuffled(random).forEach { key ->
            val left = baseMap[key]
            val right = otherMap[key]
            val selected =
                when {
                    left != null && right != null -> {
                        when (random.nextInt(3)) {
                            0 -> left
                            1 -> right
                            else -> JointProgram(
                                pivot = key.pivot,
                                a = key.a,
                                b = key.b,
                                targets = mixJointTargets(left.targets, right.targets, random)
                            )
                        }
                    }
                    left != null -> left
                    else -> right
                } ?: return@forEach

            if (chance(random, 0.8f)) joints += selected
        }
        return joints.distinctBy { it.key() }
    }

    private fun crossoverEdges(
        base: CreatureGenome,
        other: CreatureGenome,
        points: List<Float2>,
        random: Random
    ): List<EdgeGene> {
        val pointCount = points.size
        val baseEdges = base.edges
            .map { it.normalized() }
            .filter { it.a in 0 until pointCount && it.b in 0 until pointCount }
            .distinct()
        if (baseEdges.isEmpty()) return emptyList()
        if (other.points.size != pointCount) return baseEdges

        val otherEdges = other.edges
            .map { it.normalized() }
            .filter { it.a in 0 until pointCount && it.b in 0 until pointCount }
            .distinct()

        val baseSet = baseEdges.toHashSet()
        val otherSet = otherEdges.toHashSet()
        val shared = baseEdges.filter { it in otherSet }
        val exclusive = (baseEdges + otherEdges)
            .distinct()
            .filterNot { it in shared }
            .shuffled(random)

        val selected = mutableListOf<EdgeGene>()
        val degrees = IntArray(pointCount)

        fun tryAdd(edge: EdgeGene): Boolean {
            val normalized = edge.normalized()
            if (normalized.a == normalized.b) return false
            if (degrees[normalized.a] >= 4 || degrees[normalized.b] >= 4) return false
            if (distance(points[normalized.a], points[normalized.b]) <= MIN_REST_EDGE_LENGTH) return false
            if (wouldCrossExistingEdges(points, selected, normalized)) return false
            selected += normalized
            degrees[normalized.a]++
            degrees[normalized.b]++
            return true
        }

        shared.shuffled(random).forEach(::tryAdd)

        val targetEdgeCount = max(
            1,
            (((baseEdges.size + otherEdges.size) * 0.5f).roundToInt())
        ).coerceAtMost(maxReachableEdgeCount(pointCount))

        exclusive.forEach { edge ->
            val mustFill = selected.size < targetEdgeCount
            val keep = mustFill || chance(random, 0.6f)
            if (keep) tryAdd(edge)
        }

        if (selected.isEmpty()) {
            baseEdges.shuffled(random).firstOrNull { tryAdd(it) }
        }

        return selected.ifEmpty { baseEdges }
    }

    private fun mixContact(a: ContactProgram, b: ContactProgram, random: Random): ContactProgram =
        ContactProgram(
            phase = wrap01(lerpPhase(a.phase, b.phase, random.nextFloat())),
            duty = lerp(a.duty, b.duty, random.nextFloat()),
            smooth = lerp(a.smooth, b.smooth, random.nextFloat()),
            strength = lerp(a.strength, b.strength, random.nextFloat())
        )

    private fun mergeContacts(a: ContactProgram, b: ContactProgram): ContactProgram =
        ContactProgram(
            phase = wrap01(lerpPhase(a.phase, b.phase, 0.5f)),
            duty = lerp(a.duty, b.duty, 0.5f),
            smooth = lerp(a.smooth, b.smooth, 0.5f),
            strength = max(a.strength, b.strength)
        ).normalized()

    private fun randomJoint(random: Random, key: JointKey): JointProgram =
        JointProgram(
            pivot = key.pivot,
            a = key.a,
            b = key.b,
            targets = randomJointTargets(random)
        )

    private fun tournament(
        scored: List<ScoredGenome>,
        tournamentSize: Int,
        random: Random
    ): ScoredGenome {
        var best = scored[random.nextInt(scored.size)]
        repeat(max(0, tournamentSize - 1)) {
            val challenger = scored[random.nextInt(scored.size)]
            if (challenger.evaluation.fitness > best.evaluation.fitness) {
                best = challenger
            }
        }
        return best
    }

    private fun effectiveImmigrantCount(
        config: EvolutionConfig,
        stagnating: Boolean
    ): Int {
        val fraction = if (stagnating) config.stagnationImmigrantFraction else config.immigrantFraction
        val fractionalCount = ceil(config.populationSize * fraction).toInt()
        return max(config.immigrantsPerGeneration, fractionalCount)
    }

    private fun effectiveMutationRange(
        config: EvolutionConfig,
        stagnating: Boolean
    ): IntRange =
        if (!stagnating) {
            config.mutationSteps
        } else {
            (config.mutationSteps.first + config.stagnationMutationBoost.first)..
                (config.mutationSteps.last + config.stagnationMutationBoost.last)
        }

    private suspend fun evolutionCheckpoint(controller: EvolutionController?): Boolean {
        currentCoroutineContext().ensureActive()
        if (controller?.isStopRequested() == true) return false
        kotlinx.coroutines.yield()
        currentCoroutineContext().ensureActive()
        return controller?.isStopRequested() != true
    }
}

private class CreatureRuntime(
    private val genome: CreatureGenome,
    private val config: CreatureSimulationConfig
) {
    private val pointCount = genome.points.size
    private val edgeRests = genome.edges.map {
        EdgeRest(it.a, it.b, distance(genome.points[it.a], genome.points[it.b]))
    }
    private val adjacency = buildAdjacency(pointCount, genome.edges)
    private val clock = RuntimeClock()
    // The programs are sampled at fixed phases, so sample each once per cycle step rather than every step.
    private val cycleSteps = max(1, config.stepsPerCycle)
    private val footSignal = Array(genome.feet.size) { f ->
        FloatArray(cycleSteps) { genome.feet[f].sample(it / cycleSteps.toFloat()) }
    }
    private val jointSignal = Array(genome.joints.size) { j ->
        FloatArray(cycleSteps) { genome.joints[j].sample(it / cycleSteps.toFloat()) }
    }
    private val contactValues = FloatArray(pointCount)
    private val contactAnchors = Array<Float2?>(pointCount) { null }
    private val programmedActuationBudgetPerCycle = genome.programmedActuationBudgetPerCycle()
    private val jointRestAngles = FloatArray(genome.joints.size) { jointIndex ->
        val joint = genome.joints[jointIndex]
        val a = genome.points[joint.a] - genome.points[joint.pivot]
        val b = genome.points[joint.b] - genome.points[joint.pivot]
        signedAngle(a, b)
    }
    private val filteredJointTargets = FloatArray(genome.joints.size) { jointIndex ->
        normalizeAngle(jointRestAngles[jointIndex] + genome.joints[jointIndex].sample(0f) * config.jointTargetAngleScale)
    }
    private val stepJointTorques = FloatArray(genome.joints.size)
    private val stepJointTargetSlew = FloatArray(genome.joints.size)
    private var stepActuationTorque = 0f

    private val state = SimdVerlet.State3(
        x = FloatArray(0),
        y = FloatArray(0),
        z = FloatArray(0),
        px = FloatArray(0),
        py = FloatArray(0),
        pz = FloatArray(0),
        ax = FloatArray(0),
        ay = FloatArray(0),
        az = FloatArray(0),
        invMass = FloatArray(0)
    )

    private val constraints = SimdVerlet.DirectedConstraints4(
        n0 = IntArray(pointCount) { -1 },
        n1 = IntArray(pointCount) { -1 },
        n2 = IntArray(pointCount) { -1 },
        n3 = IntArray(pointCount) { -1 },
        r0 = FloatArray(pointCount),
        r1 = FloatArray(pointCount),
        r2 = FloatArray(pointCount),
        r3 = FloatArray(pointCount),
        s0 = FloatArray(pointCount),
        s1 = FloatArray(pointCount),
        s2 = FloatArray(pointCount),
        s3 = FloatArray(pointCount)
    )

    private val sim = SimdVerlet(state, null)
    private val masses = pointMasses(genome.points, edgeRests, genome.massPerLength, genome.basePointMass)

    init {
        config.contact.place(genome.points).forEachIndexed { index, point ->
            val invMass = 1f / max(0.05f, masses[index])
            sim.addParticle(
                x0 = point.x,
                y0 = point.y,
                z0 = 0f,
                px0 = point.x,
                py0 = point.y,
                pz0 = 0f,
                invMass0 = invMass
            )
        }

        val restLookup = HashMap<Long, Float>(edgeRests.size * 2)
        edgeRests.forEach { edge ->
            restLookup[edgeKey(edge.a, edge.b)] = edge.restLength
        }

        adjacency.forEachIndexed { index, neighbors ->
            neighbors.forEachIndexed { slot, neighbor ->
                constraints.setSlot(
                    index = index,
                    slot = slot,
                    neighbor = neighbor,
                    restLength = restLookup[edgeKey(index, neighbor)] ?: 0f,
                    stiffness = genome.springStiffness
                )
            }
        }

        installContactForces()
        installJointForces()
    }

    fun run(): CreatureEvaluation {
        val stepsPerCycle = max(1, config.stepsPerCycle)
        val totalSteps = max(1, (config.warmupCycles + config.measuredCycles) * stepsPerCycle)
        val warmupSteps = max(0, config.warmupCycles * stepsPerCycle)
        val dt = genome.clockPeriod / stepsPerCycle.toFloat()
        val direction = config.targetDirection.normalizedOr()

        var measuredStart = currentCenter()
        val midStep = warmupSteps + (totalSteps - warmupSteps) / 2
        var midCenter: Float2? = null
        var previousCenter = measuredStart
        var finalCenter = measuredStart
        var stabilityPreviousCenter = measuredStart
        var forwardProgress = 0f
        var stretchAccum = 0f
        var actuationAccum = 0f
        var targetSlewAccum = 0f
        var speedAccum = 0f
        var measuredSamples = 0
        var exploded = false
        val frames = mutableListOf<CreatureSnapshot>()

        for (step in 0 until totalSteps) {
            val phase = wrap01((step % stepsPerCycle) / stepsPerCycle.toFloat())
            clock.phase = phase
            clock.step = step % stepsPerCycle
            stepActuationTorque = 0f
            stepJointTorques.fill(0f)
            stepJointTargetSlew.fill(0f)

            sim.step(
                dt = dt,
                damping = genome.damping,
                gravity = 0f, // added per point as a force in installContactForces: SimdVerlet would divide it by mass
                iterations = config.solverIterations,
                constraints = constraints
            )

            if (hasExploded()) {
                exploded = true
                break
            }

            val center = currentCenter()
            val centerSpeed = (center - stabilityPreviousCenter).length() / max(1e-5f, dt)
            stabilityPreviousCenter = center
            val stepMeanStretch = meanStretchError()
            val stepPeakStretch = maxStretchError()
            if (isUnstable(centerSpeed, stepMeanStretch, stepPeakStretch)) {
                exploded = true
                break
            }
            if (step == warmupSteps) {
                measuredStart = center
                previousCenter = center
            }

            if (step >= warmupSteps) {
                val delta = center - previousCenter
                forwardProgress += max(0f, delta.dot(direction))
                speedAccum += delta.length() / max(1e-5f, dt)
                stretchAccum += stepMeanStretch
                actuationAccum += stepActuationTorque / stepsPerCycle.toFloat()
                targetSlewAccum += stepJointTargetSlew.averageOrZero() / stepsPerCycle.toFloat()
                measuredSamples++
                previousCenter = center
                finalCenter = center
                if (step == midStep) midCenter = center

                if (config.recordFrames && ((step - warmupSteps) % max(1, config.recordEvery) == 0 || step == totalSteps - 1)) {
                    frames += snapshot(step, phase, center)
                }
            }
        }

        val displacement = finalCenter - measuredStart
        val netCentroidMotion = displacement.length()
        val forwardTravel = displacement.dot(direction)
        val lateralDrift = abs(displacement.perpDot(direction))
        val meanStretch = if (measuredSamples == 0) 0f else stretchAccum / measuredSamples.toFloat()
        val measuredCycles = measuredSamples.toFloat() / stepsPerCycle.toFloat()
        val meanActuationEffort = if (measuredCycles <= 1e-6f) 0f else actuationAccum / measuredCycles
        val meanTargetSlew = if (measuredCycles <= 1e-6f) 0f else targetSlewAccum / measuredCycles
        val meanSpeed = if (measuredSamples == 0) 0f else speedAccum / measuredSamples.toFloat()
        val actuationPenalty = config.actuationEffortPenalty.coerceAtLeast(0f)
        val targetSlewPenalty = config.targetSlewPenalty.coerceAtLeast(0f)

        val fitness =
            if (exploded) {
                config.explodedFitness
            } else {
                when (config.fitnessObjective) {
                    FitnessObjective.DIRECTIONAL_PROGRESS ->
                        forwardTravel +
                            0.20f * forwardProgress -
                            0.30f * lateralDrift -
                            2.25f * meanStretch -
                            actuationPenalty * meanActuationEffort -
                            targetSlewPenalty * meanTargetSlew

                    FitnessObjective.NET_CENTROID_DISPLACEMENT ->
                        netCentroidMotion -
                            2.0f * meanStretch -
                            actuationPenalty * meanActuationEffort -
                            targetSlewPenalty * meanTargetSlew
                }
            }

        val evaluation = CreatureEvaluation(
            genome = genome,
            fitness = fitness,
            netCentroidMotion = netCentroidMotion,
            displacement = displacement,
            forwardTravel = forwardTravel,
            forwardProgress = forwardProgress,
            lateralDrift = lateralDrift,
            meanStretchError = meanStretch,
            meanActuationEffort = meanActuationEffort,
            programmedActuationBudgetPerCycle = programmedActuationBudgetPerCycle,
            meanTargetSlew = meanTargetSlew,
            meanSpeed = meanSpeed,
            frames = frames,
            exploded = exploded,
            firstHalfDisplacement = (midCenter ?: finalCenter) - measuredStart,
            secondHalfDisplacement = finalCenter - (midCenter ?: finalCenter)
        )
        val custom = config.fitness
        return if (custom == null || exploded) evaluation
        else evaluation.copy(fitness = custom(evaluation).takeIf { it.isFinite() } ?: config.explodedFitness)
    }

    private fun installContactForces() {
        genome.feet.forEachIndexed { index, program ->
            val foot = config.contact.foot(index)
            sim.addForceGenerator(index, "contact-$index", generator2 {
                while (true) {
                    val ctx = yield()
                    val dt = max(1e-5f, ctx.dt)
                    val position = Float2(ctx.state.x[index], ctx.state.y[index])
                    val velocity = Float2(
                        (ctx.state.x[index] - ctx.state.px[index]) / dt,
                        (ctx.state.y[index] - ctx.state.py[index]) / dt
                    )
                    val step = FootStep(footSignal[index][clock.step], position, velocity, genome)
                    val force = foot.force(step)
                    contactValues[index] = foot.grip
                    contactAnchors[index] = foot.anchor
                    ctx.state.ax[index] += force.x
                    ctx.state.ay[index] += force.y + config.gravity * masses[index]
                }
            })
        }
    }

    private fun installJointForces() {
        genome.joints.forEachIndexed { jointIndex, joint ->
            sim.addForceGenerator(joint.pivot, "joint-$jointIndex", generator2 {
                while (true) {
                    val ctx = yield()
                    val dt = max(1e-5f, ctx.dt)
                    val pose = currentJointMotorPose(joint, jointRestAngles[jointIndex], ctx, dt) ?: continue
                    val desiredSignal = jointSignal[jointIndex][clock.step]
                    val desiredTarget = pose.restAngle + desiredSignal * config.jointTargetAngleScale
                    val previousTarget = filteredJointTargets[jointIndex]
                    val targetBlend =
                        if (config.jointTargetFilterRate <= 1e-6f) 1f
                        else (1f - exp(-config.jointTargetFilterRate * dt)).coerceIn(0f, 1f)
                    val filteredTarget = blendAngle(previousTarget, desiredTarget, targetBlend)
                    filteredJointTargets[jointIndex] = filteredTarget
                    stepJointTargetSlew[jointIndex] =
                        abs(shortestAngleDelta(previousTarget, filteredTarget)) / max(1e-5f, dt)

                    val angleError = shortestAngleDelta(pose.currentAngle, filteredTarget)

                    var appliedTorque =
                        (config.jointMotorKp * angleError - config.jointMotorKd * pose.angleVelocity) *
                            genome.jointTorqueScale
                    val filteredSignal =
                        if (abs(config.jointTargetAngleScale) <= 1e-6f) 0f
                        else shortestAngleDelta(pose.restAngle, filteredTarget) / config.jointTargetAngleScale
                    val motorIntent = max(abs(desiredSignal), abs(filteredSignal)).coerceIn(0f, 1f)
                    val maxMotorTorque = motorIntent * genome.jointTorqueScale
                    appliedTorque = appliedTorque.coerceIn(-maxMotorTorque, maxMotorTorque)
                    appliedTorque = clampJointTorqueByImpulse(appliedTorque, pose, dt)
                    stepJointTorques[jointIndex] = appliedTorque
                    if (abs(appliedTorque) <= 1e-6f) continue

                    val faX = pose.aCoeffX * appliedTorque
                    val faY = pose.aCoeffY * appliedTorque
                    val fbX = pose.bCoeffX * appliedTorque
                    val fbY = pose.bCoeffY * appliedTorque

                    ctx.state.ax[joint.a] += faX
                    ctx.state.ay[joint.a] += faY
                    ctx.state.ax[joint.b] += fbX
                    ctx.state.ay[joint.b] += fbY

                    ctx.state.ax[joint.pivot] += pose.pivotCoeffX * appliedTorque
                    ctx.state.ay[joint.pivot] += pose.pivotCoeffY * appliedTorque
                    stepActuationTorque += abs(appliedTorque)
                }
            })
        }
    }

    private fun snapshot(step: Int, phase: Float, center: Float2): CreatureSnapshot =
        CreatureSnapshot(
            step = step,
            phase = phase,
            center = center,
            points = currentPoints(),
            down = List(pointCount) { contactValues[it] > 0.35f },
            anchors = contactAnchors.toList(),
            jointTorques = stepJointTorques.toList(),
            jointTargets = filteredJointTargets.toList()
        )

    private fun currentPoints(): List<Float2> =
        List(pointCount) { index -> Float2(state.x[index], state.y[index]) }

    private fun currentCenter(): Float2 {
        var sx = 0f
        var sy = 0f
        for (i in 0 until pointCount) {
            sx += state.x[i]
            sy += state.y[i]
        }
        return Float2(sx / pointCount.toFloat(), sy / pointCount.toFloat())
    }

    private fun meanStretchError(): Float {
        if (edgeRests.isEmpty()) return 0f
        var sum = 0f
        for (edge in edgeRests) {
            val length = distance(
                Float2(state.x[edge.a], state.y[edge.a]),
                Float2(state.x[edge.b], state.y[edge.b])
            )
            sum += abs(length - edge.restLength) / max(1e-4f, edge.restLength)
        }
        return sum / edgeRests.size.toFloat()
    }

    private fun maxStretchError(): Float {
        if (edgeRests.isEmpty()) return 0f
        var maxStretch = 0f
        for (edge in edgeRests) {
            val length = distance(
                Float2(state.x[edge.a], state.y[edge.a]),
                Float2(state.x[edge.b], state.y[edge.b])
            )
            val stretch = abs(length - edge.restLength) / max(1e-4f, edge.restLength)
            if (stretch > maxStretch) {
                maxStretch = stretch
            }
        }
        return maxStretch
    }

    private fun isUnstable(centerSpeed: Float, meanStretch: Float, peakStretch: Float): Boolean {
        if (!centerSpeed.isFinite() || !meanStretch.isFinite() || !peakStretch.isFinite()) return true
        if (centerSpeed > config.maxCenterSpeed) return true
        if (meanStretch > config.maxMeanStretchError) return true
        if (peakStretch > config.maxPeakStretchError) return true
        return false
    }

    private fun currentJointMotorPose(
        joint: JointProgram,
        restAngle: Float,
        ctx: SimdVerlet.ForceContext,
        dt: Float
    ): JointMotorPose? {
        val pivot = joint.pivot
        val a = joint.a
        val b = joint.b
        val ax = ctx.state.x[a] - ctx.state.x[pivot]
        val ay = ctx.state.y[a] - ctx.state.y[pivot]
        val bx = ctx.state.x[b] - ctx.state.x[pivot]
        val by = ctx.state.y[b] - ctx.state.y[pivot]

        val aLen2 = max(1e-4f, ax * ax + ay * ay)
        val bLen2 = max(1e-4f, bx * bx + by * by)
        val aLen = sqrt(aLen2)
        val bLen = sqrt(bLen2)
        if (aLen <= 1e-4f || bLen <= 1e-4f) return null

        val pivotVx = (ctx.state.x[pivot] - ctx.state.px[pivot]) / dt
        val pivotVy = (ctx.state.y[pivot] - ctx.state.py[pivot]) / dt
        val aVx = (ctx.state.x[a] - ctx.state.px[a]) / dt - pivotVx
        val aVy = (ctx.state.y[a] - ctx.state.py[a]) / dt - pivotVy
        val bVx = (ctx.state.x[b] - ctx.state.px[b]) / dt - pivotVx
        val bVy = (ctx.state.y[b] - ctx.state.py[b]) / dt - pivotVy

        val invMass = ctx.state.invMass
        val aInvMass = invMass?.get(a) ?: 1f
        val bInvMass = invMass?.get(b) ?: 1f
        val pivotInvMass = invMass?.get(pivot) ?: 1f

        val omegaA = (ax * aVy - ay * aVx) / aLen2
        val omegaB = (bx * bVy - by * bVx) / bLen2

        val aCoeffX = ay / aLen2
        val aCoeffY = -ax / aLen2
        val bCoeffX = -by / bLen2
        val bCoeffY = bx / bLen2

        return JointMotorPose(
            restAngle = restAngle,
            currentAngle = signedAngle(Float2(ax, ay), Float2(bx, by)),
            angleVelocity = omegaB - omegaA,
            aLen2 = aLen2,
            bLen2 = bLen2,
            aInvMass = aInvMass,
            bInvMass = bInvMass,
            pivotInvMass = pivotInvMass,
            aCoeffX = aCoeffX,
            aCoeffY = aCoeffY,
            bCoeffX = bCoeffX,
            bCoeffY = bCoeffY,
            pivotCoeffX = -(aCoeffX + bCoeffX),
            pivotCoeffY = -(aCoeffY + bCoeffY)
        )
    }

    private fun clampJointTorqueByImpulse(
        torque: Float,
        pose: JointMotorPose,
        dt: Float
    ): Float {
        val maxImpulse = config.maxJointImpulsePerStep.coerceAtLeast(0f)
        if (maxImpulse <= 1e-6f || abs(torque) <= 1e-6f) return torque

        val aCoeff = (1f / sqrt(pose.aLen2)) * pose.aInvMass
        val bCoeff = (1f / sqrt(pose.bLen2)) * pose.bInvMass
        val pivotCoeff =
            sqrt(pose.pivotCoeffX * pose.pivotCoeffX + pose.pivotCoeffY * pose.pivotCoeffY) *
                pose.pivotInvMass
        val maxCoeff = max(aCoeff, max(bCoeff, pivotCoeff))
        if (maxCoeff <= 1e-6f) return torque

        val torqueLimit = maxImpulse / (dt * maxCoeff)
        return torque.coerceIn(-torqueLimit, torqueLimit)
    }

    private fun hasExploded(): Boolean {
        for (i in 0 until pointCount) {
            val x = state.x[i]
            val y = state.y[i]
            if (!x.isFinite() || !y.isFinite()) return true
            if (abs(x) > config.maxAbsCoordinate || abs(y) > config.maxAbsCoordinate) return true
        }
        return false
    }
}

private fun pointMasses(
    points: List<Float2>,
    edges: List<EdgeRest>,
    massPerLength: Float,
    basePointMass: Float
): FloatArray {
    val masses = FloatArray(points.size) { basePointMass }
    edges.forEach { edge ->
        val share = edge.restLength * massPerLength * 0.5f
        masses[edge.a] += share
        masses[edge.b] += share
    }
    return masses
}

private fun CreatureGenome.programmedActuationBudgetPerCycle(): Float {
    val normalized = normalized()
    if (normalized.joints.isEmpty() || normalized.jointTorqueScale <= 1e-6f) return 0f

    var meanAbsSignal = 0f
    normalized.joints.forEach { joint ->
        meanAbsSignal += joint.meanAbsSignal()
    }
    return meanAbsSignal * normalized.jointTorqueScale
}

private fun maxReachableEdgeCount(pointCount: Int): Int =
    min((pointCount * 4) / 2, pointCount * (pointCount - 1) / 2)

private fun buildAdjacency(pointCount: Int, edges: List<EdgeGene>): List<List<Int>> {
    val adjacency = List(pointCount) { mutableListOf<Int>() }
    edges.map { it.normalized() }.forEach { edge ->
        adjacency[edge.a] += edge.b
        adjacency[edge.b] += edge.a
    }
    return adjacency.map { it.distinct().sorted() }
}

private fun candidateJointKeys(adjacency: List<List<Int>>): List<JointKey> {
    val keys = mutableListOf<JointKey>()
    adjacency.forEachIndexed { pivot, neighbors ->
        for (i in 0 until neighbors.size) {
            for (j in i + 1 until neighbors.size) {
                val a = min(neighbors[i], neighbors[j])
                val b = max(neighbors[i], neighbors[j])
                keys += JointKey(pivot, a, b)
            }
        }
    }
    return keys
}

private fun centroidOf(indices: Iterable<Int>, points: List<Float2>): Float2 {
    var sx = 0f
    var sy = 0f
    var count = 0
    indices.forEach { index ->
        sx += points[index].x
        sy += points[index].y
        count++
    }
    return if (count == 0) Float2(0f, 0f) else Float2(sx / count.toFloat(), sy / count.toFloat())
}

private fun degreeCounts(pointCount: Int, edges: List<EdgeGene>): IntArray {
    val degrees = IntArray(pointCount)
    edges.forEach { edge ->
        degrees[edge.a]++
        degrees[edge.b]++
    }
    return degrees
}

private fun isConnected(pointCount: Int, edges: List<EdgeGene>): Boolean {
    if (pointCount == 0) return true
    if (pointCount == 1) return true
    val adjacency = buildAdjacency(pointCount, edges)
    val visited = BooleanArray(pointCount)
    val queue = ArrayDeque<Int>()
    queue += 0
    visited[0] = true
    while (queue.isNotEmpty()) {
        val next = queue.removeFirst()
        adjacency[next].forEach { neighbor ->
            if (!visited[neighbor]) {
                visited[neighbor] = true
                queue += neighbor
            }
        }
    }
    return visited.all { it }
}

private fun connectedComponents(pointCount: Int, edges: List<EdgeGene>): List<List<Int>> {
    if (pointCount == 0) return emptyList()
    val adjacency = buildAdjacency(pointCount, edges)
    val visited = BooleanArray(pointCount)
    val components = mutableListOf<List<Int>>()
    for (start in 0 until pointCount) {
        if (visited[start]) continue
        val queue = ArrayDeque<Int>()
        val component = mutableListOf<Int>()
        queue += start
        visited[start] = true
        while (queue.isNotEmpty()) {
            val next = queue.removeFirst()
            component += next
            adjacency[next].forEach { neighbor ->
                if (!visited[neighbor]) {
                    visited[neighbor] = true
                    queue += neighbor
                }
            }
        }
        components += component.sorted()
    }
    return components
}

private fun bestConnectionCandidate(
    points: List<Float2>,
    components: List<List<Int>>,
    degrees: IntArray
): EdgeConnectionCandidate? {
    val sortedComponents = components.sortedWith(
        compareByDescending<List<Int>> { it.size }.thenBy { it.minOrNull().orEmptyIndex() }
    )
    val primary = sortedComponents.firstOrNull() ?: return null
    var best: EdgeConnectionCandidate? = null

    sortedComponents.drop(1).forEach { other ->
        primary.forEach { a ->
            if (degrees[a] >= 4) return@forEach
            other.forEach { b ->
                if (degrees[b] >= 4) return@forEach
                val length = distance(points[a], points[b])
                if (length <= MIN_REST_EDGE_LENGTH) return@forEach
                val candidate = EdgeConnectionCandidate(a, b, length)
                if (best == null || candidate.distance < best!!.distance) {
                    best = candidate
                }
            }
        }
    }

    return best
}

private fun Int?.orEmptyIndex(): Int = this ?: Int.MAX_VALUE

private fun remapAfterRemoval(value: Int, removedIndex: Int): Int =
    if (value < removedIndex) value else value - 1

private fun bridgeSet(pointCount: Int, edges: List<EdgeGene>): Set<EdgeGene> {
    val adjacency = buildAdjacency(pointCount, edges)
    val visited = BooleanArray(pointCount)
    val tin = IntArray(pointCount) { -1 }
    val low = IntArray(pointCount) { -1 }
    val bridges = mutableSetOf<EdgeGene>()
    var timer = 0

    fun dfs(v: Int, parent: Int) {
        visited[v] = true
        tin[v] = timer
        low[v] = timer
        timer++
        adjacency[v].forEach { to ->
            if (to == parent) return@forEach
            if (visited[to]) {
                low[v] = min(low[v], tin[to])
            } else {
                dfs(to, v)
                low[v] = min(low[v], low[to])
                if (low[to] > tin[v]) {
                    bridges += EdgeGene(v, to).normalized()
                }
            }
        }
    }

    if (pointCount > 0) dfs(0, -1)
    return bridges
}

private fun edgeCrossings(points: List<Float2>, edges: List<EdgeGene>): List<EdgeCrossing> {
    val normalizedEdges = edges.map { it.normalized() }
    val crossings = mutableListOf<EdgeCrossing>()
    for (i in 0 until normalizedEdges.size) {
        val first = normalizedEdges[i]
        for (j in i + 1 until normalizedEdges.size) {
            val second = normalizedEdges[j]
            if (sharesVertex(first, second)) continue
            val point =
                properSegmentIntersection(
                    points[first.a],
                    points[first.b],
                    points[second.a],
                    points[second.b]
                ) ?: continue
            crossings += EdgeCrossing(i, j, point)
        }
    }
    return crossings
}

private fun findFirstEdgeCrossing(points: List<Float2>, edges: List<EdgeGene>): EdgeCrossing? =
    edgeCrossings(points, edges).firstOrNull()

private fun wouldCrossExistingEdges(points: List<Float2>, edges: List<EdgeGene>, candidate: EdgeGene): Boolean {
    val normalizedCandidate = candidate.normalized()
    return edges.any { existing ->
        val normalizedExisting = existing.normalized()
        !sharesVertex(normalizedExisting, normalizedCandidate) &&
            properSegmentIntersection(
                points[normalizedExisting.a],
                points[normalizedExisting.b],
                points[normalizedCandidate.a],
                points[normalizedCandidate.b]
            ) != null
    }
}

private fun sharesVertex(a: EdgeGene, b: EdgeGene): Boolean =
    a.a == b.a || a.a == b.b || a.b == b.a || a.b == b.b

private fun properSegmentIntersection(
    a0: Float2,
    a1: Float2,
    b0: Float2,
    b1: Float2,
    epsilon: Float = 1e-4f
): Float2? {
    val r = a1 - a0
    val s = b1 - b0
    val rxs = r.perpDot(s)
    if (abs(rxs) <= epsilon) return null

    val delta = b0 - a0
    val t = delta.perpDot(s) / rxs
    val u = delta.perpDot(r) / rxs
    if (t <= epsilon || t >= 1f - epsilon || u <= epsilon || u >= 1f - epsilon) return null

    return a0 + r * t
}

private fun edgeKey(a: Int, b: Int): Long {
    val lo = min(a, b)
    val hi = max(a, b)
    return (lo.toLong() shl 32) xor (hi.toLong() and 0xffffffffL)
}

private fun distance(a: Float2, b: Float2): Float =
    sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y))

private fun randomUnitVector(random: Random): Float2 {
    val angle = random.nextFloat() * (2f * PI.toFloat())
    return Float2(cos(angle), kotlin.math.sin(angle))
}

private fun centerPoints(points: List<Float2>): List<Float2> {
    if (points.isEmpty()) return points
    val center = points.reduce { acc, p -> acc + p } / points.size.toFloat()
    return points.map { it - center }
}

private fun lerp(a: Float, b: Float, t: Float): Float = a * (1f - t) + b * t

private fun lerpPhase(a: Float, b: Float, t: Float): Float {
    val delta = shortestPhaseDelta(a, b)
    return wrap01(a + delta * t)
}

private fun signedAngle(a: Float2, b: Float2): Float =
    atan2(a.perpDot(b), a.dot(b))

private fun shortestAngleDelta(from: Float, to: Float): Float {
    var delta = to - from
    val tau = (2f * PI.toFloat())
    while (delta > PI.toFloat()) delta -= tau
    while (delta < -PI.toFloat()) delta += tau
    return delta
}

private fun normalizeAngle(angle: Float): Float = shortestAngleDelta(0f, angle)

private fun blendAngle(from: Float, to: Float, t: Float): Float =
    normalizeAngle(from + shortestAngleDelta(from, to) * t.coerceIn(0f, 1f))

private fun shortestPhaseDelta(from: Float, to: Float): Float {
    var delta = wrap01(to) - wrap01(from)
    if (delta > 0.5f) delta -= 1f
    if (delta < -0.5f) delta += 1f
    return delta
}

private fun normalizedJointTargets(values: List<Float>): List<Float> {
    val resized =
        when {
            values.size == JOINT_TARGET_KNOT_COUNT -> values
            values.isEmpty() -> List(JOINT_TARGET_KNOT_COUNT) { 0f }
            values.size > JOINT_TARGET_KNOT_COUNT -> values.take(JOINT_TARGET_KNOT_COUNT)
            else -> values + List(JOINT_TARGET_KNOT_COUNT - values.size) { values.last() }
        }
    return resized.map { it.coerceIn(-MAX_JOINT_TARGET_SIGNAL, MAX_JOINT_TARGET_SIGNAL) }
}

private fun cyclicSplineSample(values: List<Float>, phase: Float): Float {
    val normalized = normalizedJointTargets(values)
    if (normalized.isEmpty()) return 0f
    if (normalized.size == 1) return normalized.first()

    val count = normalized.size
    val wrapped = wrap01(phase) * count
    val i1 = floor(wrapped).toInt().mod(count)
    val t = wrapped - floor(wrapped)
    val i0 = (i1 - 1).mod(count)
    val i2 = (i1 + 1).mod(count)
    val i3 = (i1 + 2).mod(count)
    return catmullRom(normalized[i0], normalized[i1], normalized[i2], normalized[i3], t)
        .coerceIn(-MAX_JOINT_TARGET_SIGNAL, MAX_JOINT_TARGET_SIGNAL)
}

private fun catmullRom(p0: Float, p1: Float, p2: Float, p3: Float, t: Float): Float {
    val t2 = t * t
    val t3 = t2 * t
    return 0.5f * (
        (2f * p1) +
            (-p0 + p2) * t +
            (2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
            (-p0 + 3f * p1 - 3f * p2 + p3) * t3
        )
}

private fun rotateValues(values: List<Float>, steps: Int): List<Float> {
    val normalized = normalizedJointTargets(values)
    if (normalized.isEmpty()) return normalized
    val shift = ((steps % normalized.size) + normalized.size) % normalized.size
    if (shift == 0) return normalized
    return List(normalized.size) { index -> normalized[(index - shift + normalized.size) % normalized.size] }
}

private fun mutateJointTargets(
    values: List<Float>,
    random: Random,
    edits: Int = random.nextInt(1, 3),
    sigma: Float = 0.16f
): List<Float> {
    val mutated = normalizedJointTargets(values).toMutableList()
    repeat(edits.coerceAtLeast(1)) {
        val index = random.nextInt(mutated.size)
        mutated[index] = (mutated[index] + gaussian(random, sigma))
            .coerceIn(-MAX_JOINT_TARGET_SIGNAL, MAX_JOINT_TARGET_SIGNAL)
    }
    if (chance(random, 0.45f)) {
        return smoothJointTargets(mutated, passes = 1)
    }
    return mutated
}

private fun smoothJointTargets(values: List<Float>, passes: Int = 1): List<Float> {
    var current = normalizedJointTargets(values)
    repeat(passes.coerceAtLeast(0)) {
        current =
            List(current.size) { index ->
                val prev = current[(index - 1 + current.size) % current.size]
                val next = current[(index + 1) % current.size]
                (prev * 0.22f + current[index] * 0.56f + next * 0.22f)
                    .coerceIn(-MAX_JOINT_TARGET_SIGNAL, MAX_JOINT_TARGET_SIGNAL)
            }
    }
    return current
}

private fun scaleJointTargets(values: List<Float>, scale: Float): List<Float> =
    normalizedJointTargets(values).map {
        (it * scale).coerceIn(-MAX_JOINT_TARGET_SIGNAL, MAX_JOINT_TARGET_SIGNAL)
    }

private fun mixJointTargets(a: List<Float>, b: List<Float>, random: Random): List<Float> {
    val left = normalizedJointTargets(a)
    val right = normalizedJointTargets(b)
    val mixed =
        List(JOINT_TARGET_KNOT_COUNT) { index ->
            lerp(left[index], right[index], random.nextFloat())
        }
    return smoothJointTargets(mixed, passes = 1)
}

private fun randomJointTargets(random: Random): List<Float> {
    val tau = 2f * PI.toFloat()
    val primaryAmplitude = randomFloat(random, 0.18f, 0.72f)
    val secondaryAmplitude = primaryAmplitude * randomFloat(random, 0f, 0.35f)
    val bias = randomFloat(random, -0.12f, 0.12f)
    val primaryPhase = random.nextFloat()
    val secondaryPhase = random.nextFloat()

    val targets =
        List(JOINT_TARGET_KNOT_COUNT) { index ->
            val t = index / JOINT_TARGET_KNOT_COUNT.toFloat()
            val primary = sin(tau * (t + primaryPhase))
            val secondary = sin(tau * (2f * t + secondaryPhase))
            (bias + primaryAmplitude * primary + secondaryAmplitude * secondary)
                .coerceIn(-MAX_JOINT_TARGET_SIGNAL, MAX_JOINT_TARGET_SIGNAL)
        }

    return smoothJointTargets(targets, passes = 1)
}

private fun notationSegments(program: ContactProgram): List<PhaseInterval> {
    val foot = program.normalized()
    if (foot.strength <= 1e-4f || foot.duty <= 1e-4f) return emptyList()
    val halfWidth = foot.duty.coerceIn(0f, 0.95f) * 0.5f
    if (halfWidth <= 1e-4f) return emptyList()

    val start = wrap01(foot.phase - halfWidth)
    val end = wrap01(foot.phase + halfWidth)
    return if (start < end) {
        listOf(PhaseInterval(start, end))
    } else {
        listOf(
            PhaseInterval(0f, end),
            PhaseInterval(start, 1f)
        )
    }
}

private fun wrap01(value: Float): Float {
    var wrapped = value % 1f
    if (wrapped < 0f) wrapped += 1f
    return wrapped
}

private fun smoothGate(current: Float, center: Float, duty: Float, smooth: Float): Float {
    val halfWidth = duty.coerceIn(0f, 1f) * 0.5f
    if (halfWidth <= 1e-4f) return 0f
    val d = abs(shortestPhaseDelta(center, current))
    if (d >= halfWidth) return 0f

    val ramp = (halfWidth * smooth.coerceIn(0.02f, 0.98f)).coerceAtLeast(1e-4f)
    if (d <= halfWidth - ramp) return 1f

    val t = ((halfWidth - d) / ramp).coerceIn(0f, 1f)
    return t * t * (3f - 2f * t)
}


private fun gaussian(random: Random, sigma: Float): Float {
    val u1 = max(1e-6f, random.nextFloat())
    val u2 = random.nextFloat()
    val mag = sqrt(-2f * kotlin.math.ln(u1))
    val angle = 2f * PI.toFloat() * u2
    return kotlin.math.cos(angle) * mag * sigma
}

private fun FloatArray.averageOrZero(): Float =
    if (isEmpty()) 0f else sum() / size.toFloat()

private fun randomFloat(random: Random, min: Float, max: Float): Float =
    min + (max - min) * random.nextFloat()

private fun chance(random: Random, probability: Float): Boolean =
    random.nextFloat() < probability.coerceIn(0f, 1f)