Lode Runner: Apple II reverse engineering

10. Disk routines#

Lode Runner contains a copy of DOS 3.3's RWTS in order for it to access the disk. Because it does not access files, but rather just sectors of data, Lode Runner does not need the file manager part of DOS. So, the area B600-BFFF is taken by DOS routines.

The standard DOS I/O Control Block (IOB) and Device Characteristics Table (DCT) are used. Further details can be read in Beneath Apple DOS.

There is one patch that seems to have been applied. DOS has a routine called FORMDSK located at BE0D which is supposed to jump to DSKFORM at BE46, which formats a disk. Lode Runner's copy of DOS changes the jump target to 8E00, Lode Runner's FORMAT_PATCH routine.

The reason for this is to prevent formatting the Load Runner master disk via the level editor.

The routine moves to track 0, and keeps reading bytes off the disk until either it finds a sequence of D4 D5 D6, or times out. If it finds the sequence, we've detected that this disk we're trying to format is the Lode Runner master disk, and we should error out and go back to the level editor.

Otherwise we're OK to proceed with disk formatting.

⟨defines [512]⟩+=
SEEKABS     EQU     $B9A0
DSKFORM     EQU     $BEAF
⟨format patch [514]⟩=
    ORG     $8E00
FORMAT_PATCH:
    LDA     #$44
    STA     $0478       ; Used by DOS as current track.
    LDA     #$00
    JSR     SEEKABS     ; DOS routine to move disk to track in A.
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    LDA     #$20
    STA     $4F

.await_D4_D5_D6:
    DEY
    BNE     .read_byte1
    DEC     $4F
    BNE     .read_byte1
    JMP     DSKFORM     ; DOS routine to format disk

    HEX     EA EA

.read_byte1:
    LDA     $C08C,X
    BPL     .read_byte1

.check_for_D4:
    CMP     #$D4
    BNE     .await_D4_D5_D6
    NOP

.read_byte2:
    LDA     $C08C,X
    BPL     .read_byte2

.check_for_D5:
    CMP     #$D5
    BNE     .check_for_D4
    NOP

.read_byte3:
    LDA     $C08C,X
    BPL     .read_byte3

    CMP     #$D6
    BNE     .check_for_D5

    LDA     $C088,X         ; Turn motor off
    JSR     DONT_MANIPULATE_MASTER_DISK
    JMP     START_LEVEL_EDITOR
⟨defines [516]⟩+=
Used in ⟨*⟩
Defines DCT_DEVICE_TYPE, DCT_MOTOR_ON_TIME_COUNT, DCT_PHASES_PER_TRACK, DOS_IOB, IOB_BYTE_COUNT_FOR_PARTIAL_SECTOR, IOB_COMMAND_CODE, IOB_DEVICE_CHARACTERISTICS_TABLE_PTR, IOB_DRIVE_NUM, IOB_LAST_ACCESS_DRIVE, IOB_LAST_ACCESS_SLOTx16, IOB_LAST_ACCESS_VOLUME, IOB_READ_WRITE_BUFFER_PTR, IOB_RETURN_CODE, IOB_SECTOR_NUMBER, IOB_SLOTNUMx16, IOB_TRACK_NUMBER, IOB_UNUSED, IOB_VOLUME_NUMBER_EXPECTED

ACCESS_HI_SCORE_DATA_FROM_DISK reads or writes—depending on A, where 1 is read and 2 is write—the high score table from disk at track 12 sector 15 into HI_SCORE_TABLE. We then compare the 11 bytes of HI_SCORE_DATA_MARKER to where they are supposed to be in the table.

If the marker doesn't match, then we return 0, indicating that the disk doesn't have a high score table.

