← Experimental Atari Multiplayer Arcade
How Demon Attack 4-Player Was Made
A record of an experiment in using AI coding agents to modify a vintage game · 6 October 2026
- What this is
- The goal and the starting point
- Source code from a cartridge
- Traps, and an OS address
- How the original draws the screen
- Where do four cannons fit?
- The new bottom of the kernel
- Four lasers
- Four players in the game logic
- Scoreboard and set-up
- Bringing back the explosion
- Fitting it into a frame
- Automated testing
- Bugs, including the agent's own
- Timeline and numbers
- Known limits and open questions
- Lessons
- Credits
1. What this is
Demon Attack 4-Player is an experimental modification of Imagic's 1982 Demon Attack cartridge for the Atari 400/800. Up to four people play at the same time, each with their own laser cannon on their own joystick port, against the same waves of demons. The modification was made by an AI coding agent (Claude Code) working from the original program, the machine code in the cartridge, with no source code available. All testing was done automatically in an emulator.
- What the agent did. In one evening, 6 October 2026, it measured which bytes of the cartridge are code, generated assembler source that rebuilds the original cartridge byte for byte, showed that this source can be moved in memory, and removed the original's copy-protection traps. It then changed the game: four cannons on the four hardware players, four lasers on the four missiles, the original's player code run once for each player, a two-line scoreboard, set-up options for one to four players, and the original's explosion debris brought back. Version 1.0 (build b0010) was followed half an hour later by version 1.1 (build b0011), which fixed a bug found while the development report was being written.
- How it was checked. A byte-identical rebuild of the original; a relocation test that ran two builds of the source at different addresses side by side for 6,000 frames and compared their memory; and a suite of headless emulator tests: boot tests of the cartridge, program file and boot disk on an emulated Atari 800 (and of the cartridge on an XL), 31 scripted event checks, and an autopilot that played complete games, eight at a time, across the four game variants and one to four players, while timing probes checked that every part of each frame finished on time.
- What is imperfect or uncertain. Bullets and the diving bird are no longer drawn over the cannons; only one exploding cannon at a time shows the original's debris; in the worst measured frames, parts of the work finish as little as six scanlines before their deadlines. The tests were automated, an autopilot does not play like a person, and bugs the tests did not provoke may remain. Details are in section 16.
This page is adapted from the working report written by the agent that did the work, with facts checked against the project's development log and notes. "The agent" means that session.
2. The goal and the starting point
The request was a four-player simultaneous version of Demon Attack, the fourth game in the series after Wizard of Wor, Pac-Man and Joust 4-Player.
The starting point was the original cartridge image: an 8K Atari 400/800 cartridge (Imagic,
1982). A first look at the bytes showed that the program and its graphics fill only the top 4K,
$B000-$BFFF; the lower 4K are all $FF. So there was room for new code
inside the same cartridge size. The finished game is still a standard 8K cartridge that runs on
a 16K Atari 400.
No source code or disassembly of the Atari version was available, and none from anyone else was used: everything below was worked out from the cartridge itself.
The original is a one-player game with two-player variants: in some, two players take turns with one cannon, and in two of them they share one cannon cooperatively. "Four-player simultaneous" meant four cannons on the ground at the same time, on joysticks 1–4, which only the Atari 400 and 800 have (the later XL and XE models have two joystick ports).
| Player | Cannon | Joystick | Scoreboard |
|---|---|---|---|
| 1 | red (the original's player 1) | port 1 | line 1, left |
| 2 | blue (the original's player 2) | port 2 | line 1, right |
| 3 | green | port 3 | line 2, left |
| 4 | yellow | port 4 | line 2, right |
3. Source code from a cartridge
The method and most of the tools came over from Joust 4-Player, built the day before: a headless test harness, a 6502 disassembler, and a private build of the emulator with coverage counters.
A coverage emulator
The coverage emulator is a private build of the project's atari800 emulator (the version with a control socket that the headless test harness drives). It counts every executed instruction per address, can fix POKEY's random seed, and writes out the counters on request. Eight games of the original, about 20,000 frames each with random joystick input, were played and their counts merged: 1,408 instruction addresses in the ROM ran. Everything that ran is code; the rest is data, or code those games never reached.
A source generator
A generator script disassembles from the reset vector, the VBI and DLI addresses and every
covered address, and writes source for the ca65 assembler: 2,552 lines, 1,774 instructions, each
with its original address and bytes in a comment, and "(never ran)" where the coverage says so. A
symbol table holds the names the agent gave while reading the code (KERNEL,
KBAND, VBI, MOVECANNON, LASERHIT,
NEXTWAVE, CANNONSHAPE…) and marks every immediate operand that is
half of an address (lda #<KERNEL, lda #>DLIST) so the code can
move.
Assembled and compared with the cartridge, the generated source was byte-identical.
Proving the source can move
Byte-identical only proves that the source reproduces the cartridge in place. A relocation test
assembles it twice, at $A000 and 8K lower at $6000, runs both in two
emulators with the same random seed and the same scripted joystick input, and compares their RAM
every 500 frames. Getting the two runs onto the same cycle took two fixes: a start stub that waits
for a fixed frame and scanline, and an empty display list of its own (the OS screen memory differed
between the two load layouts, which changed ANTIC's DMA and so the timing). After that the two
runs stayed identical for 6,000 frames.
What the test could not see: the display list's jump-back address was a literal
$BF70 in the data, and the tracer variant loaded the low byte of a joystick table as a
literal $E3. The first is harmless in practice (the OS reloads the display list
pointer every VBI, which is also why the test could not notice it); the second sat in the
never-run game selection. Both became symbolic (<DLIST,
<(JOYTAB+3)) when the 4-player code replaced those parts.
4. Traps, and an OS address
Like Joust, the cartridge protects itself against being copied to RAM:
$B614: the VBI starts withLDA #0 : STA $B614, a store over its own opcode. In ROM nothing happens; in RAM the next frame hits aBRK. Less obvious: the kernel reads the low byte of that operand ($14) as data, the demons' height plus 13. Move the code and the demons change size.$BA8A: the "sound off" loop also stores zeros through a zero-page pointer that holds the kernel's address. In RAM it wipes the first 8 bytes of the display kernel.$BE5E: a check that the operand byte at$B615is still$14. It hangs if the code has moved.
In the 4-player source the traps are gone, with the timing kept: the kernel's 8-cycle read
became LDA #$07 : SEC : NOP : NOP. So the program-file (XEX) and boot-disk versions,
which run from RAM, work.
One more surprise: the VBI ends with JMP $E7D1, an address inside the 400/800's
OS-B ROM. On an XL the original cartridge simply hangs (checked in the emulator).
JMP SYSVBV ($E45F, the official vector) fixes it; the 4-player version
runs on an XL with one or two players.
That was build b0001: the unchanged game, reassembled into the new 8K layout (new code at
$A000, the original at $B000, where it must stay page-aligned because
its graphics are addressed with a fixed high byte), produced as a cartridge, a program file and a
boot disk. The disk and the program file use a small loader taken over from Joust that copies the
image to $A000-$BFFF and starts it (48K, BASIC off). From then on every build was
kept, together with the sources that made it.
5. How the original draws the screen
Demon Attack is an Atari 2600 game at heart, and its Atari 800 version draws like one. The display list is almost empty: some blank lines, one text line (the score), and a jump back. Everything below that is player/missile graphics written by the CPU, line by line, in a display list interrupt (DLI):
- The DLI fires at line 8 and busy-waits until line 44.
- Three bands of demons: each demon is player 0 (left half) and player 1 (right half, a mirrored copy of the shape in RAM), with position and size set per band, and shape and colour written every line.
- Below the demons, player 0 again: the column of falling bullets (or the bird that dives at the cannon in later waves).
- The cannon: player 3, 12 lines at the bottom (
$BC-$C7). The laser: missile 2. Together with the lives "bunkers" on the ground, these are the only things drawn by DMA from memory. - Then the ground (colour bars) and, still in the DLI, the game logic that needs the frame's collisions: which demon the laser hit (missile 2 against players 0/1, read after every band), and the demons' movement.
- The VBI does the rest: joystick, cannon, laser, firing, new demons, bullets, sounds.
Hits are found with the collision registers: missile 2 against players 0/1 for the demons, player 3 against player 0 for bullets and for the bird hitting the cannon.
6. Where do four cannons fit?
The Atari has four players and four missiles. The original uses players 0/1 for the demons, player 0 also for the bullets, player 3 for the cannon and missile 2 for the laser. Four cannons and four lasers need everything:
| Lines | Original | 4-player |
|---|---|---|
| demon bands | players 0/1: demon halves | the same |
| below the demons | player 0: bullets or the bird | the same, down to line $BA |
$BB | bullets | player 0 switched from bullets to cannon 1 |
$BC-$C7 | player 3: the cannon; player 0: bullets | players 0–3: the four cannons |
| everywhere | missile 2: the laser | missiles 0–3: four lasers, one colour (PRIOR "fifth player") |
The price: bullets and the bird are no longer drawn over the cannons, because there is no player left for them in those 12 lines. They vanish just above the cannon tops, and because the hardware can no longer see them overlap a cannon, their hits are computed in software, using the same pixels the collision registers used to see.
The cannon as the kernel draws it (red-brown), and the laser waiting in it (pink, lines
$BE-$C5, one pixel at x+3). The bullet column is drawn in cells of 8 lines: the cell
over lines $BD-$C0 meets the barrel rows (together $6C), the cell over
$C5-$C8 the base ($C6; with the continuous "laser" bullets of some
waves the cells are longer and the base mask is $EE). A hit is (bullet byte
shifted by the distance) AND mask ≠ 0, for each cannon.
One consequence the agent first took for a bug: a bullet exactly in the gap at x+3, where the laser sits, passes through. The original does the same (its collision check saw cannon pixels only).
7. The new bottom of the kernel
The kernel is timed to the cycle on every line (each line ends with STA WSYNC,
and about 104 CPU cycles are usable), so every change was counted:
- After each demon band the original checked "did the laser hit this band?" (14
cycles). Now the kernel stores all four collision registers
M0PL-M3PLinto per-band tables (HB0-HB3, 4 × 9 cycles); that line had time to spare. The hit logic runs after the frame. - Right after the last band (a line with about 80 free cycles once the now useless
HITCLRwas gone), players 1–3 get their cannon positions and colours, and graphics 0. - Bullets or bird on player 0 as before, but the loop stops at line
$BA. - Line
$BB: player 0 off, moved to cannon 1, cannon 1's colour. - Lines
$BC-$C7: four graphics writes per line from a 16-byte shape buffer per cannon (CSH; a cannon is hidden by clearing its buffer). Two values are loaded beforeWSYNC, so all four writes land within about 24 cycles of the line start, before the beam reaches the leftmost possible cannon.
KC_ROW: lda CSH,y ; cannon 1, row y (loaded before the line)
ldx CSH+16,y ; cannon 2
sta WSYNC
sta GRAFP0
stx GRAFP1
lda CSH+32,y ; cannon 3
sta GRAFP2
lda CSH+48,y ; cannon 4
sta GRAFP3
dey
bne KC_ROW
$BC-$C7: four cannons on players 0–3, each laser waiting in its
barrel.8. Four lasers
The laser is the only object drawn by DMA: 8 lines of missile memory at $1300. The
original wrote $20, missile 2's left pixel, over whole bytes. Four lasers share those
bytes, so the new drawing and erasing routines (LDRAW4/LERASE4) set and
clear only their own bit ($02, $08, $20, $80: the left pixel of missiles
0–3).
Missiles normally take their player's colour, and the players change colour every line (the
demon rows). PRIOR bit 4 ("fifth player") gives all four missiles the colour of playfield 3. The
kernel sets that to the original laser colour $4D at line 44, right after the
scoreboard, where playfield 3 is player 4's score colour. The OS restores it every VBI.
The lasers' horizontal positions are written at line 9, long before any laser can be on screen.
9. Four players in the game logic
The original's single-player code (explosion, cannon hit, movement, laser, fire) sat in the
VBI. It was replaced by PLAYERS4, which does the same for each player in the game,
and which runs in the DLI in the 36 lines the original spent waiting for the kernel to
start (section 12 explains why it moved there). Per player:
EXPL1: the explosion timer (as in the original: 63 frames, 16 frames of screen flash, the noise on sound channel 2); afterwards a cannon from the reserve, or out. Or a new hit:HIT1tests the bullets,BIRDCANthe diving bird.- Joystick to cannon (
MOVEC4, with the original's speed tables; in the tracer games the cannon moves 2 pixels a frame),MOVEL4for the laser (up 3–6 lines a frame depending on the wave, back into the cannon at the top), andFIRE4.
After the frame, LASERS4 finds each laser's first band in HB0-HB3 and
runs the original's hit logic for that player (normal demon, split demon half, or the bird, which
is tested in software by BIRDLAS), adding the points to that player's score.
The rest follows the original's rules, per player: 3 cannons in reserve, one more at the next
wave for every player not hit during the wave (BONUS4, 6 at most), out after the
last cannon, game over when nobody is left. The demons aim at the nearest living cannon
(NEARCX; in the original, "the cannon") and dodge the lowest flying laser. The
demons hold their fire while any cannon explodes, as the original did during its one explosion,
and the next wave waits until all explosions are over.
10. Scoreboard and set-up
The display list got a second text line (the same 10 bytes, now
$80,$70,$70,$46,<TXT1,>TXT1,$06,$41,<DLIST,>DLIST). In ANTIC mode 6
the top two bits of a character select one of four playfield colours, so each player's block is
in that player's colour: COLOR0-3 = red, blue, green, yellow. A block is 10
characters: the score, a cannon icon, and the number of cannons in reserve. The icon needs a
custom character, so the ROM font is copied to $0800 at power-on with the cannon in
place of the exclamation mark. One block is redrawn per frame. The original's bunkers on the ground, which showed
its lives, are gone; the scoreboard shows the reserves instead.
SELECT, which the original ignores, chooses one to four players (two on an XL/XE;
the 400/800 operating system is recognised by its RESET vector at $D800 or higher).
OPTION chooses game 1–4, mapped to the original's one-player games 1, 3, 5 and 7
(normal, tracer, advanced, advanced tracer). The original's two-player alternating and
cooperative variants have no use with four cannons. START or the button of joystick
1 starts a game. The demo before the first game shows the original's copyright line and a
version line.
11. Bringing back the explosion
The original's explosion is a spray of coloured debris, drawn with players 0/1 on either side
of the cannon. Build b0002 replaced it with a blinking cannon, because four explosions cannot
each have two players. Looking at the original again (b0008) showed a way: during an explosion
there are never any bullets (they are cleared at the hit, and no new volley starts while a cannon
explodes), and the bird was now also held back. So players 0/1 are free above the cannons, and
KEXPL4 draws the original's debris (the same routine, the same shapes and colours)
for one exploding cannon, 16 lines higher so that it ends above the cannon rows. That cannon
disappears as in the original; a second cannon exploding at the same moment blinks.
12. Fitting it into a frame
The kernel occupies lines 44–211 completely. Everything else has to fit in what is left,
and nothing may run late: the DLI's logic must end before the VBI at line 248 (the VBI would
interrupt it), the VBI must end before the DLI at line 8, and the players' code must end before the
kernel starts at line 44. Three profile bytes in RAM (PROFV, PROFD,
PROFT) record the latest end of each part, and the soak test fails if one is over
budget.
The first 4-player build worked, but its worst frames ended at line 40 (kernel at 44) and 240 (VBI at 248). The changes, measured one by one:
| Build | Change |
|---|---|
| b0003 | After a hit, the laser goes back into its cannon in the next frame's player code instead of after the kernel (it is not on screen in between); the bird test rejects far cannons at once. |
| b0004 | The laser's step is a subtraction (it was a loop of 3–6 steps); the bullet test rejects a far bullet column at once. |
| b0005 | The flying lasers are erased in the VBI (in the place of the removed trap's three NOPs) and redrawn before the kernel: half of the lasers' work moved out of lines 9–44. |
| b0006, b0009 | The scoreboard block moved to wherever there was room (in the end: after the kernel). |
| b0009 | The bird-against-cannon test: see the bug in section 14. |
Frames per end line of the players' code, with the same game set-up and the same autopilot (game 3, four players): b0004 (first) and b0005 (second). The kernel starts at line 44.
Final worst frames over eight soak games: the players' code ended by line 34–38, the logic after the kernel by 238–242, and the VBI 12 lines after 248.
13. Automated testing
All tests ran headless: the emulator with dummy video and audio, at turbo speed, on a private control socket, with no windows and without touching any other session's emulator. The tests used an emulated Atari 800 with Atari's OS-B ROM, and an emulated XL for the XL checks.
| Test | What it checks |
|---|---|
| Smoke test | The cartridge, the program file and the boot disk boot on an Atari 800 with OS-B, and the cartridge on an XL; a game starts, demons appear, the score rises. |
| Event tests (31 checks) | Set-ups made by writing RAM: the SELECT/OPTION cycles (and one or two players on the XL); each player's laser scores for that player only; a bullet 8 pixels away misses; a bullet on a cannon hits only that cannon; the debris and the blink; a cannon less after the explosion; out after the last cannon; the bird diving into a cannon; the bird shot by player 2 (points to player 2 only); bonus cannons per player; game over with the lasers gone and the final scores kept; a one-player game. |
| Soak test | An autopilot plays a game to the end: every cannon holds its fire button down and steers under a demon, stepping aside from the bullet column. The test checks that scores only grow, that lives stay between 0 and 6, that no laser is left after game over, and that the frame budget holds. |
| Parallel soak | Eight soak games at once: games 1–4 with one to four players. |
| Source checks | The byte-identical rebuild and the relocation test of the generated source, rerun along the way. |
14. Bugs, including the agent's own
$28, $6C, $EE, $C6), so once a frame the bird's rows are ORed
into four groups by the cannon row they meet, and each cannon needs at most four byte tests
(b0009).15. Timeline and numbers
| Time (6 Oct) | Build | Step |
|---|---|---|
| 19:25 | — | Project set up; tools taken over from the Joust 4-Player work |
| 19:30–19:57 | — | Coverage, generated source (byte-identical), relocation test |
| 20:02 | b0001 | The original in the 8K 4-player layout, traps removed, runs on an XL |
| 21:59 | b0002 | Four cannons, four lasers, scoreboard, SELECT/OPTION |
| 22:04–22:15 | b0003–b0006 | Timing |
| 22:23 | b0007 | One bullet, two cannons |
| 22:52 | b0008 | The original's explosion debris |
| 22:57 | b0009 | The bird over stacked cannons |
| 23:03 | b0010 | Version 1.0 |
| 23:34 | b0011 | Version 1.1: no frozen debris after SELECT/OPTION during an explosion (found while the report was being written) |
| Cartridge | 8K: new code 2,695 of 4,096 bytes; the original's part 3,296 bytes (it was 4,081: the single-player code it no longer needs was removed) |
| Source | 1,236 lines of new assembly in one new module; the generated source of the original,
forked, with 65 lines marked as changed (; 4P) by a recorded patch script; a
separate RAM map |
| Builds | 11, every one kept (cartridge, disk, program file and the sources that made it) |
| Tests | smoke (4 runs), 31 event checks, 8 soak games per run |
16. Known limits and open questions
- Bullets and the bird disappear in the cannon rows. No hardware player is left for them in the bottom 12 lines, so they vanish just above the cannon tops. Their hits on the cannons are computed in software from the same pixels the original's collision registers saw, including the gap in the barrel that lets a bullet through, as in the original.
- One debris explosion at a time. Only one exploding cannon shows the original's debris; a second cannon hit at the same moment blinks instead.
- Changes to what is on screen. The original's bunkers on the ground (its reserve cannons) were removed; the reserves are shown on the scoreboard instead.
- Game variants. Only the original's one-player games 1, 3, 5 and 7 are offered; its two-player alternating and cooperative variants are not.
- Timing margins. In the eight soak games, the worst frames finished the players' code by line 34–38 (the kernel starts at 44) and the work after the kernel by 238–242 (the VBI comes at 248). The one overrun found during development showed up in only one of eight parallel games.
- Hardware. Three or four players need an Atari 400 or 800, the only models with joystick ports 3 and 4; on an XL/XE the game offers one or two players. The cartridge runs with 16K of RAM; the disk and the program file need 48K with BASIC off. For the online arcade the cartridge was also checked on AltirraOS-800, the free operating system the arcade boots: it starts, recognises the 400/800 and offers four players, and copies of the machine restored from a snapshot stay in step with the original over 30,000 frames of four-player input. That check found one gap in the emulator itself (the paddle scan position was missing from its snapshots), which was fixed in the arcade's build of the emulator.
- Code the source checks never ran. Because of the wrong key in the coverage runs, the relocation test never ran the original's advanced-game and game-selection code. Those parts were checked by the byte-identical rebuild and, in the 4-player edition, by the event and soak tests, which play all four games.
- Emulator only. The development log records no testing on real Atari hardware and no play sessions with four people. The autopilot holds its fire button down and steers under the demons; it does not play like a person. The one bug found after version 1.0 was spotted in a screenshot, not by a test, so bugs in situations the tests did not provoke may remain.
17. Lessons
- Check what the coverage actually covered. The runs looked thorough (eight games), but one wrong console key made them identical. The "(never ran)" marks were the hint.
- A 2600-style kernel leaves time in odd places. The original spent 36 lines a frame waiting for its kernel; that became the four players' budget.
- When the hardware can't see it, compute it exactly. Bullets and the bird could keep their original hit areas only because the software tests reproduce the collision registers' pixels, down to the gap in the barrel.
- Measure the worst frame, not the average, and run many games: the overrun appeared once in eight parallel games.
- Look at the original again before accepting a substitute. The blinking explosion was a compromise until it turned out that the debris's players were free after all.
Credits
- Demon Attack © 1982 Imagic; game design by Rob Fulop. Imagic's Atari 400/800 cartridge is the version that was converted, and the original program, patched and without the single-player code it no longer needs, still runs in the 4-player edition.
- No outside disassembly, source code or notes were used: the agent disassembled the cartridge itself and generated the source from it with the project's own tools (section 3).
- Tools: the cc65 assembler and linker (ca65 and ld65) by Ullrich von Bassewitz and the cc65 contributors; the atari800 emulator by the Atari800 development team (the project's copy with a control socket, and a private build with coverage counters; the online arcade runs atari800 compiled to WebAssembly); ImageMagick for the screenshots; Atari's OS-B ROM in the tests; and AltirraOS by Avery Lee, the free replacement operating system the online arcade boots.