Inside Contra(NES, 1988)
Reverse engineering Konami's legendary run-and-gun — decompressing RLE tile data, extracting 232 sprites, and analyzing 6502 assembly from the original NES ROM.
> Decompressed Tile Data
Unlike most NES games, Contra has no CHR ROM. All tile graphics are RLE-compressed inside the 128KB PRG ROM and decompressed to CHR RAM at runtime. Each sheet below shows the left pattern table (sprites, left) and right pattern table (backgrounds, right) after decompression.

Bridge, mountains, water tiles and player sprites for the iconic first stage

Indoor base level tiles -- mechanical walls, corridors, and defense systems

Common pattern table data used across multiple levels -- includes boss components
Massive tile bank with sprites and background elements for multiple stages

Waterfall stage graphics -- cascading water, rocky terrain

Additional indoor base tiles -- turrets, barriers, alien structures
Rotating gun turrets and red cannon enemy sprites

Victory scene tiles -- the helicopter escape and island explosion
Sprite tiles for the player death animation
Title screen pattern tiles -- the CONTRA logo letters
Flying weapon capsule and power-up box sprites
Background pattern data for Level 1
> All Game Sprites
Contra has 232 unique sprite compositions. Each sprite is assembled from multiple 8x8 NES tiles using a variable-length encoding format. Sprite data is stored in PRG bank 1, with each entry defining tile positions, OAM attributes (flip/palette/priority), and offsets.
Player Character (Bill Rizer)
Enemy Soldiers
Bosses & Aliens
Weapons, Items & Effects
> How the NES Engine Works
NES Architecture vs. AGI
The NES is a fundamentally different architecture from Sierra's AGI engine. Instead of a virtual machine running bytecode scripts, the NES runs native 6502 machine code directly on hardware.
CPU: Ricoh 2A03 (6502)
- 8-bit processor, ~1.79 MHz
- 2KB internal RAM
- 56 instructions (no multiply/divide)
- 3 registers: A (accumulator), X, Y
- 16-bit address bus (64KB space)
- Bank switching for >32KB games
PPU: Picture Processing Unit
- 256x240 pixel output (NTSC)
- 2 pattern tables: 256 tiles each (8x8, 2bpp)
- Left table ($0000): sprites
- Right table ($1000): backgrounds
- 4 palettes of 4 colors each (sprites + BG)
- 64 hardware sprites via OAM
Konami's RLE Compression
Contra's tile data is compressed using Konami's custom RLE format. This is why the game uses CHR RAM instead of CHR ROM — the data must be decompressed before the PPU can use it. Nearly 50% of the entire ROM is compressed graphics.
Byte values and their meaning: 0xFF -- End of data stream 0x7F aa bb -- Set PPU write address to $bbaa < 0x7F (N) -- RLE: write next byte N times >= 0x80 (N) -- Literal: copy next (N & 0x7F) bytes Example compressed data: 7F 00 00 -- Set PPU address to $0000 05 3C -- Write 0x3C five times 83 A1 B2 C3 -- Write 3 literal bytes: A1, B2, C3 FF -- End The CPU bank-switches to load the correct PRG bank, then runs the decompressor which writes directly to the PPU via memory-mapped registers ($2006/$2007).
Mapper 2 (UxROM) Bank Switching
The NES CPU can only address 32KB of PRG ROM at a time, but Contra has 128KB. Mapper 2 solves this by bank switching:
$0000-$07FF 2KB Internal RAM
$2000-$2007 PPU Registers
$4000-$4017 APU + I/O Registers
$6000-$7FFF SRAM (unused in Contra)
$8000-$BFFF Switchable 16KB PRG bank (0-6)
$C000-$FFFF Fixed 16KB PRG bank (always bank 7)
Bank layout:
Bank 0: Game logic, enemy AI
Bank 1: Sprite data, drawing routines
Bank 2: Compressed graphics + level data
Bank 3: Super-tile definitions, palettes
Bank 4: Compressed graphics (levels)
Bank 5: Compressed graphics (levels)
Bank 6: Compressed graphics (levels)
Bank 7: Fixed -- NMI handler, decompressor,
level loading, core engine (always loaded)NES Tile Format (2bpp Planar)
Each NES tile is 8x8 pixels, 2 bits per pixel (4 colors), stored in 16 bytes as two bit planes:
Bytes 0-7: Bit plane 0 (low bit of each pixel)
Bytes 8-15: Bit plane 1 (high bit of each pixel)
Each byte = one row, bit 7 = leftmost pixel
Example tile row (y=0):
Plane 0, byte 0: 0b01111110 = .XXXXXX.
Plane 1, byte 8: 0b01000010 = .X....X.
Combined pixels: .2111121.
00 = transparent
01 = color 1
10 = color 2
11 = color 3
256 tiles x 16 bytes = 4KB per pattern table
2 pattern tables = 8KB total in CHR RAMNES Color Palette (subset)
The NES has a fixed palette of 64 colors. Each game selects 4 palettes of 4 colors for backgrounds and 4 palettes of 4 colors for sprites (32 colors total on screen).
Sprite Composition System
NES hardware sprites are only 8x8 or 8x16 pixels. Larger characters like the player are composed from multiple hardware sprites. Contra uses a variable-length encoding:
Each sprite entry:
Byte 0: Entry count (number of 8x8 OAM entries)
Special values:
$FE = small sprite (only 3 bytes total)
$80 = address pointer (jump to another sprite)
For each OAM entry (4 bytes):
Byte 0: Y offset from sprite origin
Byte 1: Tile index (always even -- left pattern table)
Byte 2: Attributes
Bits 0-1: Palette number (0-3)
Bit 5: Priority (0=front, 1=behind BG)
Bit 6: Horizontal flip
Bit 7: Vertical flip
Byte 3: X offset from sprite origin
The player character uses ~6-8 hardware sprites
composed into a single 24x32 pixel character.> Level Structure
The iconic opening stage -- run through dense jungle, cross bridges, fight turrets
3D perspective corridor -- destroy electric barriers and wall-mounted guns
Climb the waterfall while enemies attack from above -- unique vertical scrolling
Second indoor base with tougher enemies and more complex barrier patterns
Snowy mountain terrain with grenaders and tanks
High-tech energy corridors with moving flame traps
The alien hangar -- fight through to the final boss
The heart of the alien base -- destroy the alien heart to win
Level Data Format
Each level is defined by a header that specifies which compressed graphic data blocks to load, the super-tile definitions (16x16 pixel metatiles composed of 4 tiles), palette data, and scroll behavior. Side-scrolling levels use horizontal nametable mirroring, while “base” levels use a pseudo-3D behind-view renderer.
> 6502 Assembly Highlights
Unlike AGI's bytecode VM, Contra runs native 6502 machine code. Here are annotated excerpts from the fully disassembled ROM showing key game mechanics.
; The famous cheat code input sequence ; Up, Up, Down, Down, Left, Right, Left, Right, B, A ; Stored as controller bit patterns in a lookup table konami_code_sequence: .byte $08 ; Up .byte $08 ; Up .byte $04 ; Down .byte $04 ; Down .byte $02 ; Left .byte $01 ; Right .byte $02 ; Left .byte $01 ; Right .byte $40 ; B .byte $80 ; A ; When all 10 inputs match in sequence on the title screen: lda #$1e ; Load 30 (decimal) sta PLAYER_LIVES ; Set lives to 30 ; This is checked during the title screen input handler ; The game tracks your position in the sequence via a counter
; Called 60 times per second (NTSC) when PPU enters vblank
; This is the heartbeat of the game -- all rendering happens here
nmi_handler:
pha ; Save registers
txa
pha
tya
pha
lda #$00
sta OAMADDR ; Set OAM address to $00
lda #$02
sta OAMDMA ; DMA transfer: copy $0200-$02FF to OAM
; This updates all 64 sprite positions
jsr write_palette_colors_to_ppu
jsr write_cpu_graphics_buffer_to_ppu
inc FRAME_COUNTER ; Increment global frame counter
pla ; Restore registers
tay
pla
tax
pla
rti ; Return from interrupt; Check if player bullet hits an enemy ; Compares bounding box coordinates check_bullet_enemy_collision: lda BULLET_X sec sbc ENEMY_X ; distance = bullet_x - enemy_x bcc @no_hit ; if negative (bullet left of enemy), branch cmp #$08 ; within 8 pixels horizontally? bcs @no_hit ; too far right lda BULLET_Y sec sbc ENEMY_Y ; vertical distance bcc @no_hit cmp #$10 ; within 16 pixels vertically? bcs @no_hit ; Hit detected! jsr kill_enemy ; trigger enemy death animation jsr remove_bullet ; remove the bullet rts @no_hit: rts
> Technical Details
How This Was Built
The Contra NES ROM was downloaded from Archive.org and analyzed using custom Python tools. Konami's RLE compression format was reverse-engineered from the annotated disassembly by vermiceli, then implemented as a Python decompressor. Sprite compositions were extracted from the 233 sprite definitions in PRG bank 1.
ROM Analysis
- iNES header parsing
- PRG bank mapping (8 x 16KB)
- Entropy analysis to find tile data
- Konami RLE decompression
- NES 2bpp planar tile decoding
ROM Layout
iNES Header 16B PRG Bank 0 16KB Code PRG Bank 1 16KB Sprites PRG Bank 2 16KB GFX+Data PRG Bank 3 16KB Tiles PRG Bank 4 16KB GFX PRG Bank 5 16KB GFX PRG Bank 6 16KB GFX PRG Bank 7 16KB Engine
Tools Used
nes_common.py— ROM parserextract_tiles.py— Raw tile rendererdecompress_tiles.py— RLE decompressor- Reference: vermiceli/nes-contra-us disassembly
King's Quest vs. Contra -- Two Eras of Game Engineering
| King's Quest (1984) | Contra (1988) | |
|---|---|---|
| Platform | IBM PC / PCjr | Nintendo NES |
| CPU | Intel 8088 (4.77 MHz) | Ricoh 2A03 / 6502 (1.79 MHz) |
| Code | Bytecode VM (AGI) | Native 6502 assembly |
| Graphics | Vector drawing commands | Tile-based PPU hardware |
| Resolution | 160x168 (doubled to 320) | 256x240 |
| Colors | 16 (EGA fixed) | 25 on-screen (from 64) |
| Sprites | Software-rendered (RLE bitmaps) | Hardware OAM (64 x 8x8) |
| Compression | None (AGI v2) | Konami RLE (~50% of ROM) |
| Text | XOR encrypted (“Avis Durgan”) | No text engine (HUD only) |
| Storage | 360KB floppy | 128KB cartridge |