Lode Runner: Apple II reverse engineering

9. Guard AI#

Like the player, each guard has a column and row sprite location and a horizontal and vertical adjustment. Each guard also has an animation state and a facing direction.

Guards also maintain two timers: a gold timer and a resurrection timer. The resurrection timer comes into play when a guard is killed by a closing hole.

⟨defines [450]⟩+=
GUARD_LOCS_COL      EQU     $0C60       ; 8 bytes
GUARD_LOCS_ROW      EQU     $0C68       ; 8 bytes
GUARD_GOLD_TIMERS   EQU     $0C70       ; 8 bytes
GUARD_X_ADJS        EQU     $0C78       ; 8 bytes
GUARD_Y_ADJS        EQU     $0C80       ; 8 bytes
GUARD_ANIM_STATES   EQU     $0C88       ; 8 bytes
GUARD_FACING_DIRECTIONS     EQU     $0C90       ; 8 bytes
GUARD_RESURRECTION_TIMERS   EQU     $0C98       ; 8 bytes

GUARD_LOC_COL       EQU     $12
GUARD_LOC_ROW       EQU     $13
GUARD_ANIM_STATE    EQU     $14
GUARD_FACING_DIRECTION      EQU     $15     ; Hi bit set: facing left, otherwise facing right
GUARD_GOLD_TIMER    EQU     $16
GUARD_X_ADJ         EQU     $17
GUARD_Y_ADJ         EQU     $18
GUARD_NUM           EQU     $19

GUARD_PATTERN       EQU     $63
GUARD_PHASE         EQU     $64

GUARD_RESURRECT_COL EQU     $53
TMP_GUARD_COL       EQU     $55
TMP_GUARD_ROW       EQU     $56
GUARD_GOLD_TIMER_START_VALUE        EQU     $5F
Used in ⟨*⟩
Defines GUARD_ANIM_STATE, GUARD_ANIM_STATES, GUARD_FACING_DIRECTION, GUARD_FACING_DIRECTIONS, GUARD_GOLD_TIMER, GUARD_GOLD_TIMERS, GUARD_GOLD_TIMER_START_VALUE, GUARD_LOCS_COL, GUARD_LOCS_ROW, GUARD_LOC_COL, GUARD_LOC_ROW, GUARD_NUM, GUARD_PATTERN, GUARD_PHASE, GUARD_X_ADJ, GUARD_X_ADJS, GUARD_Y_ADJ, GUARD_Y_ADJS
⟨tables [452]⟩+=
    ORG     $621D
GUARD_GOLD_TIMER_START_VALUES:
    HEX     26 26 2E 44 47 49 4A 4B 4C 4D 4E 4F 50
Used in ⟨*⟩
Defines GUARD_GOLD_TIMER_START_VALUES
⟨nudge guards [454]⟩=
    ORG     $7582
NUDGE_GUARD_TOWARDS_EXACT_COLUMN:
    SUBROUTINE

    LDA     GUARD_X_ADJ
    CMP     #$02
    BCC     .slightly_left
    BEQ     .end

.slightly_right:
    DEC     GUARD_X_ADJ        ; Nudge guard left
    JMP     CHECK_FOR_GOLD_PICKED_UP_BY_GUARD

.slightly_left:
    INC     GUARD_X_ADJ        ; Nudge guard right
    JMP     CHECK_FOR_GOLD_PICKED_UP_BY_GUARD

.end:
    RTS

    ORG     $7595
NUDGE_GUARD_TOWARDS_EXACT_ROW:
    SUBROUTINE

    LDA     GUARD_Y_ADJ
    CMP     #$02
    BCC     .slightly_above
    BEQ     .end

.slightly_below:
    DEC     GUARD_Y_ADJ        ; Nudge guard up
    JMP     CHECK_FOR_GOLD_PICKED_UP_BY_GUARD

.slightly_above:
    INC     GUARD_Y_ADJ        ; Nudge guard down
    JMP     CHECK_FOR_GOLD_PICKED_UP_BY_GUARD

.end:
    RTS
Defines NUDGE_GUARD_TOWARDS_EXACT_COLUMN, NUDGE_GUARD_TOWARDS_EXACT_ROW

If the guard is exactly on a sprite coordinate, and there's gold there, and GUARD_GOLD_TIMER is zero or positive, then set GUARD_GOLD_TIMER to 0xFF - $53, and remove the gold.

⟨check for gold picked up by guard [456]⟩=
    ORG     $74F7
CHECK_FOR_GOLD_PICKED_UP_BY_GUARD:
    SUBROUTINE

    LDA     GUARD_X_ADJ
    CMP     #$02
    BNE     .end
    LDA     GUARD_Y_ADJ
    CMP     #$02
    BNE     .end

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

    CMP     #SPRITE_GOLD
    BNE     .end

    LDA     GUARD_GOLD_TIMER        ; Does guard have gold already?
    BMI     .end

    LDA     #$FF
    SEC
    SBC     $53
    STA     GUARD_GOLD_TIMER        ; GUARD_GOLD_TIMER = 0xFF - $53

    ; Remove gold from screen
    LDA     #SPRITE_EMPTY
    STA     (PTR2),Y
    LDY     GUARD_LOC_ROW
    STY     GAME_ROWNUM
    LDY     GUARD_LOC_COL
    STY     GAME_COLNUM
    JSR     DRAW_SPRITE_PAGE2

    LDY     GAME_ROWNUM
    LDX     GAME_COLNUM
    JSR     GET_SCREEN_COORDS_FOR
    LDA     #SPRITE_GOLD
    JMP     ERASE_SPRITE_AT_PIXEL_COORDS        ; tailcall

.end:
    RTS
⟨increment guard animation state [458]⟩=
    ORG     $7574
INC_GUARD_ANIM_STATE:
    SUBROUTINE

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

.write_lower_bound:
    STA     GUARD_ANIM_STATE       ; GUARD_ANIM_STATE = lower bound
    RTS

.check_upper_bound:
    CPX     GUARD_ANIM_STATE
    BCC     .write_lower_bound       ; GUARD_ANIM_STATE > upper bound?
    ; otherwise GUARD_ANIM_STATE <= upper bound:
    RTS
Defines INC_GUARD_ANIM_STATE
⟨guard store and load data [460]⟩=
⟨tables [462]⟩+=
    ORG     $6CCB
