Lode Runner: Apple II reverse engineering

6. Levels#

One of the appealing things about Lode Runner are its levels. 150 levels are stored in the game, and there is even a level editor included.

(a2-lode-runner): All 150 levels, as the level editor shows them. Each stored cell value 0 to 9 is also the number of the sprite drawn for it, so the images show what play hides: trapdoors (sprite 5), hidden exit ladders (sprite 6), and the start positions of the guards and the player. Click a level to see it full size. The images are generated from the disk data with make level-images; the level format is described in 06-levels.md. XekriRedmane coloured each sprite on its own, so where two blue cells meet, a single black pixel column is left between them; the images fill it with blue, as the TV does. Levels 8, 80 and 113 have six guards on the disk, one more than the game places; see the note after the guard check in the next section.

Level 1
Level 1
Level 2
Level 2
Level 3
Level 3
Level 4
Level 4
Level 5
Level 5
Level 6
Level 6
Level 7
Level 7
Level 8
Level 8
Level 9
Level 9
Level 10
Level 10
Level 11
Level 11
Level 12
Level 12
Level 13
Level 13
Level 14
Level 14
Level 15
Level 15
Level 16
Level 16
Level 17
Level 17
Level 18
Level 18
Level 19
Level 19
Level 20
Level 20
Level 21
Level 21
Level 22
Level 22
Level 23
Level 23
Level 24
Level 24
Level 25
Level 25
Level 26
Level 26
Level 27
Level 27
Level 28
Level 28
Level 29
Level 29
Level 30
Level 30
Level 31
Level 31
Level 32
Level 32
Level 33
Level 33
Level 34
Level 34
Level 35
Level 35
Level 36
Level 36
Level 37
Level 37
Level 38
Level 38
Level 39
Level 39
Level 40
Level 40
Level 41
Level 41
Level 42
Level 42
Level 43
Level 43
Level 44
Level 44
Level 45
Level 45
Level 46
Level 46
Level 47
Level 47
Level 48
Level 48
Level 49
Level 49
Level 50
Level 50
Level 51
Level 51
Level 52
Level 52
Level 53
Level 53
Level 54
Level 54
Level 55
Level 55
Level 56
Level 56
Level 57
Level 57
Level 58
Level 58
Level 59
Level 59
Level 60
Level 60
Level 61
Level 61
Level 62
Level 62
Level 63
Level 63
Level 64
Level 64
Level 65
Level 65
Level 66
Level 66
Level 67
Level 67
Level 68
Level 68
Level 69
Level 69
Level 70
Level 70
Level 71
Level 71
Level 72
Level 72
Level 73
Level 73
Level 74
Level 74
Level 75
Level 75
Level 76
Level 76
Level 77
Level 77
Level 78
Level 78
Level 79
Level 79
Level 80
Level 80
Level 81
Level 81
Level 82
Level 82
Level 83
Level 83
Level 84
Level 84
Level 85
Level 85
Level 86
Level 86
Level 87
Level 87
Level 88
Level 88
Level 89
Level 89
Level 90
Level 90
Level 91
Level 91
Level 92
Level 92
Level 93
Level 93
Level 94
Level 94
Level 95
Level 95
Level 96
Level 96
Level 97
Level 97
Level 98
Level 98
Level 99
Level 99
Level 100
Level 100
Level 101
Level 101
Level 102
Level 102
Level 103
Level 103
Level 104
Level 104
Level 105
Level 105
Level 106
Level 106
Level 107
Level 107
Level 108
Level 108
Level 109
Level 109
Level 110
Level 110
Level 111
Level 111
Level 112
Level 112
Level 113
Level 113
Level 114
Level 114
Level 115
Level 115
Level 116
Level 116
Level 117
Level 117
Level 118
Level 118
Level 119
Level 119
Level 120
Level 120
Level 121
Level 121
Level 122
Level 122
Level 123
Level 123
Level 124
Level 124
Level 125
Level 125
Level 126
Level 126
Level 127
Level 127
Level 128
Level 128
Level 129
Level 129
Level 130
Level 130
Level 131
Level 131
Level 132
Level 132
Level 133
Level 133
Level 134
Level 134
Level 135
Level 135
Level 136
Level 136
Level 137
Level 137
Level 138
Level 138
Level 139
Level 139
Level 140
Level 140
Level 141
Level 141
Level 142
Level 142
Level 143
Level 143
Level 144
Level 144
Level 145
Level 145
Level 146
Level 146
Level 147
Level 147
Level 148
Level 148
Level 149
Level 149
Level 150
Level 150

6.1 Drawing a level#

Let's see how Lode Runner draws a level. We start with the routine DRAW_LEVEL_PAGE2, which draws a level on HGR2. Note that HGR1 would be displayed, so the player doesn't see the draw happening.

We start by looping backwards over rows 15 through 0:

⟨level draw routine [140]⟩=
    ORG     $63B3
DRAW_LEVEL_PAGE2:
    SUBROUTINE
    ; Returns carry set if there was no player sprite in the level,
    ; or carry clear if there was.

    LDY     #MAX_GAME_ROW
    STY     GAME_ROWNUM

.row_loop:
Defines DRAW_LEVEL_PAGE2

We'll assume the level data is stored in a table which contains 16 pointers, one for each row. As usual in Lode Runner, the pages and offsets for those pointers are stored in separate tables. these are CURR_LEVEL_ROW_SPRITES_PTR_PAGES and CURR_LEVEL_ROW_SPRITES_PTR_OFFSETS.

⟨tables [142]⟩+=
    ORG     $1C05
CURR_LEVEL_ROW_SPRITES_PTR_OFFSETS:
    HEX     00 1C 38 54 70 8C A8 C4 E0 FC 18 34 50 6C 88 A4
CURR_LEVEL_ROW_SPRITES_PTR_PAGES:
    HEX     08 08 08 08 08 08 08 08 08 08 09 09 09 09 09 09
CURR_LEVEL_ROW_SPRITES_PTR_PAGES2:
    HEX     0A 0A 0A 0A 0A 0A 0A 0A 0A 0A 0B 0B 0B 0B 0B 0B
Used in ⟨*⟩
Defines CURR_LEVEL_ROW_SPRITES_PTR_OFFSETS, CURR_LEVEL_ROW_SPRITES_PTR_PAGES, CURR_LEVEL_ROW_SPRITES_PTR_PAGES2

At the beginning of this loop, we create two pointers which we'll simply call PTR1 and PTR2.

⟨defines [144]⟩+=
PTR1        EQU     $06     ; 2 bytes
PTR2        EQU     $08     ; 2 bytes
Used in ⟨*⟩
Defines PTR1, PTR2

