Lode Runner: Apple II reverse engineering

8. Game play#

8.1 Splash screen#

⟨splash screen [340]⟩=
    ORG     $6008
RESET_GAME:
    SUBROUTINE

    JSR     CLEAR_HGR1

    LDA     #$FF
    STA     .rd_table+1
    LDA     #$0E
    STA     .rd_table+2     ; RD_TABLE = 0x0EFF
    LDY     #$00
    STY     GAME_ROWNUM
    STY     GAME_MODE
    STY     DISK_LEVEL_LOC  ; GAME_ROWNUM = DISK_LEVEL_LOC = GAME_MODE = 0
    LDA     #$20
    STA     HGR_PAGE
    STA     DRAW_PAGE       ; HGR_PAGE = DRAW_PAGE = 0x20

    ⟨splash screen loop⟩

    STA     TXTPAGE1
    STA     HIRES
    STA     MIXCLR
    STA     TXTCLR
    JMP     LONG_DELAY

This loop writes a screen of graphics by reading from the table starting at $0F00. The table is in pairs of bytes, where the first byte is the byte offset from the beginning of the row, and the second byte is the byte to write. However, if the first byte is 0x00 then we end that row.

As in other cases, the pointer into the table is stored in the LDA instruction that reads from the table.

The code takes advantage of the fact that all bytes written to the page have their high bit set, while offsets from the beginning of the row are always less than 0x80. Thus, if we read a byte and it is 0x00, we end the loop. Otherwise, if the byte is less than 0x80 we set that as the offset. Otherwise, the byte has its high bit set, and we write that byte to the graphics page.

⟨splash screen loop [342]⟩=
.draw_splash_screen_row:
    JSR     ROW_TO_ADDR     ; ROW_ADDR = ROW_TO_ADDR(Y)
    LDY     #$00

.loop:
    INC     .rd_table+1
    BNE     .rd_table
    INC     .rd_table+2     ; RD_TABLE++

.rd_table:
    LDA     $1A84           ; A <- *RD_TABLE ($1A84 is just a dummy value)
    BEQ     .end_of_row     ; if A == 0: break
    BPL     .is_row_offset  ; if A > 0: A -> Y, .loop
    STA     (ROW_ADDR),Y    ; *(ROW_ADDR+Y) = A

    INY                     ; Y++
    BPL     .loop           ; While Y < 0x80 (really while not 00)

.is_row_offset:
    TAY
    BPL     .loop           ; Unconditional jump

.end_of_row:
    INC     GAME_ROWNUM
    LDY     GAME_ROWNUM
    CPY     #192
    BCC     .draw_splash_screen_row
⟨handle timers [344]⟩=
    ORG     $75F4
HANDLE_TIMERS:
    SUBROUTINE

    JSR     GUARD_RESURRECTIONS

    ; Increment GUARD_RESURRECT_COL mod 29

    INC     GUARD_RESURRECT_COL
    LDA     GUARD_RESURRECT_COL
    CMP     #MAX_GAME_COL+1
    BCC     .guard_col_incremented

    LDA     #$00
    STA     GUARD_RESURRECT_COL

.guard_col_incremented:
    LDX     #$1E        ; 30

.loop:
    LDA     BRICK_FILL_TIMERS,X
    STX     TMP_LOOP_CTR
    BNE     .table_ce0_nonzero
    JMP     .next

.table_ce0_nonzero:
    DEC     BRICK_FILL_TIMERS,X
    BEQ     .brick_fill_timer_expired

    LDA     BRICK_DIG_COLS,X
    STA     GAME_COLNUM
    LDA     BRICK_DIG_ROWS,X
    STA     GAME_ROWNUM

    LDA     BRICK_FILL_TIMERS,X
    CMP     #$14            ; 20
    BNE     .check_for_10

    LDA     #SPRITE_BRICK_FILL0

.draw_sprite:
    JSR     DRAW_SPRITE_PAGE2
    LDX     GAME_COLNUM
    LDY     GAME_ROWNUM
    JSR     GET_SCREEN_COORDS_FOR
    LDA     #SPRITE_EMPTY
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS

.next_:
    JMP     .next

.check_for_10:
    CMP     #$0A            ; 10
    BNE     .next_

    LDA     #SPRITE_BRICK_FILL1
    BNE     .draw_sprite            ; Unconditional

.brick_fill_timer_expired:
    LDX     TMP_LOOP_CTR
    LDY     BRICK_DIG_ROWS,X
    STY     GAME_ROWNUM
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDY     BRICK_DIG_COLS,X
    STY     GAME_COLNUM
    LDA     (PTR1),Y
    CMP     #SPRITE_EMPTY
    BNE     .check_for_brick_fill_player_kill
    JMP     .draw_brick

.check_for_brick_fill_player_kill:
    CMP     #SPRITE_PLAYER
    BNE     .check_for_brick_fill_guard_kill
    LSR     ALIVE

.check_for_brick_fill_guard_kill:
    CMP     #SPRITE_GUARD
    BEQ     .kill_guard

    CMP     #SPRITE_GOLD
    BNE     .draw_brick_
    DEC     GOLD_COUNT

.draw_brick_:
    JMP     .draw_brick

.kill_guard:
    LDA     #SPRITE_BRICK
    STA     (PTR1),Y
    STA     (PTR2),Y
    JSR     DRAW_SPRITE_PAGE1
    LDA     #SPRITE_BRICK
    JSR     DRAW_SPRITE_PAGE2
    LDX     GUARD_COUNT

.find_killed_guard:
    LDA     GUARD_LOCS_COL,X
    CMP     GAME_COLNUM
    BNE     .next_guard
    LDA     GUARD_LOCS_ROW,X
    CMP     GAME_ROWNUM
    BNE     .next_guard

    LDA     GUARD_GOLD_TIMERS,X
    BPL     .reset_guard_gold_timer
    DEC     GOLD_COUNT

.reset_guard_gold_timer:
    LDA     #$7F
    STA     GUARD_GOLD_TIMERS,X
    STX     GUARD_NUM
    JSR     LOAD_GUARD_DATA
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    LDX     GUARD_NUM
    LDY     #$01
    STY     GAME_ROWNUM

.row_loop:
    LDY     GAME_ROWNUM
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     GUARD_RESURRECT_COL

.col_loop:
    LDA     (PTR2),Y
    CMP     #$00
    BEQ     .found_good_resurrect_loc

    INC     GUARD_RESURRECT_COL
    LDY     GUARD_RESURRECT_COL
    CPY     #MAX_GAME_COL+1
    BCC     .col_loop

    INC     GAME_ROWNUM
    LDA     #$00
    STA     GUARD_RESURRECT_COL
    BEQ     .row_loop               ; unconditional

.found_good_resurrect_loc:
    TYA
    STA     GUARD_LOCS_COL,X
    LDA     GAME_ROWNUM
    STA     GUARD_LOCS_ROW,X
    LDA     #$14            ; 20
    STA     GUARD_RESURRECTION_TIMERS,X
    LDA     #$02
    STA     GUARD_Y_ADJS,X
    STA     GUARD_X_ADJS,X
    LDA     #$00
    STA     GUARD_ANIM_STATES,X
    LDY     #$00
    LDA     #$75
    JSR     ADD_AND_UPDATE_SCORE        ; SCORE += 75
    JMP     .next

.next_guard:
    DEX
    BNE     .find_killed_guard

    ; This should never fall through

.draw_brick:
    LDA     #SPRITE_BRICK
    STA     (PTR1),Y
    JSR     DRAW_SPRITE_PAGE1
    LDA     #SPRITE_BRICK
    JSR     DRAW_SPRITE_PAGE2

.next:
    LDX     TMP_LOOP_CTR
    DEX
    BMI     HANDLE_TIMERS_COMMON_RETURN
    JMP     .loop

HANDLE_TIMERS_COMMON_RETURN:
    RTS

8.2 Startup code#

The startup code is run immediately after relocating memory blocks.

⟨startup code [346]⟩=

The first address, ROMIN_RDROM_WRRAM2 is a bank-select switch. By reading it twice, we set up the memory area from $D000-$DFFF to read from the ROM, but write to RAM bank 2.

The next four softswiches set up the display for full-screen hi-res graphics, page 1.

⟨defines [348]⟩+=
ROMIN_RDROM_WRRAM2      EQU     $C081
TXTCLR                  EQU     $C050
MIXCLR                  EQU     $C052
TXTPAGE1                EQU     $C054
HIRES                   EQU     $C057
Used in ⟨*⟩
Defines HIRES, MIXCLR, ROMIN_RDROM_WRRAM2, TXTCLR, TXTPAGE1
⟨set startup softswitches [350]⟩=
    ORG     $5F7D
