Skip to content
Game 1 Unit 12 of 128 1 hr learning time

Hazard Tiles

Spike tiles on the ground damage the player on contact. The tile collision code now distinguishes between tile types — not just solid or empty, but safe or dangerous.

9% of Dash

The level has coins to collect and an obstacle to avoid — but the ground itself is always safe. Walk anywhere and nothing happens. This unit adds spike tiles to the ground. Step on them and the player snaps back to the start. The nametable is no longer just scenery — it’s a hazard.

The key idea: the tile collision code already reads which tile the player is standing on. Until now, it only checked “is there a tile here?” (non-zero = solid). Now it checks what kind of tile it is. A ground tile is safe. A spike tile is dangerous.

The Spike Tile

Tile 15 in CHR-ROM is an upward-pointing triangle — a classic spike:

SPIKE_TILE     = 15            ; Hazard tile
; Tile 15: Spikes (upward-pointing triangle)
.byte $18,$18,$3C,$3C,$7E,$7E,$FF,$FF  ; Plane 0
.byte $18,$18,$3C,$3C,$7E,$7E,$FF,$FF  ; Plane 1 (both = colour 3)

Both bit planes are identical, so the colour index is 3. On background palette 1 (the ground palette), colour 3 is $29 — bright green. The spikes are the same hue as the ground but brighter, making them stand out as danger.

Placing Spikes in the Level

Two spike tiles sit on the ground surface at columns 10–11. They’re written to the nametable after the ground fill:

    ; Write spike tiles (row 26, columns 10-11)
    bit PPUSTATUS
    lda #$23
    sta PPUADDR
    lda #$4A
    sta PPUADDR             ; PPU address $234A (row 26, col 10)
    lda #SPIKE_TILE
    sta PPUDATA
    sta PPUDATA             ; Two consecutive spike tiles

The ground write loop fills rows 26–29 with GROUND_TILE. Then the spike write overwrites two specific positions. The order matters — ground first, then spikes on top.

The level data also needs a new row template so the collision code knows about the spikes:

level_ground_spike_row:
    .byte 3,3,3,3, 3,3,3,3, 3,3,15,15, 3,3,3,3
    .byte 3,3,3,3, 3,3,3,3, 3,3,3,3, 3,3,3,3

Row 26 in the pointer table now points to level_ground_spike_row instead of level_ground_row. Columns 10–11 contain tile 15 (spikes); everything else is tile 3 (ground). The nametable and the level data must match — if one has spikes and the other doesn’t, the visuals and collision disagree.

Tile Type Checking

The hazard check runs every frame when the player is on the ground. It reads the tile beneath the player’s feet and compares it against SPIKE_TILE:

; =============================================================================
; Hazard tile check
; =============================================================================
; After the floor collision code sets on_ground, this section reads the tile
; beneath the player's feet. If it's a spike, the player resets to start.
; =============================================================================

    ; --- Check for hazard tiles ---
    lda on_ground
    beq @no_hazard

    ; Get tile below player's feet
    lda player_y
    clc
    adc #8                  ; Feet position
    lsr
    lsr
    lsr                     ; Tile row
    tax

    cpx #30
    bcs @no_hazard          ; Below nametable — safe

    lda level_rows_lo, x
    sta tile_ptr
    lda level_rows_hi, x
    sta tile_ptr+1

    lda player_x
    clc
    adc #4                  ; Sprite centre
    lsr
    lsr
    lsr                     ; Tile column
    tay

    lda (tile_ptr), y
    cmp #SPIKE_TILE
    bne @no_hazard

    ; Hit spikes! Reset player to start
    lda #PLAYER_X
    sta player_x
    lda #PLAYER_Y
    sta player_y
    lda #0
    sta vel_y
    lda #1
    sta on_ground

    ; Damage sound (pulse channel — harsh buzz)
    lda #%00111100          ; Duty 12.5%, volume 12
    sta SQ1_VOL
    lda #%00000000          ; No sweep
    sta SQ1_SWEEP
    lda #$80                ; Timer low — low pitch
    sta SQ1_LO
    lda #$01                ; Timer high=1, length counter
    sta SQ1_HI

@no_hazard:

The logic mirrors the floor collision from Unit 7: calculate the tile row from the player’s feet position, load the row pointer, calculate the tile column from the sprite centre, and read the tile with (tile_ptr), Y. The difference is what happens with the result.