We set PTR1 to the pointer corresponding to the current row, and PTR2 to the other page, though I don't know what it's for yet, I think a "background" page that contains only non-moving elements.

These are very useful fragments, and appear all over the place in the code. This fragment sets PTR1 to the current active level's row sprite data.

⟨set active row pointer [[PTR1]] for [[Y]] [146]⟩=

This fragment sets PTR2 to the current background level's row sprite data.

⟨set background row pointer [[PTR2]] for [[Y]] [148]⟩=

And this fragment sets PTR1 to the active row and PTR2 to the background row.

⟨set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]] [150]⟩=

Occasionally the sets are reversed, although the effect is identical, so:

⟨set active and background row pointers [[PTR2]] and [[PTR1]] for [[Y+1]] [152]⟩=

There's even a routine which does this, but it seems that there was a lot of inlining instead. Presumably the cycles were more important than the space.

⟨set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]] routine [154]⟩=
    ORG     $884B
GET_PTRS_TO_CURR_LEVEL_SPRITE_DATA:
    SUBROUTINE

    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    RTS
Defines GET_PTRS_TO_CURR_LEVEL_SPRITE_DATA

Occasionally we want to get the next row (i.e. for Y+1). In that case we use these fragments.

⟨set active row pointer [[PTR1]] for [[Y+1]] [156]⟩=
⟨set background row pointer [[PTR2]] for [[Y+1]] [158]⟩=
⟨set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y+1]] [160]⟩=

We also keep track of the player's sprite column and row.

⟨defines [162]⟩+=
PLAYER_COL      EQU     $00
PLAYER_ROW      EQU     $01
Used in ⟨*⟩
Defines PLAYER_COL, PLAYER_ROW

A common paradigm is to get the sprite where the player is, on the active or background page, so these fragments are repeated many times:

⟨get active sprite at player location [164]⟩=
    LDY     PLAYER_ROW
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     PLAYER_COL
    LDA     (PTR1),Y
(root chunk — tangled to file)
⟨get background sprite at player location [166]⟩=
    LDY     PLAYER_ROW
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     PLAYER_COL
    LDA     (PTR2),Y
⟨get background sprite at player location on next row [168]⟩=
    LDY     PLAYER_ROW
    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     PLAYER_COL
    LDA     (PTR2),Y
⟨level draw routine [170]⟩+=
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>

Next, we loop over the columns backwards from 27 to 0.

⟨level draw routine [172]⟩+=
    LDY     #MAX_GAME_COL
    STY     GAME_COLNUM

.col_loop:

We load the sprite from the level data.

⟨level draw routine [174]⟩+=
    LDA     (PTR1),Y

Now, as we place each sprite, we count the number of each piece we've used so far. Remember that anyone can create a level, but there are some limitations. Specifically, we are limited to 45 ladders, one player, and 5 guards. We store the counts as we go.

These values are zeroed before the DRAW_LEVEL_PAGE2 routine is called.

⟨defines [176]⟩+=
GUARD_COUNT     EQU      $8D
GOLD_COUNT      EQU      $93
LADDER_COUNT    EQU      $A3
Used in ⟨*⟩
Defines GOLD_COUNT, GUARD_COUNT, LADDER_COUNT

However, there's a flag called VERBATIM that tells us whether we want to ignore these counts and just draw the level as specified. Possibly when we're using the level editor.

⟨defines [178]⟩+=
VERBATIM        EQU      $A2
Used in ⟨*⟩
Defines VERBATIM
⟨level draw routine [180]⟩+=
    LDX     VERBATIM
    BEQ     .draw_sprite1       ; This will then unconditionally jump to
                                ; .draw_sprite2. We have to do that because of
                                ; relative jump amount limitations.

Next we handle sprite 6, which is a symbol used to denote ladder placement. If we've already got the maximum number of ladders, we just put in a space instead. For each ladder placed, we write the LADDER_LOCS table with its coordinates.

⟨tables [182]⟩+=
LADDER_LOCS_COL     EQU     $0C00   ; 48 bytes
LADDER_LOCS_ROW     EQU     $0C30   ; 48 bytes
Used in ⟨*⟩
Defines LADDER_LOCS_COL, LADDER_LOCS_ROW
⟨level draw routine [184]⟩+=
    CMP     #SPRITE_INVISIBLE_LADDER
    BNE     .check_for_gold

    LDX     LADDER_COUNT
    CPX     #45
    BCS     .remove_sprite

    INC     LADDER_COUNT
    INX
    LDA     GAME_ROWNUM
    STA     LADDER_LOCS_ROW,X
    TYA
    STA     LADDER_LOCS_COL,X

In any case, we remove the sprite from the current level data.

⟨level draw routine [186]⟩+=
.remove_sprite:
    LDA     #SPRITE_EMPTY
    STA     (PTR1),Y
    STA     (PTR2),Y

.draw_sprite1
    BEQ     .draw_sprite        ; Unconditional jump.

Next, we check for sprite 7, the gold box.

⟨level draw routine [188]⟩+=
.check_for_gold:
    CMP      #SPRITE_GOLD
    BNE     .check_for_guard

    INC     GOLD_COUNT
    BNE     .draw_sprite        ; This leads to a situation where if we wrap
                                ; GOLD_COUNT around back to 0 (so 256 boxes)
                                ; we end up falling through, which eventually
                                ; just draws the sprite anyway. So this is kind
                                ; of unconditional.

Next, we check for sprite 8, a guard. If we've already got the maximum number of guards, we just put in a space instead. For each guard placed, we write the GUARD_LOCS table with its coordinates. We also write some other guard-related tables.

⟨level draw routine [190]⟩+=
.check_for_guard:
    CMP     #SPRITE_GUARD
    BNE     .check_for_player

    LDX     GUARD_COUNT
    CPX     #5
    BCS     .remove_sprite          ; If GUARD_COUNT >= 5, remove sprite.

    INC     GUARD_COUNT
    INX
    TYA
    STA     GUARD_LOCS_COL,X
    LDA     GAME_ROWNUM
    STA     GUARD_LOCS_ROW,X
    LDA     #$00
    STA     GUARD_GOLD_TIMERS,X
    STA     GUARD_ANIM_STATES,X
    LDA     #$02
    STA     GUARD_X_ADJS,X
    STA     GUARD_Y_ADJS,X

    LDA     #SPRITE_EMPTY
    STA     (PTR2),Y
    LDA     #SPRITE_GUARD
    BNE     .draw_sprite            ; Unconditional jump.

(a2-lode-runner): Three of the shipped levels need this check: levels 8, 80 and 113 have six guards on the disk. The routine scans the board backwards, from row 15 up to row 0 and in each row from column 27 to column 0, so the sixth guard it meets, the one it removes, is the topmost guard, or the leftmost one if the topmost row has several. In level 8, below as the level editor shows it, that is the guard in row 2, column 5. In the game only the other five appear, as screenshots of level 8 in play confirm.

