← Experimental Atari Multiplayer Arcade

How Pac-Man 4-Player Was Made

Development notes on the four-player edition of Atari's 1982 Pac-Man cartridge, 5–6 October 2026. Written by the Claude Code agent that did the work, edited for publication.

Pac-Man 4-Player is an experimental modification of Atari's 1982 Pac-Man cartridge for the Atari 400/800, in which four people each steer a Pac-Man in the same maze at the same time. AI coding agents (Claude Code) built it by working from the original program, and its behaviour was checked with automated tests in an Atari emulator. These notes describe how it works, how it was tested, which bugs were found, and what is still imperfect or uncertain.

Contents
  1. Summary: what was done, how it was checked, what is uncertain
  2. The goal and the starting point
  3. Getting source code: matching the cartridge to Roklan's disk source
  4. A 16K cartridge skeleton and a build that keeps everything
  5. The core problem: eight objects, five sprites
  6. Phase 1: the sprite multiplexer
  7. Phase 2: four Pac-Men in a one-Pac-Man program
  8. Game rules for four players
  9. Score lines, high score and the title screen
  10. Making it fast enough
  11. Automated testing: the headless harness and the autopilot
  12. The bugs, and how they were found
  13. Versions 1.3 to 1.6: the boost button, VS mode and drop-in
  14. Timeline, versions and safety nets
  15. Lessons
  16. Credits

1. Summary: what was done, how it was checked, what is uncertain

What the agents did

A person set the original brief and each later feature request, chose how drop-in should behave, and reported one bug after release (section 12). The analysis, code, builds and tests were done by the agents. Bringing the cartridge into the online arcade was separate work and is not covered here.

How it was checked

What is imperfect or uncertain

2. The goal and the starting point

The brief was a four-player simultaneous version of Pac-Man for the Atari 800, starting from Atari's original Pac-Man cartridge. It was given while nobody was at the machine. The Atari 400 and 800 have four joystick ports (the later XL/XE machines have two), so four people can each steer their own Pac-Man in the same maze at the same time.

What there was to work with:

The original one-player Atari Pac-Man in the emulator: a yellow Pac-Man in the blue maze with orange dots, four ghosts near the ghost house, HIGH SCORE at the top, two reserve Pac-Men and a cherry at the bottom.
The starting point: the original cartridge running in the emulator. Ghosts are hardware players 0-3; Pac-Man is the four missiles combined into a fifth player.

3. Getting source code: matching the cartridge to Roklan's disk source

Changing a game this deeply needs source code rather than patched bytes. The plan was to turn the 8 KB cartridge into an assembler source that rebuilds it byte for byte, with meaningful names, and that can be relocated (code moved freely, which the 4-player additions require).

Rebuilding the disk source

The disk source is written for MADS, which wasn't installed. MADS is open source (written in Free Pascal), so the agent fetched its source and compiled it with the Free Pascal compiler. All four disk versions in the source (ORIGINAL, ROKLAN, ATARI82, DATASOFT) then assembled byte-identical to the reference binaries, and MADS produced full listings: every source line with its address and bytes, plus a label table.

Matching instruction streams

With the disk program's instructions, addresses and names in hand, the agent wrote a matcher in Python:

  1. A tracing disassembler follows the cartridge's code from its entry points (cartridge start, VBI, DLIs), so code and data are told apart.
  2. Both instruction streams are reduced to tokens: the opcode, plus the value for immediate operands. Addresses are left out because the two builds put things in different places.
  3. Every run of 6 tokens that occurs exactly once in each program is an anchor. From each anchor the match is extended backwards and forwards as long as the tokens agree.

Result: 94% of the cartridge's 2,925 instructions matched the disk code one for one. Each matched pair carries information across:

The source generator

A second script writes the ca65 source: names and comments from the match, data tables named by propagating known table offsets, and the cartridge's RAM areas mapped back to the disk version's (the cartridge moved its buffers down so it runs in 16K). The few things a matcher cannot know were entered by hand in a symbol table. The most important are ROM addresses hidden in immediate operands, such as LDA #$E1 / LDA #$A1 = the packed maze at $A1E1, the VBI address passed to SETVBV, and display-list and DLI low bytes. These must become #<PKMAZE / #>PKMAZE or the code breaks when it moves.

The first generated source reassembled byte-identical to the cartridge.

A fully commented disassembly of the cartridge was produced as a separate piece of work, finished by a second agent running in the background while the 4-player work went on. It names the 25 cartridge-only routines and decodes the three packing formats the cartridge uses for its maze, font and tables (run-length, nibbles, a 63-byte dictionary). It also documents every difference from the disk version, checks that the source is relocatable, and found an anti-copy trap: the title routine ends by jumping into the middle of the ghost-drawing code. In the cartridge this writes 16 bytes into ROM (no effect); in a RAM copy it overwrites the console-key code and the game hangs.