MAIN:
    LDA     ROMIN_RDROM_WRRAM2
    LDA     ROMIN_RDROM_WRRAM2
    LDA     TXTCLR
    LDA     MIXCLR
    LDA     TXTPAGE1
    LDA     HIRES

The 6502 stack, at maximum, runs from $0100-$01FF. The stack starts at $0100 plus the stack index (the S register), and grows towards $0100. Here we are setting the S register to 0x07 which makes for a very small stack – 8 bytes.

⟨set stack size [352]⟩=
    LDX     #$07
    TXS

This next part seems to set the carry only if certain bits in location $5F94 are set. I can find no writes to this location, so the effect is that the carry is cleared. It's entirely possible that this was altered by the cracker.

⟨maybe set carry but not really [354]⟩=
    CLC
    LDA     #$01
    AND     #$A4
    BEQ     .short_delay_mode
    SEC
    ; fall through to .short_delay_mode

This next part sets the delay for this game mode, and also reads the keyboard strobe softswtich. That just clears the keyboard strobe in readiness to see if a key is pressed. Then we get dumped into the main loop.

⟨ready yourself [356]⟩=
    ORG     $5F9A

.short_delay_mode:
    LDX     #$22            ; Number of times to check for keyboard press (34).
    LDY     #$02            ; Number of times to do X checks (2).
                            ; GAME_ROWNUM was initialized to 1, so we do 34*2*1 checks.
    LDA     KBDSTRB
    LDA     #JOYSTICK_MODE
    JMP     CHECK_FOR_BUTTON_DOWN

Checking for a joystick button (or equivalently the open apple and solid apple keys) to be pressed involves checking the high bit after reading the corresponding button softswitch. Here we're checking if any of the buttons are pressed.

⟨check for button down [358]⟩=
    ORG     $6199

.poll_inputs:
    LDA     INPUT_MODE

CHECK_FOR_BUTTON_DOWN:
    CMP     #KEYBOARD_MODE
    BEQ     .poll_keyboard  ; If keyboard mode, skip check button presses.
    LDA     BUTN1
    BMI     .set_level_0_and_play_game
    LDA     BUTN0
    BMI     .set_level_0_and_play_game

    ; fall through to .poll_keyboard
Defines CHECK_FOR_BUTTON_DOWN

Here we read the keyboard, which involves checking the high bit of the KBD softswitch. This also loads the ASCII code for the key. We check for a keypress in a loop based on the X and Y registers, and on GAME_ROWNUM! So we check for X x Y x GAME_ROWNUM iterations. This controls alternation between "attract-mode" gameplay and the high score screen.

⟨no button pressed [360]⟩=
    ORG     $61A9

.poll_keyboard:
    LDA     KBD
    BMI     .key_pressed
    DEX
    BNE     .poll_inputs
    DEY
    BNE     .poll_inputs
    DEC     GAME_ROWNUM
    BNE     .poll_inputs

    ; fall through to .no_button_or_key_timeout

If one of the joystick buttons was pressed:

⟨button pressed at startup [362]⟩=
    ORG     $6201

.set_level_0_and_play_game:
    LDX     #$00
    STX     DISK_LEVEL_LOC      ; DISK_LEVEL_LOC = 0
    INX
    STX     LEVELNUM            ; LEVELNUM = 1
    STX     $9D
    LDA     #$02
    STA     GAME_MODE
    JMP     INIT_GAME_DATA

And if one of the keys was pressed:

⟨key pressed at startup [364]⟩=
    ORG     $61F6

.key_pressed:
    STA     KBDSTRB     ; Clear keyboard strobe
    CMP     #$85        ; if ctrl-E:
    BEQ     .start_level_editor
    CMP     #$8D        ; if return key:
    BEQ     .read_and_display_hi_score_screen

    ; fall through to .button_pressed

Two keys are special, ctrl-E, which opens the level editor, and return, which displays the high score screen.

⟨ctrl-e pressed [366]⟩=
    ORG     $6211

.start_level_editor:
    JMP     LEVEL_EDITOR
⟨return pressed [368]⟩=
    ORG     $61E4

.read_and_display_hi_score_screen:
    LDA     #$01
    JSR     ACCESS_HI_SCORE_DATA_FROM_DISK      ; read hi score table

    ; fallthrough to .display_hi_score_screen

Finally, if no key or button was pressed and we've reached the maximum number of polls through the loop:

⟨timed out waiting for button or keypress [370]⟩=
    ORG     $61B8

.no_button_or_key_timeout:
    LDA     GAME_MODE
    BNE     .check_game_mode    ; If GAME_MODE != 0, .check_game_mode.

    ; When GAME_MODE = 0:
    LDX     #$01
    STX     GAME_MODE        ; Set GAME_MODE = 1
    STX     LEVELNUM
    STX     $AC
    STX     $9D                 ; LEVELNUM = $AC = $9D = 1
    LDX     ENABLE_SOUND
    STX     .restore_enable_sound+1    ; Save previous value of DNABLE_SOUND
    STA     ENABLE_SOUND
    JMP     INIT_GAME_DATA

.restore_enable_sound:
    LDA     #$00            ; Fixed up above
    STA     ENABLE_SOUND
    LDA     KBD
    LDX     $AC
    BEQ     .key_pressed
    JMP     LONG_DELAY
⟨check game mode [372]⟩=
    ORG     $61DE

.check_game_mode:
    ; For game mode 1, reset and play the game.
    ; For game mode 0, display the high score screen.
    CMP     #$01
    BNE     .reset_game
    BEQ     .display_hi_score_screen        ; Unconditional jump
⟨reset game if not mode 1 [374]⟩=
    ORG     $61F3

.reset_game:
    JMP     RESET_GAME

Game mode 2 displays the high score screen.

⟨display high score screen [376]⟩=
    ORG     $61E9

.display_hi_score_screen:
    JSR     HI_SCORE_SCREEN
    LDA     #$02
    STA     GAME_MODE            ; GAME_MODE = 2
    JMP     LONG_DELAY

When we change over to the high score screen or attract mode, we set the delay to the next mode very large: 195075 times around the loop.

⟨long delay attract mode [378]⟩=
    ORG     $618E

LONG_DELAY:
    JSR     WAIT_KEY
    LDX     #$FF
    LDY     #$FF
    LDA     #$03
    STA     GAME_ROWNUM

    ; fall through to .poll_inputs

8.3 Moving the player#

The player's sprite position is stored in PLAYER_COL and PLAYER_ROW, while the offset from the exact sprive location is stored in PLAYER_X_ADJ and PLAYER_Y_ADJ. These adjustments are offset by 2, so that 2 means zero offset. The player also has a PLAYER_ANIM_STATE which is an index into the SPRITE_ANIM_SEQS table. The GET_SPRITE_AND_SCREEN_COORD_AT_PLAYER gets the sprite corresponding to the player's animation state and the player's adjusted screen coordinate.

⟨tables [380]⟩+=
    ORG     $6968
SPRITE_ANIM_SEQS:
    HEX     0B 0C 0D        ; player running left
    HEX     18 19 1A        ; player monkey swinging left
    HEX     0F              ; player digging left
    HEX     13              ; player falling, facing left
    HEX     09 10 11        ; player running right
    HEX     15 16 17        ; player monkey swinging right
    HEX     25              ; player digging right
    HEX     14              ; player falling, facing right
    HEX     0E 12           ; player climbing on ladder
Used in ⟨*⟩
Defines SPRITE_ANIM_SEQS
⟨get player sprite and coord data [382]⟩=
    ORG     $6B85
GET_SPRITE_AND_SCREEN_COORD_AT_PLAYER:
    SUBROUTINE
    ; Using PLAYER_COL/ROW, PLAYER_X/Y_ADJ, and PLAYER_ANIM_STATE,
    ; return the player sprite in A, and the screen coords in X and Y.

    LDX     PLAYER_COL
    LDY     PLAYER_X_ADJ
    JSR     GET_HALF_SCREEN_COL_OFFSET_IN_Y_FOR
    STX     SPRITE_NUM          ; Used only as a temporary to save X
    LDY     PLAYER_ROW
    LDX     PLAYER_Y_ADJ
    JSR     GET_SCREEN_ROW_OFFSET_IN_X_FOR
    LDX     PLAYER_ANIM_STATE
    LDA     SPRITE_ANIM_SEQS,X
    LDX     SPRITE_NUM
    RTS

Since PLAYER_ANIM_STATE needs to play a sequence over and over, there is a routine to increment the animation state and wrap if necessary. It works by loading A with the lower bound, and X with the upper bound.

⟨increment player animation state [384]⟩=
    ORG     $6BF4
INC_ANIM_STATE:
    SUBROUTINE

    INC     PLAYER_ANIM_STATE
    CMP     PLAYER_ANIM_STATE
    BCC     .check_upper_bound      ; lower bound < PLAYER_ANIM_STATE?
    ; otherwise PLAYER_ANIM_STATE <= lower bound:

