⌁ EXE INTERNALS

ROUND 42

Reverse-engineering a 1986 DOS shooter, byte by byte

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.

© 1986 Elven Software by Mike Pooler Turbo Pascal 3.0 · flat .COM image CGA 160×100×16 low-res PC-speaker sound 60,416 bytes 42 rounds · 15 enemy types

▶ Play the faithful HTML5 clone: round42.html

The icon format

How the sprites were decoded

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.

The stream

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.)

Worked example — the round-1 bird, frame 0

; 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.

The CGA 16-colour palette

Every colour you see is one of these. The hex digit is the value stored in the sprite stream.

Decompilation

From machine code back to Pascal

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.

⚠ The Pascal on the right is a reconstruction from the machine code, not the original source (which no longer exists) — but it compiles to the same shape.

1 · Sound — the PC-speaker tone @ 0x057F

▸ disassembly
0x0581  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
▸ reconstructed pascal
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;

2 · PlotIcon — the sprite XOR blitter @ 0x3DAE

▸ disassembly
0x3DD2  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
▸ reconstructed pascal
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;

3 · the round → enemy dispatch @ 0xC381

▸ disassembly (a long if/else chain — how TP3 emits 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
▸ reconstructed pascal
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 }
📜
The whole engine, reconstructed. Every subsystem above was decoded from the EXE and reassembled into one compilable Turbo Pascal 3.0 program — ~1,500 lines: graphics, sound, all 15 enemy builders, the 5 movement modes, spawn, warp, and the round-flow main loop, each routine citing its EXE address. A clean-room logic reconstruction (names invented, Borland runtime omitted), not the lost original.
▸ read round42.pas (highlighted)  ·  raw
15 enemy types · live animation

Sprite gallery

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.

The player

Your ship

Not a sprite buffer at all — the ship is a hardcoded 6-cell pattern drawn by its own prim (0x39740x3B7A), 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.

42 rounds

The level design map

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.

Enemy AI

The six movement modes

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.

Travel rounds

The warp mazes

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.

PC speaker

One voice, re-pitched every frame

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.

EffectEnvelopeRoutine
Laser / siren200 + n·320x7E11
Explosion2000 − n·160x8DD3
Player death1000 − n·80xBB29
Phasor880 + n·160x6E79
Extra life(n+5)·1000xC023

The title melody is a data table

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].

Field notes

Memory map & key addresses

AddressWhat lives there
0x0100entry JMP → program init
0x0FB2STACKCHECK (TP3 runtime)
0x057FSound(freq)
0x3DAEPlotIcon — sprite XOR blitter
0x3974ship draw prim
0xC381round → enemy-builder dispatch
0xC19movement-mode selector
0xAC05formation spawn
0xBF19warp play loop

The enemy record (10 bytes @ 0x260 + i·10)

OffsetField
+0 / +1logical x / y (cells)
+2 / +3drawn x / y (for XOR-erase)
+4dx (word)
+6dy (word)
+8animation 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.