Level 8 as the level editor shows it: six guards, of which the one in row 2, column 5 is removed when the level is drawn

Here we insert a few unconditional branches because of relative jump limitations.

⟨level draw routine [192]⟩+=
.next_row:
    BPL     .row_loop
.next_col:
    BPL     .col_loop

Next we check for sprite 9, the player.

⟨defines [194]⟩+=
PLAYER_X_ADJ                EQU     $02     ; [0-4] minus 2 (so 2 = right on the sprite location)
PLAYER_Y_ADJ                EQU     $03     ; [0-4] minus 2 (so 2 = right on the sprite location)
PLAYER_ANIM_STATE           EQU     $04     ; Index into SPRITE_ANIM_SEQS
PLAYER_FACING_DIRECTION     EQU     $05     ; Hi bit set: facing left, otherwise facing right
Used in ⟨*⟩
Defines PLAYER_ANIM_STATE, PLAYER_X_ADJ, PLAYER_Y_ADJ
⟨level draw routine [196]⟩+=
.check_for_player:
    CMP     #SPRITE_PLAYER
    BNE     .check_for_t_thing

    LDX     PLAYER_COL
    BPL     .remove_sprite          ; If PLAYER_COL > 0, remove sprite.

    STY     PLAYER_COL
    LDX     GAME_ROWNUM
    STX     PLAYER_ROW
    LDX     #$02
    STX     PLAYER_X_ADJ
    STX     PLAYER_Y_ADJ            ; Set Player X and Y movement to 0.
    LDX     #$08
    STX     PLAYER_ANIM_STATE       ; Corresponds to sprite 9 (see SPRITE_ANIM_SEQS)

    LDA     #SPRITE_EMPTY
    STA     (PTR2),Y
    LDA     #SPRITE_PLAYER
    BNE     .draw_sprite            ; Unconditional jump.

Finally, we check for sprite 5, the t-thing, and replace it with a brick. If the sprite is anything else, we just draw it.

⟨level draw routine [198]⟩+=
.check_for_t_thing:
    CMP     #SPRITE_TRAP
    BNE     .draw_sprite
    LDA     #SPRITE_BRICK

    ; fallthrough to .draw_sprite

We finally draw the sprite, on page 2, and advance the loop.

⟨level draw routine [200]⟩+=
.draw_sprite:
    JSR     DRAW_SPRITE_PAGE2

    DEC     GAME_COLNUM
    LDY     GAME_COLNUM
    BPL     .next_col               ; Jumps to .col_loop

    DEC     GAME_ROWNUM
    LDY     GAME_ROWNUM
    BPL     .next_row               ; Jumps to .row_loop

After the loop, in verbatim mode, we copy the entire page 2 into page 1 and return. Otherwise, if we did place a player sprite, reveal the screen. If we didn't place a player sprite, that's an error!

⟨level draw routine [202]⟩+=
    LDA     VERBATIM
    BEQ     .copy_page2_to_page1

    LDA     PLAYER_COL
    BPL     .reveal_screen

    SEC                             ; Oops, no player! Return error.
    RTS

To copy the page, we'll need that second ROW_ADDR2 pointer.

⟨level draw routine [204]⟩+=
.copy_page2_to_page1:
    LDA     #$20
    STA     ROW_ADDR2+1
    LDA     #$40
    STA     ROW_ADDR+1
    LDA     #$00
    STA     ROW_ADDR2
    STA     ROW_ADDR
    TAY

.copy_loop:
    LDA     (ROW_ADDR),Y
    STA     (ROW_ADDR2),Y
    INY
    BNE     .copy_loop

    INC     ROW_ADDR2+1
    INC     ROW_ADDR+1
    LDX     ROW_ADDR+1
    CPX     #$60
    BCC     .copy_loop

    CLC
    RTS

Revealing the screen, using an iris wipe. Then, we remove the guard and player sprites!

⟨level draw routine [206]⟩+=
.reveal_screen
    JSR     IRIS_WIPE

    LDY     #MAX_GAME_ROW
    STY     GAME_ROWNUM

.row_loop2:
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     #MAX_GAME_COL
    STY     GAME_COLNUM

.col_loop2:
    LDA     (PTR1),Y
    CMP     #SPRITE_PLAYER
    BEQ     .remove
    CMP     #SPRITE_GUARD
    BNE     .next

.remove:
    LDA     #SPRITE_EMPTY
    JSR     DRAW_SPRITE_PAGE2

.next:
    DEC     GAME_COLNUM
    LDY     GAME_COLNUM
    BPL     .col_loop2

    DEC     GAME_ROWNUM
    LDY     GAME_ROWNUM
    BPL     .row_loop2

    CLC
    RTS

6.2 Iris Wipe#

Whenever a level is finished or starts, there's an iris wipe transition. The routine that starts it off is IRIS_WIPE.

The iris wipe closing in on a level

⟨defines [208]⟩+=
WIPE_COUNTER        EQU     $6D
WIPE_MODE           EQU     $A5     ; 0 for open, 1 for close.
WIPE_DIR            EQU     $72     ; 0 for close, 1 for open.
WIPE_CENTER_X       EQU     $74
WIPE_CENTER_Y       EQU     $73
Used in ⟨*⟩
Defines WIPE_COUNTER, WIPE_MODE
⟨iris wipe [210]⟩=
    ORG     $88A2
IRIS_WIPE:
    SUBROUTINE

    LDA     #88
    STA     WIPE_CENTER_Y
    LDA     #140
    STA     WIPE_CENTER_X

    LDA     WIPE_MODE
    BEQ     .iris_open

    LDX     #$AA
    STX     WIPE_COUNTER
    LDX     #$00
    STX     WIPE_DIR             ; Close

.loop_close:
    JSR     IRIS_WIPE_STEP
    DEC     WIPE_COUNTER
    BNE     .loop_close

.iris_open:
    LDA     #$01
    STA     WIPE_COUNTER
    STA     WIPE_MODE           ; So next time we will close.
    STA     WIPE_DIR            ; Open
    JSR     PUT_STATUS_LIVES
    JSR     PUT_STATUS_LEVEL

.loop_open:
    JSR     IRIS_WIPE_STEP
    INC     WIPE_COUNTER
    LDA     WIPE_COUNTER
    CMP     #$AA
    BNE     .loop_open
    RTS

The routine IRIS_WIPE_STEP does a lot of math to compute the circular iris, all parameterized on WIPE_COUNTER.

Here is a routine that divides a 16-bit value in A and X (X being LSB) by 7, storing the result in Y, with remainder in A. The routine effectively does long division. It also uses two temporaries.