GUARD_ANIM_SPRITES:
    HEX     08 2B 2C        ; running left
    HEX     30 31 32        ; monkey-traversing left
    HEX     36              ; falling left
    HEX     28 29 2A        ; running right
    HEX     2D 2E 2F        ; monkey-traversing right
    HEX     35              ; falling right
    HEX     33 34           ; climbing
Used in ⟨*⟩
Defines GUARD_ANIM_SPRITES
⟨get guard sprite and coords [464]⟩=

The GUARD_RESURRECTIONS routine handles guard resurrection. It checks each guard's resurrection timer to see if it is nonzero. If so, we decrement the guard's timer and then check the timer for specific values:

⟨guard resurrections [466]⟩=
    ORG     $7716
GUARD_RESURRECTIONS:
    SUBROUTINE

    LDX     GUARD_COUNT
    BEQ     HANDLE_TIMERS_COMMON_RETURN

    LDA     GUARD_NUM
    PHA

.loop:
    LDA     GUARD_RESURRECTION_TIMERS,X
    BEQ     .next

    STX     GUARD_NUM
    JSR     LOAD_GUARD_DATA
    LDA     #$7F
    STA     GUARD_GOLD_TIMERS,X
    LDA     GUARD_LOCS_COL,X
    STA     GAME_COLNUM
    LDA     GUARD_LOCS_ROW,X
    STA     GAME_ROWNUM

    DEC     GUARD_RESURRECTION_TIMERS,X
    BEQ     .resurrect

    LDA     GUARD_RESURRECTION_TIMERS,X
    CMP     #$13                ; 19
    BNE     .check_guard_flag_5_is_10

    ; GUARD_RESURRECTION_TIMER is 19

    LDA     #SPRITE_GUARD_EGG0
    JSR     DRAW_SPRITE_PAGE2
    JSR     GET_GUARD_SPRITE_AND_COORDS
    LDA     #SPRITE_GUARD_EGG0
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS
    JMP     .next2

.check_guard_flag_5_is_10:
    CMP     #$0A                ; 10
    BNE     .next

    ; GUARD_RESURRECTION_TIMER is 10
    LDA     #SPRITE_GUARD_EGG1
    JSR     DRAW_SPRITE_PAGE2
    JSR     GET_GUARD_SPRITE_AND_COORDS
    LDA     #SPRITE_GUARD_EGG1
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS

.next2:
    ; Restores the counter
    LDX     GUARD_NUM

.next:
    DEX
    BNE     .loop

    PLA
    STA     GUARD_NUM
    RTS

.resurrect:
    LDY     GAME_ROWNUM
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDX     GUARD_NUM
    INC     GUARD_RESURRECTION_TIMERS,X
    LDY     GAME_COLNUM
    LDA     (PTR1),Y
    BNE     .next

    ; empty
    LDA     #SPRITE_GUARD
    STA     (PTR1),Y
    LDA     #SPRITE_EMPTY
    JSR     DRAW_SPRITE_PAGE2
    LDA     #$00
    LDX     GUARD_NUM
    STA     GUARD_GOLD_TIMERS,X
    STA     GUARD_RESURRECTION_TIMERS,X
    LDA     #SPRITE_GUARD
    JSR     DRAW_SPRITE_PAGE1

    ; Play the "guard resurrection" sound
    JSR     LOAD_SOUND_DATA
    HEX     02 7C 03 78 04 74 05 70 00

    LDX     GUARD_NUM
    JMP     .next
⟨defines [468]⟩+=
    ; Init with:
    ; BYTE    %10000110
    ; BYTE    %00111110
    ; BYTE    %10000101
GUARD_PATTERNS  EQU     $60     ; 3 bytes
Used in ⟨*⟩
Defines GUARD_PATTERNS
⟨move guards [470]⟩=
    ORG     $6C82
MOVE_GUARDS:
    SUBROUTINE

    LDX     GUARD_COUNT
    BEQ     .end

    ; Increment GUARD_PHASE mod 3
    INC     GUARD_PHASE
    LDY     GUARD_PHASE
    CPY     #$03
    BCC     .incremented_phase
    LDY     #$00
    STY     GUARD_PHASE
.incremented_phase:

    LDA     GUARD_PATTERNS,Y
    STA     GUARD_PATTERN

.loop:
    LSR     GUARD_PATTERN       ; Peel off the lsb
    BCC     .bit_done
    JSR     MOVE_GUARD          ; Move a guard
    LDA     ALIVE
    BEQ     .end                ; If player is dead, end.

.bit_done:
    LDA     GUARD_PATTERN
    BNE     .loop

.end:
    RTS
⟨tables [472]⟩+=
    ORG     $6E7F
GUARD_X_ADJ_TABLE:
    HEX     02 01 02 03 02 01
Used in ⟨*⟩
Defines GUARD_X_ADJ_TABLE
⟨defines [474]⟩+=
GUARD_ACTION    EQU     $58     ; Index into GUARD_FN_TABLE

GUARD_ACTION_DO_NOTHING     EQU     #$00
GUARD_ACTION_MOVE_LEFT      EQU     #$01
GUARD_ACTION_MOVE_RIGHT     EQU     #$02
GUARD_ACTION_MOVE_UP        EQU     #$03
GUARD_ACTION_MOVE_DOWN      EQU     #$04
Used in ⟨*⟩
Defines GUARD_ACTION
⟨tables [476]⟩+=
    ORG     $6E97
GUARD_FN_TABLE:
    WORD    STORE_GUARD_DATA-1
    WORD    TRY_GUARD_MOVE_LEFT-1
    WORD    TRY_GUARD_MOVE_RIGHT-1
    WORD    TRY_GUARD_MOVE_UP-1
    WORD    TRY_GUARD_MOVE_DOWN-1
⟨move guard [478]⟩=
    ORG     $6CDB
MOVE_GUARD
    SUBROUTINE

    ; Increment GUARD_NUM mod GUARD_COUNT, except 1-based.
    INC     GUARD_NUM
    LDX     GUARD_COUNT
    CPX     GUARD_NUM
    BCS     .guard_num_incremented
    LDX     #$01
    STX     GUARD_NUM
.guard_num_incremented:

    JSR     LOAD_GUARD_DATA
    LDA     GUARD_GOLD_TIMER
    BMI     .check_sprite_at_guard_pos
    BEQ     .check_sprite_at_guard_pos

    ; GUARD_GOLD_TIMER > 0:
    DEC     GUARD_GOLD_TIMER
    LDY     GUARD_GOLD_TIMER
    CPY     #$0D
    BCS     .guard_gold_timer_expired

    ; GUARD_GOLD_TIMER < 12
    JMP     .check_gold_timer