Previously: BEQ @no_floor — tile 0 means empty, anything else means solid.

Now: CMP #SPIKE_TILE / BNE @no_hazard — specifically checks for tile 15. Ground tiles (3) pass through safely. Only spikes trigger damage.

This is the first use of CMP to distinguish between different non-zero tile values. The same pattern extends to any number of tile types: check for each one and branch accordingly.

Damage

When the player stands on a spike, four things happen:

  1. Position resets to the start (PLAYER_X, PLAYER_Y)
  2. Velocity clears to zero
  3. on_ground sets to 1
  4. A harsh buzz plays on the pulse channel

The damage sound uses duty cycle 12.5% — the harshest setting. Combined with a low pitch (timer $180), it produces a short, unpleasant buzz that’s clearly different from both the jump sound (falling sweep) and the collect sound (bright triangle ding).

    ; Damage sound (pulse channel — harsh buzz)
    lda #%00111100          ; Duty 12.5%, volume 12
    sta SQ1_VOL
    lda #%00000000          ; No sweep
    sta SQ1_SWEEP
    lda #$80                ; Timer low — low pitch
    sta SQ1_LO
    lda #$01                ; Timer high=1, length counter
    sta SQ1_HI

Spikes as Obstacles

The spikes sit at columns 10–11, right in the player’s path between the starting position (column 7) and the floating platform (columns 12–19). The player must jump over them to reach the platform and its coin.

Walking into the spike columns while on the ground triggers instant damage. The player needs to be airborne over those tiles. The hazard check only runs when on_ground is true — jumping over spikes is completely safe.

This creates a natural flow: run right, see spikes, jump over them, land on the other side. The spikes teach the player to jump proactively, not just reactively.

The Complete Code

; =============================================================================
; DASH - Unit 12: Hazard Tiles
; =============================================================================
; Spike tiles on the ground damage the player on contact. The tile collision
; code now distinguishes between solid tiles and hazard tiles.
; =============================================================================

; -----------------------------------------------------------------------------
; NES Hardware Addresses
; -----------------------------------------------------------------------------
PPUCTRL   = $2000
PPUMASK   = $2001
PPUSTATUS = $2002
OAMADDR   = $2003
PPUSCROLL = $2005
PPUADDR   = $2006
PPUDATA   = $2007
OAMDMA    = $4014
JOYPAD1   = $4016

; -----------------------------------------------------------------------------
; APU Registers
; -----------------------------------------------------------------------------
SQ1_VOL    = $4000
SQ1_SWEEP  = $4001
SQ1_LO     = $4002
SQ1_HI     = $4003
TRI_LINEAR = $4008
TRI_LO     = $400A
TRI_HI     = $400B
APU_STATUS = $4015

; -----------------------------------------------------------------------------
; Button Masks
; -----------------------------------------------------------------------------
BTN_A      = %10000000
BTN_B      = %01000000
BTN_SELECT = %00100000
BTN_START  = %00010000
BTN_UP     = %00001000
BTN_DOWN   = %00000100
BTN_LEFT   = %00000010
BTN_RIGHT  = %00000001

; -----------------------------------------------------------------------------
; Game Constants
; -----------------------------------------------------------------------------
PLAYER_X       = 60
PLAYER_Y       = 200
PLAYER_TILE    = 1
RIGHT_WALL     = 248
FLOOR_Y        = 200           ; Obstacle Y position (ground level)
GRAVITY        = 1
JUMP_VEL       = $F6
OBSTACLE_TILE  = 2
OBSTACLE_SPEED = 2
GROUND_TILE    = 3
COIN_TILE      = 4
DIGIT_ZERO     = 5             ; First digit tile (0-9 are tiles 5-14)
SPIKE_TILE     = 15            ; Hazard tile

; -----------------------------------------------------------------------------
; Memory
; -----------------------------------------------------------------------------
.segment "ZEROPAGE"
player_x:   .res 1
player_y:   .res 1
vel_y:      .res 1
buttons:    .res 1
nmi_flag:   .res 1
on_ground:  .res 1
obstacle_x: .res 1
tile_ptr:   .res 2             ; Pointer for level data lookup
score:      .res 1             ; Current score (0-255)

.segment "OAM"
oam_buffer: .res 256