4. A 16K cartridge skeleton and a build that keeps everything

Four players need far more code than fits in 8 KB, so the target became a 16K cartridge ($8000-$BFFF). It works on any Atari 400/800 with 16K of RAM, and as a RAM image the same 16K also loads from a boot disk or as an Atari DOS executable (XEX) on a 48K machine. The generated source was forked for the 4-player edition, with every edit to the original code marked ; 4P. New code went into a new source module, and later into a second one for the players.

The first build (b0001) was the unmodified game relocated to $8000, produced in every format:

A smoke test booted the cartridge, disk and XEX versions headless, started a game and steered Pac-Man. All three played, which showed that the source really was relocatable. The only layout assumption found was that both display lists had to share a memory page, and so did both DLI handlers; a new routine (SETDL4) replaced the one that relied on it.

From the first build on, the build script kept every build: all formats plus an archive of the sources that produced it, with one line per build in an index. Source files were copied aside before each edit, and the development log was only ever appended to. This followed the project's standing rule from the Wizard of Wor work: keep every version.

5. The core problem: eight objects, five sprites

The Atari has four "players" (8-pixel-wide sprites, one colour each) and four 2-pixel "missiles". The original game uses players 0-3 for the four ghosts. Pac-Man is drawn in the four missiles placed side by side, which the GTIA can colour as one "fifth player" (colour register PF3). That is five sprites for five objects, and four Pac-Men plus four ghosts are eight objects.

The options considered:

ApproachVerdict
Flicker everything (alternate frames)30 Hz flicker on every object, as in the Atari 2600 Pac-Man. Rejected.
Ghosts in sprites, Pac-Men drawn as software characters in the mazeMode-4 characters allow only four colours per line, and walls and dots already use two. Four distinct Pac-Man colours are impossible. Rejected.
Ghosts fixed on P0-P3, four Pac-Men time-shared on the missilesPac-Men often share corridors; frequent flicker. Rejected.
General vertical multiplexer: any object on any of the five sprites, and a sprite reused further down the screenEvery object solid and in its own colour almost always; flicker only when six or more objects share the same scanlines. The most complex option; chosen.

6. Phase 1: the sprite multiplexer

Phase 1 kept the game single-player and made it draw everything through the new system. If the original game still looked and played identically, the multiplexer worked.

Objects instead of sprites

Game code no longer writes sprite memory or sprite position registers. Each of the eight objects (0-3 ghosts, 4-7 Pac-Men) has an image, a horizontal and vertical position, and a colour. Every place in the original that touched sprites, about 20 sites, was redirected with a patch script (kept for the record): the ghost and Pac-Man drawing routines, the death animation, the "ghost eaten" score display, the routines that hide sprites, the colour writes, the tunnel masking.

The renderer

Every frame a routine called RENDER:

  1. finds the visible objects and their top and bottom scanlines;
  2. sorts them top to bottom;
  3. gives each a channel: P0-P3 or M (the missiles). A channel can be reused by an object lower down when there are at least two free scanlines between the two objects;
  4. draws the images into sprite memory;
  5. records, for every reused channel, an event: on which line its horizontal position and colour must change.

A display-list interrupt (DLI) carries out the events while the screen is drawn.

The timing that makes it work

DLIs can only fire at the end of a character row. In this maze a row is 8 scanlines (the maze starts on scanline 40), so the DLI for a gap may come several lines early. It then waits with STA WSYNC, which halts the CPU until the end of the current scanline. The rule worked out:

object on a reused channel starts on scanline L;
the previous object on that channel ends by line L-3  (lines L-2, L-1 are free)
writes must start on line S = L-2:
    t = L - 41;  DLI number = t div 8;  WSYNC count = t mod 8   (0 = write at once)

This was later refined: the channel may be moved from the line after the previous object ends. If a DLI line falls inside that window, the write happens with no waiting at all.

Double buffering

Sprite memory exists twice (PMBASE $20 and $28). RENDER fills the hidden copy. At the next vertical blank, a new immediate-VBI routine (VBI4) swaps them, sets each channel's first object, and hands the event list to the DLI. Nothing is ever drawn into the visible buffer, so there is no tearing.

Two complications

Multiplexer test screen: eight ghost-shaped test objects in eight different colours in the maze, four in a row along the upper corridor and four in a row lower down.
The multiplexer test: eight synthetic objects in their own colours on five hardware sprites. Six such layouts are checked pixel by pixel from the emulator's screen buffer.