.write_lower_bound:
    STA     PLAYER_ANIM_STATE       ; PLAYER_ANIM_STATE = lower bound
    RTS

.check_upper_bound:
    CPX     PLAYER_ANIM_STATE
    BCC     .write_lower_bound       ; PLAYER_ANIM_STATE > upper bound?
    ; otherwise PLAYER_ANIM_STATE <= upper bound:
    RTS
Defines INC_ANIM_STATE

This routine checks whether the player picks up gold. First we check to see if the player's location is exactly on a sprite coordinate, and return if not. Otherwise, we check the background sprite data to see if there's gold at the player's location, and return if not. So if there is gold, we decrement the gold count, put a blank sprite in the background sprite data, increment the score by 250, erase the gold sprite on the background screen at the player location, and then load up data into the sound area.

There is also a flag DIDNT_PICK_UP_GOLD which tells us whether the player did not pick up gold during this move. This flag is set to 1 just before handling the player move.

⟨defines [386]⟩+=
Used in ⟨*⟩
Defines DIDNT_PICK_UP_GOLD
⟨check for gold picked up by player [388]⟩=
    ORG     $6B9D
CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER:
    SUBROUTINE

    LDA     PLAYER_X_ADJ
    CMP     #$02
    BNE     .end
    LDA     PLAYER_Y_ADJ
    CMP     #$02
    BNE     .end

    LDY     PLAYER_ROW
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     PLAYER_COL
    LDA     (PTR2),Y

    CMP     #SPRITE_GOLD
    BNE     .end

    LSR     DIDNT_PICK_UP_GOLD  ; picked up gold
    DEC     GOLD_COUNT          ; GOLD_COUNT--

    LDY     PLAYER_ROW
    STY     GAME_ROWNUM
    LDY     PLAYER_COL
    STY     GAME_COLNUM
    LDA     #SPRITE_EMPTY
    STA     (PTR2),Y
    JSR     DRAW_SPRITE_PAGE2   ; Register and draw blank at player loc in background screen

    LDY     GAME_ROWNUM
    LDX     GAME_COLNUM
    JSR     GET_SCREEN_COORDS_FOR
    LDA     #SPRITE_GOLD
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS    ; Erase gold at player loc

    LDY     #$02
    LDA     #$50
    JSR     ADD_AND_UPDATE_SCORE            ; SCORE += 250
    JSR     LOAD_SOUND_DATA
    HEX     07 45 06 55 05 44 04 54 03 43 02 53 00

.end:
    RTS
⟨defines [390]⟩+=
KEY_COMMAND     EQU     $9E
KEY_COMMAND_LR  EQU     $9F
Used in ⟨*⟩
Defines KEY_COMMAND, KEY_COMMAND_LR
⟨tables [392]⟩+=
    ORG     $6B59