⟨routines [212]⟩+=
    ORG     $8A45
DIV_BY_7:
    SUBROUTINE
    ; Enter routine with AX set to (unsigned) numerator.
    ; On exit, Y will contain the integer portion of AX/7,
    ; and A contains the remainder.

    STX     MATH_TMPL
    LDY     #$08
    SEC
    SBC     #$07

.loop:
    PHP
    ROL     MATH_TMPH
    ASL     MATH_TMPL
    ROL
    PLP
    BCC     .adjust_up
    SBC     #$07
    JMP     .next

.adjust_up
    ADC     #$07

.next
    DEY
    BNE     .loop

    BCS     .no_adjust
    ADC     #$07
    CLC

.no_adjust
    ROL     MATH_TMPH
    LDY     MATH_TMPH
    RTS
Used in ⟨*⟩
Defines DIV_BY_7

Now, for one iris wipe step, we will need lots and lots of temporaries.

⟨defines [214]⟩+=
WIPE0       EQU     $69     ; 16-bit value
WIPE1       EQU     $67     ; 16-bit value
WIPE2       EQU     $6B     ; 16-bit value
WIPE3L      EQU     $75
WIPE4L      EQU     $76
WIPE5L      EQU     $77
WIPE6L      EQU     $78
WIPE3H      EQU     $79
WIPE4H      EQU     $7A
WIPE5H      EQU     $7B
WIPE6H      EQU     $7C
WIPE7D      EQU     $7D     ; Dividends
WIPE8D      EQU     $7E
WIPE9D      EQU     $7F
WIPE10D     EQU     $80
WIPE7R      EQU     $81     ; Remainders
WIPE8R      EQU     $82
WIPE9R      EQU     $83
WIPE10R     EQU     $84
Used in ⟨*⟩
Defines WIPE0, WIPE1, WIPE10D, WIPE10R, WIPE2, WIPE3H, WIPE3L, WIPE4H, WIPE4L, WIPE5H, WIPE5L, WIPE6H, WIPE6L, WIPE7D, WIPE7R, WIPE8D, WIPE8R, WIPE9D, WIPE9R

The first thing we do for a single step is initialize all those variables!

⟨iris wipe step [216]⟩=
    ORG     $88D7
IRIS_WIPE_STEP:
    SUBROUTINE

<<[[WIPE0 = WIPE_COUNTER]]>>
<<[[WIPE1 = 0]]>>
<<[[WIPE2 = 2 * WIPE0]]>>
<<[[WIPE2 = 3 - WIPE2]]>>

; WIPE3, WIPE4, WIPE5, and WIPE6 correspond to
; row numbers. WIPE3 is above the center, WIPE6
; is below the center, while WIPE4 and WIPE5 are on
; the center.

<<[[WIPE3 = WIPE_CENTER_Y - WIPE_COUNTER]]>>
<<[[WIPE4 = WIPE5 = WIPE_CENTER_Y]]>>
<<[[WIPE6 = WIPE_CENTER_Y + WIPE_COUNTER]]>>

; WIPE7, WIPE8, WIPE9, and WIPE10 correspond to
; column byte numbers. Note the division by 7 pixels!
; WIPE7 is left of center, WIPE10 is right of center,
; while WIPE8 and WIPE9 are on the center.

<<[[WIPE7 = (WIPE_CENTER_X - WIPE_COUNTER) / 7]]>>
<<[[WIPE8 = WIPE9 = WIPE_CENTER_X / 7]]>>
<<[[WIPE10 = (WIPE_CENTER_X + WIPE_COUNTER) / 7]]>>
Defines IRIS_WIPE_STEP

Now we loop. This involves checking WIPE1 against WIPE0:

Going around the loop involves calling DRAW_WIPE_STEP, then adjusting the numbers.

⟨iris wipe step [218]⟩+=
.loop:

⟨iris wipe loop check⟩

    JSR     DRAW_WIPE_STEP

    LDA     WIPE2+1
    BPL     .89a7

<<[[WIPE2 += 4 * WIPE1 + 6]]>>
    JMP     .8a14

.89a7:

<<[[WIPE2 += 4 * (WIPE1 - WIPE0) + 16]]>>
<<Decrement [[WIPE0]]>>
<<Increment [[WIPE3]]>>
<<Decrement [[WIPE10]] modulo 7>>
<<Increment [[WIPE7]] modulo 7>>
<<Decrement [[WIPE6]]>>

.8a14:

<<Increment [[WIPE1]]>>
<<Increment [[WIPE9]] modulo 7>>
<<Decrement [[WIPE4]]>>
<<Increment [[WIPE5]]>>
<<Decrement [[WIPE8]] modulo 7>>
    JMP     .loop

Drawing a wipe step draws all four parts. There are two rows which move north and two rows that move south. There are also two left and right offsets, one short and one long. This makes eight combinations.

⟨draw wipe step [220]⟩=
Defines DRAW_WIPE_STEP

Each part consists of two halves, right and left (or east and west).

⟨Draw wipe for south part [222]⟩=
    LDY     WIPE6H
    BNE     .draw_north
    LDY     WIPE6L
    CPY     #176
    BCS     .draw_north        ; Skip if WIPE6 >= 176

    JSR     ROW_TO_ADDR_FOR_BOTH_PAGES

    ; East side
    LDY     WIPE9D
    CPY     #40
    BCS     .draw_south_west
    LDX     WIPE9R
    JSR     DRAW_WIPE_BLOCK

.draw_south_west
    ; West side
    LDY     WIPE8D
    CPY     #40
    BCS     .draw_north
    LDX     WIPE8R
    JSR     DRAW_WIPE_BLOCK
⟨Draw wipe for north part [224]⟩=
.draw_north:
    LDY     WIPE3H
    BNE     .draw_north2
    LDY     WIPE3L
    CPY     #176
    BCS     .draw_north2        ; Skip if WIPE3 >= 176

    JSR     ROW_TO_ADDR_FOR_BOTH_PAGES

    ; East side
    LDY     WIPE9D
    CPY     #40
    BCS     .draw_north_west
    LDX     WIPE9R
    JSR     DRAW_WIPE_BLOCK

.draw_north_west
    ; West side
    LDY     WIPE8D
    CPY     #40
    BCS     .draw_north2
    LDX     WIPE8R
    JSR     DRAW_WIPE_BLOCK
⟨Draw wipe for north2 part [226]⟩=
.draw_north2:
    LDY     WIPE5H
    BNE     .draw_south2
    LDY     WIPE5L
    CPY     #176
    BCS     .draw_south2        ; Skip if WIPE5 >= 176

    JSR     ROW_TO_ADDR_FOR_BOTH_PAGES

    ; East side
    LDY     WIPE10D
    CPY     #40
    BCS     .draw_north2_west
    LDX     WIPE10R
    JSR     DRAW_WIPE_BLOCK