If the marker does match, but the very last byte in the table is nonzero, then we return 1, indicating that this is a master disk (so its level data shouldn't be touched), otherwise we return -1, this being a data disk.

⟨tables [518]⟩+=
    ORG     $63A8
HI_SCORE_DATA_MARKER:
    ; Spells out "LODE RUNNER".
    HEX     CC CF C4 C5 A0 D2 D5 CE CE C5 D2
Used in ⟨*⟩
Defines HI_SCORE_DATA_MARKER
⟨access hi score data [520]⟩=
    ORG     $6359
ACCESS_HI_SCORE_DATA_FROM_DISK:
    SUBROUTINE

    STA     IOB_COMMAND_CODE
    LDA     #$0C
    STA     IOB_TRACK_NUMBER
    LDA     #$0F
    STA     IOB_SECTOR_NUMBER
    LDA     #<HI_SCORE_DATA
    STA     IOB_READ_WRITE_BUFFER_PTR
    LDA     #>HI_SCORE_DATA
    STA     IOB_READ_WRITE_BUFFER_PTR+1
    LDA     #$00
    STA     IOB_VOLUME_NUMBER_EXPECTED
    LDY     #<DOS_IOB
    LDA     #>DOS_IOB
    JSR     INDIRECT_RWTS
    BCC     .no_error
    JMP     RESET_GAME

.no_error:
    LDY     #$0A
    LDA     #$00
    STA     MASK0       ; temp storage

.loop:
    LDA     HI_SCORE_DATA+244,Y
    EOR     HI_SCORE_DATA_MARKER,Y
    ORA     MASK0
    STA     MASK0
    DEY
    BPL     .loop

    LDA     MASK0
    BEQ     .all_zero_data

    LDA     #$00
    RTS

.all_zero_data:
    LDA     #$01
    LDX     $1FFF
    BNE     .end
    LDA     #$FF

.end:
    RTS

RECORD_HI_SCORE_DATA_TO_DISK records the player's score to disk if the player's score belongs on the high score list. It also handles getting the player's initials.

⟨defines [522]⟩+=
HIGH_SCORE_INITIALS_INDEX       EQU     $824D
HI_SCORE_TARGET_INDEX           EQU     $56     ; aliased with TMP_GUARD_ROW
Used in ⟨*⟩
Defines HIGH_SCORE_INITIALS_INDEX
⟨record hi score data [524]⟩=
    ORG     $84C8
RECORD_HI_SCORE_DATA_TO_DISK:
    SUBROUTINE

    LDA     $9D
    BEQ     .end

    LDA     SCORE
    ORA     SCORE+1
    ORA     SCORE+2
    ORA     SCORE+3
    BEQ     .end

    LDA     #$01
    JSR     ACCESS_HI_SCORE_DATA_FROM_DISK      ; read table
    ; Return value of 0 means the hi score marker wasn't present,
    ; so don't write the hi score table.
    BEQ     .end

    LDY     #$01
.loop:
    LDX     HI_SCORE_TABLE_OFFSETS,Y
    LDA     LEVELNUM
    CMP     HI_SCORE_DATA+3,X       ; level
    BCC     .next
    BNE     .record_it

    LDA     SCORE+3
    CMP     HI_SCORE_DATA+4,X
    BCC     .next
    BNE     .record_it

    LDA     SCORE+2
    CMP     HI_SCORE_DATA+5,X
    BCC     .next
    BNE     .record_it

    LDA     SCORE+1
    CMP     HI_SCORE_DATA+6,X
    BCC     .next
    BNE     .record_it

    LDA     SCORE
    CMP     HI_SCORE_DATA+7,X
    BCC     .next
    BNE     .record_it

.next:
    INY
    CPY     #$0B
    BCC     .loop

.end:
    RTS

.record_it:
    CPY     #$0A
    BEQ     .write_here
    STY     HI_SCORE_TARGET_INDEX

    ; Move the table rows to make room at index HI_SCORE_TARGET_INDEX
    LDY     #$09
.loop2:
    LDX     HI_SCORE_TABLE_OFFSETS,Y

    ; Move 8 bytes of hi score data
    LDA     #$08
    STA     ROW_COUNT       ; temporary counter
.loop3:
    LDA     HI_SCORE_DATA,X
    STA     HI_SCORE_DATA+8,X
    INX
    DEC     ROW_COUNT
    BNE     .loop3

    CPY     HI_SCORE_TARGET_INDEX
    BEQ     .write_here
    DEY
    BNE     .loop2

.write_here:
    LDX     HI_SCORE_TABLE_OFFSETS,Y
    LDA     #$A0
    STA     HI_SCORE_DATA,X
    STA     HI_SCORE_DATA+1,X
    STA     HI_SCORE_DATA+2,X
    LDA     LEVELNUM
    STA     HI_SCORE_DATA+3,X
    LDA     SCORE+3
    STA     HI_SCORE_DATA+4,X
    LDA     SCORE+2
    STA     HI_SCORE_DATA+5,X
    LDA     SCORE+1
    STA     HI_SCORE_DATA+6,X
    LDA     SCORE
    STA     HI_SCORE_DATA+7,X
    STY     WIPE0               ; temporary
    LDA     HI_SCORE_TABLE_OFFSETS,Y
    STA     .rd_loc+1
    STA     .wr_loc+1
    JSR     HI_SCORE_SCREEN

    LDA     #$40
    STA     DRAW_PAGE
    LDA     WIPE0
    CLC
    ADC     #$04
    STA     GAME_ROWNUM
    LDA     #$07
    STA     GAME_COLNUM

    LDX     #$00
    STX     HIGH_SCORE_INITIALS_INDEX
.get_initial_from_player:
    LDX     HIGH_SCORE_INITIALS_INDEX
.rd_loc:
    LDA     HI_SCORE_DATA,X     ; fixed up to add offset from above
    JSR     CHAR_TO_SPRITE_NUM
    JSR     WAIT_FOR_KEY
    STA     KBDSTRB
    CMP     #$8D
    BEQ     .return_pressed
    CMP     #$88                ; backspace/back arrow
    BNE     .other_key_pressed

    ; backspace pressed
    LDX     KBD_ENTRY_INDEX
    BEQ     .beep       ; can't backspace/back arrow past the beginning

    DEC     HIGH_SCORE_INITIALS_INDEX
    DEC     GAME_COLNUM
    JMP     .get_initial_from_player

.other_key_pressed:
    CMP     #$95            ; fwd arrow
    BNE     .check_for_allowed_chars
    LDX     KBD_ENTRY_INDEX
    CPX     #$02
    BEQ     .beep       ; can't fwd arrow past the end

    INC     GAME_COLNUM
    INC     KBD_ENTRY_INDEX
    JMP     .get_initial_from_player

.check_for_allowed_chars
    CMP     #$AE        ; period allowed
    BEQ     .put_char
    CMP     #$A0        ; space allowed
    BEQ     .put_char
    CMP     #$C1
    BCC     .beep       ; can't be less than 'A'
    CMP     #$DB
    BCS     .beep       ; can't be greater than 'Z'

.put_char
    LDY     KBD_ENTRY_INDEX
.wr_loc:
    STA     HI_SCORE_DATA,Y     ; fixed up to add offset from above
    JSR     PUT_CHAR
    INC     KBD_ENTRY_INDEX
    LDA     KBD_ENTRY_INDEX
    CMP     #$03
    BCC     .get_initial_from_player

    DEC     KBD_ENTRY_INDEX
    DEC     GAME_COLNUM
    JMP     .get_initial_from_player

.beep:
    JSR     BEEP
    JMP     .get_initial_from_player

.return_pressed:
    LDA     #$20
    STA     DRAW_PAGE
    LDA     #$02
    JSR     ACCESS_HI_SCORE_DATA_FROM_DISK      ; write hi score table
    JMP     LONG_DELAY

    ORG     $824D
KBD_ENTRY_INDEX:
    HEX     60
⟨bad data disk [526]⟩=
    ORG     $8106
BAD_DATA_DISK:
    SUBROUTINE

    JSR     CLEAR_HGR2
    LDA     #$40
    STA     DRAW_PAGE
    LDA     #$00
    STA     GAME_COLNUM
    STA     GAME_ROWNUM

    ; "DISKETTE IN DRIVE IS NOT A\r"
    ; "LODE RUNNER DATA DISK."
    JSR     PUT_STRING
    HEX     C4 C9 D3 CB C5 D4 D4 C5 A0 C9 CE A0 C4 D2 C9 D6
    HEX     C5 A0 C9 D3 A0 CE CF D4 A0 C1 8D CC CF C4 C5 A0
    HEX     D2 D5 CE CE C5 D2 A0 C4 C1 D4 C1 A0 C4 C9 D3 CB
    HEX     AE 00

    JMP     HIT_KEY_TO_CONTINUE
⟨dont manipulate master disk [528]⟩=
    ORG     $8098
DONT_MANIPULATE_MASTER_DISK:
    SUBROUTINE

    JSR     CLEAR_HGR2
    LDA     #$40
    STA     DRAW_PAGE
    LDA     #$00
    STA     GAME_COLNUM
    STA     GAME_ROWNUM

    ; "USER NOT ALLOWED TO\r"
    ; "MANIPULATE MASTER DISKETTE."
    JSR     PUT_STRING
    HEX     D5 D3 C5 D2 A0 CE CF D4 A0 C1 CC CC CF D7 C5 C4
    HEX     A0 D4 CF 8D CD C1 CE C9 D0 D5 CC C1 D4 C5 A0 CD
    HEX     C1 D3 D4 C5 D2 A0 C4 C9 D3 CB C5 D4 D4 C5 AE 00

    ; fallthrough to HIT_KEY_TO_CONTINUE
Defines DONT_MANIPULATE_MASTER_DISK

The level editor has a routine to check for a valid data disk, meaning it has a high score table and is not the master disk. In case of a disk that is not a valid data disk, we abort the current editor operation, dumping the user right into the level editor by jumping to START_LEVEL_EDITOR. Otherwise we jump to RETURN_FROM_SUBROUTINE, which apparently saved a byte over having a local RTS instruction.

⟨check for valid data disk [530]⟩=
    ORG     $807F
CHECK_FOR_VALID_DATA_DISK:
    SUBROUTINE

    LDA     #$01
    JSR     ACCESS_HI_SCORE_DATA_FROM_DISK      ; read table
    CMP     #$00        ; bad table
    BNE     .check_for_master_disk

    JSR     BAD_DATA_DISK
    JMP     START_LEVEL_EDITOR

.check_for_master_disk:
    CMP     #$01        ; master disk
    BNE     RETURN_FROM_SUBROUTINE

    JSR     DONT_MANIPULATE_MASTER_DISK
    JMP     START_LEVEL_EDITOR

Initializing a disk first DOS formats it. This zeros out all data on all tracks and sectors. Once that's done, we write track 0 sector 0 with the data from DISK_BOOT_SECTOR_DATA. Then we read the Volume Table of Contents (VTOC) at track 17 sector 0, which will contain all zeros because of the initial format. We then stick SAVED_VTOC_DATA in the disk buffer and write it to the VTOC. We do the same thing with the catalog sector at track 17 sector 15 and SAVED_FILE_DESCRIPTIVE_ENTRY_DATA.

The final step is to create a blank sector at track 12 sector 15, with the special "LODE RUNNER" marker HI_SCORE_DATA_MARKER near the end.

⟨defines [532]⟩+=
    ORG     $1DB2
DISK_BOOT_SECTOR_DATA:
    HEX     01 20 58 FC 20 93 FE 20 89 FE A0 00 B9 34 08 F0
    HEX     0E 20 F0 FD C9 8D D0 04 A9 09 85 24 C8 D0 ED A6
    HEX     2B 9D 88 C0 8A 4A 4A 4A 4A 09 C0 8D 33 08 20 0C
    HEX     FD 4C 00 C6 8D 8D 8D 8D 8D 8D 8D CC CF C4 C5 A0
    HEX     D2 D5 CE CE C5 D2 A0 C4 C1 D4 C1 A0 C4 C9 D3 CB
    HEX     BA 8D AD AD AD AD AD AD AD AD AD AD AD AD AD AD
    HEX     AD AD AD AD AD AD AD AD 8D 8D C4 C9 D3 CB C5 D4
    HEX     D4 C5 A0 D7 C9 CC CC A0 CE CF D4 A0 C2 CF CF D4
    HEX     8D 8D A0 C9 CE D3 C5 D2 D4 A0 CE C5 D7 A0 C4 C9
    HEX     D3 CB A0 C1 CE C4 8D A0 C8 C9 D4 A0 C1 A0 CB C5
    HEX     D9 A0 D4 CF A0 D2 C5 C2 CF CF D4 8D 8D A0 A0 A0
    HEX     A0 A0 A0 A0 A0 A0 A0 00 00 00 00 00 00 00 00 00
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Used in ⟨*⟩
Defines DISK_BOOT_SECTOR_DATA
⟨tables [534]⟩+=
    ORG     $8250
SAVED_VTOC_DATA:
    HEX     60 02 11 0F 04 00 00 FE 00 00 00 00 00 00 00 00
    HEX     00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    HEX     00 00 00 00 00 00 00 00 7A 00 00 00 00 00 00 00
    HEX     00 FF FF 00 00 23 0F 00 01

    ORG     $8289
SAVED_FILE_DESCRIPTIVE_ENTRY_DATA:
    HEX     22 ; Track of first track/sector list sector (T34)
    HEX     0F ; Sector of first track/sector list sector (S15)
    HEX     88 ; File type and flags: locked, S-type file
    ; File name: "^H^H^H^H^H^H^HLODE RUNNER DATA DISK  "
    HEX     88 88 88 88 88 88 88 CC CF C4 C5 A0 D2 D5 CE CE
    HEX     C5 D2 A0 C4 C1 D4 C1 A0 C4 C9 D3 CB A0 A0
Used in ⟨*⟩
Defines SAVED_FILE_DESCRIPTIVE_ENTRY_DATA, SAVED_VTOC_DATA
⟨editor initialize disk [536]⟩=
    ORG     $7D5D
EDITOR_INITIALIZE_DISK:
    SUBROUTINE

    ; "\r"
    ; ">>INITIALIZE\r"
    ; "  THIS FORMATS THE DISKETTE\r"
    ; "  FOR USER CREATED LEVELS.\r"
    ; "  (CAUTION. IT ERASES THE\r"
    ; "   ENTIRE DISKETTE FIRST)\r"
    ; "\r"
    ; "  ARE YOU SURE (Y/N) "
    JSR     PUT_STRING
    HEX     8D BE BE C9 CE C9 D4 C9 C1 CC C9 DA C5 8D A0 A0
    HEX     D4 C8 C9 D3 A0 C6 CF D2 CD C1 D4 D3 A0 D4 C8 C5
    HEX     A0 C4 C9 D3 CB C5 D4 D4 C5 8D A0 A0 C6 CF D2 A0
    HEX     D5 D3 C5 D2 A0 C3 D2 C5 C1 D4 C5 C4 A0 CC C5 D6
    HEX     C5 CC D3 AE 8D A0 A0 A8 C3 C1 D5 D4 C9 CF CE AE
    HEX     A0 C9 D4 A0 C5 D2 C1 D3 C5 D3 A0 D4 C8 C5 8D A0
    HEX     A0 A0 C5 CE D4 C9 D2 C5 A0 C4 C9 D3 CB C5 D4 D4
    HEX     C5 A0 C6 C9 D2 D3 D4 A9 8D 8D A0 A0 C1 D2 C5 A0
    HEX     D9 CF D5 A0 D3 D5 D2 C5 A0 A8 D9 AF CE A9 A0 00

    JSR     EDITOR_WAIT_FOR_KEY
    CMP     #$D9        ; Y
    BNE     .end

    NOP     ; NOP x 15
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP
    NOP

    LDA     DISK_LEVEL_LOC
    PHA

    ; Format the disk
    LDA     #DISK_ACCESS_FORMAT
    JSR     ACCESS_COMPRESSED_LEVEL_DATA

    ; Write the boot sector (T0S0)
    LDA     #<DISK_BOOT_SECTOR_DATA
    STA     IOB_READ_WRITE_BUFFER_PTR
    LDA     #>DISK_BOOT_SECTOR_DATA
    STA     IOB_READ_WRITE_BUFFER_PTR+1
    LDA     #$00
    STA     IOB_SECTOR_NUMBER
    STA     IOB_TRACK_NUMBER
    LDA     #$02
    STA     IOB_COMMAND_CODE            
    JSR     ACCESS_DISK_OR_RESET_GAME   ; write T0S0 with DISK_BOOT_SECTOR_DATA.

    ; Read the VTOC (T17S0)
    LDA     #$E0
    STA     DISK_LEVEL_LOC              ; ends up being T17S0 (the VTOC)
    LDA     #DISK_ACCESS_READ
    JSR     ACCESS_COMPRESSED_LEVEL_DATA

    ; Copy from SAVED_VTOC_DATA to DISK_BUFFER and write it.
    LDY     #$37
.loop:
    LDA     SAVED_VTOC_DATA+1,Y
    STA     DISK_BUFFER,Y
    DEY
    BPL     .loop

    LDA     #$02
    JSR     ACCESS_COMPRESSED_LEVEL_DATA

    ; Read the first catalog sector (T17S15)
    LDA     #$EF
    STA     DISK_LEVEL_LOC
    LDA     #DISK_ACCESS_READ
    JSR     ACCESS_COMPRESSED_LEVEL_DATA

    ; Copy from SAVED_FILE_DESCRIPTIVE_ENTRY_DATA the first file descriptive
    ; entry to DISK_BUFFER and write it.
    LDY     #$20
.loop2:
    LDA     SAVED_FILE_DESCRIPTIVE_ENTRY_DATA,Y
    STA     DISK_BUFFER+11,Y
    DEY
    BPL     .loop2

    ; Write it back
    LDA     #DISK_ACCESS_WRITE
    JSR     ACCESS_COMPRESSED_LEVEL_DATA

    ; Read the high score sector
    LDA     #$01
    JSR     ACCESS_HI_SCORE_DATA_FROM_DISK

    ; Copy from HI_SCORE_DATA_MARKER and write it.
    LDY     #$0A
.loop3:
    LDA     HI_SCORE_DATA_MARKER,Y
    STA     $1FF4,Y
    DEY
    BPL     .loop3

    ; Write it back
    LDA     #$02
    JSR     ACCESS_HI_SCORE_DATA_FROM_DISK

    PLA
    STA     DISK_LEVEL_LOC
.end:
    JMP     EDITOR_COMMAND_LOOP

To clear the high score table from a disk, we first read the sector where the high score table is supposed to be, and check to see if the buffer is a good high score table. If so, we zero out the first 80 bytes (the 10 high score entries) and write that back to disk.

If the disk didn't contain a good high score table, we display the BAD_DATA_DISK message and abort.

⟨editor clear high scores [538]⟩=
    ORG     $7E75
EDITOR_CLEAR_HIGH_SCORES:
    SUBROUTINE

    ; "\r"
    ; ">>CLEAR SCORE FILE\r"
    ; "  THIS CLEARS THE HIGH\r"
    ; "  SCORE FILE OF ALL\r"
    ; "  ENTRIES.\r"
    ; "\r"
    ; "  ARE YOU SURE (Y/N) "
    JSR     PUT_STRING
    HEX     8D BE BE C3 CC C5 C1 D2 A0 D3 C3 CF D2 C5 A0 C6
    HEX     C9 CC C5 8D A0 A0 D4 C8 C9 D3 A0 C3 CC C5 C1 D2
    HEX     D3 A0 D4 C8 C5 A0 C8 C9 C7 C8 8D A0 A0 D3 C3 CF
    HEX     D2 C5 A0 C6 C9 CC C5 A0 CF C6 A0 C1 CC CC 8D A0
    HEX     A0 C5 CE D4 D2 C9 C5 D3 AE 8D 8D A0 A0 C1 D2 C5
    HEX     A0 D9 CF D5 A0 D3 D5 D2 C5 A0 A8 D9 AF CE A9 A0
    HEX     00

    JSR     EDITOR_WAIT_FOR_KEY
    CMP     #$D9        ; 'Y'
    BNE     .end

    LDA     #$01
    JSR     ACCESS_HI_SCORE_DATA_FROM_DISK      ; read table
    CMP     #$00
    BNE     .good_disk
    JSR     BAD_DATA_DISK
    JMP     START_LEVEL_EDITOR

.good_disk:
    LDY     #$4F
    LDA     #$00

.loop:
    STA     HI_SCORE_DATA,Y
    DEY
    BPL     .loop

    LDA     #$02
    JSR     ACCESS_HI_SCORE_DATA_FROM_DISK      ; write table

.end:
    JMP     EDITOR_COMMAND_LOOP

10.1 Initialization#

⟨rwts targets [540]⟩=
INDIRECT_TARGET             EQU     $36     ; Init with DEFAULT_INDIRECT_TARGET
DISABLE_INTS_CALL_RWTS_PTR  EQU     $38     ; Init with DISABLE_INTS_CALL_RWTS
DISABLE_INTS_CALL_RWTS      EQU     $B7B5
Defines DISABLE_INTS_CALL_RWTS, DISABLE_INTS_CALL_RWTS_PTR, INDIRECT_TARGET
⟨indirect call [542]⟩=
    ORG     $63A5
INDIRECT_RWTS:
    SUBROUTINE
    JMP     (INDIRECT_TARGET)

    ORG     $8E50
DEFAULT_INDIRECT_TARGET:
    SUBROUTINE
    JMP     DISABLE_INTS_CALL_RWTS
Defines INDIRECT_RWTS
⟨defines [544]⟩+=
Used in ⟨*⟩
Defines GUARD_PATTERN_OFFSET
⟨Initialize game data [546]⟩=
    ORG     $6056

INIT_GAME_DATA:
    LDA     #0
    STA     SCORE
    STA     SCORE+1
    STA     SCORE+2
    STA     SCORE+3
    STA     GUARD_PATTERN_OFFSET
    STA     WIPE_MODE       ; WIPE_MODE = SCORE = $97 = 0
    STA     $53
    STA     $AB
    STA     $A8             ; $53 = $AB = $A8 = 0
    LDA     #$9B            ; 155
    STA     $A9             ; $A9 = 155
    LDA     #5
    STA     LIVES           ; LIVES = 5
    LDA     GAME_MODE
    LSR
    ; if GAME_MODE was 0 or 1 (i.e. not displaying high score screen or splash screen),
    ; play the game.
    BEQ     .put_status_and_start_game   

    ; We were displaying the high score screen or splash screen
    LDA     #1
    JSR     ACCESS_HI_SCORE_DATA_FROM_DISK      ; Read hi score data
    CMP     #$00
    BNE     .set_rwts_target
    JSR     BAD_DATA_DISK
    JMP     RESET_GAME

.set_rwts_target:
    LDA     $1FFF
    BNE     .use_dos_target
    LDA     INDIRECT_TARGET
    LDX     INDIRECT_TARGET+1
    BNE     .store_rwts_addr

.use_dos_target:
    LDA     DISABLE_INTS_CALL_RWTS_PTR
    LDX     DISABLE_INTS_CALL_RWTS_PTR+1

.store_rwts_addr:
    STA     RWTS_ADDR
    STX     RWTS_ADDR+1

.put_status_and_start_game:
    JSR     PUT_STATUS
    STA     TXTPAGE1
⟨tables [548]⟩+=
    ORG     $6CA7
GUARD_PATTERNS_LIST:
    HEX     00 00 00
    HEX     00 01 01
    HEX     01 01 01
    HEX     01 03 01
    HEX     01 03 03
    HEX     03 03 03
    HEX     03 03 07
    HEX     03 07 07
    HEX     07 07 07
    HEX     07 07 0F
    HEX     07 0F 0F
    HEX     0F 0F 0F
Used in ⟨*⟩
Defines GUARD_PATTERNS_LIST

The game loop, which runs in both attract mode and in play mode, effectively implements the following flowchart:

Flowchart of the game loop

⟨game loop [550]⟩=
    ORG     $609F

.start_game:
    LDX     #$01
    JSR     LOAD_LEVEL
    LDA     #$00
    STA     KEY_COMMAND
    STA     KEY_COMMAND_LR
    LDA     GAME_MODE
    LSR
    ; if GAME_MODE was 0 or 1 (i.e. not displaying high score screen
    ; or splash screen), play the game.
    BEQ     .play_game

    ; When GAME_MODE is 2:
    JSR     WAIT_KEY
    LDA     PLAYER_COL
    STA     GAME_COLNUM
    LDA     PLAYER_ROW
    STA     GAME_ROWNUM
    LDA     #SPRITE_PLAYER
    JSR     WAIT_FOR_KEY_WITH_CURSOR_PAGE_1

.play_game:
    LDX     #$00
    STX     DIG_DIRECTION
    STX     NOTE_INDEX

    LDA     GUARD_PATTERN_OFFSET
    CLC
    ADC     GUARD_COUNT         ; GUARD_COUNT + $97 can't be greater than 8.
    TAY
    LDX     TIMES_3_TABLE,Y     ; X = 3 * Y (goes up to Y=8)
    LDA     GUARD_PATTERNS_LIST,X
    STA     GUARD_PATTERNS
    LDA     GUARD_PATTERNS_LIST+1,X
    STA     GUARD_PATTERNS+1
    LDA     GUARD_PATTERNS_LIST+2,X
    STA     GUARD_PATTERNS+2

    LDY     GUARD_PATTERN_OFFSET
    LDA     $621D,Y
    STA     GUARD_GOLD_TIMER_START_VALUE

.game_loop:
    JSR     MOVE_PLAYER
    LDA     ALIVE
    BEQ     .died

    JSR     PLAY_SOUND

    LDA     GOLD_COUNT
    BNE     .check_player_reached_top
    JSR     ENABLE_NEXT_LEVEL_LADDERS

.check_player_reached_top:
    LDA     PLAYER_ROW
    BNE     .not_at_top
    LDA     PLAYER_Y_ADJ
    CMP     #$02
    BNE     .not_at_top

    ; Reached top of screen
    LDA     GOLD_COUNT
    BEQ     .level_cleared
    CMP     #$FF
    BEQ     .level_cleared      ; level cleared if GOLD_COUNT == 0 or -1.

.not_at_top:
    JSR     HANDLE_TIMERS
    LDA     ALIVE
    BEQ     .died
    JSR     PLAY_SOUND
    JSR     MOVE_GUARDS
    LDA     ALIVE
    BEQ     .died
    BNE     .game_loop

.level_cleared:
    INC     LEVELNUM
    INC     DISK_LEVEL_LOC
    INC     LIVES
    BNE     .lives_incremented
    DEC     LIVES               ; LIVES doesn't overflow.

.lives_incremented:
    ; Increment score by 1500, playing an ascending tune while doing so.
    LDX     #$0F
    STX     SCRATCH_5C

.loop2:
    LDY     #$01
    LDA     #$00
    JSR     ADD_AND_UPDATE_SCORE    ; SCORE += 100
    JSR     APPEND_LEVEL_CLEARED_NOTE
    JSR     APPEND_LEVEL_CLEARED_NOTE
    JSR     APPEND_LEVEL_CLEARED_NOTE
    DEC     SCRATCH_5C
    BNE     .loop2

.start_game_:
    JMP     .start_game

.died:
    DEC     LIVES
    JSR     PUT_STATUS_LIVES
    JSR     LOAD_SOUND_DATA
    HEX     02 40 02 40 03 50 03 50 04 60 04 60 05 70 05 70
    HEX     06 80 06 80 07 90 07 90 08 A0 08 A0 09 B0 09 B0
    HEX     0A C0 0A C0 0B D0 0B D0 0C E0 0C E0 0D F0 0D F0
    HEX     00

.play_died_tune:
    JSR     PLAY_SOUND
    BCS     .play_died_tune

    LDA     GAME_MODE
    LSR
    BEQ     .restore_enable_sound     ; If GAME_MODE is 0 or 1

    LDA     LIVES
    BNE     .start_game_    ; We can still play.

    ; Game over
    JSR     RECORD_HI_SCORE_DATA_TO_DISK
    JSR     SPINNING_GAME_OVER
    BCS     .key_pressed
⟨tables [552]⟩+=
    ORG     $6214
TIMES_3_TABLE:
    HEX     00 03 06 09 0C 0F 12 15 18
Used in ⟨*⟩
Defines TIMES_3_TABLE
⟨tables [554]⟩+=
    ORG     $8C35
TABLE0:
    HEX     80 80 80 80 80 80 80 80 80 80 80 80 80 80
TABLE1:
    HEX     C0 AA D5 AA D5 AA D5 AA D5 AA D5 AA D5 80
TABLE2:
    HEX     90 80 80 80 80 80 80 80 80 80 80 80 80 82
TABLE3:
    HEX     90 AA D1 A2 D5 A8 85 A8 C5 A2 D4 A2 95 82
TABLE4:
    HEX     90 82 91 A2 C5 A8 80 88 C5 A2 94 A0 90 82
TABLE5:
    HEX     90 82 90 A2 C4 A8 80 88 C5 A2 94 A0 90 82
TABLE6:
    HEX     90 82 90 A2 C4 A8 81 88 C4 A2 D4 A0 95 82
TABLE7:
    HEX     90 A2 D1 A2 C4 88 80 88 C4 A2 84 A0 85 82
TABLE8:
    HEX     90 82 91 A2 C4 88 80 88 C4 AA 84 A0 85 82
TABLE9:
    HEX     90 82 91 A2 C4 88 80 88 C4 8A 84 A0 91 82
TABLE10:
    HEX     90 AA 91 A2 C4 A8 85 A8 85 82 D4 A2 91 82

    ORG     $8CCF
ADDRESS_TABLE:
    WORD    TABLE0-13
    WORD    TABLE1-13
    WORD    TABLE2-13
    WORD    TABLE3-13
    WORD    TABLE4-13
    WORD    TABLE5-13
    WORD    TABLE6-13
    WORD    TABLE7-13
    WORD    TABLE8-13
    WORD    TABLE9-13
    WORD    TABLE10-13
Used in ⟨*⟩
Defines ADDRESS_TABLE
⟨anims [556]⟩=
    ORG     $8B1A
SPINNING_GAME_OVER:
    SUBROUTINE

    LDA     #$01
    STA     ANIM_COUNT
    LDA     #$20
    STA     HGR_PAGE

.loop:
    JSR     ANIM5
    JSR     ANIM4
    JSR     ANIM3
    JSR     ANIM2
    JSR     ANIM1
    JSR     ANIM0
    JSR     ANIM1
    JSR     ANIM2
    JSR     ANIM3
    JSR     ANIM4
    JSR     ANIM5
    JSR     ANIM10
    JSR     ANIM9
    JSR     ANIM8
    JSR     ANIM7
    JSR     ANIM6
    JSR     ANIM7
    JSR     ANIM8
    JSR     ANIM9
    JSR     ANIM10
    LDA     ANIM_COUNT
    CMP     #100
    BCC     .loop

    JSR     ANIM5
    JSR     ANIM4
    JSR     ANIM3
    JSR     ANIM2
    JSR     ANIM1
    JSR     ANIM0
    CLC
    RTS

    ORG     $8B7A
ANIM0:
    JSR     SHOW_ANIM_LINE
    HEX     00 01 02 03 04 05 06 07 08 09 0A 02 01 00
ANIM1:
    JSR     SHOW_ANIM_LINE
    HEX     00 00 01 02 03 04 05 07 09 0A 02 01 00 00
ANIM2:
    JSR     SHOW_ANIM_LINE
    HEX     00 00 00 01 02 03 04 09 0A 02 01 00 00 00
ANIM3:
    JSR     SHOW_ANIM_LINE
    HEX     00 00 00 00 01 02 03 0A 02 01 00 00 00 00
ANIM4:
    JSR     SHOW_ANIM_LINE
    HEX     00 00 00 00 00 01 03 0A 01 00 00 00 00 00
ANIM5:
    JSR     SHOW_ANIM_LINE
    HEX     00 00 00 00 00 00 01 01 00 00 00 00 00 00
ANIM6:
    JSR     SHOW_ANIM_LINE
    HEX     00 01 02 0A 09 08 07 06 05 04 03 02 01 00
ANIM7:
    JSR     SHOW_ANIM_LINE
    HEX     00 00 01 02 0A 09 07 05 04 03 02 01 00 00
ANIM8:
    JSR     SHOW_ANIM_LINE
    HEX     00 00 00 01 02 0A 09 04 03 02 01 00 00 00
ANIM9:
    JSR     SHOW_ANIM_LINE
    HEX     00 00 00 00 01 02 0A 03 02 01 00 00 00 00
ANIM10:
    JSR     SHOW_ANIM_LINE
    HEX     00 00 00 00 00 01 0A 03 01 00 00 00 00 00
⟨show anim line [558]⟩=
    ORG     $8CE5
SHOW_ANIM_LINE:
    SUBROUTINE

    PLA
    STA     TMP_PTR
    PLA
    STA     TMP_PTR+1           ; store "return" addr

    ; Fill 14 rows of pixel data from row 0x51 (81) through 0x5E (94).
    LDY     #$50
    STY     GAME_ROWNUM
    BNE     .next      ; unconditional

.loop:
    JSR     ROW_TO_ADDR
    LDY     #$00
    LDA     (TMP_PTR),Y
    ASL
    TAX
    LDA     ADDRESS_TABLE,X
    STA     .loop2+1
    LDA     ADDRESS_TABLE+1,X   ; groups of 14 bytes
    STA     .loop2+2
    LDY     #$0D

    ; Copy 13 bytes of pixel data onto screen from
    ; addr+14 to addr+26
.loop2:
    LDA     $8D08,Y             ; fixed up from above
    STA     (ROW_ADDR),Y        ; pixel data
    INY
    CPY     #$1B
    BCC     .loop2              ; Y < 27

    ; Next row
.next:
    JSR     INCREMENT_TMP_PTR
    INC     GAME_ROWNUM
    LDY     GAME_ROWNUM
    CPY     #$5F
    BCC     .loop

    LDX     ANIM_COUNT
    LDY     #$FF
.delay:
    DEY
    BNE     .delay
    DEX
    BNE     .delay
    INC     ANIM_COUNT

    LDA     INPUT_MODE
    CMP     #KEYBOARD_MODE
    BEQ     .check_for_keypress
    LDA     BUTN1
    BMI     .input_detected
    LDA     BUTN0
    BMI     .input_detected

.check_for_keypress:
    LDA     KBD
    BMI     .input_detected
    RTS

    ; Skip the rest of the big animation.
.input_detected:
    PLA
    PLA
    SEC
    LDA     KBD
    STA     KBDSTRB
    RTS

    ORG     $8D4B
ANIM_COUNT:
    HEX     9D

    ORG     $8D4C
INCREMENT_TMP_PTR:
    SUBROUTINE

    INC     TMP_PTR
    BNE     .end
    INC     TMP_PTR+1
.end:
    RTS
Defines ANIM_COUNT, INCREMENT_TMP_PTR, SHOW_ANIM_LINE