;;---------------------------------------------------------------------------- ;; Connect-4 for the Commodore 64 and VIC-20. ;; ;; By Keith Pomakis (pomakis@pobox.com) ;; 1997-05 (with 2021-03 revisions) ;; ;; Cross-assembled with ACME (https://github.com/meonwax/acme). ;;---------------------------------------------------------------------------- ;; $Id: connect4.asm,v 1.23 2021/04/10 01:50:48 pomakis Exp pomakis $ ;;---------------------------------------------------------------------------- vic20 = 0 ; 0 for C=64, 1 for VIC-20 ;;---------------------------------------------------------------------------- ;; Zero-page addresses ;;---------------------------------------------------------------------------- r0 = $fb ; a pointer c4Player = $02 c4Level = $03 c4GameState = $04 ; and $05 ; swapped in and out of zero page c4Depth = $06 c4BestIndex = $07 c4BestWorst = $08 c4Goodness = $09 c4Alpha = $0a c4Beta = $0b c4MaxAB = $0c c4Best = $0d c4LastCol = $0e c4Temp1 = $0f c4Temp2 = $10 c4Temp3 = $11 c4Temp4 = $12 c4Temp5 = $13 c4Temp6 = $14 ;;---------------------------------------------------------------------------- ;; Constants ;;---------------------------------------------------------------------------- ; game state offsets c4BoardOffset = 0 ; assumed c4ScoreArrayOffset = 42 c4ScoresOffset = 180 c4WinnerOffset = 182 c4TopPiecesOffset = 183 c4StateSize = 183 ; other constants c4None = 2 c4HighestGoodness = $7f c4LowestGoodness = $81 !if vic20 { border = $900f flashColor = 26 } else { border = $d020 flashColor = 2 } ; kernal routines chrOut = $ffd2 getIn = $ffe4 ;;---------------------------------------------------------------------------- ;; Start of program ;;---------------------------------------------------------------------------- !if vic20 { * = $1001 } else { * = $0801 } !if vic20 { !byte 74, 10 ; end of BASIC program !byte 0, 0 ; line number !byte $9e ; SYS !pet "4172:" } else { !byte 74, 8 ; end of BASIC program !byte 0, 0 ; line number !byte $9e ; SYS !pet "2124:" } !byte $8f ; REM !byte $22 ; quote c4IntroText = * !byte 147 ; clear screen !byte 13+128 !byte 9, 14 ; switch to lower case !pet "Connect 4" !byte 13+128, 13+128 !pet "by Keith Pomakis" !byte 13+128 !pet "pomakis@pobox.com" !byte 13+128 !pet "May, 1997" ; with 2021-03 refinements !byte 13+128 !byte 0, 0, 0 ldx #14 - lda $06,x sta c4ZeroPageSave,x dex bpl - lda #c4IntroText sta r0+1 jsr printStr jsr c4PlayGame ldx #14 - lda c4ZeroPageSave,x sta $06,x dex bpl - rts ;;---------------------------------------------------------------------------- ;; FUNCTION: getNum ;; Gets a single digit between 0 and 7 from the keyboard. ;; 'q' returns 0 as well. ;; ;; Input: r0 - pointer to prompt ;; Retrn: .Y - the number ;; Ruins: .A ;;---------------------------------------------------------------------------- getNum = * jsr printStr getInput = * jsr getIn cmp #0 beq getInput cmp #81 ; 'q' bne + jsr chrOut ldy #0 beq doneInput ; always true + cmp #"0" bmi + cmp #"8" bpl + pha jsr chrOut pla sec sbc #"0" tay doneInput = * jmp printCR ; jsr/rts ; flash border + ldy border lda #flashColor sta border lda $a2 ; low byte of jiffy clock clc adc #4 - cmp $a2 bne - sty border beq getInput ; always true ;;---------------------------------------------------------------------------- ;; FUNCTION: printStr ;; Prints a null-terminated string. ;; ;; Input: r0 - pointer to string ;; Retrn: .Y - length of string ;;---------------------------------------------------------------------------- printStr = * pha ldy #0 - lda (r0),y beq + jsr chrOut iny bne - ; always true + pla rts ;;---------------------------------------------------------------------------- ;; FUNCTION: printCR ;; Prints a carriage return. ;; ;; Ruins: .A ;;---------------------------------------------------------------------------- printCR = * lda #13 jmp chrOut ; jsr/rts ;;---------------------------------------------------------------------------- ;; FUNCTION: c4PlayGame ;;---------------------------------------------------------------------------- c4PlayGame = * jsr c4NewGame lda #c4PlayGameLevelStr sta r0+1 jsr getNum cpy #0 bne + rts + sty c4Level lda #0 sta c4Player - jsr c4PrintBoard jsr c4CheckForWinOrTie bcc getColNum jmp c4PlayGame getColNum = * ldy c4Player lda c4PlayGameTable,y sta c4PlayGameMovePieceChar lda #c4PlayGameMoveStr sta r0+1 jsr getNum cpy #0 beq c4PlayGame dey jsr c4DropPiece bcs getColNum jsr c4PrintBoard jsr printCR jsr c4CheckForWinOrTie bcc + jmp c4PlayGame + lda c4Player eor #$01 sta c4Player lda #c4PlayGameThinkingStr sta r0+1 jsr printStr jsr c4AutoMove jsr printCR lda c4Player eor #$01 sta c4Player jmp - c4PlayGameTable = * !pet "XO." c4PlayGameLevelStr = * !byte 13 !pet "Level (3-7)? " !byte 0 c4PlayGameMoveStr = * !byte 13 !pet "Move " c4PlayGameMovePieceChar = * !pet " ? " !byte 0 c4PlayGameThinkingStr = * !pet "Thinking" !byte 0 c4PlayGameTieStr = * !byte 13 !pet "Tie!" !byte 13, 0 c4PlayGame0WinStr = * !byte 13 !pet "You win!" !byte 13, 0 c4PlayGame1WinStr = * !byte 13 !pet "I win!" !byte 13, 0 ; sets carry flag if win or tie c4CheckForWinOrTie = * ldy #c4WinnerOffset lda (c4GameState),y cmp #c4None beq ++ + cmp #0 bne + lda #c4PlayGame0WinStr bne +++ ; always true + lda #c4PlayGame1WinStr bne +++ ; always true ++ ldy #c4TopPiecesOffset lda (c4GameState),y cmp #7 bne + lda #c4PlayGameTieStr +++ sta r0+1 jsr printStr sec rts + clc rts ;;---------------------------------------------------------------------------- ;; FUNCTION: c4PrintBoard ;; ;; Ruins: .A, .Y, R0 ;;---------------------------------------------------------------------------- c4PrintBoard = * ldx #0 - cpx c4LastCol bne + lda #164 bne ++ ; always true + lda #' ' ++ jsr chrOut inx cpx #7 bne - jsr printCR ;ldx #7 ldy #35 - txa pha lda (c4GameState),y tax lda c4PlayGameTable,x jsr chrOut pla tax dex beq + iny bne - ; always true + jsr printCR cpy #$06 beq + tya sec sbc #13 tay ldx #7 bne - ; always true + ldx #'1' - txa jsr chrOut inx cpx #'8' bne - jmp printCR ; jsr/rts ;;---------------------------------------------------------------------------- ;; FUNCTION: c4NewGame ;;---------------------------------------------------------------------------- c4NewGame = * ; Set up the "real" game state. Make sure it starts at a page boundary. ldy #0 sty c4GameState ldy #(>c4EndOfProgram) + 1 sty c4GameState+1 lda #c4None ldy #41 - sta (c4GameState),y dey bpl - lda #1 ldy #c4ScoreArrayOffset - sta (c4GameState),y iny cpy #c4ScoreArrayOffset + 138 bne - lda #69 ldy #c4ScoresOffset sta (c4GameState),y iny sta (c4GameState),y lda #c4None ldy #c4WinnerOffset sta (c4GameState),y lda #0 ldy #c4TopPiecesOffset sta (c4GameState),y sta c4Depth lda #$ff sta c4LastCol rts ;;---------------------------------------------------------------------------- ;; FUNCTION: c4DropPieceSpecial ;; ;; Input: .Y - c4DropOrder index of column to drop into ;; Retrn: .C - 0 success, 1 failure ;; Ruins: .A, .X, .Y, R0, c4Temp1, c4Temp2, c4Temp3, c4Temp4 ;;---------------------------------------------------------------------------- c4DropPieceSpecial = * tya tax ldy c4DropOrder,x ; Fall through to c4DropPiece. ;;---------------------------------------------------------------------------- ;; FUNCTION: c4DropPiece ;; ;; Input: .Y - column to drop into ;; Retrn: .C - 0 success, 1 failure ;; Ruins: .A, .X, .Y, R0, c4Temp1, c4Temp2, c4Temp3, c4Temp4 ;;---------------------------------------------------------------------------- c4DropPiece = * sty c4LastCol - lda (c4GameState),y cmp #c4None beq + tya clc adc #7 tay cpy #42 bmi - sec rts + lda c4Player sta (c4GameState),y ; Update the score. tya pha jsr c4UpdateScore pla tay ; Keep track of the number of pieces on the top row of the ; board so a tie can be spotted. cpy #35 bmi + ldy #c4TopPiecesOffset lda (c4GameState),y clc adc #1 sta (c4GameState),y + clc rts ;;---------------------------------------------------------------------------- ;; FUNCTION: c4UpdateScore ;; ;; Input: c4Player - player (0 or 1) ;; .A - index of new piece (x*7 + y) ;; Ruins: .A, .X, .Y, R0, c4Temp1, c4Temp2, c4Temp3, c4Temp4 ;;---------------------------------------------------------------------------- c4UpdateScore = * c4ScoreDiffOfThisPlayer = c4Temp3 c4ScoreDiffOfOtherPlayer = c4Temp4 ; Calculate the index of the proper array of score array indexes. ; i.e., one of c4Map00, c4Map01, ... asl tay lda c4Map,y sta c4ProperMap lda c4Map+1,y sta c4ProperMap+1 lda #0 sta c4ScoreDiffOfThisPlayer sta c4ScoreDiffOfOtherPlayer ; Loop through each of the score units in the indexed array. ldx #0 c4ProperMap = *+1 - lda $ffff,x cmp #$ff bne ++ ; We're done. Update the scores. lda c4Player beq + ldy #c4ScoresOffset lda (c4GameState),y clc adc c4ScoreDiffOfOtherPlayer sta (c4GameState),y iny lda (c4GameState),y clc adc c4ScoreDiffOfThisPlayer sta (c4GameState),y rts + ldy #c4ScoresOffset lda (c4GameState),y clc adc c4ScoreDiffOfThisPlayer sta (c4GameState),y iny lda (c4GameState),y clc adc c4ScoreDiffOfOtherPlayer sta (c4GameState),y rts ++ clc adc #c4ScoreArrayOffset ; .A now contains the index of the proper score unit of player 0. ; Double the score unit of the current player. sta c4Temp1 ldy #0 cpy c4Player beq + clc adc #69 + tay lda (c4GameState),y sta c4Temp2 asl sta (c4GameState),y ; Is this a winning move? cmp #16 bne + lda c4Player ldy #c4WinnerOffset sta (c4GameState),y lda #16 + sec sbc c4Temp2 clc adc c4ScoreDiffOfThisPlayer sta c4ScoreDiffOfThisPlayer ; Zero the score unit of the other player. lda c4Temp1 ldy #1 cpy c4Player beq + clc adc #69 + tay lda c4ScoreDiffOfOtherPlayer sec sbc (c4GameState),y sta c4ScoreDiffOfOtherPlayer lda #0 sta (c4GameState),y inx bne - ; always true ;;---------------------------------------------------------------------------- ;; FUNCTION: c4GoodnessOfCurrentPlayer ;; Calculates the "goodness" of the current board relative to the ;; current player. This amounts to score(player)-score(other(player)). ;; ;; Retrn: .A - goodness ;; .Y - current player ;;---------------------------------------------------------------------------- c4GoodnessOfCurrentPlayer = * ldy #c4ScoresOffset lda (c4GameState),y iny sec sbc (c4GameState),y ldy c4Player beq + eor #$ff clc adc #1 + rts ;;---------------------------------------------------------------------------- ;; FUNCTION: c4PushState ;; ;; Ruins: .A, .X ;;---------------------------------------------------------------------------- c4PushState = * ldx c4GameState+1 stx c4OldStateAddr1 stx c4OldStateAddr2 inx stx c4NewStateAddr1 stx c4NewStateAddr2 stx c4GameState+1 ldx #c4StateSize c4OldStateAddr1 = *+2 - lda $ff00,x c4NewStateAddr1 = *+2 sta $ff00,x dex bne - c4OldStateAddr2 = *+2 lda $ff00,x c4NewStateAddr2 = *+2 sta $ff00,x inc c4Depth rts ;;---------------------------------------------------------------------------- ;; FUNCTION: c4PollFn ;; Protect .Y! ;;---------------------------------------------------------------------------- c4PollFn = * lda #"." jmp chrOut ; jsr/rts ;;---------------------------------------------------------------------------- ;; FUNCTION: c4AutoMove ;;---------------------------------------------------------------------------- c4AutoMove = * lda #c4LowestGoodness sta c4BestWorst lda #$ff sta c4BestIndex ldy c4GameState+1 iny sty c4GameStatePage lda #0 sta c4Goodness ldy #0 -- jsr c4PushState jsr c4PollFn - sty c4Temp5 jsr c4DropPieceSpecial ldy c4Temp5 bcc + iny cpy #7 bne - dec c4Depth ; pop state dec c4GameState+1 bne c4AutoMoveFinish ; always true ; We have just dropped a piece into column c4DropOrder[.Y] + lda c4Player ldx #c4WinnerOffset c4GameStatePage = *+2 cmp $ff00,x bne + lda #c4HighestGoodness sta c4Goodness bne ++ ; always true ; Call c4Evaluate + lda #c4LowestGoodness sta c4Alpha lda #0 sec sbc c4BestWorst sta c4Beta sty c4Temp5 jsr c4Evaluate ldy c4Temp5 ++ lda c4Goodness sec sbc c4BestWorst bne + ; Make a random decision lda $a2 ; low byte of jiffy clock ror bcc c4AutoMoveContinue bcs ++ + bmi + bvs c4AutoMoveContinue bvc ++ + bvc c4AutoMoveContinue ++ lda c4Goodness sta c4BestWorst sty c4BestIndex c4AutoMoveContinue = * dec c4Depth ; pop state dec c4GameState+1 iny cpy #7 bne -- c4AutoMoveFinish = * ldy c4BestIndex cpy #$ff beq + jmp c4DropPieceSpecial ; jsr/rts + rts ;;---------------------------------------------------------------------------- ;; FUNCTION: c4Evaluate ;;---------------------------------------------------------------------------- c4Evaluate = * lda c4Level cmp c4Depth bne + jsr c4GoodnessOfCurrentPlayer sta c4Goodness rts + lda #c4LowestGoodness sta c4Best lda c4Alpha sta c4MaxAB ldy #0 sty c4Temp6 lda c4Player eor #1 sta c4Player - jsr c4PushState jsr c4DropPieceSpecial bcc + dec c4Depth ; pop state dec c4GameState+1 jmp c4EvaluateContinue2 + ldy #c4WinnerOffset lda (c4GameState),y cmp #c4None beq + lda #c4HighestGoodness sec sbc c4Depth sta c4Goodness bne ++ ; always true ; Recurse + lda c4Best pha lda c4MaxAB pha lda c4Temp6 pha lda c4Alpha pha lda c4Beta pha lda #0 sec sbc c4Beta sta c4Alpha lda #0 sec sbc c4MaxAB sta c4Beta jsr c4Evaluate pla sta c4Beta pla sta c4Alpha pla sta c4Temp6 pla sta c4MaxAB pla sta c4Best ++ lda c4Goodness sec sbc c4Best bmi + bvs c4EvaluateContinue1 bvc ++ + bvc c4EvaluateContinue1 ++ beq c4EvaluateContinue1 lda c4Goodness sta c4Best lda c4Best sec sbc c4MaxAB bmi + bvs c4EvaluateContinue1 bvc ++ + bvc c4EvaluateContinue1 ++ beq c4EvaluateContinue1 lda c4Best sta c4MaxAB c4EvaluateContinue1 = * dec c4Depth ; pop state dec c4GameState+1 lda c4Beta sec sbc c4Best bmi + bvs ++ bvc c4EvaluateContinue2 + bvc ++ c4EvaluateContinue2 = * inc c4Temp6 ldy c4Temp6 cpy #7 beq ++ jmp - ++ lda c4Player eor #1 sta c4Player lda #0 sec sbc c4Best sta c4Goodness rts ;;---------------------------------------------------------------------------- ;; DATA ;;---------------------------------------------------------------------------- c4Map = * !word c4Map00, c4Map01, c4Map02, c4Map03, c4Map04, c4Map05, c4Map06 !word c4Map10, c4Map11, c4Map12, c4Map13, c4Map14, c4Map15, c4Map16 !word c4Map20, c4Map21, c4Map22, c4Map23, c4Map24, c4Map25, c4Map26 !word c4Map30, c4Map31, c4Map32, c4Map33, c4Map34, c4Map35, c4Map36 !word c4Map40, c4Map41, c4Map42, c4Map43, c4Map44, c4Map45, c4Map46 !word c4Map50, c4Map51, c4Map52, c4Map53, c4Map54, c4Map55, c4Map56 c4Map00 !byte 03, 24, 45, $ff c4Map01 !byte 03, 02, 27, 48, $ff c4Map02 !byte 03, 02, 04, 30, 51, $ff c4DropOrder = * ; This is an evil thing to do, but it saves 7 bytes! ; I've organized the map so that c4Map03 is overloaded to represent the ; order in which the computer tries dropping pieces. c4Map03 !byte 03, 02, 04, 01, 05, 00, 06, $ff c4Map04 !byte 02, 04, 01, 36, 60, $ff c4Map05 !byte 04, 01, 39, 63, $ff c4Map06 !byte 01, 42, 66, $ff c4Map10 !byte 33, 24, 25, 46, $ff c4Map11 !byte 33, 54, 27, 28, 45, 49, $ff c4Map12 !byte 33, 54, 57, 30, 31, 48, 52, 06, $ff c4Map13 !byte 33, 54, 57, 07, 05, 34, 51, 55, 58, 60, $ff c4Map14 !byte 54, 57, 07, 36, 37, 00, 61, 63, $ff c4Map15 !byte 57, 07, 39, 40, 64, 66, $ff c4Map16 !byte 07, 42, 43, 67, $ff c4Map20 !byte 08, 24, 25, 26, 47, $ff c4Map21 !byte 08, 09, 27, 28, 29, 46, 50, 06, $ff c4Map22 !byte 08, 09, 10, 30, 31, 32, 45, 49, 53, 58, 60, $ff c4Map23 !byte 08, 09, 10, 11, 05, 34, 35, 48, 52, 56, 59, 61, 63, $ff c4Map24 !byte 09, 10, 11, 36, 37, 38, 51, 55, 62, 64, 66, $ff c4Map25 !byte 10, 11, 39, 40, 41, 00, 65, 67, $ff c4Map26 !byte 11, 42, 43, 44, 68, $ff c4Map30 !byte 12, 24, 25, 26, 06, $ff c4Map31 !byte 12, 13, 27, 28, 29, 47, 58, 60, $ff c4Map32 !byte 12, 13, 14, 30, 31, 32, 46, 50, 59, 61, 63, $ff c4Map33 !byte 12, 13, 14, 15, 05, 34, 35, 45, 49, 53, 62, 64, 66, $ff c4Map34 !byte 13, 14, 15, 36, 37, 38, 48, 52, 56, 65, 67, $ff c4Map35 !byte 14, 15, 39, 40, 41, 51, 55, 68, $ff c4Map36 !byte 15, 42, 43, 44, 00, $ff c4Map40 !byte 16, 25, 26, 58, $ff c4Map41 !byte 16, 17, 28, 29, 59, 61, $ff c4Map42 !byte 16, 17, 18, 31, 32, 47, 62, 64, $ff c4Map43 !byte 16, 17, 18, 19, 34, 35, 46, 50, 65, 67, $ff c4Map44 !byte 17, 18, 19, 37, 38, 49, 53, 68, $ff c4Map45 !byte 18, 19, 40, 41, 52, 56, $ff c4Map46 !byte 19, 43, 44, 55, $ff c4Map50 !byte 20, 26, 59, $ff c4Map51 !byte 20, 21, 29, 62, $ff c4Map52 !byte 20, 21, 22, 32, 65, $ff c4Map53 !byte 20, 21, 22, 23, 35, 47, 68, $ff c4Map54 !byte 21, 22, 23, 38, 50, $ff c4Map55 !byte 22, 23, 41, 53, $ff c4Map56 !byte 23, 44, 56, $ff c4ZeroPageSave = * c4EndOfProgram = *+15