devils-dust opens a 1280×720
scribbler window on the inside of
a mill. A hundred wisps of wool flock by the three classic boids rules — a
famous 1987 recipe for lifelike flocking, described below — above the
devil, the toothed drum that tore rags back into fibre. The drum throws
them, loses them to gravity, and takes them back off the floor. A tabbed
tuning console lets you reshape the storm while it blows: twenty-one
sliders on four tabs, a five-position speed lever
(SLOW/NORMAL/MAX/ELEVEN
and a red STOP bar), live meters on the floor, and a
machine-room drone from the buzzer
that rises and falls with the drum. Everything answers to the mouse or the
keyboard; B mutes, H hides the panel,
Q quits.
The flocking model and the term "boids" are Craig W. Reynolds's
(red3d.com/cwr/boids, and see the
Wikipedia article); this mill is an original
implementation of his published model. See
THIRD-PARTY-NOTICES.md.
Two originals, sixty years apart. The flocking is Craig W. Reynolds's boids — "Flocks, Herds, and Schools: A Distributed Behavioral Model," Computer Graphics 21(4), from SIGGRAPH '87, the annual computer-graphics conference. That paper demonstrated that lifelike group motion needs no choreographer, only three local rules: separation, alignment, cohesion. Every murmuration — the swirling cloud a starling flock makes — in every film title sequence descends from it. This mill keeps the three rules textbook. It adds one steering force Reynolds's model explicitly allows: the draft rising off the devil.
The scene itself is older. Devil's dust was the airborne fibre
that filled a West Riding shoddy mill. The devil threw it off:
the spiked drum that tore woollen rags back into spinnable fibre, the
machine the whole industry (and
this language's name) was built around. The
demo is the language's origin story, animated: mixed rag-stock colours in
the wool, a drum that spins up with iron inertia, and dust that settles
where it falls until the sweep returns it to the machine. Even the parser
agrees — the tokenizer, the part of the compiler that tears Shoddy source
apart into words, lives in src/Shoddy.Devil/. As for the
lever position past MAX: Spinal Tap — the mock-rock film
whose amplifiers famously go up to eleven — owns the rest of that
sentence.
bin/mill run mills/devils-dust/devils-dust.shoddy
or, from mills/devils-dust/, use the wrapper —
./build.sh, or ./build.ps1 on Windows:
./build.sh # run the demo (also: ./build.sh run)
./build.sh test # run the headless model checks
Like every scribbler program it must run under mill run. A
woven dotnet FILE.dll has no window backend — nothing to open
a window with. The folder also carries sound-lab.shoddy, the
workbench the drone was designed in: no wool, just instruments — nine
square/triangle/sine recipes with live keys for pitch, detune, gain, and
churn. Run it the same way when you want to audition a sound before wiring
it in.
It also ships on the Shoddy Reckoner's games shelf — scribbler and buzzer together. The drawing is presented as a page, and the drone is rendered by the app's platform audio sink, the part of the app that hands sound to the operating system. The woven program is the same one, unmodified.
Every behavioural constant — twenty-one of them — lives in a
Knobs record carried inside the simulation state, and the
panel is a pure view of it: four tabs (LIFE,
TEMPO, FLOCK, PACE) of sliders.
Each slider is drawn from a knob table in the core that knows
every knob's name, rails (its legal limits), and accessors (the words that
read and write it). Tabs and knobs are sum types
(Type Tab, Type Knob) — types whose value is
always one of a fixed set of named cases. So every dispatch is a
Select Case over names, not magic numbers.
Controls set a goal; the machine winds toward
it. An 1875 devil is tons of iron on a belt drive, and it does not jump to
a new speed. The slider handle is your lever and moves the instant you set
it; the fill and the value are the machine catching up. The lever presets
are just parked goals. SLOW winds the motion down to about a
third, with the wool's buoyancy kept — slow wool is still flying wool.
NORMAL is the defaults. MAX is every knob at its
rail. And ELEVEN is every knob a quarter past its
rail, through the raw write the sliders themselves are never given. The
STOP bar kills most of the drive in the instant of the pull
and lets the rest coast down; the wool rains out and lies where it lands.
One colour language runs the whole console: brick red at the bottom,
mute yellow low, green at normal, ivory at the legal top, ember past it.
Sliders wear the heat of their value, the lever buttons wear their liveries,
and the floor meters each wear the heat of their own reading. A red
ALOFT is an empty sky; an ember SPEED is
eleven's doing.
The twenty-one sliders, by tab (defaults are the NORMAL
lever position; every rail is reachable by hand, and only
ELEVEN goes past them):
| Slider | Tab | Default | What it turns |
|---|---|---|---|
| LOFT MIN / LOFT MAX | LIFE | 240 / 480 | Each wisp's buoyancy budget, in frames — how long fresh wool rides the flock before it tires and rains. Lower for a rain-dominated sky, higher for longer aerial displays. |
| GRAVITY | LIFE | 0.035 | The settle — how insistently everything tends floorward. The weight of the air. |
| LIFT | LIFE | 0.1 | The devil's updraft strength — how hard the drum's draft drives wool upward. |
| DRAFT R | LIFE | 260 | The updraft's reach in pixels — how far from the drum the rising air extends. |
| MIN SPD / MAX SPD | TEMPO | 0.7 / 3.1 | The speed envelope every wisp lives inside — wool never stalls dead, never streaks like a bullet. |
| FALL MAX | TEMPO | 1.3 | Terminal velocity — how lazily raining wool floats down. |
| FLUTTER | TEMPO | 0.12 | Constant turbulence — the wobble that makes it wool rather than birds. |
| TUMBLE P / TUMBLE K | TEMPO | 0.04 / 0.9 | How often a wisp catches a real tumble, and how hard the kick bends its flight line. |
| ALIGN | FLOCK | 0.025 | Reynolds's alignment — matching neighbours' headings. The most "bird" knob; zero it for pure drifting chaos. |
| COHERE | FLOCK | 0.003 | Reynolds's cohesion — drift toward the local centre of mass. Clumping. |
| SEPARATE / SEP R | FLOCK | 0.06 / 16 | Reynolds's separation — personal space: push strength and the radius it defends. |
| NEIGHBOR | FLOCK | 55 | Who counts as a neighbour, in pixels — bigger makes one big flock, smaller makes local drifts. |
| SWEEP | PACE | 0.02 | Per-frame chance landed wool is swept back to the devil — lower and the floor gathers fluff. |
| RECYCLE | PACE | 0.012 | Per-frame chance wool deep in a side exit zone returns early — spreads respawns into a steady trickle off the drum. |
| WISPS / MOTES | PACE | 100 / 180 | The live populations — wool and fine dust. Resized on the fly: a deficit sprays from the drum, a surplus is trimmed. |
| SPIN | PACE | 0.264 | Drum speed, in radians per frame (a full turn is about 6.28 radians) — the machine's tempo, and what the drone's pitch and the churn's rate follow. |
And the footer meters, each computed fresh every frame from the same values the renderer draws:
| Meter | What it reads |
|---|---|
| FPS | Measured frame rate (frames per second), smoothed — green at a healthy 30, sagging yellow when a heavy panel setting (many wisps, wide NEIGHBOR) starts to drag. |
| ALOFT / RAIN / DOWN | The lifecycle census: wisps riding the flock, raining with spent loft, and resting on the floor awaiting the sweep. The LIFE tab made visible. |
| ORDER | The flocking order parameter — how nearly the airborne wool moves as one: the size of the average velocity, divided by the average speed. 1.0 is a marching flock (starlings); near 0 is incoherent drift (wool). Watch it move as you drag ALIGN. |
| SPEED | Mean airborne speed — and the one meter that can burn ember, since only eleven pushes it past the MAX SPD rail. |
| ALT | Mean height of airborne wool above the floor, in pixels — how high the storm is riding. |
| THRU | Wool through the devil per second, smoothed — the speed of the mill's cycle: throw, flight, rain, sweep, throw again. |
The drone reads the live values, not the goal. So the
machine's inertia is audible for free. Pull the lever to
ELEVEN and the pitch climbs over the seconds the belt takes
to load. Hit STOP and the hum glides down and dies as the
drum coasts to rest. The bed is a low sine tone (a sine wave is the
smoothest tone there is) with a twin tuned slightly apart. Two close
pitches beat — pulse in loudness at the rate of their frequency difference
— and that beat is the machine's throb. It runs deep in the volume, and it
quickens with speed, because a beat is the frequency difference.
Under it sit a triangle wave a musical fifth above, a shimmer wash, and a
breath of churn at the drum's own rate.
Past the rails the chord sours by degrees. A continuous heat measure
bends the overload voice from a clean octave toward a flat ninth — from a
sweet interval toward a sour one — then swells it and crossfades it
(blends it gradually) from triangle to square. The buzz arrives and leaves
with the ember on the gauges, never as a cliff. Lever pulls speak short
low phrases; eleven's riff climbs by semitones, of course. Wool landing on
the floor pats quietly, and B silences the lot. This mill is
why the buzzer grew SoundWave. Its lab proved that below
~60 Hz (Hz is vibrations per second) a square wave stops sounding
like a tone and becomes a train of clicks to the ear. Deep bass therefore
wants sine and triangle — and now the language has them.
The pure model is devils-dust-core.shoddy:
the flock, the steering, the per-frame step, the knob table, and the
meters. A wisp carries its trail and a finite loft — a buoyancy
budget in frames — as data. While the loft lasts, the wisp rides the flock
and the draft. When it runs out, the wisp drops out of the flock in the
literal sense (separation, gravity, and flutter only) and rains down where
it is. The census, order parameter, and the other meters are read-only
folds — passes that boil a list down to a summary — over the same values
the renderer draws.
The voice is devils-dust-sound.shoddy: a
pure mapping half (drone pitch from drum speed, heat from the knobs,
gains) tested headless — with no window and no sound device — and an
effectful half that drives the buzzer. All of it is fire-and-forget: the
calls return at once, because sound never blocks — it never makes the
program wait.
The window half is devils-dust.shoddy:
drawing, the console, and the event loop. A mouse click is classified
once into a Hit value — hint, close box, tab, knob
row, lever slot, stop bar, miss — and the handler is a flat
Select Case over the result. Geometry lives in region
predicates — yes/no words about where a point falls — that read as
sentences. Every branch of the loop calls Loop directly: the
self tail call (a word ending by calling itself) that the compiler turns
into a loop.
The core never includes the scribbler or the buzzer. So all of
it — steering, lifecycle, knob table, meters — runs headless under
test.shoddy: seventy-three seeded checks with names like
SPENT WOOL RAINS DOWN, ELEVEN IS ONE LOUDER THAN
MAX, and THE CHURN IS A WHISPER. The sound file sits
between: its mapping is pure and tested, and its buzzer calls are no-ops —
calls that do nothing — when headless. The window file owns everything
that needs a window. And sound-lab.shoddy is the spare room —
the workbench where the drone's recipes were auditioned before any of them
touched the mill.
| Machine | Why | |
|---|---|---|
| seq | In the core:
Filter and Fold are the flock — neighbours,
steering sums, the census — and Taken caps each wisp's
trail. | |
| math | In the core:
Dist for neighbourhoods, Clamp/Lerp
for rails and glides, Smoothstep shapes the devil's updraft,
Sgn picks the outward wind, and the Rand* family
spawns, flutters, and tumbles. | |
| scribbler | In the
shell: the window, the fixed-FPS tick queue, and the drawing words —
trails via DrawLine, the devil via circles and rects, the
console via FillRect and the 8×8 text. | |
| buzzer | In the voice:
held notes retuned every frame for the drone (NoteOn on a
sounding channel is a pure pitch change), SoundWave for the
sine/triangle bed, Play for the lever phrases. | |
| keys | In the shell:
ClassifyKey folds raw codes into the GameKey
the keyboard dispatch matches on. | |
| str | In the shell:
PadLeft and ToFixed keep the knob readouts and
footer gauges fixed-width, so the monospace columns never jitter. |