7. Phase 2: four Pac-Men in a one-Pac-Man program

The original keeps its single Pac-Man in 20 zero-page bytes: position, screen address, sub-character counters, direction, speed sequence, mouth animation, death-animation state. All its Pac-Man routines (joystick, movement, dot eating, energizers, fold-up animation) work on those bytes.

Rewriting those routines for four Pac-Men would have meant rewriting most of the game. Instead each player got a context, a 20-byte copy of those variables. LDCTX loads a player's context into zero page and SVCTX saves it back. Every piece of original Pac-Man code runs between the two, unchanged. Four players means four passes per frame.

The original also has a variable PLYNUM ("whose turn") for its alternating 2-player mode. It indexes the maze number, dots eaten, energizers and fruit. In the 4-player game it is always 0, so those all belong to the shared board. Per-player things got their own arrays: reserve Pac-Men, the bonus flag, the score position on screen.

The ghosts' AI reads "the" Pac-Man's position. Before each ghost decides, TARGET4 puts the position of the nearest living Pac-Man there (one ghost per frame, which is enough).

The original runs its whole frame as one long fall-through chain of code: fruit, energizer, collisions, sounds, Pac-Man movement, ghosts. That chain was replaced by a 4-player sequence (VFRUIT4). It loops over the players for energizers, collisions and fruit, then for movement, and then runs the original ghost code.

Start of a four-player game: yellow, light blue, magenta and white Pac-Men at their start places in the maze, the four ghosts in and above the ghost house, and two score lines at the top in the four player colours with reserve icons.
All four Pac-Men at the start: yellow (player 1, the original start), light blue and magenta (top corridor), white (bottom).

8. Game rules for four players

Several rules had to be decided with nobody available to ask. The aim was a game that works with four people and is exactly the original with one:

A four-player game during the freeze after a ghost is eaten: the points value 400 is shown in the top corridor next to a light-blue Pac-Man, the other Pac-Men and ghosts are elsewhere in the maze, and the magenta player's score reads 470.
Player 3 eats a ghost: the freeze shows "400" where it happened, split across the eater's object and the ghost's, the same way the original splits it across its sprites.

9. Score lines, high score and the title screen

Title screen: the PAC-MAN logo in gold block letters with 4-PLAYER underneath, and (C) ATARI 1982 lower down, on a black background.
The first title screen in v1.2.

10. Making it fast enough

The first working 4-player build was too slow. All the original's game logic runs in the deferred vertical-blank interrupt, and so did RENDER, so a frame's work had to fit in one frame. With four players it didn't: frames were skipped and the game slowed down.

Measuring

The CPU gets about 17,500 cycles per frame, because ANTIC takes over half of each scanline to fetch the maze characters. To see where they went, the agent added probes that read the scanline counter (VCOUNT, units of 2 scanlines; a frame is 131) at the start and end of the logic and of each renderer stage. The first measurements, for four players:

BeforeAfter
Game logic~110 units~47 units
RENDER~70-80 units~60 units (autopilot play)
Frames with a new picture, 4 players53% (logic frames skipped) 95-99% (logic never skipped)

What changed

  1. Pac-Man's picture straight from ROM. The original copies the current mouth shape into a buffer every frame, and the multiplexer then copied it again. Now the object just points at the shape in ROM (game logic down from 110 to 64 units, together with the next item).
  2. Lookup tables instead of searches. The maze routine searched two 10-entry tables linearly, three times per call, for every Pac-Man and every ghost move. Two 256-byte tables built at power-on give the same answers in a few cycles.
  3. Redraw only what changed. For each sprite buffer, RENDER keeps a record of what every object looks like there: channel, line, image serial number, tunnel mask. Unchanged objects are left alone. A moved object only has the rows it left erased, then is redrawn.
  4. Ghosts compared before redrawing. A ghost moving sideways changes position but not its picture, and position alone costs nothing in sprite memory. Comparing the new picture with the old one took 4 players from about 86% to about 95% new pictures in autopilot play.
  5. RENDER moved to the main loop. The game logic stays in the VBI, and RENDER runs in the main program loop afterwards. Game speed can no longer drop: a slow render only means the previous picture stays up for one more frame.
  6. A display list in RAM, rebuilt per frame (double buffered like the sprites), with DLIs only on rows where a channel actually moves. Twenty empty DLIs per frame had cost about 1,000 cycles.
  7. Smaller items: one loop per player for energizer, collision and fruit checks; a 16-byte context for living players; the energizer test only on energizer rows; an O(1) choice of an empty channel; allocation alternating top-down and bottom-up, so that when objects must be left out it isn't always the same one.