.guard_gold_timer_expired:
    LDX     GUARD_NUM
    LDA     GUARD_RESURRECTION_TIMERS,X
    BEQ     .resurrect_guard_
    JMP     STORE_GUARD_DATA            ; tailcall

.resurrect_guard_:
    JMP     .resurrect_guard                       

.check_sprite_at_guard_pos:
    LDY     GUARD_LOC_ROW
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    LDA     (PTR2),Y

    CMP     #SPRITE_LADDER
    BEQ     .ladder_
    CMP     #SPRITE_ROPE
    BNE     .not_rope_or_ladder
    LDA     GUARD_Y_ADJ
    CMP     #$02
    BEQ     .ladder_

.not_rope_or_ladder:
    LDA     GUARD_Y_ADJ
    CMP     #$02
    BCC     .blank_or_player           ; if GUARD_Y_ADJ < 2
    LDY     GUARD_LOC_ROW
    CPY     #MAX_GAME_ROW
    BEQ     .ladder_          ; Row == 15
    <<set active and background row pointers [[PTR2]] and [[PTR1]] for [[Y+1]]>>
    LDY     GUARD_LOC_COL
    LDA     (PTR1),Y

    CMP     #SPRITE_EMPTY
    BEQ     .blank_or_player
    CMP     #SPRITE_PLAYER
    BEQ     .blank_or_player
    CMP     #SPRITE_GUARD
    BEQ     .ladder_

    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BEQ     .ladder_
    CMP     #SPRITE_STONE
    BEQ     .ladder_
    CMP     #SPRITE_LADDER
    BNE     .blank_or_player

.ladder_:
    JMP     .ladder

.blank_or_player:
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    JSR     NUDGE_GUARD_TOWARDS_EXACT_COLUMN
    LDA     #$06
    LDY     GUARD_FACING_DIRECTION
    BMI     .set_guard_anim_state
    LDA     #$0D
.set_guard_anim_state
    STA     GUARD_ANIM_STATE

    INC     GUARD_Y_ADJ
    LDA     GUARD_Y_ADJ
    CMP     #$05
    BCS     $6dc0           ; If GUARD_Y_ADJ > 4

    LDA     GUARD_Y_ADJ
    CMP     #$02
    BNE     .resurrect_guard

    JSR     CHECK_FOR_GOLD_PICKED_UP_BY_GUARD
    LDY     GUARD_LOC_ROW
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BNE     .resurrect_guard

    LDA     GUARD_GOLD_TIMER
    BPL     .reset_gold_timer
    DEC     GOLD_COUNT
.reset_gold_timer:
    LDA     GUARD_GOLD_TIMER_START_VALUE
    STA     GUARD_GOLD_TIMER
    LDY     #$00
    LDA     #$75
    JSR     ADD_AND_UPDATE_SCORE        ; SCORE += 75

    ; Play the guard kill tune
    JSR     LOAD_SOUND_DATA
    HEX     06 20 04 30 02 40 00

.resurrect_guard:
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS
    JMP     STORE_GUARD_DATA            ; tailcall

.6dc0:
    LDA     #$00
    STA     GUARD_Y_ADJ                 ; set vertical adjust to -2
    LDY     GUARD_LOC_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BNE     .set_real_sprite
    LDA     #SPRITE_EMPTY
.set_real_sprite:
    STA     (PTR1),Y

    INC     GUARD_LOC_ROW               ; move guard down
    LDY     GUARD_LOC_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    LDA     (PTR1),Y
    CMP     #SPRITE_PLAYER
    BNE     .get_background_sprite
    LSR     ALIVE                       ; set player to dead
.get_background_sprite:
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BNE     .place_guard_at_loc
    LDA     GUARD_GOLD_TIMER
    BPL     .place_guard_at_loc

    ; What's above the guard?
    LDY     GUARD_LOC_ROW
    DEY
    STY     GAME_ROWNUM
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    STY     GAME_COLNUM
    LDA     (PTR2),Y
    CMP     #SPRITE_EMPTY
    BEQ     .drop_gold
    DEC     GOLD_COUNT
    JMP     .6e46

.drop_gold:
    LDA     #SPRITE_GOLD
    STA     (PTR1),Y
    STA     (PTR2),Y
    JSR     DRAW_SPRITE_PAGE2
    LDY     GAME_ROWNUM
    LDX     GAME_COLNUM
    JSR     GET_SCREEN_COORDS_FOR
    LDA     #SPRITE_GOLD
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS

.6e46
    LDY     GUARD_LOC_ROW
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDA     #$00
    STA     GUARD_GOLD_TIMER
    LDY     GUARD_LOC_COL

.place_guard_at_loc
    LDA     #SPRITE_GUARD
    STA     (PTR1),Y

    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS
    JMP     STORE_GUARD_DATA            ; tailcall

.check_gold_timer:
    CPY     #$07
    BCC     .ladder

    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    LDY     GUARD_GOLD_TIMER
    LDA     GUARD_X_ADJ_TABLE-7,Y       ; GUARD_X_ADJ_TABLE[GUARD_GOLD_TIMER-7]
    STA     GUARD_X_ADJ
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS
    JMP     STORE_GUARD_DATA            ; tailcall

    ORG     $6E85
.ladder
    LDX     GUARD_LOC_COL
    LDY     GUARD_LOC_ROW
    JSR     DETERMINE_GUARD_MOVE

    ; Go to a guard movement function in the GUARD_FN_TABLE
    ASL
    TAY
    LDA     GUARD_FN_TABLE+1,Y
    PHA
    LDA     GUARD_FN_TABLE,Y
    PHA
    RTS

To determine which direction a guard should move, all directions are checked for a candidate target, and the minimum distance from the candidate target to the player determines the move.

⟨defines [480]⟩+=
Used in ⟨*⟩
Defines BEST_GUARD_DIST
⟨determine guard move [482]⟩=
    ORG     $70D8
DETERMINE_GUARD_MOVE:
    SUBROUTINE

    STX     TMP_GUARD_COL
    STY     TMP_GUARD_ROW
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     TMP_GUARD_COL
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BNE     .end_if_row_is_not_player_row
    LDA     GUARD_GOLD_TIMER
    BEQ     .end_if_row_is_not_player_row
    BMI     .end_if_row_is_not_player_row
    LDA     #GUARD_ACTION_MOVE_UP
    RTS

