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.

8,192Bytes of ROM
128Bytes of RAM
14Rooms
13Items
4Render kernels
1.19 MHzCPU

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:

BANK 0 — $D000–$DFFF (4,096 bytes) — Game Logic
Room handlers
Collision
Inventory
Input
Scoring
Sound
Movement
BANK 1 — $F000–$FFFF (4,096 bytes) — Display + Data
4 Kernels
Sprite GFX
Playfield GFX
Color tables
Music
Vec
Room event dispatch
Display kernels
Sprite data
Playfield data
Tables + Input
Sound + Music
The trampoline
6 bytes of zero-page RAM at $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.

8 frames @ ~7.5 fps (every 8th game frame)

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:

$80–$83
System: scanline counter, currentRoomId ($81), frame counter (= RNG seed), time-of-day clock
$84–$89
Trampoline / scratch: 6-byte bank-switch stub (LDA strobe + JMP target), reused as scratch between switches
$8A
Player input state — direction + button flags, bit-rotated state machine
$8B–$8C
activeMesaID / secretArkMesaID — the Ark hides at a random mesa, set from frame counter at game start
$8D
Grapple/whip state machine
$8E–$8F
Missile 0 Y shadow; weapon status (bit 7 = active)
$90–$9D
Room flags, sprite offsets, dig limiter, swarm counter, grenade timer, ark room flag, death status
$9E–$9F
Adventure points (lower = better rank) + lives (starts at 3)
$A0–$AE
Scoring: penalty/bonus tracking (grenade=2pts, escape prison=13pts, find Ark=10pts)
$AF–$B6
Per-room state array — indexed by room ID. Wall destruction, item cycles, market status, spider aggression
$B7–$C2
Inventory: 6 slots × 2-byte sprite pointers = 12 bytes. Identity derived from pointer math.
$C3–$C7
Selection state, item count, selected ID, basket bitmask, pickup bitmask
$C8–$D3
Object positions: parallel X/Y arrays for 5 TIA objects (P0, P1, M0, M1, BL)
$D4–$EA
Kernel state, GFX pointers, Indy sprite data, PF pointers, dynamicGfxData (6 bytes: destructible wall bitmasks)
$EB–$F2
Room transition saves: Indy's X/Y, scroll offset, thief positions — preserved when you leave a room
$F3–$FF
Stack (13 bytes, grows down from $FF, reset each overscan)
"How does it know where you are?"
One byte. $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:

4 motion tables cycling

      

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:

KernelRoomsTechnique
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

Player 0
Room enemies/objects. Multiplexed across 5 thief zones. Room-specific: snakes, marketplace items, spiders.
Player 1
Indiana Jones. Always. 8px wide, 11 scanlines. Domurat's sprite design.
Missile 0
Spider webs, tsetse fly swarm, mesa well markers. 1px wide, context-dependent.
Missile 1
Indy's weapon: bullets, whip tip, grapple hook. Active ~15 frames per use.
Ball
Swiss army knife: snake body (per-line HMOVE), timepiece, inventory selector, Map Room sun.
Playfield
Room architecture. 20-bit pattern mirrored. Destructible segments in temple rooms.

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.

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