.draw_north2_west
    ; West side
    LDY     WIPE7D
    CPY     #40
    BCS     .draw_south2
    LDX     WIPE7R
    JSR     DRAW_WIPE_BLOCK
⟨Draw wipe for south2 part [228]⟩=
.draw_south2:
    LDY     WIPE4H
    BNE     .end
    LDY     WIPE4L
    CPY     #176
    BCS     .end        ; Skip if WIPE4 >= 176

    JSR     ROW_TO_ADDR_FOR_BOTH_PAGES

    ; East side
    LDY     WIPE10D
    CPY     #40
    BCS     .draw_south2_west
    LDX     WIPE10R
    JSR     DRAW_WIPE_BLOCK

.draw_south2_west
    ; West side
    LDY     WIPE7D
    CPY     #40
    BCS     .end
    LDX     WIPE7R
    JMP     DRAW_WIPE_BLOCK           ; tail call

.end:
    RTS

Drawing a wipe block depends on whether we're opening or closing on the level. Closing on the level just blacks out pixels on page 1. Opening on the level copies some pixels from page 2 into page 1.

⟨draw wipe block [230]⟩=
    ORG     $8AF6
DRAW_WIPE_BLOCK:
    SUBROUTINE
    ; Enter routine with X set to the column byte and Y set to
    ; the pixel number within that byte (0-6). ROW_ADDR and
    ; ROW_ADDR2 must contain the base row address for page 1
    ; and page 2, respectively.

    LDA     WIPE_DIR
    BNE     .open
    
    LDA     (ROW_ADDR),Y
    AND     WIPE_BLOCK_CLOSE_MASK,X
    STA     (ROW_ADDR),Y
    RTS

.open:
    LDA     (ROW_ADDR2),Y
    AND     WIPE_BLOCK_OPEN_MASK,X
    ORA     (ROW_ADDR),Y
    STA     (ROW_ADDR),Y
    RTS
⟨tables [232]⟩+=
    ORG     $8B0C
WIPE_BLOCK_CLOSE_MASK:
    BYTE     %11110000
    BYTE     %11110000
    BYTE     %11110000
    BYTE     %11110000
    BYTE     %10001111
    BYTE     %10001111
    BYTE     %10001111
WIPE_BLOCK_OPEN_MASK:
    BYTE     %10001111
    BYTE     %10001111
    BYTE     %10001111
    BYTE     %10001111
    BYTE     %11110000
    BYTE     %11110000
    BYTE     %11110000
Used in ⟨*⟩
Defines WIPE_BLOCK_CLOSE_MASK, WIPE_BLOCK_OPEN_MASK
⟨iris wipe loop check [234]⟩=
    LDA     WIPE1+1
    CMP     WIPE0+1
    BCC     .draw_wipe_step ; Effectively, if WIPE1 > WIPE0, jump to .draw_wipe_step.
    BEQ     .8969           ; Otherwise jump to .loop1, which...

.loop1:
    LDA     WIPE1
    CMP     WIPE0
    BNE     .end
    LDA     WIPE1+1
    CMP     WIPE0+1
    BNE     .end            ; If WIPE0 != WIPE1, return.
    JMP     DRAW_WIPE_STEP

.end:
    RTS

.8969:
    LDA     WIPE1
    CMP     WIPE0
    BCS     .loop1          ; The other half of the comparison from .loop.

.draw_wipe_step:

6.2.1 Initialization#

⟨[[WIPE0 = WIPE_COUNTER]] [236]⟩=
    LDA     WIPE_COUNTER
    STA     WIPE0
    LDA     #$00
    STA     WIPE0+1         ; WIPE0 = WIPE_COUNTER
⟨[[WIPE1 = 0]] [238]⟩=
    ; fallthrough with A = 0
    STA     WIPE1
    STA     WIPE1+1         ; WIPE1 = 0
⟨[[WIPE2 = 2 * WIPE0]] [240]⟩=
    LDA     WIPE0
    ASL
    STA     WIPE2
    LDA     WIPE0+1
    ROL
    STA     WIPE2+1         ; WIPE2 = 2 * WIPE0
⟨[[WIPE2 = 3 - WIPE2]] [242]⟩=
    LDA     #$03
    SEC
    SBC     WIPE2
    STA     WIPE2
    LDA     #$00
    SBC     WIPE2+1
    STA     WIPE2+1         ; WIPE2 = 3 - WIPE2
⟨[[WIPE3 = WIPE_CENTER_Y - WIPE_COUNTER]] [244]⟩=
    LDA     WIPE_CENTER_Y
    SEC
    SBC     WIPE_COUNTER
    STA     WIPE3L
    LDA     #$00
    SBC     #$00
    STA     WIPE3H          ; WIPE3 = WIPE_CENTER_Y - WIPE_COUNTER
⟨[[WIPE4 = WIPE5 = WIPE_CENTER_Y]] [246]⟩=
    LDA     WIPE_CENTER_Y
    STA     WIPE4L
    STA     WIPE5L
    LDA     #$00
    STA     WIPE4H
    STA     WIPE5H          ; WIPE4 = WIPE5 = WIPE_CENTER_Y
⟨[[WIPE6 = WIPE_CENTER_Y + WIPE_COUNTER]] [248]⟩=
    LDA     WIPE_CENTER_Y
    CLC
    ADC     WIPE_COUNTER
    STA     WIPE6L
    LDA     #$00
    ADC     #$00
    STA     WIPE6H          ; WIPE6 = WIPE_CENTER_Y + WIPE_COUNTER
⟨[[WIPE7 = (WIPE_CENTER_X - WIPE_COUNTER) / 7]] [250]⟩=
    LDA     WIPE_CENTER_X
    SEC
    SBC     WIPE_COUNTER
    TAX
    LDA     #$00
    SBC     #$00
    JSR     DIV_BY_7
    STY     WIPE7D
    STA     WIPE7R          ; WIPE7 = (WIPE_CENTER_X - WIPE_COUNTER) / 7
⟨[[WIPE8 = WIPE9 = WIPE_CENTER_X / 7]] [252]⟩=
    LDX     WIPE_CENTER_X
    LDA     #$00
    JSR     DIV_BY_7
    STY     WIPE8D
    STY     WIPE9D
    STA     WIPE8R
    STA     WIPE9R          ; WIPE8 = WIPE9 = WIPE_CENTER_X / 7