.end_if_row_is_not_player_row:
    LDY     TMP_GUARD_ROW
    CPY     PLAYER_ROW
    BEQ     .7100
    JMP     .end

.7100:
    LDY     TMP_GUARD_COL
    STY     $57
    CPY     PLAYER_COL
    BCS     .loop2
    ; If TMP_GUARD_COL < PLAYER_COL:

.loop:
    INC     $57
    LDY     TMP_GUARD_ROW
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     $57
    LDA     (PTR2),Y

    CMP     #SPRITE_LADDER
    BEQ     .is_ladder_or_rope
    CMP     #SPRITE_ROPE
    BEQ     .is_ladder_or_rope

    LDY     TMP_GUARD_ROW
    CPY     #MAX_GAME_ROW
    BEQ     .is_ladder_or_rope

    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     $57
    LDA     (PTR2),Y
    CMP     #SPRITE_EMPTY
    BEQ     .end
    CMP     #SPRITE_TRAP
    BEQ     .end

.is_ladder_or_rope:
    LDY     $57
    CPY     PLAYER_COL
    BNE     .loop

    ; PLAYER_COL == $57:
    LDA     #GUARD_ACTION_MOVE_RIGHT
    RTS

.loop2:
    DEC     $57
    LDY     TMP_GUARD_ROW
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     $57
    LDA     (PTR2),Y

    CMP     #SPRITE_LADDER
    BEQ     .is_ladder_or_rope2
    CMP     #SPRITE_ROPE
    BEQ     .is_ladder_or_rope2

    LDY     TMP_GUARD_ROW
    CPY     #MAX_GAME_ROW
    BEQ     .is_ladder_or_rope2

    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     $57
    LDA     (PTR2),Y
    CMP     #SPRITE_EMPTY
    BEQ     .end
    CMP     #SPRITE_TRAP
    BEQ     .end

.is_ladder_or_rope2:
    LDY     $57
    CPY     PLAYER_COL
    BNE     .loop2

    ; PLAYER_COL == $57:
    LDA     #GUARD_ACTION_MOVE_LEFT
    RTS

.end:
    LDA     #GUARD_ACTION_DO_NOTHING
    STA     GUARD_ACTION
    LDA     #$FF
    STA     BEST_GUARD_DIST
    LDX     TMP_GUARD_COL
    LDY     TMP_GUARD_ROW
    JSR     DETERMINE_GUARD_LEFT_RIGHT_LIMITS
    JSR     SHOULD_GUARD_MOVE_UP_OR_DOWN
    JSR     SHOULD_GUARD_MOVE_LEFT
    JSR     SHOULD_GUARD_MOVE_RIGHT
    LDA     GUARD_ACTION
    RTS
⟨defines [484]⟩+=
Used in ⟨*⟩
Defines GUARD_LEFT_COL_LIMIT, GUARD_RIGHT_COL_LIMIT
⟨determine guard left right limits [486]⟩=
    ORG     $743E
DETERMINE_GUARD_LEFT_RIGHT_LIMITS:
    SUBROUTINE

    STX     GUARD_LEFT_COL_LIMIT
    STX     GUARD_RIGHT_COL_LIMIT
    STY     ROWNUM

.loop:
    LDA     GUARD_LEFT_COL_LIMIT
    BEQ     .loop2

    LDY     ROWNUM
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     GUARD_LEFT_COL_LIMIT
    DEY
    LDA     (PTR1),Y

    CMP     #SPRITE_BRICK
    BEQ     .loop2
    CMP     #SPRITE_STONE
    BEQ     .loop2

    CMP     #SPRITE_LADDER
    BEQ     .next
    CMP     #SPRITE_ROPE
    BEQ     .next

    LDY     ROWNUM
    CPY     #MAX_GAME_ROW
    BEQ     .next

    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     GUARD_LEFT_COL_LIMIT
    DEY
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BEQ     .next
    CMP     #SPRITE_STONE
    BEQ     .next
    CMP     #SPRITE_LADDER
    BNE     .end_loop

.next:
    DEC     GUARD_LEFT_COL_LIMIT
    BPL     .loop

.end_loop:
    DEC     GUARD_LEFT_COL_LIMIT

.loop2:
    LDA     GUARD_RIGHT_COL_LIMIT
    CMP     #MAX_GAME_COL
    BEQ     .end

    LDY     ROWNUM
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     GUARD_RIGHT_COL_LIMIT
    INY
    LDA     (PTR1),Y

    CMP     #SPRITE_BRICK
    BEQ     .end
    CMP     #SPRITE_STONE
    BEQ     .end

    CMP     #SPRITE_LADDER
    BEQ     .next_loop2
    CMP     #SPRITE_ROPE
    BEQ     .next_loop2

    LDY     ROWNUM
    CPY     #MAX_GAME_ROW
    BEQ     .next_loop2

    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     GUARD_RIGHT_COL_LIMIT
    INY
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BEQ     .next_loop2
    CMP     #SPRITE_STONE
    BEQ     .next_loop2
    CMP     #SPRITE_LADDER
    BNE     .end_loop2

.next_loop2:
    INC     GUARD_RIGHT_COL_LIMIT
    BPL     .loop2

.end_loop2:
    INC     GUARD_RIGHT_COL_LIMIT

.end:
    RTS
Defines DETERMINE_GUARD_LEFT_RIGHT_LIMITS
⟨try guard move left [488]⟩=
    ORG     $6FBC
TRY_GUARD_MOVE_LEFT:
    SUBROUTINE

    LDY     GUARD_LOC_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDX     GUARD_X_ADJ
    CPX     #$03
    BCS     .move_left           ; horizontal adjustment > 0

    ; horizontal adjustment <= 0
    LDY     GUARD_LOC_COL
    BEQ     .store_guard_data       ; Can't go any more left

    DEY
    LDA     (PTR1),Y
    CMP     #SPRITE_GUARD
    BEQ     .store_guard_data
    CMP     #SPRITE_STONE
    BEQ     .store_guard_data
    CMP     #SPRITE_BRICK
    BEQ     .store_guard_data

    LDA     (PTR2),Y
    CMP     #SPRITE_TRAP
    BNE     .move_left

.store_guard_data:
    JMP     STORE_GUARD_DATA        ; tailcall

.move_left:
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    JSR     NUDGE_GUARD_TOWARDS_EXACT_ROW
    LDA     #$FF
    STA     GUARD_FACING_DIRECTION      ; face left
    DEC     GUARD_X_ADJ
    BPL     .check_for_gold_pickup

    ; horizontal adjustment underflow
    JSR     GUARD_DROP_GOLD
    LDY     GUARD_LOC_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BNE     .store_sprite
    LDA     #SPRITE_EMPTY