VALID_CTRL_KEYS:
    ; ctrl-
    ; ^ @ [ R A S J K H U X Y M
    ; Esc:         ctrl-[
    ; Down arrow:  ctrl-J
    ; Up arrow:    ctrl-K
    ; Right arrow: ctrl-U
    ; Left arrow:  ctrl-H
    ; Return:      ctrl-M
    HEX     9E 80 9B 92 81 93 8A 8B 88 95 98 99 8D 00

    ORG     $6B67
CTRL_KEY_HANDLERS:
    ; These get pushed onto the stack, then an RTS is issued.
    ; Remember that the 6502's return stack contains the address
    ; to return to *minus 1*, so these values are actually one less
    ; than the function to jump to.
    WORD    CTRL_CARET_HANDLER-1
    WORD    CTRL_AT_HANDLER-1
    WORD    ESC_HANDLER-1
    WORD    CTRL_R_HANDLER-1
    WORD    CTRL_A_HANDLER-1
    WORD    CTRL_S_HANDLER-1
    WORD    DOWN_ARROW_HANDLER-1
    WORD    UP_ARROW_HANDLER-1
    WORD    LEFT_ARROW_HANDLER-1
    WORD    RIGHT_ARROW_HANDLER-1
    WORD    CTRL_X_HANDLER-1
    WORD    CTRL_Y_HANDLER-1
    WORD    RETURN_HANDLER-1
⟨check for input [394]⟩=
    ORG     $6A12
CHECK_FOR_INPUT:
    SUBROUTINE

    LDA     GAME_MODE
    CMP     #$01
    BEQ     CHECK_FOR_MODE_1_INPUT

    LDX     KBD
    STX     KBDSTRB
    STX     SPRITE_NUM
    BMI     .key_pressed

    LDA     INPUT_MODE
    CMP     #KEYBOARD_MODE
    BEQ     .end                    ; If keyboard mode, end.

.check_buttons_:
    JMP     READ_JOYSTICK_FOR_COMMAND

.key_pressed:
    CPX     #$A0
    BCS     .non_ctrl_key_pressed
    ; ctrl key pressed
    STX     SPRITE_NUM
    LDY     #$FF

.loop:
    INY
    LDA     VALID_CTRL_KEYS,Y
    BEQ     .non_ctrl_key_pressed

    CMP     SPRITE_NUM
    BNE     .loop

    TYA
    ASL
    TAY
    LDA     CTRL_KEY_HANDLERS+1,Y
    PHA
    LDA     CTRL_KEY_HANDLERS,Y
    PHA
    RTS                             ; JSR to CTRL_KEY_HANDLERS[Y], then return.

.non_ctrl_key_pressed:
    LDA     INPUT_MODE
    CMP     #JOYSTICK_MODE
    BEQ     .check_buttons_         ; If joystick mode, check buttons.

    LDX     SPRITE_NUM
    STX     KEY_COMMAND
    STX     KEY_COMMAND_LR

.end:
    RTS

Hitting ctrl-^ increments both lives and level number, but also kills the player.

⟨ctrl handlers [396]⟩=
    ORG     $6A56
CTRL_CARET_HANDLER:
    SUBROUTINE

    INC     LIVES
    INC     LEVELNUM
    INC     DISK_LEVEL_LOC
    LSR     ALIVE       ; set player dead
    LSR     $9D
    RTS
Defines CTRL_CARET_HANDLER

Hitting ctrl-@ increments lives.

⟨ctrl handlers [398]⟩+=
    ORG     $6A61
CTRL_AT_HANDLER:
    SUBROUTINE

    INC     LIVES
    BNE     .have_lives
    DEC     LIVES           ; LIVES = 255
.have_lives:
    JSR     PUT_STATUS_LIVES
    LSR     $9D
    JMP     CHECK_FOR_INPUT

There's some dead code which seems to increase the GUARD_PATTERN_OFFSET and then kill the player, but not remove a life.

⟨dead code [400]⟩+=
    ORG     $6A6F
INC_GUARD_PATTERN_OFFSET:
    SUBROUTINE

    INC     GUARD_PATTERN_OFFSET
    INC     LIVES
    LSR     ALIVE
    RTS
Used in ⟨*⟩
Defines INC_GUARD_PATTERN_OFFSET

Hitting ESC pauses the game, and ESC then unpauses the game.

⟨ctrl handlers [402]⟩+=
    ORG     $6A76
ESC_HANDLER:
    SUBROUTINE

    JSR     WAIT_KEY_QUEUED
    CMP     #$9B            ; key pressed is ESC?
    BNE     ESC_HANDLER
    JMP     CHECK_FOR_INPUT

Hitting ctrl-R sets lives to 1 and sets player to dead, ending the game. Hitting ctrl-A shifts ALIVE, which just kills you.

⟨ctrl handlers [404]⟩+=
    ORG     $6A80
CTRL_R_HANDLER:
    SUBROUTINE

    LDA     #$01
    STA     LIVES

CTRL_A_HANDLER:
    LSR     ALIVE           ; Set player to dead
    RTS
Defines CTRL_A_HANDLER, CTRL_R_HANDLER
Uses ALIVE, LIVES

Hitting ctrl-S toggles sound.

⟨ctrl handlers [406]⟩+=
    ORG     $6A87
CTRL_S_HANDLER:
    SUBROUTINE

    LDA     ENABLE_SOUND
    EOR     #$FF
    STA     ENABLE_SOUND
    JMP     CHECK_FOR_INPUT
Defines CTRL_S_HANDLER

Hitting ctrl-J switches to joystick controls, and hitting ctrl-K switches to keyboard controls.

⟨ctrl handlers [408]⟩+=
    ORG     $6A90
DOWN_ARROW_HANDLER:
    SUBROUTINE

    LDA     #JOYSTICK_MODE
    STA     INPUT_MODE
    JMP     CHECK_FOR_INPUT

    ORG     $6A97
UP_ARROW_HANDLER:
    SUBROUTINE

    LDA     #KEYBOARD_MODE
    STA     INPUT_MODE
    JMP     CHECK_FOR_INPUT
Defines DOWN_ARROW_HANDLER, UP_ARROW_HANDLER

Hitting the left arrow and right arrow decreases and increases the FRAME_PERIOD, effectively speed up and slowing down the game.

⟨ctrl handlers [410]⟩+=
    ORG     $6ABC
RIGHT_ARROW_HANDLER:
    SUBROUTINE

    LDA     FRAME_PERIOD
    BEQ     LEFT_ARROW_HANDLER_end
    DEC     FRAME_PERIOD    
    JMP     CHECK_FOR_INPUT

    ORG     $6AC5
LEFT_ARROW_HANDLER:
    SUBROUTINE

    LDA     FRAME_PERIOD
    CMP     #$0F
    BEQ     LEFT_ARROW_HANDLER_end
    INC     FRAME_PERIOD

LEFT_ARROW_HANDLER_end:
    JMP     CHECK_FOR_INPUT
Defines LEFT_ARROW_HANDLER, RIGHT_ARROW_HANDLER

Hitting ctrl-X reverses one axis of the joystick, while hitting ctrl-Y reverses the other axis. These thresholds are used by READ_JOYSTICK_FOR_COMMAND.

⟨ctrl handlers [412]⟩+=
    ORG     $6A9E
CTRL_X_HANDLER:
    SUBROUTINE

    LDA     PADDLE0_THRESH1
    LDX     PADDLE0_THRESH2
    STA     PADDLE0_THRESH2
    STX     PADDLE0_THRESH1
    JMP     CHECK_FOR_INPUT

    ORG     $6AAD
CTRL_Y_HANDLER:
    SUBROUTINE

    LDA     PADDLE1_THRESH1
    LDX     PADDLE1_THRESH2
    STA     PADDLE1_THRESH2
    STX     PADDLE1_THRESH1
    JMP     CHECK_FOR_INPUT
⟨defines [414]⟩+=
BRICK_DIG_COLS      EQU     $0CA0       ; 31 bytes of col nums
BRICK_DIG_ROWS      EQU     $0CC0       ; 31 bytes of row nums
BRICK_FILL_TIMERS   EQU     $0CE0       ; 31 bytes of fill timers
⟨return handler [416]⟩=
    ORG     $77AC
RETURN_HANDLER:
    SUBROUTINE

    JSR     HI_SCORE_SCREEN     ; show high score screen
    LDX     #$FF
    LDY     #$FF
    LDA     #$04
    STA     SCRATCH_A1          ; loop 256x256x4 times

.loop:
    LDA     INPUT_MODE
    CMP     #KEYBOARD_MODE                ; Keyboard mode
    BEQ     .check_keyboard

    LDA     BUTN1
    BMI     .button_pressed
    LDA     BUTN0
    BMI     .button_pressed

.check_keyboard:
    LDA     KBD
    BMI     .button_pressed

    DEX
    BNE     .loop
    DEY
    BNE     .loop
    DEC     SCRATCH_A1
    BNE     .loop

.button_pressed:
    STA     KBDSTRB
    STA     TXTPAGE1
    JSR     CLEAR_HGR2
    LDY     #MAX_GAME_ROW
    STY     GAME_ROWNUM

.loop2:
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     #MAX_GAME_COL
    STY     GAME_COLNUM

.loop3:
    LDA     (PTR2),Y
    CMP     #SPRITE_TRAP
    BNE     .draw_sprite
    LDA     #SPRITE_BRICK
.draw_sprite:
    JSR     DRAW_SPRITE_PAGE2

    DEC     GAME_COLNUM
    LDY     GAME_COLNUM
    BPL     .loop3

    DEC     GAME_ROWNUM
    LDY     GAME_ROWNUM
    BPL     .loop2

    LDX     #$1E
.loop4:
    STX     TMP_LOOP_CTR
    LDA     BRICK_FILL_TIMERS,X
    BEQ     .next4

    LDY     BRICK_DIG_ROWS,X
    STY     GAME_ROWNUM
    LDY     BRICK_DIG_COLS,X
    STY     GAME_COLNUM
    CMP     #$15
    BCC     .check_b

    LDA     #SPRITE_EMPTY
    JSR     DRAW_SPRITE_PAGE2
    JMP     .next4

.check_b:
    CMP     #$0B
    BCC     .draw_sprite_56
    LDA     #$37
    JSR     DRAW_SPRITE_PAGE2
    JMP     .next4

.draw_sprite_56:
    LDA     #$38
    JSR     DRAW_SPRITE_PAGE2

.next4:
    LDX     TMP_LOOP_CTR
    DEX
    BPL     .loop4

    LDX     GUARD_COUNT
    BEQ     .check_for_input

.loop5:
    LDA     GUARD_RESURRECTION_TIMERS,X
    STX     TMP_LOOP_CTR
    BEQ     .next5

    LDY     GUARD_LOCS_COL,X
    STY     GAME_COLNUM
    LDY     GUARD_LOCS_ROW,X
    STY     GAME_ROWNUM
    CMP     #$14
    BCS     .next5

    CMP     #$0B
    BCC     .draw_sprite_58
    LDA     #$39                ; sprite 57
    BNE     .draw_sprite2       ; unconditional

.draw_sprite_58:
    LDA     #$3A

.draw_sprite2:
    JSR     DRAW_SPRITE_PAGE2

.next5:
    LDX     TMP_LOOP_CTR
    DEX
    BNE     .loop5

.check_for_input:
    JMP     CHECK_FOR_INPUT

During pregame mode 1, we don't check for gameplay input. Instead, we use CHECK_FOR_MODE_1_INPUT for input. We first check if the user has pressed a key or hit a joystick button, and if so, we simulate killing the attract-mode player. However, if nothing was pressed, we check if the simulated player is pressing a key, and handle that.

⟨tables [418]⟩+=
    ORG     $6A0B
VALID_KEY_COMMANDS:
    HEX     C9      ; 'I'
    HEX     CA      ; 'J'
    HEX     CB      ; 'K'
    HEX     CC      ; 'L'
    HEX     CF      ; 'O'
    HEX     D5      ; 'U'
    HEX     A0      ; space
Used in ⟨*⟩
Defines VALID_KEY_COMMANDS
⟨check for mode 1 input [420]⟩=
    ORG     $69B8
CHECK_FOR_MODE_1_INPUT:
    SUBROUTINE

    LDA     KBD
    BMI     .key_pressed

    LDA     INPUT_MODE
    CMP     #KEYBOARD_MODE
    BEQ     .nothing_pressed
    
    ; Check joystick buttons also
    LDA     BUTN1
    BMI     .key_pressed
    LDA     BUTN0
    BPL     .nothing_pressed

.key_pressed:
    ; Simulate killing the attact-mode player.
    LSR     $AC
    LSR     ALIVE
    LDA     #$01
    STA     LIVES
    RTS

.nothing_pressed:
    LDA     $AB
    BNE     .sim_keypress

    LDY     #$00
    LDA     ($A8),Y
    STA     $AA
    INY
    LDA     ($A8),Y
    STA     $AB

    ; {$A8,$A9} += 2
    LDA     $A8
    CLC
    ADC     #$02
    STA     $A8
    LDA     $A9
    ADC     #$00
    STA     $A9

.sim_keypress:
    LDA     $AA
    AND     #$0F
    TAX
    LDA     VALID_KEY_COMMANDS,X
    STA     KEY_COMMAND
    LDA     $AA
    LSR
    LSR
    LSR
    LSR
    TAX
    LDA     VALID_KEY_COMMANDS,X
    STA     KEY_COMMAND_LR
    DEC     $AB
    RTS

The READ_JOYSTICK_FOR_COMMAND routine reads the paddle buttons and joystick state. If a paddle button is pressed, it translates to the dig left or dig right keyboard command. Otherwise, the joystick state is read and compared to the thresholds 0x12 and 0x2E, which can be reversed through the ctrl-X key, and translated to left, right, up, or down keyboard commands.

Flowchart of the joystick reading routine

⟨tables [422]⟩+=
    ORG     $6B81
PADDLE0_THRESH1:
    HEX     12
PADDLE0_THRESH2:
    HEX     2E
PADDLE1_THRESH1:
    HEX     12
PADDLE1_THRESH2:
    HEX     2E
Used in ⟨*⟩
Defines PADDLE0_THRESH1, PADDLE0_THRESH2, PADDLE1_THRESH1, PADDLE1_THRESH2
⟨check buttons [424]⟩=
    ORG     $6AD0
READ_JOYSTICK_FOR_COMMAND:
    SUBROUTINE

    LDA     BUTN1
    BPL     .check_butn0
    LDA     #$D5                  ; 'U' (dig)
    BNE     .store_key_command    ; unconditional

.check_butn0:
    LDA     BUTN0
    BPL     .read_paddles
    LDA     #$CF                  ; 'O' (dig)

.store_key_command
    STA     KEY_COMMAND
    STA     KEY_COMMAND_LR
    RTS

.read_paddles:
    JSR     READ_PADDLES
    LDY     PADDLE0_VALUE

    LDA     PADDLE0_THRESH2
    CMP     #$2E
    BEQ     .6afa
    ; PADDLE0_THRESH2 != 46

    CPY     PADDLE0_THRESH2
    BCS     .6b03
    ; PADDLE0_VALUE < PADDLE0_THRESH2

    LDA     #$CC                  ; 'L' (right)
    BNE     .check_paddle_1       ; unconditional

.6afa:
    CPY     PADDLE0_THRESH2
    BCC     .6b03
    ; PADDLE0_VALUE >= PADDLE0_THRESH2

    LDA     #$CC                  ; 'L' (right)
    BNE     .check_paddle_1       ; unconditional

.6b03:
    LDA     PADDLE0_THRESH1
    CMP     #$2E
    BEQ     .6b13
    ; PADDLE0_THRESH1 != 46

    CPY     PADDLE0_THRESH1
    BCS     .6b1c
    ; PADDLE0_VALUE < PADDLE0_THRESH1
    LDA     #$CA                  ; 'J' (left)
    BNE     .check_paddle_1       ; unconditional

.6b13:
    CPY     PADDLE0_THRESH1
    BCC     .6b1c
    ; PADDLE0_VALUE >= PADDLE0_THRESH1
    LDA     #$CA                  ; 'J' (left)
    BNE     .check_paddle_1       ; unconditional

.6b1c:
    LDA     #$C0                   ; '@'

.check_paddle_1:
    STA     KEY_COMMAND_LR

    LDY     PADDLE1_VALUE

    LDA     PADDLE1_THRESH1
    CMP     #$2E
    BEQ     .6b32
    ; PADDLE1_THRESH1 != 46

    CPY     PADDLE1_THRESH1
    BCS     .6b3b
    ; PADDLE1_VALUE >= PADDLE1_THRESH1

    LDA     #$C9                         ; 'I' (up)
    BNE     .store_key_command_end       ; unconditional

.6b32:
    CPY     PADDLE1_THRESH1
    BCC     .6b3b
    ; PADDLE1_VALUE < PADDLE1_THRESH1

    LDA     #$C9                         ; 'I' (up)
    BNE     .store_key_command_end       ; unconditional

.6b3b:
    LDA     PADDLE1_THRESH2
    CMP     #$2E
    BEQ     .6b4b
    ; PADDLE1_THRESH2 != 46

    CPY     PADDLE1_THRESH2
    BCS     .6b54
    ; PADDLE1_VALUE >= PADDLE1_THRESH2

    LDA     #$CB                         ; 'K' (down)
    BNE     .store_key_command_end       ; unconditional

.6b4b:
    CPY     PADDLE1_THRESH2
    BCC     .6b54
    ; PADDLE1_VALUE < PADDLE1_THRESH2

    LDA     #$CB                         ; 'K' (down)
    BNE     .store_key_command_end       ; unconditional

.6b54:
    LDA     #$C0                         ; '@'

.store_key_command_end:
    STA     KEY_COMMAND
    RTS

8.4 Player movement#

Player movement is generally handled by functions which check whether the player can move in a given direction, and then either fail with carry set, or succeed, and the player is moved, with carry cleared.

Recall that the player is at the gross sprite location given by PLAYER_COL and PLAYER_ROW, but with a plus-or-minus adjustment given by a horizontal adjustment PLAYER_X_ADJ and a vertical adjustment PLAYER_Y_ADJ.

We will refer to the player as "exactly on" the sprite if the adjustment in the direction we're interested in is zero. Again, recall that the adjustment values are offset by 2, so an adjustment of zero is a value of 2, and the adjustment ranges from -2 to +2.

We can refer to the player as slightly above, below, left of, or right of, an exact sprite coordinate if the adjustment is not zero.

There are two routines which nudge the player towards an exact sprite row or column. Generally this is done when the player does something that has to take place on an exact row or column, such as climbing a ladder or traversing a rope, and serves to make the transition to an aligned row or column more smooth. Each time the player is nudged, we also check if the player landed on gold.

⟨try moving up [426]⟩=
    ORG     $6C13
NUDGE_PLAYER_TOWARDS_EXACT_COLUMN:
    SUBROUTINE

    LDA     PLAYER_X_ADJ
    CMP     #$02
    BCC     .player_slightly_left
    BEQ     .end

.player_slightly_right:
    DEC     PLAYER_X_ADJ        ; Nudge player left
    JMP     CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER

.player_slightly_left:
    INC     PLAYER_X_ADJ        ; Nudge player right
    JMP     CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER

.end:
    RTS

    ORG     $6C26
NUDGE_PLAYER_TOWARDS_EXACT_ROW:
    SUBROUTINE

    LDA     PLAYER_Y_ADJ
    CMP     #$02
    BCC     .player_slightly_above
    BEQ     .end

.player_slightly_below:
    DEC     PLAYER_Y_ADJ        ; Nudge player up
    JMP     CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER

.player_slightly_above:
    INC     PLAYER_Y_ADJ        ; Nudge player down
    JMP     CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER

.end:
    RTS
Defines NUDGE_PLAYER_TOWARDS_EXACT_COLUMN, NUDGE_PLAYER_TOWARDS_EXACT_ROW

Now the logic for attempting to move up is:

The steps involved in actually moving the player up are:

⟨try moving up [428]⟩+=
    ORG     $66BD
TRY_MOVING_UP:
    SUBROUTINE

    ⟨get background sprite at player location⟩
    CMP     #SPRITE_LADDER
    BEQ     .ladder_here

    LDY     PLAYER_Y_ADJ
    CPY     #$03
    BCC     .cannot_move       ; if PLAYER_Y_ADJ <= 2

    ; and if there's no ladder below, you can't move up.
    ⟨get background sprite at player location on next row⟩
    CMP     #SPRITE_LADDER
    BEQ     .move_player_up

.cannot_move:
    SEC
    RTS

.ladder_here:
    LDY     PLAYER_Y_ADJ
    CPY     #$03
    BCS     .move_player_up             ; if PLAYER_Y_ADJ > 2

    ; If you're at the top, you can't move up even if there's a ladder.
    LDY     PLAYER_ROW
    BEQ     .cannot_move       ; if PLAYER_ROW == 0, set carry and return

    ; You can't move up if there's a brick, stone, or trap above.
    LDA     CURR_LEVEL_ROW_SPRITES_PTR_OFFSETS-1,Y
    STA     PTR1
    LDA     CURR_LEVEL_ROW_SPRITES_PTR_PAGES-1,Y
    STA     PTR1+1
    LDY     PLAYER_COL
    LDA     (PTR1),Y                    ; Get the sprite on the row above.

    CMP     #SPRITE_BRICK
    BEQ     .cannot_move
    CMP     #SPRITE_STONE
    BEQ     .cannot_move
    CMP     #SPRITE_TRAP
    BEQ     .cannot_move       ; If brick, stone, or trap, set carry and return

.move_player_up:
    JSR     GET_SPRITE_AND_SCREEN_COORD_AT_PLAYER
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    LDY     PLAYER_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    JSR     NUDGE_PLAYER_TOWARDS_EXACT_COLUMN
    DEC     PLAYER_Y_ADJ                ; Move player up
    BPL     TRY_MOVING_UP_check_for_gold

    ; PLAYER_Y_ADJ rolled over.

    ; Restore the sprite at the player's former location:
    ; If background page at player location is brick, put an empty at the
    ; (previous) player location on active page, otherwise copy the background
    ; sprite to the active page.
    LDY     PLAYER_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BNE     .set_on_real_page
    LDA     #SPRITE_EMPTY
.set_on_real_page:
    STA     (PTR1),Y

    DEC     PLAYER_ROW                  ; Move player up
    LDY     PLAYER_ROW
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     PLAYER_COL
    LDA     #SPRITE_PLAYER
    STA     (PTR1),Y            ; Write player sprite to active page.
    LDA     #$04
    STA     PLAYER_Y_ADJ        ; Set adjustment to +2
    BNE     TRY_MOVING_UP_inc_anim_state     ; unconditional

TRY_MOVING_UP_check_for_gold:
    JSR     CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER

TRY_MOVING_UP_inc_anim_state:
    LDA     #$10
    LDX     #$11
    JSR     INC_ANIM_STATE      ; player climbing on ladder
    JSR     DRAW_PLAYER
    CLC
    RTS

For attempting to move down, the logic is:

The steps involved in actually moving the player down are:

⟨try moving down [430]⟩=
    ORG     $6766
TRY_MOVING_DOWN:
    SUBROUTINE

    LDY     PLAYER_Y_ADJ
    CPY     #$02
    BCC     .move_player_down   ; player slightly above, so can move down.

    LDY     PLAYER_ROW
    CPY     #MAX_GAME_ROW
    BCS     .cannot_move        ; player on row >= 15, so cannot move.

    <<set active row pointer [[PTR1]] for [[Y+1]]>>
    LDY     PLAYER_COL
    LDA     (PTR1),Y
    CMP     #SPRITE_STONE
    BEQ     .cannot_move
    CMP     #SPRITE_BRICK
    BNE     .move_player_down   ; Row below is stone or brick, so cannot move.

.cannot_move:
    SEC
    RTS

.move_player_down:
    JSR     GET_SPRITE_AND_SCREEN_COORD_AT_PLAYER
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    LDY     PLAYER_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    JSR     NUDGE_PLAYER_TOWARDS_EXACT_COLUMN
    INC     PLAYER_Y_ADJ                ; Move player down
    LDA     PLAYER_Y_ADJ
    CMP     #$05
    BCC     .check_for_gold_

    ; adjustment overflow
    LDY     PLAYER_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BNE     .set_on_real_page
    LDA     #SPRITE_EMPTY
.set_on_real_page:
    STA     (PTR1),Y

    INC     PLAYER_ROW
    LDY     PLAYER_ROW
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     PLAYER_COL
    LDA     #SPRITE_PLAYER
    STA     (PTR1),Y            ; Write player sprite to active page.
    LDA     #SPRITE_EMPTY
    STA     PLAYER_Y_ADJ        ; Set adjustment to -2
    JMP     TRY_MOVING_UP_inc_anim_state

.check_for_gold_:
    JMP     TRY_MOVING_UP_check_for_gold

For attempting to move left, the logic is:

The steps involved in actually moving the player left are:

The animation is either monkey-traversing if the player moves onto a rope, or running otherwise.

⟨try moving left [432]⟩=
    ORG     $65D3
TRY_MOVING_LEFT:
    SUBROUTINE

    LDY     PLAYER_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDX     PLAYER_X_ADJ
    CPX     #$03
    BCS     .move_player_left       ; player slightly right, so can move left.

    LDY     PLAYER_COL  
    BEQ     .cannot_move            ; col == 0, so cannot move.

    DEY
    LDA     (PTR1),Y
    CMP     #SPRITE_STONE
    BEQ     .cannot_move
    CMP     #SPRITE_BRICK
    BEQ     .cannot_move
    CMP     #SPRITE_TRAP
    BNE     .move_player_left       ; brick, stone, or trap to left, so cannot move.

.cannot_move:
    RTS

.move_player_left:
    JSR     GET_SPRITE_AND_SCREEN_COORD_AT_PLAYER
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    LDA     #$FF
    STA     PLAYER_FACING_DIRECTION             ; face left
    JSR     NUDGE_PLAYER_TOWARDS_EXACT_ROW
    DEC     PLAYER_X_ADJ
    BPL     .check_for_gold

    ; adjustment overflow
    LDY     PLAYER_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BNE     .set_on_level
    LDA     #SPRITE_EMPTY
.set_on_level:
    STA     (PTR1),Y

    DEC     PLAYER_COL
    DEY
    LDA     #SPRITE_PLAYER
    STA     (PTR1),Y            ; Write player sprite to active page.
    LDA     #$04
    STA     PLAYER_X_ADJ        ; Set adjustment to +2
    BNE     .inc_anim_state     ; Unconditional

.check_for_gold:
    JSR     CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER

.inc_anim_state:
    LDY     PLAYER_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_ROPE
    BEQ     .anim_state_monkeying

    LDA     #$00
    LDX     #$02
    BNE     .done               ; Unconditional

.anim_state_monkeying:
    LDA     #$03
    LDX     #$05

.done:
    JSR     INC_ANIM_STATE
    JMP     DRAW_PLAYER

Moving right has the same logic as moving left, except in the other direction.

⟨try moving right [434]⟩=
    ORG     $6645
TRY_MOVING_RIGHT:
    SUBROUTINE

    LDY     PLAYER_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDX     PLAYER_X_ADJ
    CPX     #$02
    BCC     .move_player_right      ; player slightly left, so can move right.

    LDY     PLAYER_COL
    CPY     #MAX_GAME_COL
    BEQ     .cannot_move            ; col == 27, so cannot move.

    INY
    LDA     (PTR1),Y
    CMP     #SPRITE_STONE
    BEQ     .cannot_move
    CMP     #SPRITE_BRICK
    BEQ     .cannot_move
    CMP     #SPRITE_TRAP
    BNE     .move_player_right      ; brick, stone, or trap to right, so cannot move.

.cannot_move:
    RTS

.move_player_right:
    JSR     GET_SPRITE_AND_SCREEN_COORD_AT_PLAYER
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    LDA     #$01
    STA     PLAYER_FACING_DIRECTION             ; face right
    JSR     NUDGE_PLAYER_TOWARDS_EXACT_ROW
    INC     PLAYER_X_ADJ
    LDA     PLAYER_X_ADJ
    CMP     #$05
    BCC     .check_for_gold

    ; adjustment overflow
    LDY     PLAYER_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BNE     .set_on_level
    LDA     #SPRITE_EMPTY
.set_on_level:
    STA     (PTR1),Y

    INC     PLAYER_COL
    INY
    LDA     #SPRITE_PLAYER
    STA     (PTR1),Y            ; Write player sprite to active page.
    LDA     #$00
    STA     PLAYER_X_ADJ        ; Set adjustment to -2
    BEQ     .inc_anim_state     ; Unconditional

.check_for_gold:
    JSR     CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER

.inc_anim_state:
    LDY     PLAYER_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_ROPE
    BEQ     .anim_state_monkeying

    LDA     #$08
    LDX     #$0A
    BNE     .done               ; Unconditional

.anim_state_monkeying:
    LDA     #$0B
    LDX     #$0D

.done:
    JSR     INC_ANIM_STATE
    JMP     DRAW_PLAYER

8.5 Digging#

Provided there's nothing preventing the player from digging, digging involves a brick animation below and next to the player, and a "debris" animation above the dig site.

Digging: the brick and debris animation

The DIG_DIRECTION location stores which direction we're digging in, and the DIG_ANIM_STATE location stores how far along in the 13-step animation cycle we are.

⟨defines [436]⟩+=
DIG_DIRECTION       EQU     $9C     ; 0xFF = left, 0x00 = not digging, 0x01 = right
DIG_ANIM_STATE      EQU     $A0     ; 00-0C
Used in ⟨*⟩
Defines DIG_DIRECTION

The DIG_DEBRIS_LEFT_SPRITES, DIG_DEBRIS_RIGHT_SPRITES and DIG_BRICK_SPRITES tables contain the sprites used during the animation. There's also a little sequence of notes that plays while digging, given by DIG_NOTE_PITCHES and DIG_NOTE_DURATIONS.

⟨tables [438]⟩+=
    ORG     $697A
DIG_DEBRIS_LEFT_SPRITES:
    HEX     1B 1B 1C 1C 1D 1D 1E 1E 00 00 00 00
DIG_DEBRIS_RIGHT_SPRITES:
    HEX     26 26 27 27 1D 1D 1E 1E 00 00 00 00
DIG_BRICK_SPRITES:
    HEX     1F 1F 20 20 21 21 22 22 23 23 24 24
DIG_NOTE_PITCHES:
    HEX     20 20 20 20 20 20 20 20 24 24 24 24 24
DIG_NOTE_DURATIONS:
    HEX     04 04 04 04 04 04 04 04 03 03 02 02 01
Used in ⟨*⟩
Defines DIG_BRICK_SPRITES, DIG_DEBRIS_LEFT_SPRITES, DIG_DEBRIS_RIGHT_SPRITES, DIG_NOTE_DURATIONS, DIG_NOTE_PITCHES

The player cannot dig to the left if they're on the bottom-most row or the leftmost column, or if there's no brick below and to the left. Also, there has to be nothing to the left of the player.

⟨try digging left [440]⟩=
    ORG     $67D8
    SUBROUTINE

.cannot_dig_:
    JMP     .stop_digging

TRY_DIGGING_LEFT:
    LDA     #$FF
    STA     DIG_DIRECTION
    STA     KEY_COMMAND
    STA     KEY_COMMAND_LR      ; DIG_DIRECTION = KEY_COMMANDs = 0xFF
    LDA     #$00
    STA     DIG_ANIM_STATE      ; DIG_ANIM_STATE = 0

TRY_DIGGING_LEFT_check_can_dig_left:
    LDY     PLAYER_ROW
    CPY     #MAX_GAME_ROW
    BCS     .cannot_dig_        ; row >= 15, so cannot dig.

    INY
    JSR     GET_PTRS_TO_CURR_LEVEL_SPRITE_DATA
    LDY     PLAYER_COL
    BEQ     .cannot_dig_        ; col == 0, so cannot dig left.

    DEY
    LDA     (PTR1),Y
    CMP     #SPRITE_BRICK
    BNE     .cannot_dig_        ; no brick below and to the left, so cannot dig left.

    LDY     PLAYER_ROW
    JSR     GET_PTRS_TO_CURR_LEVEL_SPRITE_DATA
    LDY     PLAYER_COL
    DEY
    LDA     (PTR1),Y
    CMP     #SPRITE_EMPTY
    BNE     .not_empty_to_left  ; not empty to the left, so maybe cannot dig left.

    ; Can dig!
    JSR     GET_SPRITE_AND_SCREEN_COORD_AT_PLAYER
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    JSR     NUDGE_PLAYER_TOWARDS_EXACT_COLUMN
    JSR     NUDGE_PLAYER_TOWARDS_EXACT_ROW
    LDY     DIG_ANIM_STATE
    LDA     DIG_NOTE_PITCHES,Y
    LDX     DIG_NOTE_DURATIONS,Y
    JSR     APPEND_NOTE

    LDX     DIG_ANIM_STATE
    LDA     #$00                ; running left
    CPX     #$06
    BCS     .note_0             ; DIG_ANIM_STATE >= 6
    LDA     #$06                ; digging left
.note_0:
    STA     PLAYER_ANIM_STATE
    JSR     DRAW_PLAYER

    LDX     DIG_ANIM_STATE
    CPX     #$0C
    BEQ     .move_player_left
    CPX     #$00
    BEQ     .draw_curr_dig              ; Don't have to erase previous dig debris sprite

    ; Erase the previous dig debris sprite
    LDA     DIG_DEBRIS_LEFT_SPRITES-1,X
    PHA
    LDX     PLAYER_COL
    DEX
    LDY     PLAYER_ROW
    JSR     GET_SCREEN_COORDS_FOR
    PLA
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS

    LDX     DIG_ANIM_STATE
.draw_curr_dig:
    LDA     DIG_DEBRIS_LEFT_SPRITES,X
    PHA
    LDX     PLAYER_COL
    DEX
    STX     GAME_COLNUM
    LDY     PLAYER_ROW
    STY     GAME_ROWNUM
    JSR     GET_SCREEN_COORDS_FOR
    PLA
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS     ; Draw current dig debris sprite above dig site

    LDX     DIG_ANIM_STATE
    LDA     DIG_BRICK_SPRITES,X
    INC     GAME_ROWNUM
    JSR     DRAW_SPRITE_PAGE1               ; Draw dig brick sprite at dig site

    INC     DIG_ANIM_STATE
    CLC
    RTS

.not_empty_to_left:
    LDY     PLAYER_ROW
    INY
    STY     GAME_ROWNUM
    LDY     PLAYER_COL
    DEY
    STY     GAME_COLNUM
    LDA     #SPRITE_BRICK
    JSR     DRAW_SPRITE_PAGE1           ; Draw brick below and to the left of player

    LDX     DIG_ANIM_STATE
    BEQ     .stop_digging

    ; Erase previous dig debris sprite
    DEX
    LDA     DIG_DEBRIS_LEFT_SPRITES,X
    PHA
    LDY     PLAYER_ROW
    LDX     PLAYER_COL
    DEX
    JSR     GET_SCREEN_COORDS_FOR
    PLA
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS

.stop_digging:
    LDA     #$00
    STA     DIG_DIRECTION
    SEC
    RTS

.move_player_left:
    LDX     PLAYER_COL
    DEX
    JMP     DROP_PLAYER_IN_HOLE
⟨try digging right [442]⟩=
    ORG     $689E
    SUBROUTINE

.cannot_dig_:
    JMP     .stop_digging

TRY_DIGGING_RIGHT:
    LDA     #$01
    STA     DIG_DIRECTION
    STA     KEY_COMMAND
    STA     KEY_COMMAND_LR      ; DIG_DIRECTION = KEY_COMMANDs = 0x01
    LDA     #$0C
    STA     DIG_ANIM_STATE      ; DIG_ANIM_STATE = 0x0C

TRY_DIGGING_RIGHT_check_can_dig_right:
    LDY     PLAYER_ROW
    CPY     #MAX_GAME_ROW
    BCS     .cannot_dig_        ; row >= 15, so cannot dig.

    INY
    JSR     GET_PTRS_TO_CURR_LEVEL_SPRITE_DATA
    LDY     PLAYER_COL
    CPY     #MAX_GAME_COL
    BCS     .cannot_dig_        ; col >= 27, so cannot dig right.

    INY
    LDA     (PTR1),Y
    CMP     #SPRITE_BRICK
    BNE     .cannot_dig_        ; no brick below and to the right, so cannot dig right.

    LDY     PLAYER_ROW
    JSR     GET_PTRS_TO_CURR_LEVEL_SPRITE_DATA
    LDY     PLAYER_COL
    INY
    LDA     (PTR1),Y
    CMP     #SPRITE_EMPTY
    BNE     .not_empty_to_right  ; not empty to the right, so maybe cannot dig right.

    ; Can dig!
    JSR     GET_SPRITE_AND_SCREEN_COORD_AT_PLAYER
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    JSR     NUDGE_PLAYER_TOWARDS_EXACT_COLUMN
    JSR     NUDGE_PLAYER_TOWARDS_EXACT_ROW
    LDY     DIG_ANIM_STATE
    LDA     DIG_NOTE_PITCHES-12,Y
    LDX     DIG_NOTE_DURATIONS-12,Y
    JSR     APPEND_NOTE

    LDX     DIG_ANIM_STATE
    LDA     #$08                ; running right
    CPX     #$12
    BCS     .note_0             ; DIG_ANIM_STATE >= 0x12
    LDA     #$0E                ; digging right
.note_0:
    STA     PLAYER_ANIM_STATE
    JSR     DRAW_PLAYER

    LDX     DIG_ANIM_STATE
    CPX     #$18
    BEQ     .move_player_right
    CPX     #$0C
    BEQ     .draw_curr_dig              ; Don't have to erase previous dig debris sprite

    ; Erase the previous dig debris sprite
    LDA     DIG_DEBRIS_LEFT_SPRITES-1,X
    PHA
    LDX     PLAYER_COL
    INX
    LDY     PLAYER_ROW
    JSR     GET_SCREEN_COORDS_FOR
    PLA
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS

    LDX     DIG_ANIM_STATE
.draw_curr_dig:
    LDA     DIG_DEBRIS_LEFT_SPRITES,X
    PHA
    LDX     PLAYER_COL
    INX
    STX     GAME_COLNUM
    LDY     PLAYER_ROW
    STY     GAME_ROWNUM
    JSR     GET_SCREEN_COORDS_FOR
    PLA
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS     ; Draw current dig debris sprite above dig site

    INC     GAME_ROWNUM
    LDX     DIG_ANIM_STATE
    LDA     DIG_BRICK_SPRITES-12,X
    JSR     DRAW_SPRITE_PAGE1               ; Draw dig brick sprite at dig site

    INC     DIG_ANIM_STATE
    CLC
    RTS

.not_empty_to_right:
    LDY     PLAYER_ROW
    INY
    STY     GAME_ROWNUM
    LDY     PLAYER_COL
    INY
    STY     GAME_COLNUM
    LDA     #SPRITE_BRICK
    JSR     DRAW_SPRITE_PAGE1           ; Draw brick below and to the right of player

    LDX     DIG_ANIM_STATE
    CPX     #$0C
    BEQ     .stop_digging

    ; Erase previous dig debris sprite
    DEX
    LDA     DIG_DEBRIS_LEFT_SPRITES,X
    PHA
    LDX     PLAYER_COL
    INX
    LDY     PLAYER_ROW
    JSR     GET_SCREEN_COORDS_FOR
    PLA
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS

.stop_digging:
    LDA     #$00
    STA     DIG_DIRECTION
    SEC
    RTS

.move_player_right:
    LDX     PLAYER_COL
    INX
    JMP     DROP_PLAYER_IN_HOLE
⟨drop player in hole [444]⟩=
    ORG     $6C39
DROP_PLAYER_IN_HOLE:
    SUBROUTINE

    LDA     #$00
    STA     DIG_DIRECTION       ; Stop digging

    LDY     PLAYER_ROW
    INY                         ; Move player down

    STX     GAME_COLNUM
    STY     GAME_ROWNUM
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDA     #SPRITE_EMPTY
    LDY     GAME_COLNUM
    STA     (PTR1),Y                ; Set blank sprite at player location in active page
    JSR     DRAW_SPRITE_PAGE1
    LDA     #SPRITE_EMPTY
    JSR     DRAW_SPRITE_PAGE2       ; Draw blank at player location on both graphics pages

    DEC     GAME_ROWNUM
    LDA     #SPRITE_EMPTY
    JSR     DRAW_SPRITE_PAGE1       ; Draw blank at location above player
    INC     GAME_ROWNUM
    LDX     #$FF

.loop:
    INX
    CPX     #$1E
    BEQ     .end
    LDA     BRICK_FILL_TIMERS,X
    BNE     .loop

    LDA     GAME_ROWNUM
    STA     BRICK_DIG_ROWS,X
    LDA     GAME_COLNUM
    STA     BRICK_DIG_COLS,X
    LDA     #$B4
    STA     BRICK_FILL_TIMERS,X
    SEC

.end:
    RTS

The MOVE_PLAYER routine handle continuation of digging, player falling, and player keyboard input.

⟨move player [446]⟩=
    ORG     $64BD
MOVE_PLAYER:
    SUBROUTINE

    LDA     #$01
    STA     DIDNT_PICK_UP_GOLD   ; Reset DIDNT_PICK_UP_GOLD

    ; If we're digging, see if we can keep digging.
    LDA     DIG_DIRECTION
    BEQ     .not_digging
    BPL     .digging_right
    JMP     TRY_DIGGING_LEFT_check_can_dig_left

.digging_right:
    JMP     TRY_DIGGING_RIGHT_check_can_dig_right

.not_digging:
    LDY     PLAYER_ROW
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     PLAYER_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_LADDER
    BEQ     .check_for_keyboard_input_      ; ladder at background location?
    CMP     #SPRITE_ROPE
    BNE     .check_if_player_should_fall    ; rope at background location?
    LDA     PLAYER_Y_ADJ
    CMP     #$02
    BEQ     .check_for_keyboard_input_      ; player at exact sprite row?

    ; player is not on exact sprite row, fallthrough.

.check_if_player_should_fall:
    LDA     PLAYER_Y_ADJ
    CMP     #$02
    BCC     .make_player_fall               ; player slightly above sprite row?

    LDY     PLAYER_ROW
    CPY     #MAX_GAME_ROW
    BEQ     .check_for_keyboard_input_      ; player exactly sprite row 15?

    ; Check the sprite at the player location
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y+1]]>>

    LDY     PLAYER_COL
    LDA     (PTR1),Y
    CMP     #SPRITE_EMPTY
    BEQ     .make_player_fall
    CMP     #SPRITE_GUARD
    BEQ     .check_for_keyboard_input_
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BEQ     .check_for_keyboard_input_
    CMP     #SPRITE_STONE
    BEQ     .check_for_keyboard_input_
    CMP     #SPRITE_LADDER
    BNE     .make_player_fall