.segment "BSS"

; =============================================================================
; iNES Header
; =============================================================================
.segment "HEADER"
    .byte "NES", $1A
    .byte 2
    .byte 1
    .byte $01
    .byte $00
    .byte 0,0,0,0,0,0,0,0

; =============================================================================
; Code
; =============================================================================
.segment "CODE"

; --- Reset ---
reset:
    sei
    cld
    ldx #$40
    stx $4017
    ldx #$FF
    txs
    inx
    stx PPUCTRL
    stx PPUMASK
    stx $4010
    stx APU_STATUS

@vblank1:
    bit PPUSTATUS
    bpl @vblank1

    lda #0
@clear_ram:
    sta $0000, x
    sta $0100, x
    sta $0200, x
    sta $0300, x
    sta $0400, x
    sta $0500, x
    sta $0600, x
    sta $0700, x
    inx
    bne @clear_ram

@vblank2:
    bit PPUSTATUS
    bpl @vblank2

    ; --- Load palette ---
    bit PPUSTATUS
    lda #$3F
    sta PPUADDR
    lda #$00
    sta PPUADDR

    ldx #0
@load_palette:
    lda palette_data, x
    sta PPUDATA
    inx
    cpx #32
    bne @load_palette

    ; --- Clear nametable 0 ---
    bit PPUSTATUS
    lda #$20
    sta PPUADDR
    lda #$00
    sta PPUADDR

    lda #0
    ldy #4
    ldx #0
@clear_nt:
    sta PPUDATA
    dex
    bne @clear_nt
    dey
    bne @clear_nt

    ; --- Write ground tiles (rows 26-29) ---
    bit PPUSTATUS
    lda #$23
    sta PPUADDR
    lda #$40
    sta PPUADDR             ; PPU address $2340 (row 26)

    lda #GROUND_TILE
    ldx #128                ; 4 rows × 32 tiles
@write_ground:
    sta PPUDATA
    dex
    bne @write_ground

    ; --- Write spike tiles (row 26, columns 10-11) ---
    bit PPUSTATUS
    lda #$23
    sta PPUADDR
    lda #$4A
    sta PPUADDR             ; PPU address $234A (row 26, col 10)
    lda #SPIKE_TILE
    sta PPUDATA
    sta PPUDATA             ; Two consecutive spike tiles

    ; --- Write platform tiles (row 20, columns 12-19) ---
    bit PPUSTATUS
    lda #$22
    sta PPUADDR
    lda #$8C
    sta PPUADDR             ; PPU address $228C (row 20, col 12)

    lda #GROUND_TILE
    ldx #8                  ; 8 tiles wide
@write_platform:
    sta PPUDATA
    dex
    bne @write_platform

    ; --- Write wall tiles (rows 24-25, columns 22-23) ---
    bit PPUSTATUS
    lda #$23
    sta PPUADDR
    lda #$16
    sta PPUADDR             ; PPU address $2316 (row 24, col 22)
    lda #GROUND_TILE
    sta PPUDATA
    sta PPUDATA             ; Cols 22-23

    bit PPUSTATUS
    lda #$23
    sta PPUADDR
    lda #$36
    sta PPUADDR             ; PPU address $2336 (row 25, col 22)
    lda #GROUND_TILE
    sta PPUDATA
    sta PPUDATA             ; Cols 22-23

    ; --- Write initial score display ---
    bit PPUSTATUS
    lda #$20
    sta PPUADDR
    lda #$22
    sta PPUADDR             ; PPU address $2022 (row 1, col 2)
    lda #DIGIT_ZERO         ; Tile for "0"
    sta PPUDATA

    ; --- Set attributes (platform + wall + ground palettes) ---
    bit PPUSTATUS
    lda #$23
    sta PPUADDR
    lda #$E8
    sta PPUADDR             ; PPU address $23E8 (attribute row 5)

    ldx #0
