Raiders of the Lost Ark
Atari 2600 · CX2659 · 1982 · Howard Scott Warshaw
This is the actual ROM data from the cartridge, extracted and rendered. Every sprite below is drawn from the real bytes in the 8KB ROM. Every playfield layout is reconstructed from the PF register values. Every color is mapped from the TIA's NTSC palette. Here's what's really inside the cartridge that confused an entire generation of kids.
01The NTSC TIA Color Palette
The TIA chip generates color from a 7-bit value: 4 bits of hue (high nibble) and 3 bits of luminance (bits 1-3; bit 0 is ignored). That's 128 colors total. Every color you see in the game comes from this palette. Hover for the hex value.
The room color tables in ROM are just indices into this palette. Each room stores 4 values: background, playfield, Indy's color, and the P0 object color.
Room color assignments (from ROM)
02ROM Layout — 8,192 Bytes
The cartridge is split into two 4KB banks. Bank switching is triggered by reading
$FFF8 or $FFF9. Here's what's packed into each bank:
$84–$89 hold executable code written
fresh before each bank switch. LDA $FFF8; JMP $target. The CPU jumps to
RAM, reads the strobe (switching the ROM window), then lands in the new bank. These
same 6 bytes double as scratch registers between switches.
03Sprites Extracted from ROM
Every sprite below is rendered directly from the bytes stored in the cartridge. Atari 2600 sprites are 8 pixels wide, one byte per scanline, stored bottom-to-top. Bit 7 is the leftmost pixel. These are the actual graphics data from the ROM.
Indiana Jones — 8-frame walk cycle
11 scanlines tall. Sprite art by Jerome Domurat. The hat is iconic even at this resolution.
Indiana Jones — Parachuting
15 scanlines. The parachute canopy doubles his height. Single use, consumed on deployment.
Thieves — 4-frame animation
16 scanlines tall. The body is identical across all frames — only the legs change. In the Thieves' Den, five of these are drawn from a single P0 sprite, repositioned across ~32-scanline zones. Per-scanline color gradient gives them the robed look.
Spiders — 4-frame animation
9 scanlines. These appear in the Spider Room and fire web projectiles via Missile 0.
Shining Light — 4 frames
8 scanlines. Animated sparkle effect in the Room of Shining Light.
Special sprites
Developer easter egg: HSW
Howard Scott Warshaw's initials hidden in the ROM as two sprite graphics.
04Inventory Items — 8 Bytes Each
13 usable items, each stored as 8 bytes. The item's identity is encoded in the
ROM pointer itself: itemId = (lowByte - base) / 8. No separate ID table.
The pointer is the identity. Two birds, zero extra bytes.
Timepiece clock faces
8 orientations showing time of day. The Ball object draws the clock hands.
Raw hex: inventory sprite table
05Room Playfield Graphics
The TIA draws a playfield from three registers: PF0 (4 bits), PF1 (8 bits), PF2 (8 bits) = 20 pixels across the left half. The right half is a mirror (REFLECT mode). Each playfield pixel is 4 color clocks wide. Here are the actual playfield layouts from the ROM, rendered at the TIA's native resolution.
Static rooms (Kernel 1 — single PF row per room)
Scrolling rooms (Kernel 2 — PF changes every scanline)
Rooms 6–10 use tall data arrays that change PF1 and PF2 every scanline as the camera scrolls. These form the dungeon corridors, mesa landscapes, and valley terrain.
06128-Byte RAM Map
The 6532 RIOT chip provides 128 bytes at $80–$FF. That's it.
The stack grows down from $FF and gets reset every overscan. Here's
every byte accounted for:
$81 holds currentRoomId (0–13). When Indy
crosses a screen boundary, the game writes a new value. That's the entire world map
tracking system. Room transitions save Indy's position to $EB–$F2
so he reappears in the right spot when he returns.
07The Snake Trick — Animated
The snake in the Well of Souls is rendered using the Ball object — a single 1-pixel-wide TIA sprite. Each scanline, the game writes a new HMOVE value to shift the ball left or right. Over ~32 scanlines, the accumulated offsets create a sinuous wiggling shape. Here's the actual motion table data, animated:
08Four Display Kernels
The 2600 has no framebuffer. The CPU feeds the TIA pixel data in real-time, 262 times per frame. Most games use one rendering routine. Raiders uses four:
| Kernel | Rooms | Technique |
|---|---|---|
| Static Sprite | 0–5 | Fixed PF. P0 data stream with interleaved color commands — bit 7 set = TIA register write, not pixel data. One stream carries both shape and color. |
| Scrolling PF | 6–10 | PF1/PF2 change every scanline from tall lookup tables. Camera scrolls via offset index. Destructible walls use 6-byte bitmask at $E5–$EA. |
| Multiplexed | 11–12 | 5 enemies from 1 sprite. P0 repositioned per ~32-scanline zone via RESP0+HMOVE. P1 (Indy) drawn via PHP/stack manipulation. |
| Ark Pedestal | 13 | Single-height scanlines. Rainbow color cycling (COLUP0++ each line). Indy's height = adventure points. Lower score = taller pedestal. |
Multiplexed sprite: how 5 thieves come from 1
09TIA Hardware — Every Object Used
10Bank Switching via RAM Trampoline
The game switches between two 4KB banks four times per frame. But when you switch, the code you're executing disappears. Warshaw's solution: write a 6-byte machine code stub into RAM, jump there, and let the stub do the switch.
; The trampoline at $84-$89 (rewritten before each switch): $84: LDA $FFF8 ; read strobe → Bank 0 appears (or $FFF9 for Bank 1) $87: JMP $target ; jump to destination in the new bank ; Caller does: JMP $0084 ; jump into RAM — safe from the bank switch ; Frame timing: ; VBLANK → bank switch → room dispatch (Bank 0) ; KERNEL → bank switch → display kernel (Bank 1) ; OVERSCAN→ bank switch → collision handlers (Bank 0) ; AUDIO → bank switch → sound update (Bank 1)
11Two Controllers
One joystick button. 13 items. Warshaw needed "use", "drop", and "select" — impossible with one button. His solution: take both controller ports.
| Port | Stick | Button |
|---|---|---|
| Right (SWCHA bits 0–3) | Move Indy 8 directions | INPT5 — use selected item |
| Left (SWCHA bits 4–7) | Move inventory selector L/R | INPT4 — drop selected item |
LDA SWCHA ; one register, both joysticks ; bits 7-4 = left stick (inventory) ; bits 3-0 = right stick (movement) LDA INPT4 ; left fire = drop item LDA INPT5 ; right fire = use item ; Optimization: even frames process full input + weapon aiming ; Odd frames skip aiming math → halves CPU cost ; At 60fps, 30Hz input feels fine for this game's pace
12How It All Works
Finding the Ark
At power-on, secretArkMesaID ($8C) is set from the
frame counter's current value — whatever frame you happened to press RESET on.
The Ark's mesa is whichever one matches this value. Every playthrough is different.
You need the Head of Ra in the Map Room at the right time of day to find it.
Grappling hook → mesa grid teleport
; Convert hook pixel position to mesa grid coordinates: ; RegionY = ((HookY - 6) + WorldScrollOffset) / 16 ; RegionX = ((HookX - 16) / 32) LDA hookY SEC SBC #$06 CLC ADC worldScrollOffset LSR / LSR / LSR / LSR ; ÷16 → region Y TAY LDA hookX SEC SBC #$10 LSR / LSR / LSR / LSR / LSR ; ÷32 → region X LDA mesaGridMapTable,X ; lookup destination mesa
Mesa grid map (4×8, from ROM)
Destructible walls
; 6 bytes at $E5-$EA track wall state. One bit per segment. ; Weapon hit clears a bit → wall segment gone forever. LDA dynamicGfxData,Y ; load wall byte for this row AND weaponMaskInverse ; clear the hit bit STA dynamicGfxData,Y ; segment destroyed — permanent ; Leave the room, come back → hole is still there. ; 6 bytes buy permanent world modification.
Collision detection (free from hardware)
; TIA checks pixel overlap automatically. Just read the registers: LDA CXM1P ; Missile 1 vs Players → bullet hit someone? LDA CXM1FB ; Missile 1 vs Playfield/Ball → hit a wall? LDA CXP1FB ; Player 1 vs Playfield → Indy walking into wall? LDA CXPPMM ; Player-Player (bit 7) → Indy touched enemy? ; No bounding boxes. No math. The TIA does it for free. ; Results dispatch through per-room handler jump tables.