Nine Instructions
I have an original Pac-Man PCB sitting in my closet. Pulled from an arcade cabinet decades ago, it remains in good condition despite its age. I’ve been meaning to do something with it beyond confirming that it still boots, so I pulled the ROM chips and extracted twenty-two binary files.
My plan was to disassemble the code, read through a bit of it, and learn something about how one of my favorite games was built. What I didn’t expect was how much a 44-year-old codebase would teach me. This is the first in a series of posts about reverse engineering Pac-Man.

The board runs a Zilog Z80 CPU clocked at 3.072 MHz. It contains eight 2KB chips, each holding a specific address range of the program.
| File | Size | PCB Socket | Address Range |
|---|---|---|---|
pm1_prg1.6e | 2KB | 6E | 0000−07FF |
pm1_prg2.6k | 2KB | 6K | 0800−0FFF |
pm1_prg3.6f | 2KB | 6F | 1000−17FF |
pm1_prg4.6m | 2KB | 6M | 1800−1FFF |
pm1_prg5.6h | 2KB | 6H | 2000−27FF |
pm1_prg6.6n | 2KB | 6N | 2800−2FFF |
pm1_prg7.6j | 2KB | 6J | 3000−37FF |
pm1_prg8.6p | 2KB | 6P | 3800−3FFF |
When concatenated in address order, the data in these chips produces the full 16KB of executable code. The remaining fourteen files hold graphics tiles, sprite data, and small PROMs for color generation and sound — twenty-two binary files in total.
Disassembling the binary produced 9,328 lines of raw Z80 assembly. There are no labels, comments, or even distinction between code and other data. Figuring out what any of it actually does requires cross-referencing registers and addressing modes.
The first surprise: Pac-Man has no game loop.
There’s no while(true) spinning somewhere, no tick function being called from a scheduler. Instead, the display hardware fires a maskable interrupt every vertical blank — the brief pause when the CRT’s electron beam resets from the bottom of the screen back to the top, roughly 60.606 times per second — and that interrupt is the game. The hardware uses the Z80’s Interrupt Mode 2, which routes every VBLANK through a vector table at $3F00 and into the ISR at $008D. That handler does everything in a single pass: read input, move Pac-Man, move ghosts, check collisions, update the display. If the code doesn’t finish before the next interrupt fires, the game simply slows down. The entire game is, architecturally, an interrupt handler.
This isn’t how anyone would design it today, but there’s something clarifying about it. No event queues, no deferred processing, no abstraction layers. Every frame is a straight shot from input to pixels. Understanding the interrupt-driven architecture matters because every cycle in that ISR is spoken for — which is why the speed system had to be this efficient.
The Shift Register#
The game needs entities to move at fractional speeds — 75% of full speed, or 47%, or any fraction at all. On a CPU with no floating point, no division instruction, and barely enough clock cycles to finish before the next interrupt, how do you do fractional movement?
The solution from Shigeo Funaki, the programmer behind the Z80 code, is a 32-bit shift register for each speed context. Every frame, the game shifts the entire 32-bit value left by one bit. If the bit that fell off the top was a 1, the entity moves one pixel. If it was a 0, the entity stays put. And here’s the trick: when a 1 shifts out, a 1 is fed back in at the bottom. When a 0 shifts out, nothing is added. The number of set bits is conserved forever.
So if you initialize the register with 24 bits set out of 32, the entity moves 24 out of every 32 frames — 75% speed. Sixteen bits gives you 50%. Eight bits, 25%. It’s a ring counter implemented in software. No multiplication, no division, no lookup tables at runtime.
The Z80 implementation:
ld hl,($4D58) ; load low 16 bits
add hl,hl ; shift left, top bit goes to carry
ld ($4D58),hl ; store
ld hl,($4D56) ; load high 16 bits
adc hl,hl ; shift left, carry chains from low word
ld ($4D56),hl ; store
ret nc ; carry clear = no movement
ld hl,$4D58
inc (hl) ; carry set = movement; feed back a 1
Nine instructions. The entire fractional speed system for one entity is nine instructions.
I kept rereading it. The Z80 can only shift 16 bits at a time, so the 32-bit shift is stitched together from two halves, with the carry flag carrying the bit that falls off the bottom half up into the top. The bit that falls off the top becomes the movement decision. ret nc does double duty — it’s both the speed check and the early exit. And the feedback inc (hl) works because the low half was just shifted left, which guarantees its bottom bit is zero.
It’s the kind of code that looks obvious once you understand it and impossible before you do.
The shift register doesn’t exist in isolation. Each ghost has five separate accumulators — one for each speed context: normal movement, frightened mode, tunnel speed, and for Blinky alone, two acceleration stages that kick in when few dots remain. The game checks which context applies and runs the appropriate register. Since each is independent, switching modes doesn’t cause speed glitches. The same nine instructions, selecting from different addresses, drive the entire movement system for every entity on screen.
The Speed Table#
The initial bit patterns are loaded from a data table in ROM at $330F during level setup. Eight 42-byte entries cover every level the game can produce, selected through a two-level indirection that maps the current level to a speed-group index, then into the table. Each entry holds seven 4-byte accumulator seeds — one per speed context — plus fourteen bytes of other level parameters.
Here are those seeds, as fractions of the 32-bit register:
| Entity | Level 1 | Levels 2–4 | Levels 5–20 | Level 21+ |
|---|---|---|---|---|
| Pac-Man normal | 16/32 | 18/32 | 20/32 | 18/32 |
| Pac-Man fright | 18/32 | 19/32 | 20/32 | 18/32 |
| Ghost normal | 15/32 | 17/32 | 19/32 | 19/32 |
| Ghost frightened | 10/32 | 11/32 | 12/32 | 9/32 |
| Ghost tunnel | 8/32 | 9/32 | 10/32 | 10/32 |
| Blinky Elroy 1 | 16/32 | 18/32 | 20/32 | 20/32 |
| Blinky Elroy 2 | 17/32 | 19/32 | 21/32 | 21/32 |
One note on baselines. These counts are fraction-of-hardware-max, where 32/32 would mean moving one pixel per VBLANK. The familiar “75% ghost speed on Level 1” normalizes instead to Pac-Man’s top speed of 20/32 — against which Level-1 ghost at 15/32 comes out as 75%. My earlier illustration (24 bits set = 75%) describes the mechanism; the actual ROM seeds use that alternate baseline.
You can read the difficulty curve straight off the table. Pac-Man accelerates from 16/32 at Level 1 to 20/32 at Level 5, then eases back. Ghosts accelerate through Level 5 and never slow down. The tunnels are always the slowest context — a safe zone by design. Blinky’s Elroy gears always sit one and two bits above the shared ghost speed. There is no code that decides any of this. It is all here, in one 336-byte table.
The code is a fixed engine. The game’s character lives in its data.
Today you’d solve this with floating-point math, a delta-time accumulator, or whatever your engine ships with. It would be correct, extensible, and forgotten the next day. That’s not a failure of modern engineering — it’s what abstraction is for. But when you have surplus cycles, you find a solution that works. When you have none, you find the one that fits. The second kind of code is worth remembering.
Nine instructions. About 100 clock cycles per entity per frame. No drift.