invaders opens a 480×640
scribbler window and puts you at
the bottom of it with a cannon. Left and Right arrows move, Space fires,
Q or Escape quits. Aliens descend at random. A laser that
reaches one clears both and scores. An alien that reaches the cannon line
ends the game. The buzzer supplies the
sound — the shot, the hit, and the end each get their own effect.
Space Invaders is a trademark of Taito Corporation. This mill is an
independent educational homage, not affiliated with or endorsed by the trademark
owner. See THIRD-PARTY-NOTICES.md.
Space Invaders — Tomohiro Nishikado, Taito, 1978 — is the game that made the descending-alien loop a genre. Its accelerating heartbeat taught a generation what game feel was: the aliens speed up as their ranks thin. That was originally a hardware accident — fewer sprites (moving screen shapes) meant faster frames. This mill is not a cabinet-accurate port. It's the arcade idea reduced to its essentials — descend, shoot, survive — and rebuilt as a demonstration of how a purely functional game loop works when a window, a frame clock, and a sound queue are involved.
bin/mill run mills/invaders/invaders.shoddy
or, from mills/invaders/, use the wrapper —
./build.sh, or ./build.ps1 on Windows:
./build.sh # run the game (also: ./build.sh run)
./build.sh test # run the headless simulation smoke check
One thing to know: invaders is a scribbler program, so it must run under
mill run. A woven dotnet FILE.dll has no window
backend — the platform code that actually opens a window. The exception is
a host that brings its own backend. The game also ships on the
Shoddy Reckoner's games shelf, where
the app presents the scribbler as a page and feeds it events — the same
woven program, unmodified.
The pure model is invaders-core.shoddy: the
state record, the per-frame simulation, and the input actions. It includes
no scribbler, so it runs headless — with no window at all — and is
unit-tested that way, by test.shoddy in the folder and
src/Shoddy.Tests/InvadersTests.cs in the golden suite.
The window half is invaders.shoddy: drawing,
sound, and the event loop. Frames come from ScribblerTick at a
fixed FPS (frames per second), so the loop sits at zero CPU between frames,
and input arrives on the same queue. Every branch of the loop's
Select Case calls Loop directly, or closes the
window. That direct call is a genuine self tail call — a call made as the
function's very last act — which the compiler turns into a loop. A helper
that called Loop back would be mutual recursion — two
functions calling each other — and would grow the stack one frame per
tick.
The design point worth stealing is how sound stays out of the
core. Effects are inferred by comparing the game state before and
after a step: a laser appearing is a shot, the score rising is a hit,
Ended flipping on is the end. So the pure model needs no sound
hooks and still tests headless. The shell reads the diff — the
before-and-after difference; the core never knows the buzzer exists.
The seam between invaders-core.shoddy and
invaders.shoddy is where the window begins. The core is
everything the game is — record, simulation, input actions. It
could not open a window if it wanted to: it never includes the scribbler.
The shell is everything the game presents: drawing, sound, and the
event loop, none of which decides anything.
The split earns its keep in the tests: the golden suite steps the simulation frame by frame with no window and no frame clock. The sound design above also enforces it. Because effects are inferred from state diffs rather than fired from inside the rules, nothing ever pulls the buzzer (or any other effect) down into the core. This is the simplest of the three game mills, so it's the one to read first if you want the core/shell pattern at its clearest.
| Machine | Why | |
|---|---|---|
| seq | In the core:
Any answers the collision and end-of-game questions — has any
laser reached any alien, has any alien reached the cannon line. | |
| math | In the core:
RandInt rolls the alien spawns, Clamp keeps the
cannon on screen, and Dist is the circle-vs-circle hit
test. | |
| keys | In the core:
GameKey folds raw key codes into the friendly GameKeys the
input actions consume — so "left arrow" is a value the pure model can
match on. | |
| scribbler | In the
shell: the window itself — the drawing surface and
ScribblerTick, the fixed-FPS queue that delivers both frames
and input, letting the loop idle at zero CPU between ticks. | |
| buzzer | In the shell: the shot, the hit, and the game-over sound — triggered by diffing game state, never from inside the rules. |