⟨[[WIPE10 = (WIPE_CENTER_X + WIPE_COUNTER) / 7]] [254]⟩=
    LDA     WIPE_CENTER_X
    CLC
    ADC     WIPE_COUNTER
    TAX
    LDA     #$00
    ADC     #$00
    JSR     DIV_BY_7
    STY     WIPE10D
    STA     WIPE10R         ; WIPE10 = (WIPE_CENTER_X + WIPE_COUNTER) / 7

6.2.2 All that math stuff#

⟨[[WIPE2 += 4 * WIPE1 + 6]] [256]⟩=
    LDA     WIPE1
    ASL
    STA     MATH_TMPL
    LDA     WIPE1+1
    ROL
    STA     MATH_TMPH       ; MATH_TMP = WIPE1 * 2

    LDA     MATH_TMPL
    ASL
    STA     MATH_TMPL
    LDA     MATH_TMPH
    ROL
    STA     MATH_TMPH       ; MATH_TMP *= 2

    LDA     WIPE2
    CLC
    ADC     MATH_TMPL
    STA     MATH_TMPL
    LDA     WIPE2+1
    ADC     MATH_TMPH
    STA     MATH_TMPH       ; MATH_TMP += WIPE2

    LDA     #$06
    CLC
    ADC     MATH_TMPL
    STA     WIPE2
    LDA     #$00
    ADC     MATH_TMPH
    STA     WIPE2+1        ; WIPE2 = MATH_TMP + 6
⟨[[WIPE2 += 4 * (WIPE1 - WIPE0) + 16]] [258]⟩=
    LDA     WIPE1
    SEC
    SBC     WIPE0
    STA     MATH_TMPL
    LDA     WIPE1+1
    SBC     WIPE0+1
    STA     MATH_TMPH       ; MATH_TMP = WIPE1 - WIPE0

    LDA     MATH_TMPL
    ASL
    STA     MATH_TMPL
    LDA     MATH_TMPH
    ROL
    STA     MATH_TMPH       ; MATH_TMP *= 2

    LDA     MATH_TMPL
    ASL
    STA     MATH_TMPL
    LDA     MATH_TMPH
    ROL
    STA     MATH_TMPH       ; MATH_TMP *= 2

    LDA     MATH_TMPL
    CLC
    ADC     #$10
    STA     MATH_TMPL
    LDA     MATH_TMPH
    ADC     #$00
    STA     MATH_TMPH       ; MATH_TMP += 16

    LDA     MATH_TMPL
    CLC
    ADC     WIPE2
    STA     WIPE2
    LDA     MATH_TMPH
    ADC     WIPE2+1
    STA     WIPE2+1        ; WIPE2 += MATH_TMP
⟨Decrement [[WIPE0]] [260]⟩=
    LDA     WIPE0
    PHP
    DEC     WIPE0
    PLP
    BNE     .b9ec
    DEC     WIPE0+1         ; WIPE0--
.b9ec
⟨Increment [[WIPE3]] [262]⟩=
    INC     WIPE3L
    BNE     .89f2
    INC     WIPE3H          ; WIPE3++
.89f2
⟨Decrement [[WIPE10]] modulo 7 [264]⟩=
    DEC     WIPE10R
    BPL     .89fc
    LDA     #$06
    STA     WIPE10R
    DEC     WIPE10D
.89fc
⟨Increment [[WIPE7]] modulo 7 [266]⟩=
    INC     WIPE7R
    LDA     WIPE7R
    CMP     #$07
    BNE     .8a0a
    LDA     #$00
    STA     WIPE7R
    INC     WIPE7D
.8a0a
⟨Decrement [[WIPE6]] [268]⟩=
    DEC     WIPE6L
    LDA     WIPE6L
    CMP     #$FF
    BNE     .8a14
    DEC     WIPE6H
⟨Increment [[WIPE1]] [270]⟩=
    INC     WIPE1
    BNE     .8a1a
    INC     WIPE1+1          ; WIPE1++
.8a1a
⟨Increment [[WIPE9]] modulo 7 [272]⟩=
    INC     WIPE9R
    LDA     WIPE9R
    CMP     #$07
    BNE     .8a28
    LDA     #$00
    STA     WIPE9R
    INC     WIPE9D
.8a28
⟨Decrement [[WIPE4]] [274]⟩=
    DEC     WIPE4L
    LDA     WIPE4L
    CMP     #$FF
    BNE     .8a32
    DEC     WIPE4H
.8a32
⟨Increment [[WIPE5]] [276]⟩=
    INC     WIPE5L
    BNE     .8a38
    INC     WIPE5H          ; WIPE5++
.8a38
⟨Decrement [[WIPE8]] modulo 7 [278]⟩=
    DEC     WIPE8R
    BPL     .8a42
    LDA     #$06
    STA     WIPE8R
    DEC     WIPE8D
.8a42

6.3 Level data#

Now that we have the ability to draw a level from level data, we need a routine to get that level data. Recall that level data needs to be stored in pointers specified in the CURR_LEVEL_ROW_SPRITES_PTR_ tables.

6.3.1 Getting the compressed level data#

The level data is stored in the game in compressed form, so we first grab the data for the level and put it into the 256-byte DISK_BUFFER buffer. This buffer is the same as the DOS read/write buffer, so that level data can be loaded directly from disk. Levels on disk are stored starting at track 3 sector 0, with levels being stored in consecutive sectors, 16 per track.

There's one switch here, GAME_MODE, which dictates whether we're going to display the high-score screen, attract-mode game play, the splash screen, or an actual level for playing.

Also, if we're in attract mode, instead of loading the level from disk, we load the level from the game image, which contains the first three "standard" levels.

One additional feature is that you can start the routine with A being 1 to read a level, 2 to write a level, and 4 to format the entire disk. Writing and formatting is used by the level editor. But see also FORMAT_PATCH for prevention of formatting Lode Runner itself.

⟨defines [280]⟩+=
GAME_MODE               EQU     $A7

GAME_MODE_SPLASH_SCREEN         EQU     #$00
GAME_MODE_ATTRACT_MODE          EQU     #$01
GAME_MODE_PLAY_MODE             EQU     #$02
GAME_MODE_PLAY_IN_EDITOR        EQU     #$03
GAME_MODE_4                     EQU     #$04
GAME_MODE_LEVEL_EDITOR          EQU     #$05

DISK_BUFFER             EQU     $0D00       ; 256 bytes
RWTS_ADDR               EQU     $24         ; 2 bytes
DISK_LEVEL_LOC          EQU     $96
Used in ⟨*⟩
Defines GAME_MODE
⟨jump to RWTS indirectly [282]⟩=
JMP_RWTS        EQU     $23     ; JMP $0000, gets loaded with RWTS address later
Defines JMP_RWTS
⟨defines [284]⟩+=
Used in ⟨*⟩
Defines DISK_ACCESS_FORMAT, DISK_ACCESS_READ, DISK_ACCESS_WRITE
⟨load compressed level data [286]⟩=
    ORG     $630E
