Ollin

SwarmChemistry

every particle carries its own copy of the rule it moves by. When two particles touch, one copy overwrites the other, so a recipe spreads when the particles that hold it keep meeting others and winning

Run
swift run --package-path Examples Example-Simulation-SwarmChemistry
Source
Examples/Simulation/SwarmChemistry/Sketch.swift
import Ollin

/// Swarm chemistry: every particle carries its own copy of the rule it moves by, and on
/// contact one copy overwrites the other. There is no generation boundary and nothing is
/// being scored. A recipe spreads because the particles holding it keep meeting particles
/// holding something else and winning, which turns out to be enough.
///
/// Six random recipes start out sharing the population equally. Color is the recipe
/// itself (cohesion, alignment and separation as red, green and blue), so a takeover is
/// visible as one color eating the others, and a mutation as a shift in shade rather
/// than a new color. Watch the tally: it usually ends with one line holding almost
/// everything, but which one is not decided in advance.
///
/// `competition` is what "winning" means, and it is the whole character of the run.
/// Turn transmission off and the recipes freeze, leaving the plain mixture of kinds the
/// model started as.
///
/// The six opening recipes are rolled from this sketch's `variation`, so every launch is
/// a different contest. Like Particle Life's matrix, most rolls are unremarkable and a
/// few are alive, and the interesting stretch is while several lines still hold ground:
/// once one has taken everything there is nothing left to decide. Step to another
/// variation for a fresh set.
@main
final class SwarmChemistry_Example: Sketch {
    var chem: SwarmChemistry!
    var tally: [Int] = []

    @Param(icon: "arrow.triangle.2.circlepath") var spreading = true
    @Param(icon: "trophy") var winner = SwarmChemistry.Competition.majority
    @Param(0 ... 1, icon: "sparkles") var mutation = 0.2
    @Param(0.01 ... 0.4, icon: "arrow.up.and.down") var mutationSize = 0.1

    override func setup() {
        chem = makeSwarmChemistry(count: 4000, kinds: 6)
    }

    override func draw() {
        chem.transmits = spreading
        chem.competition = winner
        chem.mutationRate = mutation
        chem.mutationAmount = mutationSize

        // A translucent wipe rather than a hard clear: these particles move a few points
        // a step, so a still frame of dots shows density where the trails show movement.
        background(Color(white: 0.04).withAlpha(0.14))
        stepSwarmChemistry(chem)
        drawParticles(chem)

        // Reading the tally stalls on the GPU, so do it a few times a second, not every
        // frame.
        if frameCount % 20 == 0 { tally = chem.snapshotLineageCounts() }
        drawTally()
    }

    /// The scoreboard the model never keeps for itself: how many particles each of the
    /// six opening lines still holds.
    private func drawTally() {
        guard !tally.isEmpty else { return }
        let total = max(tally.reduce(0, +), 1)
        let barWidth = 220.0, barHeight = 14.0, gap = 6.0
        let x = 40.0
        var y = height - 40 - (barHeight + gap) * Double(tally.count)
        textSize(13)
        for (line, held) in tally.enumerated() {
            let share: Double = Double(held) / Double(total)
            let hue: Double = Double(line) / Double(tally.count)
            let labelX: Double = x + barWidth + 8
            let labelY: Double = y + barHeight - 2
            noStroke()
            fill(Color(white: 0.22))
            drawRect(x, y, barWidth, barHeight)
            fill(Color(hue: hue, saturation: 0.6, brightness: 0.95))
            drawRect(x, y, barWidth * share, barHeight)
            fill(Color(white: 0.75))
            drawText(String(held), labelX, labelY)
            y += barHeight + gap
        }
        drawCaption("Swarm chemistry · 4,000 particles, 6 opening recipes · who is left")
    }
}

↑↓ move ↩ open