@write_attrs:
    lda attr_data, x
    sta PPUDATA
    inx
    cpx #24
    bne @write_attrs

    ; --- Set up player sprite (OAM entry 0) ---
    lda #PLAYER_Y
    sta oam_buffer+0
    lda #PLAYER_TILE
    sta oam_buffer+1
    lda #0
    sta oam_buffer+2
    lda #PLAYER_X
    sta oam_buffer+3

    ; Set up obstacle sprite (OAM entry 1)
    lda #FLOOR_Y
    sta oam_buffer+4
    lda #OBSTACLE_TILE
    sta oam_buffer+5
    lda #1
    sta oam_buffer+6
    lda #255
    sta oam_buffer+7

    ; Set up collectible sprites (OAM entries 2-4)
    ; Coin 0: on the platform
    lda #152                ; Y = 152 (sits on row 20 platform)
    sta oam_buffer+8
    lda #COIN_TILE
    sta oam_buffer+9
    lda #2                  ; Sprite palette 2 (yellow)
    sta oam_buffer+10
    lda #128                ; X = 128 (centre of platform)
    sta oam_buffer+11

    ; Coin 1: past the wall on the ground
    lda #FLOOR_Y
    sta oam_buffer+12
    lda #COIN_TILE
    sta oam_buffer+13
    lda #2
    sta oam_buffer+14
    lda #200                ; X = 200
    sta oam_buffer+15

    ; Coin 2: in the air (jump to collect)
    lda #168                ; Y = 168 (above ground, reachable by jumping)
    sta oam_buffer+16
    lda #COIN_TILE
    sta oam_buffer+17
    lda #2
    sta oam_buffer+18
    lda #32                 ; X = 32 (left side)
    sta oam_buffer+19

    ; Initialise game state
    lda #PLAYER_X
    sta player_x
    lda #PLAYER_Y
    sta player_y
    lda #0
    sta vel_y
    sta score               ; Score starts at 0
    lda #1
    sta on_ground
    lda #255
    sta obstacle_x

    ; Hide other sprites (entries 5-63)
    lda #$EF
    ldx #20
@hide_sprites:
    sta oam_buffer, x
    inx
    bne @hide_sprites

    ; Enable APU channels: pulse 1 + triangle
    lda #%00000101
    sta APU_STATUS

    ; Reset scroll position
    bit PPUSTATUS
    lda #0
    sta PPUSCROLL
    sta PPUSCROLL

    ; Enable rendering
    lda #%10000000
    sta PPUCTRL
    lda #%00011110
    sta PPUMASK

; =============================================================================
; Main Loop
; =============================================================================
main_loop:
    lda nmi_flag
    beq main_loop
    lda #0
    sta nmi_flag

    ; --- Read controller ---
    lda #1
    sta JOYPAD1
    lda #0
    sta JOYPAD1

    ldx #8
@read_pad:
    lda JOYPAD1
    lsr a
    rol buttons
    dex
    bne @read_pad

    ; --- Jump check ---
    lda buttons
    and #BTN_A
    beq @no_jump
    lda on_ground
    beq @no_jump
    lda #JUMP_VEL
    sta vel_y
    lda #0
    sta on_ground

    ; Play jump sound (pulse channel)
    lda #%10111000
    sta SQ1_VOL
    lda #%10111001
    sta SQ1_SWEEP
    lda #$C8
    sta SQ1_LO
    lda #$00
    sta SQ1_HI

@no_jump:

    ; --- Move left (with wall check) ---
    lda buttons
    and #BTN_LEFT
    beq @not_left
    lda player_x
    beq @not_left

    lda player_y
    clc
    adc #4
    lsr
    lsr
    lsr
    tax

    lda level_rows_lo, x
    sta tile_ptr
    lda level_rows_hi, x
    sta tile_ptr+1

    lda player_x
    sec
    sbc #1
    lsr
    lsr
    lsr
    tay

    lda (tile_ptr), y
    bne @not_left

    dec player_x
@not_left:

    ; --- Move right (with wall check) ---
    lda buttons
    and #BTN_RIGHT
    beq @not_right
    lda player_x
    cmp #RIGHT_WALL
    bcs @not_right

    lda player_y
    clc
    adc #4
    lsr
    lsr
    lsr
    tax

    lda level_rows_lo, x
    sta tile_ptr
    lda level_rows_hi, x
    sta tile_ptr+1

    lda player_x
    clc
    adc #8
    lsr
    lsr
    lsr
    tay

    lda (tile_ptr), y
    bne @not_right

    inc player_x