ACCESS_COMPRESSED_LEVEL_DATA:
    SUBROUTINE
    ; Enter routine with A set to command: 1 = read, 2 = write, 4 = format

    STA     IOB_COMMAND_CODE
    LDA     GAME_MODE
    LSR
    ; If GAME_MODE is 0 or 1, copy level data from image
    BEQ     .copy_level_data_from_image

    ; Otherwise, read/write/format level on disk
    LDA     DISK_LEVEL_LOC
    LSR
    LSR
    LSR
    LSR
    CLC
    ADC     #$03
    STA     IOB_TRACK_NUMBER            ; track 3 + (DISK_LEVEL_LOC >> 4)
    LDA     DISK_LEVEL_LOC
    AND     #$0F
    STA     IOB_SECTOR_NUMBER           ; sector DISK_LEVEL_LOC & 0x0F
    LDA     #<DISK_BUFFER
    STA     IOB_READ_WRITE_BUFFER_PTR
    LDA     #>DISK_BUFFER
    STA     IOB_READ_WRITE_BUFFER_PTR+1 ; IOB_READ_WRITE_BUFFER_PTR = 0D00
    LDA     #$00
    STA     IOB_VOLUME_NUMBER_EXPECTED  ; any volume

ACCESS_DISK_OR_RESET_GAME:
    LDY     #<DOS_IOB
    LDA     #>DOS_IOB
    JSR     JMP_RWTS
    BCC     .end
    JMP     RESET_GAME      ; On error

.end:
    RTS

.copy_level_data_from_image:
    ⟨Copy level data⟩

We're not really using ROW_ADDR here as a row address, just as a convenient place to store a pointer. Also, we can see that the attract-mode level data is stored in 256-byte pages at 9F00, A000, and A100. Level numbers start from 1, so 9E00 doesn't actually contain level data.

Since the game is supposed to come with 150 levels, there is not enough room to store all of it, so the rest of the level data must be on disk. Only the first few levels are in memory.

⟨Copy level data [288]⟩=
    <<[[ROW_ADDR = $9E00 + LEVELNUM * $0100]]>>
    <<Copy data from [[ROW_ADDR]] into [[DISK_BUFFER]]>>
⟨[[ROW_ADDR = $9E00 + LEVELNUM * $0100]] [290]⟩=
    LDA     LEVELNUM        ; 1-based
    CLC
    ADC     #$9E
    STA     ROW_ADDR+1
    LDY     #$00
    STY     ROW_ADDR        ; ROW_ADDR <- 9E00 + LEVELNUM * 0x100
⟨Copy data from [[ROW_ADDR]] into [[DISK_BUFFER]] [292]⟩=
.copyloop:
    LDA     (ROW_ADDR),Y
    STA     DISK_BUFFER,Y
    INY
    BNE     .copyloop
    RTS

Since levels are 28 sprites across with two sprites per byte, we'll show the hex data as 14 bytes per line. There is no level data past the 16th line.

⟨tables [294]⟩+=
    ORG     $9F00
LEVEL1_DATA:
    HEX     06 00 00 07 00 00 07 00 00 06 00 00 03 06
    HEX     13 11 11 11 11 11 11 11 11 03 00 00 03 06
    HEX     03 00 00 00 00 00 00 07 00 43 44 44 03 76
    HEX     11 11 11 11 31 11 11 11 11 01 00 00 13 11
    HEX     11 11 11 11 33 00 00 00 00 00 00 00 03 00
    HEX     11 11 11 31 43 44 44 44 03 00 00 00 03 00
    HEX     71 70 11 33 00 00 00 08 03 70 00 08 03 70
    HEX     11 11 31 03 00 00 13 11 21 22 11 11 13 11 
    HEX     00 00 30 00 00 00 03 00 00 00 00 00 03 00
    HEX     00 00 30 00 00 00 03 00 00 00 00 00 03 00
    HEX     00 00 38 00 70 00 43 44 44 44 44 44 03 70
    HEX     13 11 11 11 11 11 03 70 00 00 70 00 13 11
    HEX     03 00 00 00 00 00 03 11 11 11 11 01 03 00
    HEX     03 00 00 00 00 00 03 00 00 00 00 00 03 00
    HEX     03 00 00 70 00 00 03 00 90 70 00 00 03 00 
    HEX     11 11 11 11 11 11 11 11 11 11 11 11 11 11 
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
LEVEL2_DATA:
    HEX     11 11 61 11 11 11 11 11 11 11 11 01 11 11
    HEX     06 00 61 01 44 44 44 04 00 00 00 00 00 10
    HEX     16 61 60 01 71 00 10 13 00 00 00 00 80 10
    HEX     06 11 11 01 11 11 11 13 11 11 07 31 11 11
    HEX     61 00 00 47 44 11 11 13 11 11 11 31 01 11
    HEX     11 11 11 01 00 01 10 13 11 10 11 31 01 11
    HEX     00 00 00 80 00 00 10 13 11 10 01 31 71 10
    HEX     13 11 11 11 13 73 10 13 11 10 01 31 11 11 
    HEX     03 00 00 00 13 11 11 13 11 07 01 31 11 10
    HEX     03 00 00 00 00 11 11 13 11 11 01 31 11 17
    HEX     03 00 00 00 01 00 00 03 00 00 00 30 00 10
    HEX     13 11 13 11 31 21 21 21 21 21 21 31 21 21
    HEX     13 11 13 11 31 11 11 11 00 11 11 31 21 21
    HEX     03 00 03 00 30 11 11 11 01 10 11 31 00 00
    HEX     13 11 11 11 31 11 11 11 11 07 11 11 11 31
    HEX     93 00 00 00 30 70 10 11 11 01 80 00 00 30 
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
LEVEL3_DATA:
    HEX     00 00 00 30 00 00 00 00 00 00 00 00 00 00
    HEX     80 07 00 30 00 00 00 00 00 00 00 00 70 08
    HEX     13 81 07 30 00 00 00 00 00 00 00 70 18 31
    HEX     03 10 01 30 11 11 36 11 11 31 00 10 01 30
    HEX     03 00 00 00 00 00 33 07 00 30 00 00 00 30
    HEX     03 00 00 00 00 00 37 03 00 30 00 00 00 30
    HEX     03 00 00 00 00 00 33 07 00 00 00 00 00 30
    HEX     03 00 00 00 00 00 37 03 00 00 00 00 00 30 
    HEX     03 00 00 00 00 00 33 07 00 00 00 00 00 30
    HEX     03 00 00 00 00 00 37 03 00 00 00 00 00 30
    HEX     03 00 00 00 00 00 33 07 00 00 00 00 00 30
    HEX     03 00 00 00 00 00 37 03 00 00 00 00 00 30
    HEX     03 00 00 00 00 00 33 07 00 00 00 00 00 30
    HEX     03 00 00 00 00 00 37 03 00 00 00 00 00 30
    HEX     03 00 00 00 09 00 33 07 00 00 00 00 00 30
    HEX     03 00 30 11 11 11 11 11 11 11 11 03 00 30 
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Used in ⟨*⟩
Defines LEVEL1_DATA, LEVEL2_DATA, LEVEL3_DATA