.store_sprite:
    STA     (PTR1),Y

    DEC     GUARD_LOC_COL
    DEY
    LDA     (PTR1),Y
    CMP     #SPRITE_PLAYER
    BNE     .place_guard_sprite

    ; kill player
    LSR     ALIVE

.place_guard_sprite:
    LDA     #SPRITE_GUARD
    STA     (PTR1),Y

    LDA     #$04
    STA     GUARD_X_ADJ     ; horizontal adjustment = +2
    BNE     .determine_anim_set           ; unconditional

.check_for_gold_pickup:
    JSR     CHECK_FOR_GOLD_PICKED_UP_BY_GUARD

.determine_anim_set:
    LDY     GUARD_LOC_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_ROPE
    BEQ     .rope

    LDA     #$00
    LDX     #$02
    BNE     .inc_anim_state

.rope:
    LDA     #$03
    LDX     #$05

.inc_anim_state:
    JSR     INC_GUARD_ANIM_STATE
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS
    JMP     STORE_GUARD_DATA                ; tailcall
⟨try guard move right [490]⟩=
    ORG     $7047
TRY_GUARD_MOVE_RIGHT:
    SUBROUTINE

    LDY     GUARD_LOC_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    LDX     GUARD_X_ADJ
    CPX     #$02
    BCC     .move_right           ; horizontal adjustment < 0

    ; horizontal adjustment >= 0
    LDY     GUARD_LOC_COL
    CPY     #MAX_GAME_COL
    BEQ     .store_guard_data       ; Can't go any more right

    INY
    LDA     (PTR1),Y
    CMP     #SPRITE_GUARD
    BEQ     .store_guard_data
    CMP     #SPRITE_STONE
    BEQ     .store_guard_data
    CMP     #SPRITE_BRICK
    BEQ     .store_guard_data

    LDA     (PTR2),Y
    CMP     #SPRITE_TRAP
    BNE     .move_right

.store_guard_data:
    JMP     STORE_GUARD_DATA        ; tailcall

.move_right:
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    JSR     NUDGE_GUARD_TOWARDS_EXACT_ROW
    LDA     #$01
    STA     GUARD_FACING_DIRECTION      ; face right
    INC     GUARD_X_ADJ
    LDA     GUARD_X_ADJ
    CMP     #$05
    BCC     .check_for_gold_pickup

    ; horizontal adjustment overflow
    JSR     GUARD_DROP_GOLD
    LDY     GUARD_LOC_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_BRICK
    BNE     .store_sprite
    LDA     #SPRITE_EMPTY

.store_sprite:
    STA     (PTR1),Y

    INC     GUARD_LOC_COL
    INY
    LDA     (PTR1),Y
    CMP     #SPRITE_PLAYER
    BNE     .place_guard_sprite

    ; kill player
    LSR     ALIVE

.place_guard_sprite:
    LDA     #SPRITE_GUARD
    STA     (PTR1),Y

    LDA     #$00
    STA     GUARD_X_ADJ     ; horizontal adjustment = -2
    BEQ     .determine_anim_set           ; unconditional

.check_for_gold_pickup:
    JSR     CHECK_FOR_GOLD_PICKED_UP_BY_GUARD

.determine_anim_set:
    LDY     GUARD_LOC_COL
    LDA     (PTR2),Y
    CMP     #SPRITE_ROPE
    BEQ     .rope

    LDA     #$07
    LDX     #$09
    BNE     .inc_anim_state

.rope:
    LDA     #$0A
    LDX     #$0C

.inc_anim_state:
    JSR     INC_GUARD_ANIM_STATE
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS
    JMP     STORE_GUARD_DATA                ; tailcall
⟨try guard move up [492]⟩=
    ORG     $6EA1
GUARD_DO_NOTHING:
    SUBROUTINE

    ; if GUARD_GOLD_TIMER > 0, GUARD_GOLD_TIMER++
    LDA     GUARD_GOLD_TIMER
    BEQ     .store_guard_data
    BMI     .store_guard_data
    INC     GUARD_GOLD_TIMER        
.store_guard_data:
    JMP     STORE_GUARD_DATA

    ORG     $6EAC
TRY_GUARD_MOVE_UP:
    SUBROUTINE

    LDY     GUARD_Y_ADJ
    CPY     #$03
    BCS     .move_up        ; vertical adjustment > 0

    LDY     GUARD_LOC_ROW
    BEQ     GUARD_DO_NOTHING
    DEY
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    LDA     (PTR1),Y

    CMP     #SPRITE_BRICK
    BEQ     GUARD_DO_NOTHING
    CMP     #SPRITE_STONE
    BEQ     GUARD_DO_NOTHING
    CMP     #SPRITE_TRAP
    BEQ     GUARD_DO_NOTHING
    CMP     #SPRITE_GUARD
    BEQ     GUARD_DO_NOTHING

.move_up:
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    JSR     NUDGE_GUARD_TOWARDS_EXACT_COLUMN
    LDY     GUARD_LOC_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    DEC     GUARD_Y_ADJ
    BPL     TRY_GUARD_MOVE_UP_check_for_gold

    ; vertical adjustment underflow
    JSR     GUARD_DROP_GOLD
    LDY     GUARD_LOC_COL
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BNE     .set_active_sprite
    LDA     #SPRITE_EMPTY
.set_active_sprite:
    STA     (PTR1),Y

    DEC     GUARD_LOC_ROW
    LDY     GUARD_LOC_ROW
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    LDA     (PTR1),Y

    CMP     #SPRITE_PLAYER
    BNE     .set_guard_sprite

    ; kill player
    LSR     ALIVE

.set_guard_sprite:
    LDA     #SPRITE_GUARD
    STA     (PTR1),Y

    LDA     #$04
    STA     GUARD_Y_ADJ         ; vertical adjust = +2
    BNE     TRY_GUARD_MOVE_UP_inc_anim_state     ; unconditional

TRY_GUARD_MOVE_UP_check_for_gold:
    JSR     CHECK_FOR_GOLD_PICKED_UP_BY_GUARD

TRY_GUARD_MOVE_UP_inc_anim_state:
    LDA     #$0E
    LDX     #$0F
    JSR     INC_GUARD_ANIM_STATE
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     DRAW_SPRITE_AT_PIXEL_COORDS
    JMP     STORE_GUARD_DATA