11. Automated testing: the headless harness and the autopilot

Nobody was at the machine during the work, so everything had to be checked by software. The project's emulator runs headless: no window, a private control socket, maximum speed. Several emulators can run in parallel without affecting any other session. Tests read memory, press console keys and joysticks, and examine the emulator's screen buffer pixel by pixel.

TestWhat it checks
Smoke testCartridge, XEX and boot disk all boot, start a game and play
Multiplexer testEight synthetic objects in six hard layouts: every object's pixels on screen, in its colour, in place, over several frames
Event scenariosSituations random play rarely reaches: player 3 eats a ghost (freeze, points to player 3 only); player 2 eats the fruit; player 4 crosses 10,000; player 1 runs through the tunnel; the maze is cleared while player 2 waits to come back
Soak testRandom play to game over, logging every state change (deaths, respawns, outs, freezes, the last-Pac-Man sequence)
Rate and timing testsNew pictures per frame; CPU time of logic and renderer
Autopilot and marathonAn autopilot: breadth-first search over the maze characters to the nearest dot, avoiding dangerous ghosts and chasing blue ones. Four autopilots clear mazes and eat ghosts, which random steering never does.

The marathon run had four autopilots with 99 reserve Pac-Men each. It played 41 mazes (90,000 frames), ate 302 ghosts and lost 123 Pac-Men with no crash or stall. Other checks: the cartridge on an emulated 16K Atari 800, an XL offering only two players, and a visibility check. In that check, every living Pac-Man not overlapped by another sprite was on screen in every one of 180 samples. Later versions added tests for the boost, VS mode and drop-in to the same suite.

Maze 29 of the marathon run at the READY! pause: four Pac-Men at their start places, scores between about 44,000 and 74,000 in the four player colours, three reserve icons per player, and a row of keys as the level fruit at the bottom.
Maze 29 of the marathon: keys as the level fruit, every player still with a full row of reserve icons (99 each, shown as three).

12. The bugs, and how they were found

DLIs a line late (b0002, fixed in b0003). Two stray dashes appeared at the top corners of the maze. The first maze line was drawn with the ROM font: the font switch came one scanline late. A DLI starts about 8 cycles into the last scanline of a row, and on a maze row only about 49 CPU cycles are left before that line ends. The first DLI handler needed about 76 cycles to reach its first WSYNC. A rewritten fast path takes about 38.
The whole maze in the ROM font (b0004). A new variable had been placed on top of the ghost colour array, so every DLI took the wrong branch. After this, an overlap check runs over all hand-assigned RAM addresses.
Bug screenshot: the maze drawn as rows of garbled pink and white text characters instead of walls and dots, while the ghosts, a Pac-Man and the score line are still drawn normally.
The b0004 bug: the maze drawn with the score-line font.
Title screen 8 lines low (fixed in b0019). The title is rebuilt by the VBI every frame, and the routine that set the display list also wrote ANTIC's display-list register directly. Once the VBI ran into the visible frame, that restarted the display mid-screen. The rule since: write the OS shadow, which the OS copies during vertical blank.
Stray pixels under "HIGH SCORE" (fixed in b0022). They appeared only now and then. The new code's wrapper around the original title DLI handler added 13 cycles, and that occasionally pushed its WSYNC past the end of the line, so the logo's top line was drawn in the wrong font. The title DLIs are now handled in full by the new code.
The first title screen glitched (reported by a person after release). For the first four seconds after power-on, the PAC-MAN logo appeared in the maze font. The headless tests had missed it because they start a few seconds after power-on. Cause: the cartridge installs its VBI before the new initialisation runs. The first VBI builds the title, lands in the middle of that initialisation, and then its RAM clear wipes a value the title had just set. The fix moved the new initialisation before the VBI install.
Bug screenshot: the title screen's PAC-MAN logo shown as scattered fragments of the wrong font in gold on a black screen.
The first title screen before the fix.
…and the fix broke the disk and XEX versions (b0025-b0026). The smoke test caught it at once: those two formats hung at the title, while the cartridge was fine. Loading from disk overwrites the OS's own screen at $9C20, so ANTIC was displaying program bytes as a display list full of DLI requests. The old order had happened to switch to the title's display list before DLIs were turned on; the new order didn't, and the title DLI handler ran on almost every scanline. The fix that stayed (b0027) writes ANTIC's display-list register directly when the list actually changes, and only the shadow when it is the same list. That keeps the 8-line fix as well.

13. Versions 1.3 to 1.6: the boost button, VS mode and drop-in

