Everything below — every sprite, every animation frame, every enemy's motion and the
42-round design — was pulled straight out of the original ROUND42.EXE by
disassembling it and replaying its graphics routines. No guessing, no redrawing. This page is
the field notes.
▶ Play the faithful HTML5 clone: round42.html
The game stores no plain bitmaps. Each sprite is a compressed XOR stream that
a blitter (PlotIcon at 0x3DAE) walks one cell at a time.
Re-implementing that blitter exactly reproduces every sprite pixel-for-pixel.
A sprite is a list of (colour, advance) word pairs. For each pair the blitter
XORs the attribute byte (two pixels: high nibble = left, low nibble = right; nibble 0
= transparent) into a screen cell, then jumps ahead by advance bytes. A row is
0xA0 (160) bytes, a cell is 2; advance 0 ends the sprite.
The trick that makes it work: track the absolute screen offset, so an
advance > 2 leaves real transparent gaps. (An early decoder treated every pair as
adjacent, compacting the gaps — which is what broke the symmetry of the round-1 bird.)
; source @ 0x2D27, replayed (0xB0, +0x9E) ; left px = B (l.cyan) (0xB0, +2) ; → wing tips ...terminates at advance 0 ↓ becomes ↓ b . . . . . b ; symmetric! . b b . b b . . . . b . . .
15 enemy builders live in the code; a switch on the round number
(0xC381) picks which builder fills the four animation-frame buffers each round.
Every colour you see is one of these. The hex digit is the value stored in the sprite stream.
The game was compiled with Turbo Pascal 3.0 and the source was sold off in 1986, then lost. But TP3's code generation is simple and literal, so the original Pascal reads right back out of the disassembly. Here are three routines — the real instructions on the left, the source they most likely compiled from on the right.
Sound — the PC-speaker tone @ 0x057F0x0581 mov ax, 0x34DD ; DX:AX = 1,193,181 0x0584 mov dx, 0x12 ; (the PIT input clock) 0x0587 cmp dx, bx ; bx = freq; too small? 0x0589 jae ret 0x058B div bx ; ax = 1193181 / freq 0x058F in al, 0x61 ; speaker gate 0x0595 or al, 3 0x0597 out 0x61, al 0x0599 mov al, 0xB6 ; ch.2, square wave 0x059B out 0x43, al 0x059D out 0x42, al ; divisor lo 0x05A3 out 0x42, al ; divisor hi
procedure Sound(freq : word); var divisor : word; begin if freq <= 18 then exit; { avoid /0 overflow } divisor := 1193181 div freq; { clock / wanted Hz } Port[$61] := Port[$61] or 3; { gate speaker on } Port[$43] := $B6; { ch.2, square } Port[$42] := Lo(divisor); Port[$42] := Hi(divisor); end;
PlotIcon — the sprite XOR blitter @ 0x3DAE0x3DD2 mov si, ax ; si = y*0xA0 + x*2 0x3DD8 inc si ; +1 -> attr byte .next: 0x3DDC mov bl, [di] ; bl = colour 0x3DDE add di, 2 0x3DE0 mov cl, [di] ; cl = advance 0x3DE2 add di, 2 ; ... wait for retrace (0x3DA) ... 0x3DF6 xor bl, [si] ; XOR into the cell 0x3E02 mov [si], bl 0x3E05 cmp cl, 0 ; advance = 0 -> end 0x3E08 je .done 0x3E0C add si, cx ; jump ahead 0x3E0E jmp .next
procedure PlotIcon(x, y : integer; iseg, iofs : word); var cell : word; colour, advance : byte; begin cell := y * 160 + x * 2 + 1; { B800 attr cell } repeat colour := Mem[iseg:iofs]; Inc(iofs, 2); advance := Mem[iseg:iofs]; Inc(iofs, 2); WaitRetrace; { CGA vsync } MemB800[cell] := MemB800[cell] xor colour; Inc(cell, advance); until advance = 0; end;
case)0xC388 mov al, [bp+4] ; al = round 0xC38D cmp ax, 1 0xC390 je .r1 0xC3A1 cmp ax, 2 0xC3A4 je .r2 0xC3B5 cmp ax, 3 ; ... 42 comparisons ... .r1: call 0x9F23 ; BuildBird .r2: call 0x7C7B ; BuildRobot .r3: call 0x7944 ; BuildGem .r4: call 0x6365 ; SetupWarp
procedure BuildRound(round : byte); begin case round of 1, 34 : BuildBird; { spr0 · bounce } 2, 29 : BuildRobot; { spr1 · march } 3, 25, 31 : BuildGem; { spr2 · march } 5, 38 : BuildSpinner; { spr3 · bounce } 7, 15, 26 : BuildPyramid; { spr5 · fall } { ...the other enemy types... } 4,8,12,16,20,24,28, 32,35,36,37,39,40,41 : SetupWarp; { mazes } 42 : BuildBoss; { spr14 · home } end; end;
The enemy state itself is a classic Pascal record — 10 bytes, one per slot in the wave array at 0x260:
type Enemy = record X, Y : byte; { +0 +1 logical cell position } DrawnX, DrawnY : byte; { +2 +3 last drawn pos (XOR-erase) } DX, DY : word; { +4 +6 velocity } Frame : byte; { +8 animation frame } end; var Wave : array[0..45] of Enemy; { @ 0x260 }
Pulled from the EXE and animated at their real frame data and CGA colours. Several sprites cycle colour as well as shape across frames — which is why a single wave shows the same creature in four colours at once. The robots are stored solid-white and recoloured per instance at runtime.
Not a sprite buffer at all — the ship is a hardcoded 6-cell pattern drawn by its own prim (0x3974 → 0x3B7A), XOR-plotted around a single reference cell. Decoded straight from the draw routine.
b light-cyan body · a light-green wing-tips · f white core. A little arc-fighter, 6×3 cells.
Each round routes through the dispatch at 0xC381 to one enemy builder (its sprite) and one movement mode. Every 4th-ish round is a warp maze. Same sprite ⇒ same motion, always — the AI is bound to the creature, not the level number.
A global [0xC19] selects the per-frame mover. Enemies step in ±1-cell increments; one moves each cadence tick (round-robin), so survivors speed up as a wave thins.
Warp rounds (builder 0x6365, play loop 0xBF19) are static: the whole winding tunnel is drawn at once through a magenta striped wall. Your ship rises from the bottom to the top automatically — a counter [0xC4B] ticking 12 → 2 — while you steer left/right to stay in the gap. Touch the wall (read by prim 0x397A) and you die. Reach the top and the round is cleared.
Real warp rounds: 4, 8, 12, 16, 20, 24, 28, 32, 35, 36, 37, 39, 40, 41 — a cluster near the end, not simply every fourth.
The PC has a single speaker channel (PIT channel 2, square wave). The game calls
Sound() from inside per-frame object routines, so the speaker is continuously
re-pitched by whatever updated last — a constant morphing warble, not discrete beeps.
| Effect | Envelope | Routine |
|---|---|---|
| Laser / siren | 200 + n·32 ↑ | 0x7E11 |
| Explosion | 2000 − n·16 ↓ | 0x8DD3 |
| Player death | 1000 − n·8 ↓ | 0xBB29 |
| Phasor | 880 + n·16 ↑ | 0x6E79 |
| Extra life | (n+5)·100 ↑ | 0xC023 |
A byte sequence at mem 0x2CE6 indexes a word frequency table at 0x2CD9:
; freqs (idx0 = rest) [rest, 220, 247, 262, 294, 330, 349] A3 B3 C4 D4 E4 F4 ; melody 1 5 1 5 1 6 1 6 … 4 3 4 3 2 3 4 4 4 4 4 1 1 1 …
Sound(freq) @ 0x057F sets the PIT divisor =
1,193,181 / freq. F9/F10 flip the mute mask [0x0C57].
| Address | What lives there |
|---|---|
| 0x0100 | entry JMP → program init |
| 0x0FB2 | STACKCHECK (TP3 runtime) |
| 0x057F | Sound(freq) |
| 0x3DAE | PlotIcon — sprite XOR blitter |
| 0x3974 | ship draw prim |
| 0xC381 | round → enemy-builder dispatch |
| 0xC19 | movement-mode selector |
| 0xAC05 | formation spawn |
| 0xBF19 | warp play loop |
| Offset | Field |
|---|---|
| +0 / +1 | logical x / y (cells) |
| +2 / +3 | drawn x / y (for XOR-erase) |
| +4 | dx (word) |
| +6 | dy (word) |
| +8 | animation frame |
Formation = a 7/6/7 block of 20, constant across rounds (top y15, mid y24, bottom y35, x-spacing 8 cells). The per-round difference is the sprite and the motion, never the layout.