@not_right:

    ; --- Apply gravity ---
    lda vel_y
    clc
    adc #GRAVITY
    sta vel_y

    ; --- Apply velocity to Y position ---
    lda player_y
    clc
    adc vel_y
    sta player_y

    ; --- Tile collision (vertical) ---
    lda vel_y
    bmi @no_floor

    lda player_y
    clc
    adc #8
    lsr
    lsr
    lsr
    tax

    cpx #30
    bcs @on_solid

    lda level_rows_lo, x
    sta tile_ptr
    lda level_rows_hi, x
    sta tile_ptr+1

    lda player_x
    clc
    adc #4
    lsr
    lsr
    lsr
    tay

    lda (tile_ptr), y
    beq @no_floor

@on_solid:
    lda player_y
    clc
    adc #8
    and #%11111000
    sec
    sbc #8
    sta player_y
    lda #0
    sta vel_y
    lda #1
    sta on_ground
    jmp @done_floor

@no_floor:
    lda #0
    sta on_ground

@done_floor:

    ; --- Check for hazard tiles ---
    lda on_ground
    beq @no_hazard

    ; Get tile below player's feet
    lda player_y
    clc
    adc #8                  ; Feet position
    lsr
    lsr
    lsr                     ; Tile row
    tax

    cpx #30
    bcs @no_hazard          ; Below nametable — safe

    lda level_rows_lo, x
    sta tile_ptr
    lda level_rows_hi, x
    sta tile_ptr+1

    lda player_x
    clc
    adc #4                  ; Sprite centre
    lsr
    lsr
    lsr                     ; Tile column
    tay

    lda (tile_ptr), y
    cmp #SPIKE_TILE
    bne @no_hazard

    ; Hit spikes! Reset player to start
    lda #PLAYER_X
    sta player_x
    lda #PLAYER_Y
    sta player_y
    lda #0
    sta vel_y
    lda #1
    sta on_ground

    ; Damage sound (pulse channel — harsh buzz)
    lda #%00111100          ; Duty 12.5%, volume 12
    sta SQ1_VOL
    lda #%00000000          ; No sweep
    sta SQ1_SWEEP
    lda #$80                ; Timer low — low pitch
    sta SQ1_LO
    lda #$01                ; Timer high=1, length counter
    sta SQ1_HI

@no_hazard:

    ; --- Check collectibles ---
    ldx #8                  ; OAM entry 2 (coin 0)
    jsr check_collect
    ldx #12                 ; OAM entry 3 (coin 1)
    jsr check_collect
    ldx #16                 ; OAM entry 4 (coin 2)
    jsr check_collect

    ; --- Move obstacle ---
    lda obstacle_x
    sec
    sbc #OBSTACLE_SPEED
    sta obstacle_x

    ; --- Collision with obstacle ---
    lda on_ground
    beq @no_collide

    lda player_y
    cmp #(FLOOR_Y - 7)
    bcc @no_collide

    lda obstacle_x
    cmp #240
    bcs @no_collide

    lda player_x
    clc
    adc #8
    cmp obstacle_x
    bcc @no_collide
    beq @no_collide

    lda obstacle_x
    clc
    adc #8
    cmp player_x
    bcc @no_collide
    beq @no_collide

    lda #PLAYER_X
    sta player_x

@no_collide:

    ; --- Update sprite positions ---
    lda player_y
    sta oam_buffer+0
    lda player_x
    sta oam_buffer+3

    lda #FLOOR_Y
    sta oam_buffer+4
    lda obstacle_x
    sta oam_buffer+7

    jmp main_loop

; =============================================================================
; Subroutines
; =============================================================================

; -----------------------------------------------------------------------------
; check_collect: Check if the player overlaps a collectible sprite
; Input:  X = OAM buffer offset (8, 12, or 16)
; Effect: If overlapping, hides the sprite, increments score, plays sound
; -----------------------------------------------------------------------------
check_collect:
    ; Skip if already collected
    lda oam_buffer, x       ; Sprite Y position
    cmp #$EF
    beq @done

    ; --- Y overlap ---
    ; Player bottom vs item top
    lda player_y
    clc
    adc #8
    cmp oam_buffer, x
    bcc @done
    beq @done

    ; Item bottom vs player top
    lda oam_buffer, x
    clc
    adc #8
    cmp player_y
    bcc @done
    beq @done

    ; --- X overlap ---
    ; Player right vs item left
    lda player_x
    clc
    adc #8
    cmp oam_buffer+3, x
    bcc @done
    beq @done

    ; Item right vs player left
    lda oam_buffer+3, x
    clc
    adc #8
    cmp player_x
    bcc @done
    beq @done

    ; Collected! Hide the sprite and add to score
    lda #$EF
    sta oam_buffer, x
    inc score

    ; Play collect sound (triangle channel)
    lda #%00011000          ; Linear counter: halt=0, reload=24 (~100ms)
    sta TRI_LINEAR
    lda #$29                ; Timer low — bright pitch (~1330 Hz)
    sta TRI_LO
    lda #$00                ; Timer high=0, length counter=0 (10 frames)
    sta TRI_HI

