← 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.
- Summary: what was done, how it was checked, what is uncertain
- The goal and the starting point
- Getting source code: matching the cartridge to Roklan's disk source
- A 16K cartridge skeleton and a build that keeps everything
- The core problem: eight objects, five sprites
- Phase 1: the sprite multiplexer
- Phase 2: four Pac-Men in a one-Pac-Man program
- Game rules for four players
- Score lines, high score and the title screen
- Making it fast enough
- Automated testing: the headless harness and the autopilot
- The bugs, and how they were found
- Versions 1.3 to 1.6: the boost button, VS mode and drop-in
- Timeline, versions and safety nets
- Lessons
- Credits
1. Summary: what was done, how it was checked, what is uncertain
What the agents did
- Turned the original 8 KB cartridge into assembler source that rebuilds it byte for byte, by matching it instruction by instruction against Roklan's own source code of the disk version of the game (section 3). A second agent, working in the background, produced a fully commented disassembly of the cartridge from the same material.
- Wrote the four-player additions on top of the unchanged original code: a sprite multiplexer that shows eight objects with five hardware sprites, a per-player copy of the original Pac-Man state, the four-player rules, new score lines and title-screen options (sections 4–9).
- Added the later features in versions 1.3 to 1.6: a speed boost on the fire button, a VS mode in which Pac-Men can eat each other, and drop-in rejoining (section 13).
- Built, tested and recorded every version. All 40 builds up to v1.6 (build b0040) were kept.
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
- Every build was booted and played in headless copies of the atari800 emulator, controlled by test scripts that press console keys and joysticks, read memory, and compare the emulator's screen buffer pixel by pixel (section 11).
- The cartridge, boot disk and XEX versions were each tested. The cartridge was also run on an emulated 16K Atari 800, and v1.6 was also checked with the Altirra replacement operating system that the arcade boots.
- A search-based autopilot played long games. One run covered 41 mazes (90,000 frames), ate 302 ghosts and lost 123 Pac-Men with no crash or stall. For v1.6, 20,000-frame runs with random steering and random fire for every player produced between 11 and 30 rejoins per run, again with no crash or stall.
- CPU time was measured with probes that read the scanline counter (section 10).
What is imperfect or uncertain
- Not every frame gets a new picture with four players. With one to three players every frame does. With four players moving around the maze about 95% of frames do; with all four boosting at once it measured 84–88% (v1.3), and long VS-mode test runs measured 74–79%. A missed picture leaves the previous one on screen for one more frame; the game's speed does not change.
- Flicker. When more than five objects share the same scanlines, one of them is left out on alternate frames and flickers.
- Skipped game-logic frames. In the long VS-mode test, every run of both v1.5 and v1.6 skipped four game-logic frames, and two of four long VS stress runs of v1.6 skipped one each. The cause has not been investigated. In the marathon run, logic frames were skipped only at maze changes, while the new maze is copied.
- Measurements vary between runs. The headless emulator's start-up is not frame-exact, so timing-dependent numbers differ slightly from run to run.
- Emulator only. The development log records no testing on real Atari hardware and no play sessions with four people. The four-player rules (section 8) are design choices made during development.
- Tests only see what they look at. The one bug reported by a person was in the first seconds after power-on, which the tests skipped, and its first fix broke two of the three formats (section 12).
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 cartridge image: 8 KB of 6502 code and data at
$A000-$BFFF. Atari's 1982 port, developed by Roklan, software by Joe Hellesen. - Roklan's own source code of the disk version ("REVISION 3.0, 10/03/82"), adapted for the MADS assembler by JAC! in 2018. A comparison showed it is not the cartridge: only about a third of the cartridge's bytes appear in it verbatim.
- No public disassembly of the cartridge could be found online.
- The toolkit from earlier work in the same project: the cc65 assembler and linker, a modified atari800 emulator with a control socket (memory, joysticks, screenshots, screen buffer), and the experience of the Wizard of Wor 4-player conversion done earlier the same day.
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:
- A tracing disassembler follows the cartridge's code from its entry points (cartridge start, VBI, DLIs), so code and data are told apart.
- 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.
- 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:
- labels: a disk label at a matched instruction names the cartridge address;
- variables: when the disk says
LDA PMVPOSand the cartridge saysLDA $89,$89isPMVPOS(decided by votes over all matches; the zero-page layout turned out to be identical); - comments: Roklan's line comments and block comments were copied onto the matching cartridge lines.
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.
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 raw 16K ROM image, and a cartridge image with the header used by the atari800 and Altirra emulators (type 2);
- a boot disk: a small boot loader reads the image into RAM and starts it the way the OS starts a cartridge;
- an XEX executable, which blanks the screen, loads the image, sets the DOS vectors and starts it.
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:
| Approach | Verdict |
|---|---|
| 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 maze | Mode-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 missiles | Pac-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 screen | Every 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:
- finds the visible objects and their top and bottom scanlines;
- sorts them top to bottom;
- 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;
- draws the images into sprite memory;
- 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
- Channel M's colour is PF3, which the maze also uses, for the inverse "READY!" and "GAME OVER" letters. While such text is on screen, channel M isn't used.
- Hardware collisions are useless when a sprite carries different objects. They became software tests: Pac-Man against ghost reproduces the original's rule ("missiles 1 and 2 touch the ghost, and the heights are within 5 lines"); a separate test covers Pac-Man against the fruit.
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.
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:
- Lives: three Pac-Men each (one in play, two in reserve) and one more at 10,000 points, as in the original.
- Caught while others play: resetting the whole board every time anyone is caught would stop a four-player game constantly. Instead the caught Pac-Man stands still for half a second, plays the original fold-up animation with its sound, and waits a second. It then comes back at its start, using a reserve, and blinks for two seconds during which it can't be caught. The others carry on.
- Caught as the last Pac-Man on the board: the original death sequence runs (the
ghosts stop and vanish, then the fold-up). A new routine,
RST4, brings back everybody with a reserve and restarts the round, as the original does, or ends the game. With one player this is the original behaviour. - Sharing: the first to eat a dot, energizer or fruit scores it. A ghost scores for the Pac-Man that ate it, with the original freeze and the score shown at that spot. The maze is cleared together; a player who is folding up or waiting when it's cleared comes back for the next maze.
- Start places: player 1 at the original start; players 2 and 3 on the top corridor, left and right; player 4 on the bottom corridor. Each start had to be consistent with the game's internal coordinates (screen address and sub-character counters).
- Colours: yellow (the original), light blue, magenta, white. These are clear of the four ghost colours and of the blue of frightened ghosts.
- Two players now play simultaneously; the original alternated.
9. Score lines, high score and the title screen
- Scores: the original's two text lines ("1UP HIGH SCORE 2UP") are one colour only. They became two ANTIC mode-6 lines, where each character can have one of four colours. Players 1 and 2 share line 1 (left and right) and players 3 and 4 line 2. A DLI between the lines switches two colour registers from the player 1/2 colours to the player 3/4 colours. The original score routine was kept; it only got a per-player screen position and colour bits.
- Reserve icons: a copy of the ROM font in RAM with a Pac-Man glyph in an unused slot, up to three icons per player next to the score.
- High score: drawn in the maze's bottom row, where the original showed its reserve icons, using twelve new 3×5-pixel glyphs (0-9, H, I) added to the maze font. It is also shown on the title screen.
- Title screen: SELECT cycles 1-4 players (1-2 on an XL/XE, recognised by its operating system). The version and build number appear on screen, and since v1.2 "4-PLAYER" sits under the logo. That line used to show the logo's own font, which has no letters, so a new DLI switches it to the letter font.
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:
| Before | After | |
|---|---|---|
| Game logic | ~110 units | ~47 units |
| RENDER | ~70-80 units | ~60 units (autopilot play) |
| Frames with a new picture, 4 players | 53% (logic frames skipped) | 95-99% (logic never skipped) |
What changed
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
| Test | What it checks |
|---|---|
| Smoke test | Cartridge, XEX and boot disk all boot, start a game and play |
| Multiplexer test | Eight synthetic objects in six hard layouts: every object's pixels on screen, in its colour, in place, over several frames |
| Event scenarios | Situations 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 test | Random play to game over, logging every state change (deaths, respawns, outs, freezes, the last-Pac-Man sequence) |
| Rate and timing tests | New pictures per frame; CPU time of logic and renderer |
| Autopilot and marathon | An 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.
12. The bugs, and how they were found
WSYNC. A rewritten fast path
takes about 38.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.$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
- Modes. SELECT now steps through a table of seven modes (1–4 players, then 2–4 players VS); on an XL/XE the ones with more than two players are skipped.
- Power. When
MEET4sees the energizer bits (BIGDT1) change during a player's check, that player is powered until the blue time ends. - Vulnerable Pac-Men take the ghosts' blue/white flash. Two powered Pac-Men can't eat each other, and one that is blinking after coming back is safe.
- Eating used the ghosts' 200–1600 chain (one shared count) and the fruit's gobble sound, without the freeze, so play never stops. In v1.5, following a request that eating another player should be worth a lot of points, a Pac-Man is worth 5,000, then 10,000 and 20,000 for a second and third on the same energizer, on a count of its own.
- The pac-ghost is a new player state. The object shows a ghost picture in the player's colour, which moves two steps per frame toward home.
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.
- The meter is one charge byte per player (120 frames). Holding the button drains it by 1 per frame. Run empty, the player is locked out, and the charge refills by 1 every other frame. That gives exactly 2 seconds of boost and a 4-second recharge, which the test confirms frame for frame. A boost let go early refills at the same rate without the lock-out.
- The dot is a new character in the score-line font, in the free space after each score, drawn in the player's colour while the boost is usable.
- The SHIINK is a 28-frame table played on POKEY voices 3 and 4, which only the ghost-eaten "gulp" otherwise uses (and play stops for that). It goes: three frames of white noise ("sh"), a pure tone sweeping up from about 0.8 to 1.9 kHz ("ii"), then a 2 kHz ring with a quieter partial at 2.5× above it, fading out ("nk"). The test records it as WAV audio and checks the shape frame by frame. It stops at once if play stops (pause, freeze, death, maze cleared).
- Speed lines had to fit in a sprite 8 colour clocks wide, around a body that already takes 7. Lines above and below the body looked like brackets; one-clock dashes behind it looked like noise. What reads as speed is a smear: the front of Pac-Man stays round, while its back half breaks into three lines that run one column past the body, with the rows between them cut back. Moving up or down, the same idea turned sideways uses the free rows of the 16-row object box. A build-step script turns ASCII art for "right" and "up" into all 12 pictures: left is right mirrored, and down is up flipped, as in the original's own shapes. It can also draw a preview at the TV's pixel shape. In the game, the routine that picks Pac-Man's shape already knows the direction and the mouth frame; it passes them on, and a 10-byte table chooses the speed picture.
- Cost: the first version added 7 scanlines a frame even with nobody boosting: a divide-by-10 lookup on every step, and the dots redrawn every frame. Passing the shape on directly and redrawing the dots every 4th frame brought it down to under 3. With everyone holding the button, the game now does less work than v1.3, because the boost runs out.
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).
- Through the game's own respawn. The out state already sits in the
per-player loop that runs every frame of live play. A new branch there,
JOIN4, gives the player 3 Pac-Men and calls the ordinary "come back" routine, which takes one: the Pac-Man appears at its start, blinking and untouchable for two seconds, with a full boost. Because that loop runs only while play goes on (not in the last Pac-Man's death scene, a freeze, a new maze, the start of a round or a pause), somebody is always on the board when a player drops in, and the game over is unchanged: when the last Pac-Man on the board is caught and nobody has a reserve, the game ends. - A fresh press. The fire button is also the boost, so a player who is boosting when caught would otherwise be back the moment they're out. One byte per player remembers "the button was seen up while out"; only then does a press count. The same byte, set to $80 on the way back in, keeps the joining press from being a boost until the button is let go.
- The score goes back to "00", exactly as a new game draws it, and the bonus flag is cleared so 10,000 earns a Pac-Man again. Just before, the old score is compared with the high score (the end-of-game check, split into a per-player routine), so the run that ended still counts.
- The prompt. "FIRE" in full-size letters filled the dot and icon places
exactly, but on the left it ran straight into the score ("12340FIRE"). Two new
characters in the score font each hold two letters 3 pixels wide, the size of the
maze's
HIdigits, so the word sits in the middle with a gap on either side. It blinks once a second. - A lost keypress. The title's main loop threw away keys by storing $FF in
CHon every pass. The firstDhandler readCH, then stored $FF some 25 cycles later, so a key that arrived in between vanished. It showed up as one failed test run out of three; the title test now pressesD30 times at different moments (the faulty build lost one of them), andCHis now cleared only after a key was read from it. The headless harness also turned out to hold a key for a single frame, so the "held key" test now sends the key before every frame, and checks that the OS's key repeat really happened.
14. Timeline, versions and safety nets
| Time (5 Oct) | Builds | Step |
|---|---|---|
| 16:36-17:05 | — | Setup; MADS built; disk source matched; byte-identical cartridge source |
| 17:07 | b0001 | Original relocated to a 16K cartridge; cartridge, disk, XEX |
| 17:17-17:21 | b0002-b0003 | Phase 1: multiplexer (original game through it) |
| 17:35 | b0004-b0005 | Phase 2: four players |
| 17:40-17:54 | b0006-b0017 | Speed work |
| 17:56-18:14 | b0018-b0023 | Fixes, high score, title, autopilot tests |
| 18:18 | b0024 | v1.0 |
| 20:00-20:06 | b0025-b0027 | v1.1: first title screen fix |
| 20:11 | b0028 | v1.2: "4-PLAYER" under the logo |
| 20:46-21:07 | b0029-b0032 | v1.3: turbo button, VS mode, pac-ghost routes |
| 21:37-21:45 | b0033-b0034 | v1.4: 2 s boost with a 4 s recharge, ready dot, SHIINK, speed lines |
| 21:52-21:53 | b0035-b0036 | v1.5: VS: 5,000 / 10,000 / 20,000 for eating a Pac-Man |
| 6 Oct 00:05-00:16 | b0037-b0040 | v1.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
- Find the original source, even an imperfect one. A different build of the same program, aligned instruction by instruction, turned an anonymous ROM into named, commented code in about half an hour (see the timeline).
- Wrap, don't rewrite. The context switch let the original Pac-Man routines run unchanged four times per frame. Most of the code that runs is the original's, which is why the game behaves like the original.
- On the Atari, cycles before a
WSYNCare scarce. A DLI has about 40 cycles to reach its firstWSYNCon a busy mode line; every wrapper and extra check can push the next write a scanline late. - Never restart ANTIC from code that may run after the frame has begun.
- Separate what must keep time from what only needs to look right. Game logic in the VBI, rendering in the main loop: a late picture is harmless, a skipped game frame isn't.
- Interrupt code and main-loop code must not share scratch memory or buffers, the same rule learned earlier on another Atari game in this project, Violent Checkers.
- Test from power-on, in every format. The one bug reported by a person was in the first seconds the tests skipped, and its first fix broke two of the three formats. Those formats were caught only because the smoke test runs all three.
- An autopilot covers more than random input. Random steering never clears a maze; a breadth-first-search bot of about 20 lines exercised maze changes, ghost eating and late levels over long runs.
Credits
This modification builds directly on other people's work:
- Joe Hellesen and Roklan Corp. wrote the original Atari 400/800 Pac-Man for Atari (1982): the cartridge, and the source code of the disk version (revision 3.0, 10/03/82). Most of the code that runs in the 4-player edition is still theirs.
- JAC! adapted Roklan's disk source for the MADS assembler (2018), with build options for the original, Roklan, Atari and Datasoft disk versions. That port supplied the names, comments and listings the cartridge was matched against (section 3); without it the cartridge's code would have had to be named from scratch.
- MADS (Mad-Assembler) by Tomasz Biela assembled the disk source; it was compiled with the Free Pascal compiler.
- Tools used throughout: the cc65 assembler and linker, the atari800 emulator (the online arcade runs it compiled to WebAssembly), and AltirraOS by Avery Lee, the free replacement operating system the online arcade boots.
- Pac-Man was created by Namco (1980).
Pac-Man © Bandai Namco; Atari 400/800 version © 1982 Atari, Inc.; developed by Roklan Corp. (Joe Hellesen). This is an unofficial, experimental modification.