⟨try guard move down [494]⟩=
    ORG     $6F39
TRY_GUARD_MOVE_DOWN:
    SUBROUTINE

    LDY     GUARD_Y_ADJ
    CPY     #$02
    BCC     .move_down        ; vertical adjustment < 0

    LDY     GUARD_LOC_ROW
    CPY     #MAX_GAME_ROW
    BCS     .store_guard_data
    INY
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    LDA     (PTR1),Y

    CMP     #SPRITE_STONE
    BEQ     .store_guard_data
    CMP     #SPRITE_GUARD
    BEQ     .store_guard_data
    CMP     #SPRITE_BRICK
    BNE     .move_down

.store_guard_data:
    JMP     STORE_GUARD_DATA

.move_down:
    JSR     GET_GUARD_SPRITE_AND_COORDS
    JSR     ERASE_SPRITE_AT_PIXEL_COORDS
    JSR     NUDGE_GUARD_TOWARDS_EXACT_COLUMN
    LDY     GUARD_LOC_ROW
    <<set active and background row pointers [[PTR1]] and [[PTR2]] for [[Y]]>>
    INC     GUARD_Y_ADJ
    LDA     GUARD_Y_ADJ
    CMP     #$05
    BCC     .check_for_gold

    ; vertical adjustment overflow
    JSR     GUARD_DROP_GOLD
    LDY     GUARD_LOC_COL
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BNE     .set_active_sprite
    LDA     #SPRITE_EMPTY
