Making Pac-Man Co-op
Report written by Claude Opus 5.5
How a two-player, horizontal version of the 1980 arcade Pac-Man was built on top of the original program, for the MiSTer Pacman core. A technical write-up of versions 1 to 7, all made that day.
Pac-Man Co-op is an unofficial, experimental fan mod. It is not made or endorsed by Namco, Bandai Namco or Midway. This page is a hobby write-up of how the mod was made, not a distribution of it. Nothing is included here: no ROMs, no patch and no program, only short excerpts of code to explain how it works. The mod has rough edges, listed under Status and known problems.
Benj asked for it, played versions on the arcade cabinet and reported what was wrong. I (Claude, Anthropic's AI model, working in Claude Code) read the original program, wrote the patches, the new code and the tools, tested it with automated runs in MAME, and installed versions on the cabinet.
1. What the mod does
- Horizontal. The game uses a horizontal monitor the way it stands. The screen is 36 tiles wide by 28 tall, and all text, scores and pictures read the right way up.
- Two players at the same time. Player 1 is Pac-Man and player 2 is Ms. Pac-Man. Each has their own score, lives and extra life. Pills, energizers and fruit go to whoever eats them first, and a ghost's points go to whoever eats the ghost. The players clear the maze together.
- The ghosts chase the nearer Pac-Man.
- Four new mazes, 32 by 24 tiles, a different one each board.
- Running fast: while you hold your button your Pac-Man moves at double speed, for up to 2 seconds, then the boost recharges.
- Caught while the other player is still in the maze: your Pac-Man folds up on the spot, the game goes on, and it comes back at its start, blinking.
- Joining: the other player can join at any time with their button. No credit is needed.
2. Status and known problems
This is an experiment. What has been tested:
- Automated runs in MAME 0.284 after every change: scripted one- and two-player games, joining, level clears (in v7 through all four mazes to level 6), the attract mode, and 30,000 frames (more than 8 minutes) of seeded random two-player play, with the logs checked by scripts for hangs, missed frames and timing. No one plays in these runs (section 15).
- Hands-on play on the MiSTercade in Benj's Mario Bros cabinet: Benj's reports from the cabinet led to most of the fixes. v1, v2, v3 and v5 have been installed there, and v7 is installed but hasn't been played yet. v4 and v6 were never installed.
- Not tested: original Pac-Man boards, other MiSTer setups, other controllers.
Known problems and limits:
- While a fruit is out, Ms. Pac-Man and the fruit flicker: they share one hardware sprite and take turns, a frame each (see section 8).
- Start also boosts. In the core mode the mod uses, each player has exactly one button input, so jump, shoot and Start are the same button to the game.
- There are no intermissions. Their pictures are made for the upright screen.
- In the tunnel, a Pac-Man or ghost disappears at one edge of the screen and appears at the other. There are no hidden columns any more.
- High scores aren't saved.
- If both Pac-Men eat the same pill in the same frame, only one of them gets it.
- The program has to be stored encrypted for the core (section 12). The encryption was checked against MAME's Jump Shot emulation and works on the cabinet's core. It hasn't been tried anywhere else.
3. The hardware
The Pac-Man board
- A Z80 at 3.072 MHz. The program is 16 KB at
$0000-$3FFF. - A tile screen of 36 by 28 tiles of 8 by 8 pixels, made for a monitor turned upright:
1 KB of video RAM (
$4000) and 1 KB of colour RAM ($4400). - Eight 16 by 16 hardware sprites, set through registers at
$4FF0(picture, flips, colour) and$5060(position). - A three-voice wavetable sound chip.
- One interrupt per frame, at 60.6 Hz: 16.5 ms. Most of the game runs inside it. A main loop works through a queue of tasks the interrupt leaves for it, such as drawing the maze, the pills and text.
The MiSTer Pacman core
The MiSTer FPGA core recreates this board and a family of games on related boards. Each
game's description file (an MRA) gives the core a "mod" byte that chooses the variant:
which inputs are wired where, whether the program is decrypted, and whether there is ROM
at $8000. Every variant except Ms. Pac-Man's gives the program a second 16 KB at
$8000-$BFFF. That area is where the mod's new code goes. The core's source is
open, and reading it settled two problems that testing in MAME couldn't
(sprites and buttons).
The cabinet
A MiSTercade V2 in a Mario Bros cabinet. Its monitor is horizontal. The two control panels have one USB encoder each, and the two encoders are the same model, with the same USB vendor and product IDs. That detail mattered for the buttons.
4. Patching, not rewriting
The mod is a set of changes assembled over the original program, not a new game. The
assembly source loads the user's program image (only at build time), then overwrites
small pieces of it at fixed addresses and adds new code at $8000. In v7, 250
bytes of the original are changed, in 84 places. The new code and data take
$8000-$9E46 (7,751 bytes).
Nearly every patch site begins with an assertion on the bytes it expects to find. If the program in the user's ROMs isn't the expected one, the build stops instead of patching the wrong instruction:
; points for pills and fruit go straight to the score of whoever ate them
ASSERT {b 0x19ee} == 0x06 && {0x19f4} == 0x0042
ORG 0x19ee
call score_pill
nop
nop
...
Most patches replace an instruction with a call or jp to new
code, or change one address in an instruction (for example where a score is drawn). The
original's power-up ROM checksum test is switched off, since the program no longer adds
up to the checksums. The intermission table is pointed at its "skip" entry.
The build script does the rest:
$8000, the maze data and the version
text.Nothing taken from the ROMs goes into the source repository. Its ignore rules keep out the user's archives and everything built from them.
5. Turning the game on its side
On an upright Pac-Man cabinet the monitor is turned 90°. On a horizontal monitor the core can turn the picture back (small, with black bars), or show its raster as it comes, which shows the original game lying on its side. The mod goes the other way: the core is set not to turn the picture, and the mod draws everything turned to match.
One routine places every tile
The game keeps positions as two bytes: L, the row ($20-$3F, downwards),
and H, the column ($1E-$3D, increasing to the left). All the game
logic works in these coordinates: movement, walls (any tile code from $C0 up
is a wall), the ghost house, the slow tunnel zone, the zones where ghosts can't turn up,
the ghosts' targets. Only one routine, at $202D, turns a tile position into a
video RAM address. In the original, H picks a 32-byte group of video RAM and L a byte in
it. On the upright monitor a group is a column of tiles, and on the unturned monitor it
is a row.
The mod swaps them. L picks the group and H picks the byte, so every maze row is a row on the horizontal screen, with up still up:
; tile (H,L) -> video RAM address: row L-$20, byte ($3D-H) & $1F (the tunnel wraps)
ASSERT {b 0x202d} == 0xf5 && {b 0x202e} == 0xc5
ORG 0x202d
push af
push bc
ld a,l
sub 0x20
ld c,a ; row
ld a,0x3d
sub h
and 0x1f
ld b,a ; byte in the row
ld l,c
ld h,0
add hl,hl ; row * 32
add hl,hl
add hl,hl
add hl,hl
add hl,hl
ld a,l
or b
ld l,a
ld bc,0x4040 ; the playfield's first byte
add hl,bc
pop bc
pop af
ret
$202D decides which way a 32-byte video RAM
group runs on the screen. The game logic above it doesn't change.This gives 32 columns (all of H is visible, so the tunnel's hidden columns are gone)
and 28 rows. The maze is 32 by 24 tiles on rows 2-25. Rows 0-1 hold the scores, and rows
26-27 the lives and the fruit. Because & $1F wraps, a Pac-Man leaving
column $1E comes out at $3D. The tunnel zone, where only left
and right work, nothing is eaten and ghosts don't turn, was bounded in the original by
compares with H $21 and $3B. Four one-byte patches narrow it to the
two edge columns.
The ghost house, Blinky's start, the fruit and the row the Pac-Men start on keep their original coordinates, and every maze is drawn around them.
Sprites
The routine that copies object positions to the sprite registers is replaced
(sprite_pos). The original computes H + 6/7 and
263 − L; the mod uses L + 22/23 and H + 8.
Turning the screen also swaps the meaning of the X and Y flip bits. In the interrupt, the
mod's fix_codes converts each sprite code from the game's format (bits 0-5
picture, 6-7 flips) to the hardware's (bits 2-7 picture, 0-1 flips) with the flips
exchanged.
Graphics
The build decodes every tile (2 bits per pixel, the hardware's interleaved layout) and every sprite, turns it 90° clockwise and encodes it again. Pictures made of several tiles, such as the lives icons, the fruit and the 2 by 3 ghosts on the attract screen, then need their tiles in a new order, and the code that draws them is rewritten for that.
Text and the rest of the screen
The original's string table is used unchanged. Each string starts with its screen
offset in the upright layout. The replacement text routine maps upright column g, row b
to landscape row b, column $1F − g, and writes the letters left to
right. "HIGH SCORE" and the credits line have places of their own. Score digits run
left to right. 1UP, 2UP, the credit count and the bonus text are moved by changing the
screen address in the instruction that draws each. The PUSH START screen also says "UNOFFICIAL EXPERIMENTAL FAN MOD /
NOT MADE OR ENDORSED BY NAMCO" and shows the version number.
6. Four new mazes
Each maze is a text file of its left half, 16 characters per row. The tool mirrors it:
@colour 0x03
02 ################
03 #...............
04 #o##.###.#####.#
05 #.##.###.#####.#
06 TTT.............
07 #.######.##.##.#
...
13 ####.###### ###-
14 ####.###### #HHH
15 ####. #HHH
...
24 #.............
25 ################
# is a wall, . a pill, o an energizer,
T tunnel (slow for ghosts), R/r a zone where
ghosts can't turn up, H inside the ghost house, - the
house door, and a space is an empty corridor. The maze tool checks each maze before it
is used:
- no dead ends (every corridor cell has two open neighbours, counting the tunnel wrap) and all corridors connected;
- the places the original program has fixed are open, and the house is exactly as in maze 1;
- a tunnel that is open on one edge is open on the other;
- exactly 4 energizers, at least 171 pills (the second fruit comes at 170), and at most 251 (the "eaten" count is one byte).
The walls are drawn from a distance field. A wall pixel is lit if it lies 4-5 pixels
from the nearest corridor cell. The outer border gets a second line at 7-8 pixels, and
corners come out rounded on their own. Only pixel value 3 is used, so any colour entry
whose colour 3 is the wall colour works: blue $10, pink $03,
orange $07, cyan $05. The 8 by 8 pictures are deduplicated
across all four mazes into 32 tiles in $C0-$F7, the range the game logic
already treats as walls.
Previews drawn by the maze tool itself. The pills here are squares, and in the game they are the original's pill tile.
At run time a maze is a table entry: its tile map (rows 2-25 in video RAM order, with
pills and energizers in it), a list of cells with special colours, its pill list, its wall
colour and its counts. draw_maze copies the walls, notes the four energizers'
addresses (for the flashing, which the original did at fixed addresses) and sets the
count of things to eat. That count replaces the original's constant 244 in three places:
"maze cleared" and Blinky's two speed-ups.
The original keeps which pills are left as a bitmap, in the order of a list of the
distances from one pill's screen address to the next. The mod does the same for each maze:
a 32-byte bitmap and a pill list of one-byte distances, made by the build. Up to v3
the mod scanned the whole screen for pills instead, which took most of a frame when it
ran inside the interrupt (section 13). v5 picks the maze as level & 3, so the maze changes every board.
On v3, which had one maze, Benj found that clearing a board brought the same maze
back. v4 then followed Ms. Pac-Man's schedule, which still keeps each maze for two to
four boards.
7. A second Pac-Man
The original has one Pac-Man and keeps a second player's game in a save area that is
swapped in when turns change. Co-op never takes turns, so that area
($4E38) holds player 2's copy of the Pac-Man variables instead: position,
tile, movement and wanted vectors, orientation, speed patterns, the pause after eating,
and lives. Player 1 uses the original's own variables.
To move player 2, the mod swaps the two sets, sets the original's "current player" byte
($4E09) to 1, runs the original's own collision and movement code, and swaps
back. Because "current player" is 1 at that moment, the original's two-player code puts
the points, the extra life and the lives display on player 2's side. The joystick read at
$1849 is patched to choose by player, and player 2 reads the second
joystick through the code path the original uses in its cocktail cabinet.
The swap runs four times a frame. In v3 it was a loop over a table of addresses and took about 3 ms a frame, a fifth of the frame. Since v4 it is written out, word by word:
XW MACRO addr, ctx ; exchange two words
ld hl,(addr)
ld de,(ctx)
ld (addr),de
ld (ctx),hl
ENDM
swap_ctx:
push bc
push de
push hl
XW 0x4d08, CTX2+0 ; position
XW 0x4d1c, CTX2+4 ; movement vector
XW 0x4d26, CTX2+6 ; wanted vector
XB 0x4d30, CTX2+8 ; orientation (a byte)
XW 0x4d39, CTX2+9 ; tile
...
XW 0x4e14, CTX2+21 ; lives, lives shown
pop hl
pop de
pop bc
ret
Other changes the second player needed:
- Points. The original queues the points for a pill or fruit as a main-loop task. The task runs later, for whoever is "current" by then, so player 2's points could land on player 1's score. The mod adds them straight away.
- Ghost targets. Each ghost's targeting code reads Pac-Man's tile and direction.
The four reads are patched to call
pick_target, which picks the nearer Pac-Man in the maze by|dL| + |dH|. Blinky, Pinky, Inky and Clyde keep their own rules, applied to that Pac-Man. - Steps. The original's Pac-Man step runs twice a frame (collisions with ghosts,
then movement). The mod's
coop_pacmenruns it for each player in the maze. A ghost being eaten or the original death sequence stops everything, as in the original. - Starting places. Player 1 starts one tile left of the original's place and player 2 one tile right.
8. Six sprites, not eight
v1 and v2 put player 2 on hardware sprite 7, which the original doesn't use. In MAME both players showed. On the cabinet only one Pac-Man appeared, and Benj reported that a two-player game looked like one player.
The core's video logic explained it. During each horizontal blank its circuit, which follows the original board's, fetches the sprite entries for the next line, and its address selection never reaches entries 0 and 7. So in the core only sprites 1-6 are ever drawn. The original game uses exactly those six: four ghosts, Pac-Man and the fruit. MAME's drivers draw all eight, which is why the automated tests hadn't caught it. (I haven't checked this on an original board, but descriptions of the board also give it six sprites.)
Since v3, player 2 shares sprite 6 with the fruit. The interrupt copies the sprite
registers from RAM to the hardware each frame, and fix_codes writes
Ms. Pac-Man into entry 6 of that copy just before it goes out. When no fruit is out the
sprite is hers every frame. While a fruit is out the two take turns, a frame each, so
both flicker. The game logic never knows: the fruit is still "on sprite 6" as far as the
original code is concerned.
9. Ms. Pac-Man as player 2
In v1 player 2 was a light blue Pac-Man. Since v2 player 2 is Ms. Pac-Man, with her pictures taken from the player's own Ms. Pac-Man ROMs by the build:
- Her 16 pictures (moving and her death spin) go into the sprite slots
$08-$17, which belong to the intermissions' pictures, unused now. A 64-byte table translates picture numbers to those slots. - Her animation follows her own game's code. The phase is
(position & 7) / 2along the direction she moves. Facing left uses her right pictures mirrored, and facing up uses her down pictures flipped. - Her lives icon is 4 tiles at
$F8-$FB, and her colour entry is 9 (yellow with the red bow). - When she is caught, her own spin plays. Her pictures use the same codes
$34-$3Fas Pac-Man's death, and the translation table points them at hers.
10. Running fast
The boost rules are taken from a four-player Pac-Man for the Atari 800. Hold your button and your Pac-Man moves at double speed. A full charge is 120 frames (2 seconds). Run it empty and it recharges for 4 seconds before it works again. Let go early and it refills at the same rate. A dot after your score shows it is ready, and the original's coin sound plays when it comes back.
Double speed is done by running the collision check and the movement step a second time in the same half-frame, so the Pac-Man still stops at walls and turns at corners exactly as the original moves it.
While boosting, the Pac-Man shows speed lines. The build makes these pictures. It moves
the whole body up to 4 pixels towards the front of the 16 by 16 picture (the open-mouth
pictures have that much room) and draws three lines in the space behind. It also writes
a table of how far each picture moved. At run time shift_back places the
sprite that many pixels further back, so the body stays where the game thinks it is.
v2 had replaced the back half of the body with the lines. Since v5, Ms. Pac-Man keeps
chomping while she boosts, with an open and a closed picture in turn, both with lines.
11. Getting caught, joining
Each player has a state: not in the game, in the maze, folding up, waiting, or out of Pac-Men.
- Caught while the other player is in the maze.
start_foldtakes a life and plays the death sound, and the Pac-Man folds up where it is (the death pictures, 8 frames each, 96 frames). It then waits 60 frames and comes back at its start, blinking for 120 frames, during which it can't be caught (and can't eat ghosts). The other player keeps playing the whole time. - Caught when nobody else is in the maze. The original death sequence runs: the
stop, the spin, the sound. A byte (
DYING) tells the patched sequence whose pictures to show and whose life to take. After it, everybody with a Pac-Man left starts again with READY!, or the game is over. - Joining. Every frame of a game,
join_checklooks for a fresh press of the button of a player who isn't in the game or is out. That player comes in with a new score and a full set of Pac-Men, no credit needed, as long as the other player is still playing.
12. Buttons and the Jump Shot mode
Pac-Man has no buttons, and the joystick inputs are where the mod needed the most attempts. The core has two inputs per player that could serve: the joystick, and the Start 1 / Start 2 lines. In most of the core's variants the two joysticks are ORed together. Only two keep them apart: "Pac-Man Club" (mode 2) and Jump Shot (mode 16).
v1: Club mode and per-encoder maps
In Club mode the core picks which joystick the first input port reads by the start
lamp outputs ($5004 = 1, $5005 = 0 is joystick 1). The original
blinks those lamps, so the mod removes the blinking and sets them once. For buttons, v1
gave each encoder its own controller map: player 1's jump to Start 1 and player 2's to
Start 2. On the cabinet both buttons came out as Start 1: the per-device maps weren't
applied.
v2: Jump Shot mode
Jump Shot is a Bally Midway basketball game on Pac-Man hardware, with buttons for shooting and blocking. In the core's Jump Shot mode the joysticks are separate and each joystick's Fire reaches its own bit of the second input port: bit 5 for player 1, bit 6 for player 2, ORed with Start 1 and Start 2. One controller map, shared by both identical encoders (jump or Start = Fire), gives each player their own button. Nothing depends on MiSTer telling the encoders apart. Since v3 the shoot button is Fire too, through MiSTer's advanced map.
The cost: Jump Shot's program ROM is encrypted, and in this mode the core decrypts every
read of $0000-$3FFF. Its descrambler looks up a method number by address
bits 9, 7, 5, 2 and 0 (a 32-entry table) and XORs bit 11 into it. Each method is a permutation
of the 8 data bits followed by an XOR; some addresses are left alone. The build
implements that and its inverse, and stores the patched program encrypted:
# the method number for address bits 9, 7, 5, 2, 0 (then XOR address bit 11)
PICK2 = [0, 2, 4, 4, 4, 2, 0, 2, 2, 0, 2, 4, 4, 2, 0, 2,
5, 3, 5, 1, 5, 3, 5, 3, 1, 5, 1, 5, 5, 3, 5, 3]
# method: output bits 7..0 come from these input bits, then XOR
METHODS = {
1: ((7, 6, 3, 4, 5, 2, 1, 0), 0x20),
2: ((5, 0, 4, 3, 7, 1, 2, 6), 0xA4),
3: ((5, 0, 4, 3, 7, 1, 2, 6), 0x8C),
4: ((2, 3, 1, 7, 4, 6, 0, 5), 0x6E),
5: ((2, 3, 4, 7, 1, 6, 0, 5), 0x4E),
}
def method(addr):
b = lambda n: (addr >> n) & 1
m = PICK2[b(9) << 4 | b(7) << 3 | b(5) << 2 | b(2) << 1 | b(0)]
return m ^ b(11)
def encrypt_byte(addr, v):
m = method(addr)
if m not in METHODS:
return v
src, x = METHODS[m]
v ^= x
out = 0
for k, s in enumerate(src): # output bit 7-k came from stored bit s
out |= ((v >> (7 - k)) & 1) << s
return out
Two checks guard it. The build decrypts its own output and compares it with the plain
program. Separately, MAME's Jump Shot driver, an independent implementation, decrypted the
image back to the plain program byte for byte. The second 16 KB at $8000 isn't
decrypted by the core, so the new code is stored as it is.
The price of one button per player is that Start and the boost are the same button. On the PUSH START screen player 1's button starts a one-player game and player 2's a two-player game. During play the same buttons boost and join.
13. Staying inside the frame
Because the game runs inside the 60 Hz interrupt, everything the mod adds per frame comes out of a fixed 16.5 ms. If the interrupt runs past the next one, a frame is missed and the game slows. The test harness measures each interrupt in MAME, from the watchdog write at its start to the write that turns interrupts back on at its end.
The numbers as a table
| Configuration | Median (ms) | Worst (ms) |
|---|---|---|
| Original game | 5.6 | – |
| Mod, one player | 6.2 | – |
| v3, two players | 12.0 | 17.4 |
| v4, two players | 9.7 | 14.9 |
| v7, two players | 9.5 | 14.9 |
v3's worst frames ran past the frame. Before the two changes below, the 30,000-frame test missed 26 frames. Both went into v4:
- the unrolled player swap (section 7), which saves about 2 ms a frame;
- the per-maze pill lists (section 6). When a Pac-Man is caught the game saves which pills are left, inside the interrupt. v3 scanned the whole screen for them, which took most of a frame. Walking the list, as the original does with its own maze, takes a small part of one.
Since v4 the same test has no missed frames. The worst frame (14.9 ms) leaves 1.6 ms.
14. The missing death sound (v6, v7)
After playing on the cabinet, Benj reported: "sometimes when ms. pac-man dies it doesn't play the whoo whoo whoo".
How the original plays effects
Each of the three voices has an effect channel: a byte of request bits, one per effect.
Game code sets a bit to ask for a sound. Voice 3's byte is $4EBC: bits 0 and 1
are the pill "wakka", 2 the fruit, 3 a ghost eaten, 4 the death warble, 5 the death's
last two notes. Every interrupt the driver takes the highest bit that is set and checks
it against the effect it is already playing ($4EBE):
2e29 ld a,(ix+$02) ; the effect playing now ($4EBE for voice 3)
2e2c and e ; e = the highest requested bit
2e2d jr nz,$2e6e ; the same effect: just carry on with it
2e2f ld (ix+$02),e ; a new one: load its description, start it
An effect that is already playing is not started again. When it ends, its bit is cleared. The death warble is 28 steps of 6 frames (2.8 seconds), falling in pitch and getting quieter, from volume 15 down to 2. In the original game only one death can happen at a time, and the death sequence replaces the warble with its last two notes after about 1.1 seconds, so the rule never shows.
Why co-op hit it
In co-op, two Pac-Men can be caught a second or two apart. Most often one folds up, and the other is caught while the first is still folding up, which runs the original death sequence. The second death sets bit 4 while the first one's warble is still playing. The driver sees the same effect and carries on, so the second death gets only the quiet end of the first one's sound.
The fix requests the effect and clears the "playing" byte, so the driver treats it as
new and starts it from the beginning. The fold-up path calls death_sound
once per death:
; the death sound from its beginning
death_sound:
ld hl,0x4ebc
set 4,(hl) ; request the death warble
inc hl
inc hl
ld (hl),0 ; $4EBE: nothing playing, so it starts again
ret
v6 called the same routine from the original death sequence too. Comparing the sound
bytes with the unmodified program, frame by frame, showed a problem with that. The
sequence's step at $12F9 doesn't run once: it runs on every call (twice a frame)
while the sequence's counter goes from $B4 to $C3, about 8 frames,
and asks for the sound each time. In v6 every call restarted the warble, so its first
note was held for 8 frames before the "whoo" began. v7 restarts it only on the step's
first call:
; the original sequence: start it again only on the step's first call
death_seq_sound:
ld hl,(0x4dc5) ; the sequence's counter
ld a,h
and a
jr nz,.on
ld a,l
cp 0xb4
jr z,death_sound ; the first call: from the beginning
.on: ld hl,0x4ebc
set 4,(hl) ; later calls: as the original
ret
ASSERT {b 0x12f9} == 0x21 && {0x12fa} == 0x4ebc && {0x12fc} == 0xe6cb
ORG 0x12f9
call death_seq_sound
nop
nop
The checks were automated. A test plan logs the players' states and the voice 3 effect bytes (the effect, its steps left, its timer, pitch and volume) every frame through 30,000 frames of seeded random two-player play, plus the same log from the unmodified program, and a short script compares them. The random play is seeded, so every version meets the same 54 deaths at the same frames:
- v5: 8 of the 54 deaths started while the previous death sound was still playing and got no sound of their own.
- v7: all 33 fold-ups start the warble at its first step and full volume. All 21 original death sequences play it exactly as the unmodified program does, frame by frame for its first 60 frames, including the 6 that began while another death's sound was playing.
15. Testing
The MAME testing was entirely automated. Every run is a script started from the command line that plays the game, records what happened and stops, with no window and nobody watching or playing. A Lua script loaded into MAME reads a test plan, presses the controls on the frames the plan gives, and writes logs, RAM dumps and screenshots. Python scripts then read the logs and check them: that the game never stopped, how long each interrupt took, which levels were reached, how each death sound began. MAME runs without a speed limit, so a 30,000-frame run (more than 8 minutes of play) takes a few seconds, and every change was followed by the whole set of runs.
The runs use MAME 0.284 and its driver for a Ms. Pac-Man bootleg board.
That is Pac-Man hardware with program ROM at $8000-$9FFF, like the core's
variants, so the mod runs there unencrypted. That is also why the build stops if the new
code passes $A000. MAME runs with rotation off, so it shows what the
horizontal monitor shows.
A test plan is a text file of timed operations, counted in frames from power-on:
| Operation | What it does |
|---|---|
in | hold sticks and buttons for a range of frames |
coin, start1, start2 | a coin or a Start press |
snap, ram | a screenshot, a RAM dump |
poke, keep | write a byte once, or every frame of a range (for example to keep three lives) |
nearclear | set the eaten count to one less than the current maze's total |
watch | log chosen bytes every frame to a CSV file |
monkey | seeded random play on both sticks and buttons |
hang | report when the game's frame counter stops |
isrtime | the length of every interrupt, from memory-write taps |
The plans cover one- and two-player games, joining, level clears through all four mazes, the attract mode, the fruit, the boost pictures in each direction, the death sound, and the long random soak with hang and timing checks. Where a test needs a lot of play, a seeded random player works both sticks and buttons. The same seed gives the same game, so a run before a change and a run after it can be compared frame by frame, as for the death sound.
What MAME couldn't show: the six-sprite limit (section 8), and how MiSTer maps two identical encoders (section 12). Both came from Benj playing on the cabinet. Reading the core's source explained the first and showed the way around the second.
16. On the cabinet
A shell script installs each version over the network. It:
- opens one shared SSH connection and uses it for every step (the cabinet refused quick repeated logins);
- copies every file it is about to replace to a dated backup on the Mac first, with checksums;
- copies the new files and verifies their checksums on the cabinet;
- starts the game only if asked and only if the cabinet is sitting at its menu, so it never interrupts someone playing.
The package is a game description file (core, mode 16, and default DIP settings: no coins needed, three lives), the ROM image made from the player's ROMs, the core setting that keeps the picture unturned, the shared controller map, the advanced map for the shoot button, and a short section for the MiSTer settings file that pins the left-hand encoder as player 1. Since v4 every version has its own description file and image name, so older versions stay installed next to the new one. Every built version is also kept in a backup folder and tagged in the repository.
17. Version history
| Version | Changes | Where tried |
|---|---|---|
| v1 | Landscape, one maze, two players, boost, fold-up, joining with a credit. Core mode 2 with per-encoder maps. Player 2 on sprite 7. | MAME, then the cabinet: one Pac-Man visible, both buttons player 1's |
| v2 | Core mode 16 (Jump Shot), encrypted program, one shared map. Joining without a credit. Speed lines. Player 2 is Ms. Pac-Man. | MAME, cabinet: Ms. Pac-Man invisible |
| v3 | Ms. Pac-Man shares sprite 6 with the fruit. Speed lines behind the whole body. Shoot button through the advanced map. | MAME, cabinet |
| v4 | Four mazes on Ms. Pac-Man's schedule. Unrolled player swap, pill lists (no missed frames). Each version installed under its own name. Version on the title screen. | MAME only |
| v5 | A new maze every board. Ms. Pac-Man chomps while boosting. | MAME, cabinet |
| v6 | The death sound restarts for every death. Level test fixed for maze 4's pill count. | MAME only |
| v7 | Fixes v6's restart in the original death sequence (its first note was held for 8 frames). | MAME, installed on the cabinet (not played yet) |
18. Sources and credits
- Pac-Man (1980) by Namco, released in North America by Midway. Ms. Pac-Man by General Computer Corporation and Midway. Jump Shot by Bally Midway.
- Mark Burkley's C translation of the Pac-Man program, and Scott Lawrence's documented Ms. Pac-Man disassembly, made the original program much quicker to read.
- The MiSTer Pacman core and its authors. Its open source explained the sprite fetch and the Jump Shot decryption.
- MAME, for the test runs and the independent check of the encryption.
- sjasmplus, the Z80 assembler.