.check_for_keyboard_input_:
    JMP     .check_for_keyboard_input

.make_player_fall:
    LDA     #$00
    STA     $9B                     ; $9B = 0
    JSR     GET_SPRITE_AND_SCREEN_COORD_AT_PLAYER
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS

    LDA     #$07                    ; Next anim state: player falling, facing left
    LDX     PLAYER_FACING_DIRECTION
    BMI     .player_facing_left
    LDA     #$0F                    ; Next anim state: player falling, facing right
.player_facing_left:
    STA     PLAYER_ANIM_STATE

    JSR     NUDGE_PLAYER_TOWARDS_EXACT_COLUMN

    INC     PLAYER_Y_ADJ            ; Move down one
    LDA     PLAYER_Y_ADJ
    CMP     #$05
    BCS     .adjustment_overflow

    JSR     CHECK_FOR_GOLD_PICKED_UP_BY_PLAYER
    JMP     DRAW_PLAYER            ; tailcall

.adjustment_overflow:
    LDA     #$00
    STA     PLAYER_Y_ADJ            ; Set vertical adjust to -2

    LDY     PLAYER_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDY     PLAYER_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BNE     .set_on_level
    LDA     #SPRITE_EMPTY
.set_on_level:
    STA     (PTR1),Y
    INC     PLAYER_ROW          ; Move down
    LDY     PLAYER_ROW
    <<set active row pointer [[PTR1]] for [[Y]]>>

    LDY     PLAYER_COL
    LDA     #SPRITE_PLAYER
    STA     (PTR1),Y
    JMP     DRAW_PLAYER     ; tailcall

