; "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 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 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 sta TXTHI jsr PRINTXT lda DIFFLVL ; Show difficulty level jsr ECHO lda #TXT_WINNER2 sta TXTHI jsr PRINTXT jsr PRINMOV ; Display total moves lda #TXT_REPLAY sta TXTHI jsr PRINTXT jsr GETYN ; Get valid Y or N input bne ENDGAME jmp NEWGAME ENDGAME 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 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 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