The next request asked for two things. Holding the fire button on any joystick should double that player's speed. And a VS mode: a Pac-Man that eats an energizer can eat the other Pac-Men as well as the ghosts, and an eaten Pac-Man's "pac-ghost" races back to its starting position.

Turbo

Because every Pac-Man already runs through the original's movement code once per frame (PACSTEP in its own context), double speed is a second step in the same frame while that player's STRIG reads 0. The first version called the whole step twice. That was correct but expensive. With all four players holding the button the game logic needed about 35 more scanlines per frame, and the sprite picture rate fell from 96% to 63%. The original's speed counter (SPDSEQ) only lets Pac-Man move on some calls. On the others a step only re-reads the stick, re-checks the maze and re-selects the same picture. PACST2, the extra step, now runs just the speed counter and does the rest only when the step really moves. That brought four turbo players back to 84–88%, with two or three players at 100% and no game-logic frame lost. One concern was that a faster Pac-Man might jump over the exact square where the original checks for an energizer. A test from every direction and frame phase showed it can't: even at turbo speed Pac-Man moves at most one 2-line step per frame.

VS mode

The pac-ghost needed a route home through the maze, and the cartridge already contained a map of the maze. The original's ghosts decide at the "crossings" of ten row and ten column positions, and the tables of open directions at each crossing are packed in the ROM. A new build step, a Python script, unpacks those tables with the disassembly's decoder and checks that every passage is two-way. It then runs a breadth-first search from each player's start and writes, for each of the 90 crossings, the first move of the shortest way home: a 400-byte table assembled into the cartridge. In the game the pac-ghost looks up its crossing's move, keeps going straight along the corridor in between, and becomes a Pac-Man again when it reaches its start. A test drops a pac-ghost on every crossing for every player (360 trips): all reach home, the longest in 96 frames.

A title-screen trap

"n PLAYER VS GAME" is three characters longer than the original's text. The first version corrected the original's line after it was written. The test then read memory that did not match the (correct) screen. The cause: the title's interrupt routine rebuilds the score lines every frame and runs on past the top of the next frame, so the test's read landed between the original's write and the correction. The screen was right only because the correction still came before the beam reached those lines. Now both option lines are written once, with every character's final value, so there is no in-between state to show.

v1.4: a boost that runs out, with speed lines

A follow-up request limited the boost: it should last 2 seconds and then need to recharge, in about four seconds, with a dot and a "SHIINK" sound effect when it is ready again; and Pac-Man's picture should show speed lines while boosting.

v1.6: drop-in

For the online arcade, where a player who is out otherwise watches until the game ends, the behaviour chosen was drop-in: a player who is out presses fire and is back, with three Pac-Men and a score of 0, while the others play on. It is a title option, on at power-on, switched with the D key (the arcade drives the title with SELECT and can't type, so there it is always on).

14. Timeline, versions and safety nets

Time (5 Oct)BuildsStep
16:36-17:05—Setup; MADS built; disk source matched; byte-identical cartridge source
17:07b0001Original relocated to a 16K cartridge; cartridge, disk, XEX
17:17-17:21b0002-b0003Phase 1: multiplexer (original game through it)
17:35b0004-b0005Phase 2: four players
17:40-17:54b0006-b0017Speed work
17:56-18:14b0018-b0023Fixes, high score, title, autopilot tests
18:18b0024v1.0
20:00-20:06b0025-b0027v1.1: first title screen fix
20:11b0028v1.2: "4-PLAYER" under the logo
20:46-21:07b0029-b0032v1.3: turbo button, VS mode, pac-ghost routes
21:37-21:45b0033-b0034v1.4: 2 s boost with a 4 s recharge, ready dot, SHIINK, speed lines
21:52-21:53b0035-b0036v1.5: VS: 5,000 / 10,000 / 20,000 for eating a Pac-Man
6 Oct 00:05-00:16b0037-b0040v1.6: drop-in (fire to rejoin), title option on the D key

All builds (40 by v1.6) were kept, each with the sources that produced it. Every edited file was copied aside first (33 copies of source files alone), and timestamped backups of the whole project were taken along the way. The v1.2 image used 12,721 of the cartridge's 16,378 bytes (v1.3: 13,923; v1.4: 14,571; v1.6: 14,867). The 4-player code is about 2,500 lines of new assembly in two source modules, plus 178 lines of the original marked as changed (; 4P).

15. Lessons

Credits

This modification builds directly on other people's work:

Pac-Man © Bandai Namco; Atari 400/800 version © 1982 Atari, Inc.; developed by Roklan Corp. (Joe Hellesen). This is an unofficial, experimental modification.