.check_for_keyboard_input:
    LDA     $9B
    BNE     .check_for_key  ; $9B doesn't play note
    LDA     #$64
    LDX     #$08
    JSR     PLAY_NOTE       ; play note, pitch 0x64, duration 8.

.check_for_key:
    LDA     #$20
    STA     $A4
    STA     $9B
    JSR     CHECK_FOR_INPUT
    LDA     KEY_COMMAND
    CMP     #$C9            ; 'I'
    BNE     .check_for_K
    JSR     TRY_MOVING_UP
    BCS     .check_for_J    ; couldn't move up
    RTS

.check_for_K:
    CMP     #$CB            ; 'K'
    BNE     .check_for_U
    JSR     TRY_MOVING_DOWN
    BCS     .check_for_J
    RTS

.check_for_U:
    CMP     #$D5            ; 'U'
    BNE     .check_for_O
    JSR     TRY_DIGGING_LEFT
    BCS     .check_for_J
    RTS

.check_for_O:
    CMP     #$CF            ; 'O'
    BNE     .check_for_J
    JSR     TRY_DIGGING_RIGHT
    BCS     .check_for_J
    RTS

.check_for_J:
    LDA     KEY_COMMAND_LR
    CMP     #$CA            ; 'J'
    BNE     .check_for_L
    JMP     TRY_MOVING_LEFT