6.3.2 Uncompressing and displaying the level#

Loading the level also sets the player ALIVE flag to 1 (alive). Throughout the code, LSR ALIVE simply sets the flag to 0 (dead).

⟨defines [296]⟩+=
ALIVE       EQU     $9A
Used in ⟨*⟩
Defines ALIVE
⟨load level [298]⟩=
    ORG     $6238
LOAD_LEVEL:
    SUBROUTINE
    ; Enter routine with X set to whether the level should be
    ; loaded verbatim or not.

    STX     VERBATIM

    ⟨Initialize level counts⟩

    LDA     #$01
    STA     ALIVE       ; Set player live
    ; A happens to also be DISK_ACCESS_READ.
    JSR     ACCESS_COMPRESSED_LEVEL_DATA

    ⟨uncompress level data⟩
Defines LOAD_LEVEL
⟨defines [300]⟩+=
Used in ⟨*⟩
Defines LEVEL_DATA_INDEX

Here we are initializing variables in preparation for loading the level data. Since drawing the level will keep track of ladder, gold, and guard count, we need to zero them out. There are also some areas of memory whose purpose is not yet known, and these are zeroed out also.

⟨Initialize level counts [302]⟩=
    LDX     #$FF
    STX     PLAYER_COL
    INX
    STX     LADDER_COUNT
    STX     GOLD_COUNT
    STX     GUARD_COUNT
    STX     GUARD_NUM
    STX     DIG_ANIM_STATE
    STX     LEVEL_DATA_INDEX
    STX     TMP
    STX     GAME_ROWNUM
    TXA

    LDX     #30
.loop1
    STA     BRICK_FILL_TIMERS,X
    DEX
    BPL     .loop1

    LDX     #$05
.loop2
    STA     GUARD_RESURRECTION_TIMERS,X
    DEX
    BPL     .loop2

The level data is stored in "compressed" form, just 4 bits per sprite since we don't use any higher ones to define a level. For each of the 16 game rows, we load up the compressed row data and break it apart, one 4-bit sprite per column.

Once we've done that, we draw the level using DRAW_LEVEL_PAGE2. That routine returns an error if there was no player sprite in the level. If there was no error, we simply return. Otherwise we have to handle the error condition, since there's no point in playing without a player!

⟨uncompress level data [304]⟩=
    LDY     GAME_ROWNUM
.row_loop:
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    ⟨uncompress row data⟩
    <<next compressed row for [[row_loop]]>>

    JSR     DRAW_LEVEL_PAGE2
    BCC     .end                ; No error

⟨handle no player sprite in level⟩

.end:
    RTS

.reset_game:
    JMP     RESET_GAME

Each row will have their sprite data stored at locations specified by the CURR_LEVEL_ROW_SPRITES_PTR_ tables.

To uncompress the data for a row, we use the counter in TMP as an odd/even switch so that we know which 4-bit chunk (nibble) in a byte we want. Even numbers are for the low nibble while odd numbers are for the high nibble.

In addition, if we encounter any sprite number 10 or above then we replace it with sprite 0 (all black).

⟨uncompress row data [306]⟩=
    LDA     #$00
    STA     GAME_COLNUM

.col_loop:
    LDA     TMP                         ; odd/even counter
    LSR
    LDY     LEVEL_DATA_INDEX
    LDA     DISK_BUFFER,Y
    BCS     .628c                       ; odd? 
    AND     #$0F
    BPL     .6292                       ; unconditional jump
.628c

    LSR
    LSR
    LSR
    LSR
    INC     LEVEL_DATA_INDEX

.6292
    INC     TMP

    LDY     GAME_COLNUM
    CMP     #10
    BCC     .629c
    LDA     #SPRITE_EMPTY                ; sprite >= 10 -> sprite 0
.629c:

    STA     (PTR1),Y
    STA     (PTR2),Y

    INC     GAME_COLNUM
    LDA     GAME_COLNUM
    CMP     #28
    BCC     .col_loop                   ; loop while GAME_COLNUM < 28
⟨next compressed row for [[row_loop]] [308]⟩=
    INC     GAME_ROWNUM
    LDY     GAME_ROWNUM
    CPY     #16
    BCC     .row_loop                   ; loop while GAME_ROWNUM < 16

When there's no player sprite in the level, a few things can happen. Firstly, if DISK_LEVEL_LOC is zero, we're going to jump to RESET_GAME. Otherwise, we set DISK_LEVEL_LOC to zero, increment \$97, set X to 0xFF, and retry LOAD_LEVEL from the very beginning.

⟨handle no player sprite in level [310]⟩=
    LDA     DISK_LEVEL_LOC
    BEQ     .reset_game

    LDX     #$00
    STX     DISK_LEVEL_LOC
    INC     GUARD_PATTERN_OFFSET
    DEX
    JMP     LOAD_LEVEL
⟨dead code [312]⟩=
    ORG     $62C7
COMPRESS_AND_SAVE_LEVEL_DATA:
    SUBROUTINE

    LDA     #$00
    STA     LEVEL_DATA_INDEX
    STA     TMP
    STA     GAME_ROWNUM

.loop:
    LDY     GAME_ROWNUM
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     #$00
    STY     GAME_COLNUM

.loop2:
    LDA     TMP
    LSR
    LDA     (PTR1),Y
    BCS     .shift_left

    STA     SPRITE_NUM
    BPL     .next

.shift_left:
    ASL
    ASL
    ASL
    ASL
    ORA     SPRITE_NUM
    LDY     LEVEL_DATA_INDEX
    STA     DISK_BUFFER,Y
    INC     LEVEL_DATA_INDEX

.next:
    INC     TMP
    INC     GAME_COLNUM
    LDY     GAME_COLNUM
    CPY     #MAX_GAME_COL+1
    BCC     .loop2

    INC     GAME_ROWNUM
    LDA     GAME_ROWNUM
    CMP     #MAX_GAME_ROW+1
    BCC     .loop

    LDA     #DISK_ACCESS_WRITE
    JMP     ACCESS_COMPRESSED_LEVEL_DATA    ; tailcall
Used in ⟨*⟩
Defines COMPRESS_AND_SAVE_LEVEL_DATA