@done:
    rts

; =============================================================================
; NMI Handler
; =============================================================================
nmi:
    pha
    txa
    pha
    tya
    pha

    ; --- OAM DMA ---
    lda #0
    sta OAMADDR
    lda #>oam_buffer
    sta OAMDMA

    ; --- Update score display on nametable ---
    bit PPUSTATUS           ; Reset address latch
    lda #$20
    sta PPUADDR
    lda #$22
    sta PPUADDR             ; PPU address $2022 (row 1, col 2)
    lda score
    clc
    adc #DIGIT_ZERO         ; Convert score to tile index
    sta PPUDATA

    ; --- Reset scroll (required after PPUADDR writes) ---
    lda #0
    sta PPUSCROLL
    sta PPUSCROLL

    lda #1
    sta nmi_flag

    pla
    tay
    pla
    tax
    pla
    rti

irq:
    rti

; =============================================================================
; Data
; =============================================================================

palette_data:
    ; Background palettes
    .byte $0F, $00, $10, $20   ; Palette 0: greys (sky)
    .byte $0F, $09, $19, $29   ; Palette 1: greens (ground)
    .byte $0F, $00, $10, $20
    .byte $0F, $00, $10, $20
    ; Sprite palettes
    .byte $0F, $30, $16, $27   ; Palette 0: white (player)
    .byte $0F, $16, $27, $30   ; Palette 1: red (obstacle)
    .byte $0F, $28, $38, $30   ; Palette 2: yellow (coins)
    .byte $0F, $30, $16, $27

attr_data:
    ; Attribute row 5 ($23E8) — platform
    .byte $00, $00, $00, $05, $05, $00, $00, $00
    ; Attribute row 6 ($23F0) — wall + ground
    .byte $50, $50, $50, $50, $50, $54, $50, $50
    ; Attribute row 7 ($23F8) — ground (top quadrants)
    .byte $05, $05, $05, $05, $05, $05, $05, $05

; -----------------------------------------------------------------------------
; Level Data
; -----------------------------------------------------------------------------
level_empty_row:
    .byte 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0
    .byte 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0

level_platform_row:
    .byte 0,0,0,0, 0,0,0,0, 0,0,0,0, 3,3,3,3
    .byte 3,3,3,3, 0,0,0,0, 0,0,0,0, 0,0,0,0

level_wall_row:
    .byte 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0
    .byte 0,0,0,0, 0,0,3,3, 0,0,0,0, 0,0,0,0

level_ground_row:
    .byte 3,3,3,3, 3,3,3,3, 3,3,3,3, 3,3,3,3
    .byte 3,3,3,3, 3,3,3,3, 3,3,3,3, 3,3,3,3

level_ground_spike_row:
    .byte 3,3,3,3, 3,3,3,3, 3,3,15,15, 3,3,3,3
    .byte 3,3,3,3, 3,3,3,3, 3,3,3,3, 3,3,3,3

