pac-vt100 is Pac in a terminal: a 39×25 maze of
walls, dots, and cherries, four ghosts, and nothing on screen that isn't a
character. Eat every dot to win. A cherry makes the ghosts edible for a few
seconds. There is no window and no scribbler. The screen is any VT100/ANSI
terminal — a terminal that understands a standard set of control codes for
moving the cursor. The vt100 machine
paints it with escape strings — short character sequences that carry those
control codes. Keys arrive through the InKey builtin, which is
non-blocking (it never stops to wait for a key) and unechoed (the key is
not printed back to the screen). WASD, the arrow keys, or the numeric
keypad move. Q or Escape quits. The menu's C
toggles colour. With colour off, the game only ever emits cursor
addressing, never a colour code — exactly as in the C original.
PAC-MAN is a trademark of Bandai Namco Entertainment Inc. This mill is an
independent educational homage, not affiliated with or endorsed by the trademark
owner. See THIRD-PARTY-NOTICES.md.
Pac-Man itself needs no introduction — Namco, 1980, the most recognizable
maze in software. This mill is a rewrite of a compact C terminal rendition.
(The original C source is not redistributed here; see
THIRD-PARTY-NOTICES.md.) It keeps the same
board[25][39] maze, the same ghost direction scheme, and the
same colour-toggle menu. What changed in the crossing is the interesting
part. The C original mutates one global board — changes it in place — and
lets its read loop run as fast as the computer allows. The Shoddy version
instead threads an immutable state record — a bundle of values that is
never changed, only replaced by a new one — through a tick that fires at a
fixed rate. Its timing constants are therefore re-tuned rather than copied.
The C original's EDIBLE_TIMEOUT 300 loop iterations become 80
engine ticks, and so on. The header of pac-core.shoddy shows
the arithmetic.
bin/mill run mills/pac-vt100/pac.shoddy
or, from mills/pac-vt100/, 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
It wants a real ANSI-capable terminal at least 80×26 (Windows Terminal, or any Unix terminal, is fine). The game runs one tick per frame at 100 ms, paced by the clock machine.
It also ships on the Shoddy Reckoner's games shelf. There, the app's terminal control supplies the 80×26 ANSI screen and feeds keystrokes down a pipe. The woven program is the same one, unmodified.
The pure model is pac-core.shoddy. It
holds the maze, the state record, the per-tick simulation — ghost AI,
timers, collisions, eating, winning and losing — and the input actions.
Coordinates are 0-based cells: counting starts at zero, not one.
Directions use the C original's scheme (0 none, 1 up, 2 right, 3 down,
4 left). The board is an Array of 25 row
Strings holding # walls, . dots,
+ cherries, and eaten spaces. PutCell returns a
new board; nothing mutates. The core contains no cursor addressing at all,
so it runs headless — with no screen attached. That lets it be unit-tested
that way, twice over: by test.shoddy in the folder and by
src/Shoddy.Tests/PacTests.cs in the golden suite.
The terminal half is pac.shoddy: painting,
the menu, and the loop. It has one sly trick. Print appends a
newline, so each frame is assembled into a single string that ends by
parking the cursor at row 1. The newline then lands harmlessly on row 2
and can never scroll the maze away. GameLoop,
MenuWait, and WaitKey are recursive — each one
calls itself — and every branch makes that call directly, as the last
thing it does. Those are genuine self tail calls, which the compiler turns
into loops.
The C original is one file. It mutates one global board inside one
free-running read loop, with simulation, drawing, and input interleaved
line by line. That mix is exactly what makes such programs hard to test.
The port cuts along the effect line instead.
pac-core.shoddy is everything the game is — maze,
state record, ghost AI, timers, collisions, input actions — and emits not
one cursor movement. pac.shoddy is everything the game
shows, and decides nothing.
That split is why the core can be tested twice over — by
test.shoddy in the folder and by
src/Shoddy.Tests/PacTests.cs in the golden suite. Both drive
the simulation tick by tick with no terminal anywhere. The split also made
the timing retune honest. When the free-running loop's constants had to
become fixed-rate tick counts, every change landed in the core, where a
test could hold it still. None landed in the paint code, where a change
would only show up as feel.
| Machine | Why | |
|---|---|---|
| seq | In the core:
Any, All, Contains, and
Flatten — the questions the simulation asks of the maze and
the ghosts (any dot left? is a ghost on this cell?). | |
| str | In the core: the
board is 25 row Strings, and Replace does the
row surgery. PutCell rebuilds a row around one changed cell
instead of mutating it. | |
| math | In the core:
RandInt and Pick are the ghosts' dice. | |
| vt100 | Both directions
of the terminal protocol. The direction the core needs is the
surprise: it uses EvalKey to classify raw InKey
input, because arrow and keypad presses arrive as escape sequences and
decoding them is part of the pure input model. The shell uses the emit
side — cursor addressing and clearing. (The machine stops at attributes,
so the ANSI colour codes live in the shell itself.) | |
| clock | In the shell:
SleepUntil paces the loop to one tick per 100 ms frame
without drift. |