.check_for_L:
    CMP     #$CC            ; 'L'
    BNE     .end
    JMP     TRY_MOVING_RIGHT

.end:
    RTS

ENABLE_NEXT_LEVEL_LADDERS goes through the registered ladder locations from last to first. Recall that the ladder indices are 1-based, so that LADDER_LOCS_[0] does not contain ladder data. Instead, that location is used as scratch space by this routine.

Recall also that LADDER_LOCS_[X] is negative if there is no ladder corresponding to entry X.

For each ladder, if there's a non-blank sprite on the background sprite page for it, we set LADDER_LOCS_COL to 1.

However, if there is a blank sprite on the background sprite page for it, then set it to the ladder sprite, and if it's also blank on the active sprite page, set that to the ladder sprite, too. Then draw the ladder on the background and active graphics pages, remove the ladder from the registered locations, and keep going.

Once all ladder locations have been gone through, if LADDER_LOCS_COL is 1—that is, if there was a non-blank sprite on the background sprite page for any ladder location—then decrement the gold count. Since this routine is only called when GOLD_COUNT is zero, this sets GOLD_COUNT to -1.

⟨do ladders [448]⟩=
    ORG     $8631
ENABLE_NEXT_LEVEL_LADDERS:
    SUBROUTINE

    LDA     #$00
    STA     LADDER_LOCS_COL     ; LADDER_LOCS_COL = 0

    LDX     LADDER_COUNT
    STX     .count              ; .count backwards from LADDER_COUNT to 0