; Row pointer tables (30 entries — one per nametable row)
level_rows_lo:
    .byte <level_empty_row      ; Row 0
    .byte <level_empty_row      ; Row 1
    .byte <level_empty_row      ; Row 2
    .byte <level_empty_row      ; Row 3
    .byte <level_empty_row      ; Row 4
    .byte <level_empty_row      ; Row 5
    .byte <level_empty_row      ; Row 6
    .byte <level_empty_row      ; Row 7
    .byte <level_empty_row      ; Row 8
    .byte <level_empty_row      ; Row 9
    .byte <level_empty_row      ; Row 10
    .byte <level_empty_row      ; Row 11
    .byte <level_empty_row      ; Row 12
    .byte <level_empty_row      ; Row 13
    .byte <level_empty_row      ; Row 14
    .byte <level_empty_row      ; Row 15
    .byte <level_empty_row      ; Row 16
    .byte <level_empty_row      ; Row 17
    .byte <level_empty_row      ; Row 18
    .byte <level_empty_row      ; Row 19
    .byte <level_platform_row   ; Row 20: floating platform
    .byte <level_empty_row      ; Row 21
    .byte <level_empty_row      ; Row 22
    .byte <level_empty_row      ; Row 23
    .byte <level_wall_row       ; Row 24: wall
    .byte <level_wall_row       ; Row 25: wall
    .byte <level_ground_spike_row ; Row 26: ground with spikes
    .byte <level_ground_row     ; Row 27
    .byte <level_ground_row     ; Row 28
    .byte <level_ground_row     ; Row 29

level_rows_hi:
    .byte >level_empty_row      ; Row 0
    .byte >level_empty_row      ; Row 1
    .byte >level_empty_row      ; Row 2
    .byte >level_empty_row      ; Row 3
    .byte >level_empty_row      ; Row 4
    .byte >level_empty_row      ; Row 5
    .byte >level_empty_row      ; Row 6
    .byte >level_empty_row      ; Row 7
    .byte >level_empty_row      ; Row 8
    .byte >level_empty_row      ; Row 9
    .byte >level_empty_row      ; Row 10
    .byte >level_empty_row      ; Row 11
    .byte >level_empty_row      ; Row 12
    .byte >level_empty_row      ; Row 13
    .byte >level_empty_row      ; Row 14
    .byte >level_empty_row      ; Row 15
    .byte >level_empty_row      ; Row 16
    .byte >level_empty_row      ; Row 17
    .byte >level_empty_row      ; Row 18
    .byte >level_empty_row      ; Row 19
    .byte >level_platform_row   ; Row 20: floating platform
    .byte >level_empty_row      ; Row 21
    .byte >level_empty_row      ; Row 22
    .byte >level_empty_row      ; Row 23
    .byte >level_wall_row       ; Row 24: wall
    .byte >level_wall_row       ; Row 25: wall
    .byte >level_ground_spike_row ; Row 26: ground with spikes
    .byte >level_ground_row     ; Row 27
    .byte >level_ground_row     ; Row 28
    .byte >level_ground_row     ; Row 29

; =============================================================================
; Vectors
; =============================================================================
.segment "VECTORS"
    .word nmi
    .word reset
    .word irq

; =============================================================================
; CHR-ROM
; =============================================================================
.segment "CHARS"

; Tile 0: Empty
.byte $00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00

; Tile 1: Running figure
.byte %00110000
.byte %00110000
.byte %01111000
.byte %00110000
.byte %00110000
.byte %00101000
.byte %01000100
.byte %01000100
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000

; Tile 2: Diamond obstacle
.byte %00011000
.byte %00111100
.byte %01111110
.byte %11111111
.byte %11111111
.byte %01111110
.byte %00111100
.byte %00011000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000

; Tile 3: Ground block (light top edge, solid body)
.byte %11111111              ; Plane 0
.byte %11111111
.byte %11111111
.byte %11111111
.byte %11111111
.byte %11111111
.byte %11111111
.byte %11111111
.byte %11111111              ; Plane 1 (row 0: colour 3 = highlight)
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000

; Tile 4: Coin
.byte %00111100              ; Plane 0
.byte %01111110
.byte %11111111
.byte %11111111
.byte %11111111
.byte %11111111
.byte %01111110
.byte %00111100
.byte %00000000              ; Plane 1 (all zero = colour 1 only)
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000
.byte %00000000

; Tiles 5-14: Digits 0-9
; Both planes identical = colour 3 (white on BG palette 0)

; Tile 5: Digit 0
.byte $70,$88,$88,$88,$88,$88,$70,$00
.byte $70,$88,$88,$88,$88,$88,$70,$00

; Tile 6: Digit 1
.byte $20,$60,$20,$20,$20,$20,$70,$00
.byte $20,$60,$20,$20,$20,$20,$70,$00

; Tile 7: Digit 2
.byte $70,$88,$08,$30,$40,$80,$F8,$00
.byte $70,$88,$08,$30,$40,$80,$F8,$00