.set_active_sprite:
    STA     (PTR1),Y

    INC     GUARD_LOC_ROW
    LDY     GUARD_LOC_ROW
    <<set active row pointer [[PTR1]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    LDA     (PTR1),Y

    CMP     #SPRITE_PLAYER
    BNE     .set_guard_sprite

    ; kill player
    LSR     ALIVE

.set_guard_sprite:
    LDA     #SPRITE_GUARD
    STA     (PTR1),Y

    LDA     #$00
    STA     GUARD_Y_ADJ         ; vertical adjust = -2
    JMP     TRY_GUARD_MOVE_UP_inc_anim_state

.check_for_gold:
    JMP     TRY_GUARD_MOVE_UP_check_for_gold

This routine is called whenever we move a guard and the horizontal or vertical adjustment under- or overflows. If and only if GUARD_GOLD_TIMER is zero, decrement GUARD_GOLD_TIMER, and if there is nothing at the guard location, then drop gold at the location.

⟨guard drop gold [496]⟩=
    ORG     $753E
GUARD_DROP_GOLD:
    SUBROUTINE

    LDA     GUARD_GOLD_TIMER
    BPL     .end
    INC     GUARD_GOLD_TIMER
    BNE     .end

    ; GUARD_GOLD_TIMER == 0
    LDY     GUARD_LOC_ROW
    STY     GAME_ROWNUM
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     GUARD_LOC_COL
    STY     GAME_COLNUM
    LDA     (PTR2),Y

    CMP     #SPRITE_EMPTY
    BNE     .decrement_flag_0

    ; Put gold at location
    LDA     #SPRITE_GOLD
    STA     (PTR2),Y
    JSR     DRAW_SPRITE_PAGE2
    LDY     GAME_ROWNUM
    LDX     GAME_COLNUM
    JSR     GET_SCREEN_COORDS_FOR
    LDA     #SPRITE_GOLD
    JMP     DRAW_SPRITE_AT_PIXEL_COORDS

.decrement_flag_0:
    DEC     GUARD_GOLD_TIMER

.end:
    RTS

The PSUEDO_DISTANCE returns a distance measure between the player and the given A, X coordinate based on whether the point is above, below, or on the same row as the player row.

If the point is on the same row as the player, then the return value is the horizontal distance between the current guard and the point. Otherwise, if the point is above the player row, return 200 plus the vertical distance between the point and the player. Otherwise, the point is below the player row, so return 100 plus the vertical distance between the point and the player.

⟨pseudo distance [498]⟩=
    ORG     $72D4
PSEUDO_DISTANCE:
    SUBROUTINE

    STA     TMP
    CMP     PLAYER_ROW
    BNE     .tmp_not_player_row

    ; TMP == PLAYER_ROW
    ; return | X - GUARD_LOC_COL |

    CPX     GUARD_LOC_COL
    BCC     .x_lt_guard_col

    ; X >= GUARD_LOC_COL
    TXA
    ; A = X - GUARD_LOC_COL
    SEC
    SBC     GUARD_LOC_COL
    RTS

.x_lt_guard_col:
    STX     TMP

    ; A = GUARD_LOC_COL - X
    LDA     GUARD_LOC_COL
    SEC
    SBC     TMP
    RTS

.tmp_not_player_row:
    ; If TMP >= PLAYER_ROW, return 200 + | TMP - PLAYER_ROW |
    ; otherwise return 100 + | TMP - PLAYER_ROW |

    BCC     .tmp_lt_player_row

    ; TMP >= PLAYER_ROW
    ; A = TMP - PLAYER_ROW + 200
    SEC
    SBC     PLAYER_ROW
    CLC
    ADC     #200
    RTS

.tmp_lt_player_row
    ; A = PLAYER_ROW - TMP + 100
    LDA     PLAYER_ROW
    SEC
    SBC     TMP
    CLC
    ADC     #100
    RTS
Defines PSUEDO_DISTANCE
⟨should guard move left [500]⟩=
    ORG     $71A1
    SUBROUTINE
.return:
    RTS

SHOULD_GUARD_MOVE_LEFT:
    LDY     GUARD_LEFT_COL_LIMIT
    CPY     TMP_GUARD_COL
    BEQ     .return

    LDY     TMP_GUARD_ROW
    CPY     #MAX_GAME_ROW
    BEQ     .check_here

    ; Check below:

    ; Get background sprite at TMP_GUARD_ROW + 1, col = GUARD_LEFT_COL_LIMIT
    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     GUARD_LEFT_COL_LIMIT
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BEQ     .check_here
    CMP     #SPRITE_STONE
    BEQ     .check_here

    LDX     GUARD_LEFT_COL_LIMIT
    LDY     TMP_GUARD_ROW
    JSR     GUARD_FIND_CANDIDATE_ROW_BELOW

    LDX     GUARD_LEFT_COL_LIMIT
    JSR     PSEUDO_DISTANCE
    CMP     BEST_GUARD_DIST
    BCS     .check_here        ; dist >= BEST_GUARD_DIST?

    ; dist < BEST_GUARD_DIST
    STA     BEST_GUARD_DIST     ; dist
    LDA     #GUARD_ACTION_MOVE_LEFT
    STA     GUARD_ACTION

.check_here:
    LDY     TMP_GUARD_ROW
    BEQ     .next

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

    CMP     #SPRITE_LADDER
    BNE     .next

    ; Ladder here
    LDY     TMP_GUARD_ROW
    LDX     GUARD_LEFT_COL_LIMIT
    JSR     GUARD_FIND_CANDIDATE_ROW_ABOVE

    LDX     GUARD_LEFT_COL_LIMIT
    JSR     PSEUDO_DISTANCE
    CMP     BEST_GUARD_DIST
    BCS     .next        ; dist >= BEST_GUARD_DIST?

    ; dist < BEST_GUARD_DIST
    STA     BEST_GUARD_DIST     ; dist
    LDA     #GUARD_ACTION_MOVE_LEFT
    STA     GUARD_ACTION

.next:
    INC     GUARD_LEFT_COL_LIMIT
    JMP     SHOULD_GUARD_MOVE_LEFT
⟨should guard move right [502]⟩=
    ORG     $720B
    SUBROUTINE
.return:
    RTS

SHOULD_GUARD_MOVE_RIGHT:
    LDY     GUARD_RIGHT_COL_LIMIT
    CPY     TMP_GUARD_COL
    BEQ     .return

    LDY     TMP_GUARD_ROW
    CPY     #MAX_GAME_ROW
    BEQ     .check_here

    ; Check below:

    ; Get background sprite at TMP_GUARD_ROW + 1, col = GUARD_RIGHT_COL_LIMIT
    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     GUARD_RIGHT_COL_LIMIT
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BEQ     .check_here
    CMP     #SPRITE_STONE
    BEQ     .check_here

    LDX     GUARD_RIGHT_COL_LIMIT
    LDY     TMP_GUARD_ROW
    JSR     GUARD_FIND_CANDIDATE_ROW_BELOW

    LDX     GUARD_RIGHT_COL_LIMIT
    JSR     PSEUDO_DISTANCE
    CMP     BEST_GUARD_DIST
    BCS     .check_here        ; dist >= BEST_GUARD_DIST?

    ; dist < BEST_GUARD_DIST
    STA     BEST_GUARD_DIST     ; dist
    LDA     #GUARD_ACTION_MOVE_RIGHT
    STA     GUARD_ACTION

.check_here:
    LDY     TMP_GUARD_ROW
    BEQ     .next

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

    CMP     #SPRITE_LADDER
    BNE     .next

    ; Ladder here
    LDY     TMP_GUARD_ROW
    LDX     GUARD_RIGHT_COL_LIMIT
    JSR     GUARD_FIND_CANDIDATE_ROW_ABOVE

    LDX     GUARD_RIGHT_COL_LIMIT
    JSR     PSEUDO_DISTANCE
    CMP     BEST_GUARD_DIST
    BCS     .next        ; dist >= BEST_GUARD_DIST?

    ; dist < BEST_GUARD_DIST
    STA     BEST_GUARD_DIST     ; dist
    LDA     #GUARD_ACTION_MOVE_RIGHT
    STA     GUARD_ACTION

.next:
    DEC     GUARD_RIGHT_COL_LIMIT
    JMP     SHOULD_GUARD_MOVE_RIGHT
⟨should guard move up or down [504]⟩=
    ORG    $7275
SHOULD_GUARD_MOVE_UP_OR_DOWN:
    SUBROUTINE

    LDY     TMP_GUARD_ROW
    CPY     #MAX_GAME_ROW
    BEQ     .should_guard_move_up

    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     TMP_GUARD_COL
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BEQ     .should_guard_move_up
    CMP     #SPRITE_STONE
    BEQ     .should_guard_move_up

    LDX     TMP_GUARD_COL
    LDY     TMP_GUARD_ROW
    JSR     GUARD_FIND_CANDIDATE_ROW_BELOW
    LDX     TMP_GUARD_COL
    JSR     PSEUDO_DISTANCE
    CMP     BEST_GUARD_DIST
    BCS     .should_guard_move_up
    STA     BEST_GUARD_DIST
    LDA     #GUARD_ACTION_MOVE_DOWN
    STA     GUARD_ACTION

.should_guard_move_up:
    LDY     TMP_GUARD_ROW
    BEQ     .end

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

    CMP     #SPRITE_LADDER
    BNE     .end
    ; ladder

    LDX     TMP_GUARD_COL
    LDY     TMP_GUARD_ROW
    JSR     GUARD_FIND_CANDIDATE_ROW_ABOVE
    LDX     TMP_GUARD_COL
    JSR     PSEUDO_DISTANCE
    CMP     BEST_GUARD_DIST
    BCS     .end
    STA     BEST_GUARD_DIST
    LDA     #GUARD_ACTION_MOVE_UP
    STA     GUARD_ACTION

.end:
    RTS
⟨defines [506]⟩+=
CHECK_CURR_TMP_ROW  EQU     $5C
CHECK_TMP_COL       EQU     $5D
CHECK_TMP_ROW       EQU     $5E
Used in ⟨*⟩
Defines CHECK_TMP_COL, CHECK_TMP_ROW

The GUARD_FIND_CANDIDATE_ROW_BELOW is called when determining if the guard should move down. It scans below the guard for a candidate row. Upon entry, we store X and Y in CHECK_TMP_COL and CHECK_TMP_ROW. Next, we scan from CHECK_TMP_ROW to the bottommost game row.

For each row:

And if we haven't returned in the loop, just return the MAX_GAME_ROW.

⟨guard find candidate row below [508]⟩=
    ORG     $739A
    SUBROUTINE
.return_tmp_row:
    LDA     CHECK_TMP_ROW
    RTS

GUARD_FIND_CANDIDATE_ROW_BELOW:
    STY     CHECK_TMP_ROW
    STX     CHECK_TMP_COL

    ; for CHECK_TMP_ROW = Y; CHECK_TMP_ROW <= MAX_GAME_ROW; CHECK_TMP_ROW++

.loop:
    ; if background sprite below tmp coords is brick or stone, return tmp row.

    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     CHECK_TMP_COL
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BEQ     .return_tmp_row
    CMP     #SPRITE_STONE
    BEQ     .return_tmp_row

    ; Not brick or stone below
    ; if background sprite at tmp coords is empty, then next tmp row.

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

    CMP     #SPRITE_EMPTY
    BEQ     .next

    CPY     #$00
    BEQ     .check_right      ; cannot check to left

    ; if background sprite to left of tmp coords is rope,
    ; then tmp_row -> curr_tmp_row
    DEY
    LDA     (PTR2),Y        ; Check to left

    CMP     #SPRITE_ROPE
    BEQ     .store_as_curr_tmp_row

    ; if background sprite to left and below tmp coords is brick, stone, or ladder,
    ; then tmp_row -> curr_tmp_row
    LDY     CHECK_TMP_ROW
    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     CHECK_TMP_COL
    DEY
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BEQ     .store_as_curr_tmp_row
    CMP     #SPRITE_STONE
    BEQ     .store_as_curr_tmp_row
    CMP     #SPRITE_LADDER
    BNE     .check_right

    ; Otherwise check right

.store_as_curr_tmp_row:
    ; Store tmp row as curr tmp row, and if at or below player, return curr tmp row.
    LDY     CHECK_TMP_ROW
    STY     CHECK_CURR_TMP_ROW
    CPY     PLAYER_ROW
    BCS     .return_curr_tmp_row
    ; CHECK_TMP_ROW < PLAYER_ROW

.check_right:
    LDY     CHECK_TMP_COL
    CPY     #MAX_GAME_COL
    BCS     .next           ; can't check right

    ; if background sprite to right is rope,
    ; then tmp_row -> curr_tmp_row
    INY
    LDA     (PTR2),Y

    CMP     #SPRITE_ROPE
    BEQ     .store_as_curr_tmp_row_2

    ; if background sprite to right and below tmp coords is brick, stone, or ladder,
    ; then tmp_row -> curr_tmp_row
    LDY     CHECK_TMP_ROW
    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     CHECK_TMP_COL
    INY
    LDA     (PTR2),Y

    CMP     #SPRITE_BRICK
    BEQ     .store_as_curr_tmp_row_2
    CMP     #SPRITE_LADDER
    BEQ     .store_as_curr_tmp_row_2
    CMP     #SPRITE_STONE
    BNE     .next
    ; Brick, ladder, or stone.

.store_as_curr_tmp_row_2:
    LDY     CHECK_TMP_ROW
    STY     CHECK_CURR_TMP_ROW
    CPY     PLAYER_ROW
    BCS     .return_curr_tmp_row
    ; CHECK_TMP_ROW < PLAYER_ROW

.next:
    INC     CHECK_TMP_ROW
    LDY     CHECK_TMP_ROW
    CPY     #MAX_GAME_ROW+1
    BCS     .return_max_game_row
    JMP     .loop

.return_max_game_row:
    LDA     #MAX_GAME_ROW
    RTS

.return_curr_tmp_row:
    LDA     CHECK_CURR_TMP_ROW
    RTS
Defines GUARD_FIND_CANDIDATE_ROW_BELOW
⟨guard find candidate row above [510]⟩=
    ORG     $72FD
    SUBROUTINE
.not_ladder:
    LDA     CHECK_TMP_ROW         ; row
    RTS

GUARD_FIND_CANDIDATE_ROW_ABOVE:
    STY     CHECK_TMP_ROW         ; row
    STX     CHECK_TMP_COL         ; col

.loop:
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     CHECK_TMP_COL         ; col
    LDA     (PTR2),Y    ; sprite on background

    CMP     #SPRITE_LADDER
    BNE     .not_ladder     ; no ladder at row, col -> just return row.

    ; There is a ladder at row, col
    DEC     CHECK_TMP_ROW     ; row--     ; up one
    LDY     CHECK_TMP_COL     ; col
    BEQ     .at_leftmost

    DEY             ; to left (col-1)
    LDA     (PTR2),Y

    ; To left of ladder is brick, stone, or ladder: .blocked_on_left
    CMP     #SPRITE_BRICK
    BEQ     .blocked_on_left
    CMP     #SPRITE_STONE
    BEQ     .blocked_on_left
    CMP     #SPRITE_LADDER
    BEQ     .blocked_on_left

    LDY     CHECK_TMP_ROW     ; row (that is now up one)
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     CHECK_TMP_COL     ; col
    DEY
    LDA     (PTR2),Y    ; sprite on background

    CMP     #SPRITE_ROPE
    BNE     .at_leftmost

    ; There is a rope above the ladder

.blocked_on_left:
    ; If row <= PLAYER_ROW (on or above player row), return row
    LDY     CHECK_TMP_ROW     ; row
    STY     SCRATCH_5C
    CPY     PLAYER_ROW
    BCC     .return_scratch_5C
    BEQ     .return_scratch_5C

.at_leftmost:
    LDY     CHECK_TMP_COL     ; col
    CPY     #MAX_GAME_COL
    BEQ     .at_rightmost

    ; Look at background sprite below and to the right
    LDY     CHECK_TMP_ROW     ; row
    <<set background row pointer [[PTR2]] for [[Y+1]]>>
    LDY     CHECK_TMP_COL
    INY
    LDA     (PTR2),Y        ; get background sprite at row+1, col+1

    ; Below and to the right of ladder is brick, stone, or ladder: .blocked_below
    CMP     #SPRITE_BRICK
    BEQ     .blocked_below
    CMP     #SPRITE_STONE
    BEQ     .blocked_below
    CMP     #SPRITE_LADDER
    BEQ     .blocked_below

    ; Look at background sprite to the right
    LDY     CHECK_TMP_ROW     ; row
    <<set background row pointer [[PTR2]] for [[Y]]>>
    LDY     CHECK_TMP_COL     ; col
    INY
    LDA     (PTR2),Y    ; get background sprite at row, col+1

    CMP     #SPRITE_ROPE
    BNE     .at_rightmost

    ; There is a rope to the right of the ladder

.blocked_below:
    ; If row <= PLAYER_ROW (on or above player row), return row
    LDY     CHECK_TMP_ROW     ; row
    STY     SCRATCH_5C
    CPY     PLAYER_ROW
    BCC     .return_scratch_5C
    BEQ     .return_scratch_5C

.at_rightmost:
    ; If row < 1, return row, otherwise loop.
    LDY     CHECK_TMP_ROW     ; row
    CPY     #$01
    BCC     .return_Y
    JMP     .loop

.return_Y:
    TYA
    RTS

.return_scratch_5C:
    LDA     SCRATCH_5C
    RTS
Defines GUARD_FIND_CANDIDATE_ROW_ABOVE