546 lines
17 KiB
NASM
546 lines
17 KiB
NASM
; "15 puzzle" for the Apple I
|
|
|
|
; Jeff Jetton
|
|
; Februrary 2020
|
|
|
|
; Written for the dasm assembler, but should assemble under others
|
|
; with a few tweaks here and there.
|
|
|
|
; Marcel van Kervinck
|
|
; March 2020
|
|
;
|
|
; Adapted for Apple-1 emulation on Gigaton TTL computer.
|
|
; Gigatron-specific changes:
|
|
; - Replace INCMOVE with code that doesn't use BCD mode
|
|
; - Accomodate for smaller screen
|
|
; - Reduce ERR_MAX from 9 to 8
|
|
; - Be more economical, but consistent, with newlines
|
|
; - Squeeze instructions into 6 lines
|
|
; - Shorten INVALID CHOICE message to SORRY
|
|
; - Split win-message over 2 lines
|
|
; - Start from $400
|
|
; - Jump to $C100 on exit, so we go through the emulator menu again
|
|
; General changes:
|
|
; - Put a space after the move number for readability
|
|
; - Go back to welcome and instructions after ERR_MAX wrong entries
|
|
; Total size: 984 bytes
|
|
|
|
|
|
processor 6502
|
|
|
|
; Contants
|
|
KBD equ $D010 ; Location of input character from keyboard
|
|
KBDCR equ $D011 ; Keyboard control: Indicator that a new input
|
|
; character is ready
|
|
PRBYTE equ $FFDC ; WozMon routine to diaplay register A in hex
|
|
ECHO equ $FFEF ; WozMon routine to display register A char
|
|
GETLINE equ $FF1F ; Entry point back to WozMonitor
|
|
ERR_MAX equ $08 ; Bad game inputs before redisplaying board
|
|
|
|
|
|
; Zero-page variables
|
|
seg.u VARS
|
|
|
|
org $0040
|
|
|
|
PUZZ ds 16 ; The puzzle board state
|
|
CURMOV ds 1 ; ASCII code of the letter chosen as the current move
|
|
CUROFF ds 1 ; Offset in the board of the location of that move's letter
|
|
EMPOFF ds 1 ; Current offset of the empty space
|
|
TEMPCMP ds 1 ; Stores some temp bits used for row/col comparisons in HNDLMOV
|
|
ADJFAC ds 1 ; Adjustment factor to use when shifting tiles in HNDLMOV
|
|
DIFFLVL ds 1 ; Difficulty level (raw ascii of number chosen)
|
|
MOVELO ds 1 ; Tracks total number of moves
|
|
MOVEHI ds 1 ; ...using two bytes of binary-coded decimal (0000-9999)
|
|
TXTLO ds 1 ; Where the printxt routine looks for the string address
|
|
TXTHI ds 1 ; (two bytes)
|
|
PRNG ds 1 ; Running pseudo-random number generator
|
|
SHUFCTR ds 1 ; Counter of valid moves when "shuffling" new puzzle in INITPUZ
|
|
PREVMOV ds 1 ; Keeps track of previous randomly-selected move in INITPUZ
|
|
ERRCNT ds 1 ; Tracks number of invalid moves in a row (to redisplay board)
|
|
|
|
|
|
; Main program --------------------------------------------------------------
|
|
|
|
seg CODE
|
|
org $0400
|
|
|
|
; Init the program
|
|
cld ; Start with BCD mode off
|
|
ldx #$FF
|
|
txs ; Reset stack to $FF
|
|
lda #42 ; Set PNRG seed
|
|
sta PRNG
|
|
RESTART jsr INITPUZ ; Set up an ordered puzzle board
|
|
|
|
; Show welcome message and ask if user wants instructions
|
|
lda #<TXT_WELCOME ; Store low byte of text data location
|
|
sta TXTLO
|
|
lda #>TXT_WELCOME ; Store high byte of text data location
|
|
sta TXTHI
|
|
jsr PRINTXT ; Call generic print function
|
|
|
|
; Get and handle answer to instructions question
|
|
jsr GETYN
|
|
bne NEWGAME ; Skip to game if they didn't type "Y"
|
|
lda #<TXT_INSTRUCT ; Otherwise, print instructions...
|
|
sta TXTLO
|
|
lda #>TXT_INSTRUCT
|
|
sta TXTHI
|
|
jsr PRINTXT
|
|
jsr PRINPUZ ; Show the (currently solved) puzzle as an example
|
|
|
|
NEWGAME jsr GETDIFF ; Ask for difficulty level
|
|
jsr SHUFPUZ ; Shuffle up a new puzzle and reset move counter
|
|
jsr PRINPUZ ; Print initial board state
|
|
jsr NEWLINE
|
|
|
|
NXTMOVE jsr INCMOVE ; Handle next move by first incrementing move counter
|
|
jsr PRINMOV ; Show that move number
|
|
GETMOVE jsr GETKEY ; Grab keyboard input
|
|
cmp #"Q" ; Is user trying to quit?
|
|
bne NOQUIT
|
|
jmp ENDGAME
|
|
|
|
NOQUIT jsr HNDLMOV ; Get move and, if valid, update puzzle board
|
|
lda CUROFF ; Is the current offset 16 (i.e. move was invalid?)
|
|
cmp #16
|
|
bne SKIPERR ; Skip error display if the move was valid
|
|
dec ERRCNT ; Restart game after ERR_MAX errors
|
|
beq RESTART
|
|
jsr PRINERR ; Show error message
|
|
jmp GETMOVE ; Get new move, but don't incr. counter or show move #
|
|
|
|
SKIPERR jsr PRINPUZ ; Display current board state
|
|
jsr NEWLINE
|
|
jsr CHKWIN ; Check for a winning board (sets Z if so)
|
|
beq WINNER
|
|
jmp NXTMOVE
|
|
|
|
WINNER jsr PRINYAY ; Display a random interjection
|
|
lda #<TXT_WINNER1 ; Display next part of winning message
|
|
sta TXTLO
|
|
lda #>TXT_WINNER1
|
|
sta TXTHI
|
|
jsr PRINTXT
|
|
lda DIFFLVL ; Show difficulty level
|
|
jsr ECHO
|
|
lda #<TXT_WINNER2 ; Display rest of winning message
|
|
sta TXTLO
|
|
lda #>TXT_WINNER2
|
|
sta TXTHI
|
|
jsr PRINTXT
|
|
jsr PRINMOV ; Display total moves
|
|
|
|
lda #<TXT_REPLAY ; Prompt for playing another round
|
|
sta TXTLO
|
|
lda #>TXT_REPLAY
|
|
sta TXTHI
|
|
jsr PRINTXT
|
|
jsr GETYN ; Get valid Y or N input
|
|
bne ENDGAME
|
|
jmp NEWGAME
|
|
|
|
ENDGAME lda #<TXT_BYE
|
|
sta TXTLO
|
|
lda #>TXT_BYE
|
|
sta TXTHI
|
|
jsr PRINTXT
|
|
jmp $C100 ; Return to menu [Gigatron] and WozMonitor
|
|
|
|
|
|
|
|
|
|
; Subroutines ***************************************************************
|
|
|
|
|
|
NEWLINE SUBROUTINE ; Just print out a newline (destroys A)
|
|
lda #$0D
|
|
jmp ECHO
|
|
|
|
|
|
|
|
PRINTXT SUBROUTINE ; Put string pointer in TXTLO & TXTHI before calling
|
|
ldy #0 ; Y is the offset within the string
|
|
.loop lda (TXTLO),Y ; Load A with whatever's at pointer + Y
|
|
beq .end ; If char data is zero, that's the end
|
|
jsr ECHO ; Otherwise, print it
|
|
iny
|
|
jmp .loop
|
|
.end rts ; Note that we've destoyed A & Y
|
|
|
|
|
|
|
|
GETKEY SUBROUTINE ; Get one character of input and munge it into valid ASCII
|
|
; Also cycles the PRNG while waiting for key input!
|
|
jsr NEXTRND ; Cycle PRNG
|
|
lda KBDCR ; Check PIA for keyboard input
|
|
bpl GETKEY ; Loop if A is "positive" (bit 7 low)
|
|
lda KBD ; Get the keyboard character
|
|
and #%01111111 ; Clear bit 7, which is always set for some reason
|
|
jmp ECHO ; Always echo what the user just typed (Thanks Marcel!)
|
|
|
|
|
|
|
|
GETYN SUBROUTINE ; Gets a valid Y or N response from user. Sets Z flag on Y.
|
|
jsr GETKEY
|
|
cmp #"N"
|
|
beq .nope
|
|
cmp #"Y"
|
|
beq .yup
|
|
jsr PRINERR
|
|
jmp GETYN ; Try again
|
|
.nope tsx ; This clears the zero flag
|
|
.yup rts
|
|
|
|
|
|
|
|
NEXTRND SUBROUTINE ; Cycle the PRNG (simple 8-bit Xorshift)
|
|
lda PRNG
|
|
asl
|
|
bcc .noeor
|
|
eor #$A9
|
|
.noeor sta PRNG
|
|
rts
|
|
|
|
|
|
|
|
PRINPUZ SUBROUTINE ; Display the current puzzle state
|
|
; And reset the error counter
|
|
jsr NEWLINE
|
|
jsr NEWLINE
|
|
ldx #0 ; Offset into PUZZ data
|
|
ldy #4 ; Counts columns, for linebreaks
|
|
.loop lda PUZZ,X
|
|
jsr ECHO ; Print current slot
|
|
dey
|
|
bne .skipln ; Skip to inx if not at end of line
|
|
ldy #4 ; Otherwise reset y and do a newline
|
|
jsr NEWLINE
|
|
.skipln inx
|
|
cpx #16
|
|
bne .loop
|
|
lda #ERR_MAX
|
|
sta ERRCNT
|
|
rts
|
|
|
|
|
|
|
|
INCMOVE SUBROUTINE ; Increments the two-byte BCD move counter,
|
|
; without using BCD mode, up to 9999
|
|
inc MOVELO
|
|
lda MOVELO
|
|
and #$0F
|
|
cmp #$0A
|
|
bcs INCMOVE
|
|
lda MOVELO
|
|
sec
|
|
sbc #$A0
|
|
bcc .done
|
|
sta MOVELO
|
|
.nexthi inc MOVEHI
|
|
lda MOVEHI
|
|
and #$0F
|
|
cmp #$0A
|
|
bcs .nexthi
|
|
.done rts
|
|
|
|
|
|
PRINMOV SUBROUTINE ; Displays move number. Destroys A.
|
|
lda MOVEHI
|
|
beq .low ; Skip to the low byte if high byte is still zero
|
|
jsr PRBYTE
|
|
.low lda MOVELO
|
|
jsr PRBYTE
|
|
lda #" "
|
|
jmp ECHO
|
|
|
|
|
|
|
|
PRINERR SUBROUTINE ; Displays a standard input error message
|
|
lda #<TXT_SORRY
|
|
sta TXTLO
|
|
lda #>TXT_SORRY
|
|
sta TXTHI
|
|
jmp PRINTXT
|
|
|
|
|
|
|
|
PRINYAY SUBROUTINE ; Prints a randomly-selected expression of joy
|
|
lda PRNG ; Put latest random number in A
|
|
and #%00111000 ; Get bits 3-5 as a "multiple of 8" offset
|
|
tax
|
|
.loop lda YAYLKP,X
|
|
beq .done
|
|
jsr ECHO
|
|
inx
|
|
jmp .loop
|
|
.done rts
|
|
|
|
|
|
|
|
INITPUZ SUBROUTINE ; Create a new, ordered puzzle
|
|
; Put the space in the last byte of the puzz data
|
|
lda #" "
|
|
sta PUZZ + 15
|
|
lda #15
|
|
sta EMPOFF
|
|
; Work backwards through alphabet for the rest of the puzzle
|
|
ldx #15
|
|
ldy #"O"
|
|
.loop tya
|
|
sta.wx PUZZ-1 ; STA PUZZ-1,X
|
|
dey
|
|
dex
|
|
bne .loop
|
|
rts
|
|
|
|
|
|
|
|
SHUFPUZ SUBROUTINE ; Randomly shuffle puzzle board and init game variables
|
|
; Reset move counter and seed previous valid move variable
|
|
lda #0
|
|
sta MOVELO
|
|
sta MOVEHI
|
|
sta PREVMOV
|
|
; Get number of "shuffle" moves based on DIFFLVL and DIFFLKP table
|
|
ldx DIFFLVL
|
|
lda.wx DIFFLKP-"1" ; DIFFLKP-"1", X
|
|
sta SHUFCTR
|
|
; Push tiles around at random for SHUFCTR number of valid moves
|
|
.loop2 jsr NEXTRND
|
|
lda PRNG
|
|
and #%00001111 ; Get lower 4 bits of current random value
|
|
clc
|
|
adc #"A" ; Convert random 0-15 value to ascii A-P
|
|
cmp PREVMOV ; We don't want the same letter as last valid move
|
|
beq .loop2
|
|
jsr HNDLMOV ; Try making that move
|
|
lda CUROFF ; ...was it valid?
|
|
cmp #16
|
|
beq .loop2 ; No? Then it doesn't count. Try again.
|
|
lda CURMOV ; Otherwise, it "counts", so we'll remember the move
|
|
sta PREVMOV
|
|
dec SHUFCTR ; ...and decrement the counter
|
|
bne .loop2
|
|
; As long as the number of "shuffles" is odd, it's unlikely that we've
|
|
; randomly wound up back at a solved board. But since it COULD happen
|
|
; (moreso at low diff levels), we'd better check and redo if so...
|
|
jsr CHKWIN ; CHKWIN puts 0 in A right before returning if a winner
|
|
beq SHUFPUZ
|
|
rts
|
|
|
|
|
|
|
|
HNDLMOV SUBROUTINE ; Handle the current move in register A
|
|
|
|
; There are three steps to be done:
|
|
; 1. Figure out the offset of the chosen letter (if a valid letter)
|
|
; 2. Determine the adjustment factor based on the relationship between
|
|
; the chosen tile and the empty space, or mark move as invalid
|
|
; 3. Use the adjustment factor to shift the tile(s) appropriately
|
|
|
|
; Step 1: Did the user pick a letter that's on the board? If so, where?
|
|
sta CURMOV ; Store input letter as current move
|
|
cmp #" " ; Did user type a space?
|
|
beq .badmv
|
|
ldx #0
|
|
.loop lda PUZZ,X ; Look at one of the tiles on the board
|
|
cmp CURMOV ; Is it the current move?
|
|
beq .found
|
|
inx ; Nope. Bump up the offset...
|
|
cpx #16 ; Are we at the end of the board?
|
|
bne .loop ; No? Keep checking
|
|
jmp .badmv ; Otherwise, we're done. User chose a weird letter.
|
|
|
|
; Step 2: Is the tile moveable toward the space? If so, how?
|
|
.found stx CUROFF
|
|
; A moveable tile must be on the same row or column as the empty space
|
|
; First check if CUROFF is on the same ROW as EMPOFF (bits 2 & 3 match)
|
|
txa
|
|
and #%00001100 ; Isolate bits 2 & 3 of the offset of the current move
|
|
sta TEMPCMP ; Remember the result
|
|
lda EMPOFF
|
|
and #%00001100 ; Isolate bits 2 & 3 of the offset of the empty spot
|
|
cmp TEMPCMP ; Do the bits match?
|
|
bne .chkcol ; No? Bummer. Branch ahead and check for same column.
|
|
txa ; Yes? Subtract EMPOFF from CUROFF (which is still in X)
|
|
sec
|
|
sbc EMPOFF
|
|
bpl .posadj
|
|
lda #-1 ; CUROFF < EMPOFF = adjustment factor of -1
|
|
jmp .shift
|
|
.posadj lda #1 ; CUROFF > EMPOFF = adjustment factor of +1
|
|
jmp .shift
|
|
|
|
.chkcol ; Check if CUROFF is on the same COLUMN as EMPOFF (bits 0 & 1 match)
|
|
txa ; Get current move offset from X again
|
|
and #%00000011 ; Isolate bits 0 & 1
|
|
sta TEMPCMP
|
|
lda EMPOFF
|
|
and #%00000011 ; Same for offset of empty space
|
|
cmp TEMPCMP ; If they match, they're in the same column
|
|
beq .setadj
|
|
.badmv ldx #16 ; Load X with 16, indicating an invalid move
|
|
stx CUROFF
|
|
rts ; Back to the game at hand
|
|
|
|
.setadj txa
|
|
sec
|
|
sbc EMPOFF
|
|
bpl .posad2
|
|
lda #-4 ; CUROFF < EMPOFF = adjustment factor of -4 (1 row "up")
|
|
jmp .shift
|
|
.posad2 lda #4 ; CUROFF > EMPOFF = adjustment factor of 4 (1 row down)
|
|
|
|
; Step 3: Shift the tiles appropriately.
|
|
; This section takes care of swaping the space with an adjacent tile,
|
|
; over and over until the space is in the original move offset.
|
|
; The amount/direction by which the space is shifted each time is the
|
|
; "adjustment factor" which lives register in A by this point.
|
|
.shift sta ADJFAC ; A contains the current adjustment factor. Remember it.
|
|
.again lda EMPOFF ; Put current EMPOFF into X
|
|
tax
|
|
clc
|
|
adc ADJFAC ; Temp offset is current empoff plus adjustment factor
|
|
tay ; Maintain that temp offset in Y
|
|
lda PUZZ,Y ; Put whatever's in temp offset into A
|
|
sta PUZZ,X ; And store it where the empty space was
|
|
lda #" " ; The empty space...
|
|
sta PUZZ,Y ; Goes where the temp offset is
|
|
sty EMPOFF ; And that temp offset is the new empty space offset
|
|
cpy CUROFF ; Is the temp offset where the original move offset is?
|
|
bne .again
|
|
rts
|
|
|
|
|
|
|
|
CHKWIN SUBROUTINE ; Compare current board to sorted board
|
|
ldx #15
|
|
lda #"O"
|
|
sta TEMPCMP
|
|
.loop lda.wx PUZZ-1 ; lda PUZZ-1,X
|
|
cmp TEMPCMP
|
|
bne .nowin
|
|
dec TEMPCMP
|
|
dex
|
|
bne .loop
|
|
; If we made it this far, we have a winning board and the Zero flag
|
|
; will be set (due to the previous dex), indicating the win
|
|
.nowin rts ; But if we jumped here, Z will be unset (due to failed cmp)
|
|
; indicating a non-winning board state
|
|
|
|
|
|
|
|
|
|
GETDIFF SUBROUTINE ; Prompt for and get/set difficulty level
|
|
; Check for valid input and puts result in DIFFLVL
|
|
lda #<TXT_DIFFASK
|
|
sta TXTLO
|
|
lda #>TXT_DIFFASK
|
|
sta TXTHI
|
|
jsr PRINTXT
|
|
.input jsr GETKEY
|
|
sta DIFFLVL ; Store input as ASCII code, not as actual number
|
|
cmp #"1" ; Compare input (in register A) to ascii 1
|
|
bpl .nxtchk ; If result is positive, input was >= 1. So far so good.
|
|
jmp .inval
|
|
.nxtchk lda #"5" ; Subtract input from ascii 5
|
|
sec
|
|
sbc DIFFLVL
|
|
ZERO bmi .inval ; If result is negative, input was > 5 and invalid
|
|
rts ; At this point, we're good, so return
|
|
.inval jsr PRINERR
|
|
jmp .input
|
|
|
|
|
|
|
|
; Stored data ****************************************************************
|
|
|
|
DIFFLKP ; Lookup table translating difficulty levels to number of "shuffles"
|
|
; Low values should be odd (reduced chance of shuffling a solved board)
|
|
.byte 3 ; Level 1, only three moves
|
|
.byte 9 ; 2
|
|
.byte 19 ; 3
|
|
.byte 35 ; 4
|
|
.byte 255 ; Level 5, the full monty
|
|
|
|
YAYLKP ; Lookup table of strings for a random winner message
|
|
; First seven strings are padded to ensure 8 byte offsets
|
|
dc "HOORAY!"
|
|
.byte 0
|
|
dc "HUZZAH!"
|
|
.byte 0
|
|
dc "WOOHOO!"
|
|
.byte 0
|
|
dc "YIPPIE!"
|
|
.byte 0
|
|
dc "SWEET!"
|
|
.byte 0
|
|
.byte 0
|
|
dc "COOL!"
|
|
.byte 0
|
|
.byte 0
|
|
.byte 0
|
|
dc "NICE!"
|
|
.byte 0
|
|
.byte 0
|
|
.byte 0
|
|
dc "GADZOOKS!"
|
|
.byte 0
|
|
|
|
TXT_WELCOME
|
|
.byte $0D
|
|
.byte $0D
|
|
dc "15 PUZZLE - BY JEFF JETTON"
|
|
.byte $0D
|
|
.byte $0D
|
|
dc "INSTRUCTIONS (Y/N)? "
|
|
.byte $00
|
|
|
|
TXT_INSTRUCT
|
|
.byte $0D
|
|
.byte $0D
|
|
dc "TYPE A LETTER ON THE SAME",$0D
|
|
dc "ROW OR COLUMN AS THE EMPTY",$0D
|
|
dc "SPACE TO SLIDE THAT LETTER",$0D
|
|
dc "(AND ANY BETWEEN) TOWARDS",$0D
|
|
dc "THE SPACE. TYPE Q TO QUIT.",$0D
|
|
dc "THIS IS THE SOLVED PUZZLE:"
|
|
.byte $00
|
|
|
|
TXT_DIFFASK
|
|
.byte $0D
|
|
dc "DIFFICULTY LEVEL (1-5)? "
|
|
.byte $00
|
|
|
|
TXT_SORRY
|
|
.byte $0D
|
|
dc "SORRY. TRY AGAIN: "
|
|
.byte $00
|
|
|
|
TXT_WINNER1
|
|
dc " YOU SOLVED",$0D
|
|
dc "A LEVEL "
|
|
.byte $00
|
|
|
|
TXT_WINNER2
|
|
dc " PUZZLE!"
|
|
.byte $0D
|
|
.byte $0D
|
|
dc "TOTAL MOVES: "
|
|
.byte $00
|
|
|
|
TXT_REPLAY
|
|
.byte $0D
|
|
.byte $0D
|
|
dc "PLAY AGAIN (Y/N)? "
|
|
.byte $00
|
|
|
|
TXT_BYE
|
|
.byte $0D
|
|
.byte $0D
|
|
dc "BYE!"
|
|
.byte $0D
|
|
byte $00
|