; Tile 8: Digit 3
.byte $70,$88,$08,$30,$08,$88,$70,$00
.byte $70,$88,$08,$30,$08,$88,$70,$00

; Tile 9: Digit 4
.byte $10,$30,$50,$90,$F8,$10,$10,$00
.byte $10,$30,$50,$90,$F8,$10,$10,$00

; Tile 10: Digit 5
.byte $F8,$80,$F0,$08,$08,$88,$70,$00
.byte $F8,$80,$F0,$08,$08,$88,$70,$00

; Tile 11: Digit 6
.byte $30,$40,$80,$F0,$88,$88,$70,$00
.byte $30,$40,$80,$F0,$88,$88,$70,$00

; Tile 12: Digit 7
.byte $F8,$08,$10,$20,$20,$20,$20,$00
.byte $F8,$08,$10,$20,$20,$20,$20,$00

; Tile 13: Digit 8
.byte $70,$88,$88,$70,$88,$88,$70,$00
.byte $70,$88,$88,$70,$88,$88,$70,$00

; Tile 14: Digit 9
.byte $70,$88,$88,$78,$08,$10,$60,$00
.byte $70,$88,$88,$78,$08,$10,$60,$00

; Tile 15: Spikes (upward-pointing triangle)
.byte $18,$18,$3C,$3C,$7E,$7E,$FF,$FF  ; Plane 0
.byte $18,$18,$3C,$3C,$7E,$7E,$FF,$FF  ; Plane 1 (both = colour 3)

.res 8192 - 256, $00

Dash Unit 12

Two bright green spike triangles on the ground, between the player and the platform. The player starts to the left. Walk right without jumping and the spikes send you back to the start. Jump over them to reach the platform and the coins beyond.

Try This: More Spike Patches

Add a second spike area on the far right side (columns 26–27), between the wall and the edge. Update level_ground_spike_row with spike tiles at those columns too. The nametable write needs a second PPUADDR/PPUDATA sequence for the new positions.

Try This: Ceiling Spikes

Place spike tiles on the underside of the platform (row 21, columns 12–19). Use a vertically flipped spike tile (a new CHR tile with the triangle pointing down) or reuse the same tile. These damage the player when jumping up into them — add a check for vertical collision with spikes, not just floor contact.

Try This: Spike Sound Variation

Change the damage sound to use the triangle channel instead of the pulse channel. A deep triangle note (TRI_LO = $FF, linear counter reload = 16) sounds different from the harsh pulse buzz. Compare both and decide which better communicates “danger”.

If It Doesn’t Work

  • Spikes don’t appear? Check the nametable write. The PPU address must be $234A (row 26, column 10). If the address is wrong, the spikes render in the wrong position — or overwrite other tiles.
  • Spikes appear but don’t hurt? The level data must match the nametable. If level_ground_spike_row still has ground tiles (3) at columns 10–11, the collision code sees ground, not spikes. Both the visual tiles and the data tiles must be 15.
  • Player dies everywhere on the ground? Make sure only row 26 uses level_ground_spike_row. Rows 27–29 should still point to level_ground_row. If all ground rows have spikes in the data, the hazard check fires wherever the player stands.
  • Player dies while jumping over spikes? The hazard check must be guarded by lda on_ground / beq @no_hazard. If this check is missing, the hazard runs even while airborne.
  • No damage sound? The pulse channel writes use SQ1_VOL, not TRI_LINEAR. Make sure you’re writing to the correct registers — the pulse and triangle have different addresses.

What You’ve Learnt

  • Tile types — not all non-zero tiles are the same. CMP distinguishes between ground (safe) and spikes (hazard). The same pattern handles any number of tile types.
  • Hazard tiles — background tiles that damage on contact. The nametable and level data must agree on which tiles are hazards.
  • Level data templates — a new row template (level_ground_spike_row) mixes tile types in a single row. Different rows can use different templates, creating varied terrain.
  • Damage response — resetting the player’s position, velocity, and state. The same pattern will later deduct lives instead of just resetting.
  • Sound design — duty cycle 12.5% produces the harshest pulse wave. Low pitch + harsh timbre = danger. Each game event now has a distinct sound character.

What’s Next

One hit and the player resets — harsh but temporary. In Unit 13, the player gets three lives. Each spike hit or obstacle collision costs a life. Run out and it’s game over. The HUD expands to show lives alongside the score.