.loop:
    LDX     .count
    BEQ     .dec_gold_count_if_no_ladder

    LDA     LADDER_LOCS_COL,X   ; A = LADDER_LOCS_COL[X]
    BMI     .next               ; If not present, next.

    STA     GAME_COLNUM         ; GAME_COLNUM = LADDER_LOCS_COL[X]
    LDA     LADDER_LOCS_ROW,X
    STA     GAME_ROWNUM         ; GAME_ROWNUM = LADDER_LOCS_ROW[X]
    TAY
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDY     GAME_COLNUM
    LDA     (PTR2),Y            ; A = sprite at ladder loc
    BNE     .set_col_to_1

    LDA     #SPRITE_LADDER
    STA     (PTR2),Y            ; Set background sprite to ladder
    LDA     (PTR1),Y
    BNE     .draw_ladder        ; .draw_ladder if active sprite not blank

    LDA     #SPRITE_LADDER
    STA     (PTR1),Y            ; Set active sprite to ladder

.draw_ladder:
    LDA     #SPRITE_LADDER
    JSR     DRAW_SPRITE_PAGE2   ; Draw ladder on background page

    LDX     GAME_COLNUM
    LDY     GAME_ROWNUM
    JSR     GET_SCREEN_COORDS_FOR
    LDA     #SPRITE_LADDER
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS ; Draw ladder on active page

    LDX     .count
    LDA     #$FF
    STA     LADDER_LOCS_COL,X       ; Remove ladder loc
    BMI     .next                   ; Unconditional

.set_col_to_1:
    LDA     #$01
    STA     LADDER_LOCS_COL         ; LADDER_LOCS_COL = 1

.next:
    DEC     .count
    JMP     .loop

.dec_gold_count_if_no_ladder:
    LDA     LADDER_LOCS_COL
    BNE     .end
    DEC     GOLD_COUNT

.end:
    RTS

.count:
    BYTE    0