|
Post by Rod on Dec 9, 2020 12:47:02 GMT
Seven columns and six rows seems to be the defacto standard as does four in a row. So perhaps we should stick with that. Also who goes first should vary between games.
|
|
|
Post by B+ on Dec 9, 2020 16:00:59 GMT
Well I guess the question is best pointed to original makers of game. My reluctance with anything other that 8x8 is lack of symmetry which I equate to unfair game to player who has to go 2nd. Like Tic-tac-toe, a perfectly played game should end in draw.
|
|
|
Post by Rod on Dec 9, 2020 18:34:06 GMT
That's exactly why the inventor created a 7x6 board. He wanted to ensure there would be a winner. He did not think people would like a game that could be drawn.
|
|
|
Post by B+ on Dec 9, 2020 19:34:57 GMT
Well OK, so as long as the AI is the one to go 2nd we are fine! It could care less who wins but to be popular with humans it matters if you want them to enjoy the game more than a couple of times. So you have that carrot dangling before you, if you play perfectly, you will win.
|
|
|
Post by B+ on Dec 14, 2020 5:04:02 GMT
OK here are some brains to try and connect to a JB Interface. You guys can probably do it faster and better than I. I just got it working, found a trick it kept falling for, so I tweaked the scoring system and have yet to beat the thing now. SUB AIMove ' What this sub does in English: ' This sub assigns the value to playing each column, then plays the best value with following caveats: ' + If it finds a winning move, it will play that immediately. ' + If it finds a spoiler move, it will play that if no winning move was found. ' + It will poisen the column's scoring, if opponent can play a winning move if AI plays this column, ' but it might be the only legal move left. We will have to play it if no better score was found.
'Global variables ' N is the Number of Columns and Rows of the grid ' NM1 = N - 1 ' AIX = a legal column number to tell display sub what move the AI just made ' AIY = a legal row number to tell display sub what move the AI just made ' WinX, WinY, WinD = These tell the display routine where the win is; WinD is the direction from WinX, WinY. ' Grid(NM1, NM1) = this is the game board array
'REDIM SHARED DX(7), DY(7) SHARED means Global ' DX(0) = 1: DY(0) = 0 ' : DString$(0) = "East" ' DX(1) = 1: DY(1) = 1 ' : DString$(1) = "South East" ' DX(2) = 0: DY(2) = 1 ' : DString$(2) = "South" ' DX(3) = -1: DY(3) = 1 ' : DString$(3) = "South West" ' DX(4) = -1: DY(4) = 0 ' : DString$(4) = "West" ' DX(5) = -1: DY(5) = -1 ' : DString$(5) = "North West" ' DX(6) = 0: DY(6) = -1 ' : DString$(6) = "North" ' DX(7) = 1: DY(7) = -1 ' : DString$(7) = "North East"
' This sub uses 3 assistent FUNCTIONs and the 3rd needs the CheckWin Function, so 4 supplementary FUNCTIONs
'FUNCTION GetOpenRow (forCol) ' if the forCol has an empty row, this returns the lowest, if filled it returns N. ' DIM i ' <<< local to procedure ' GetOpenRow = N 'assume none open ' IF forCol < 0 OR forCol > NM1 THEN EXIT FUNCTION ' FOR i = NM1 TO 0 STEP -1 ' IF Grid(forCol, i) = 0 THEN GetOpenRow = i: EXIT FUNCTION ' NEXT 'END FUNCTION
'FUNCTION GR (c, r) ' if c, r are out of bounds returns N else returns grid(c, r) ' ' need to check the grid(c, r) but only if c, r is on the board ' IF c < 0 OR c > NM1 OR r < 0 OR r > NM1 THEN GR = N ELSE GR = Grid(c, r) 'END FUNCTION
'FUNCTION Stupid (c, r) ' returns -1 (true) if this (c, r) is played by AI it would be STUPID because opponent can win! ' DIM pr ' <<< local to procedure ' Grid(c, r) = AI ' pr = GetOpenRow(c) ' IF pr <> N THEN ' Grid(c, pr) = P ' IF CheckWin = 4 THEN Stupid = -1 ' Grid(c, pr) = 0 ' END IF ' Grid(c, r) = 0 'END FUNCTION
'FUNCTION CheckWin ' DIM gridFull, r, c, s, i ' <<< local to procedure ' ' return WinX, WinY, WinD along with +/- score, returns N if grid full, 0 if no win and grid not full ' gridFull = N ' FOR r = NM1 TO 0 STEP -1 'bottom to top ' FOR c = 0 TO NM1 ' IF Grid(c, r) THEN 'check if c starts a row ' IF c < NM1 - 2 THEN ' s = 0 ' FOR i = 0 TO 3 ' s = s + Grid(c + i, r) ' NEXT ' IF s = 4 OR s = -4 THEN ' WinX = c: WinY = r: WinD = 0 ' CheckWin = s: EXIT FUNCTION ' END IF ' END IF ' IF r > 2 THEN 'check if c starts a col ' s = 0 ' FOR i = 0 TO 3 ' s = s + Grid(c, r - i) ' NEXT ' IF s = 4 OR s = -4 THEN ' WinX = c: WinY = r: WinD = 6 'north ' CheckWin = s: EXIT FUNCTION ' END IF ' END IF ' IF r > 2 AND c < NM1 - 2 THEN 'check if c starts diagonal up to right ' s = 0 ' FOR i = 0 TO 3 ' s = s + Grid(c + i, r - i) ' NEXT ' IF s = 4 OR s = -4 THEN ' north east ' WinX = c: WinY = r: WinD = 7 ' CheckWin = s: EXIT FUNCTION ' END IF ' END IF ' IF r > 2 AND c > 2 THEN 'check if c starts a diagonal up to left ' s = 0 ' FOR i = 0 TO 3 ' s = s + Grid(c - i, r - i) ' NEXT ' IF s = 4 OR s = -4 THEN ' north west ' WinX = c: WinY = r: WinD = 5 ' CheckWin = s: EXIT FUNCTION ' END IF ' END IF ' ELSE ' gridFull = 0 ' at least one enpty cell left ' END IF 'grid is something ' NEXT ' NEXT ' CheckWin = gridFull 'END FUNCTION
' these are the local variables for this SUB procedure DIM c, r, d, cntA, cntP, bestScore, startR, startC, iStep, test, goodF, i DIM openRow(NM1) ' find open rows once DIM scores(NM1) ' evaluate each column's potential AIX = -1: AIY = -1 ' set these when AI makes move, they are signal to display procedure AI's move. FOR c = 0 TO NM1 openRow(c) = GetOpenRow(c) r = openRow(c) IF r <> N THEN FOR d = 0 TO 3 ' 4 directions to build connect 4's that use cell c, r startC = c + -3 * DX(d): startR = r + -3 * DY(d) FOR i = 0 TO 3 ' here we backup from the potential connect 4 in opposite build direction of c, r cntA = 0: cntP = 0: goodF = -1 ' reset counts and flag for good connect 4 'from this start position run 4 steps forward to count all connects involving cell c, r FOR iStep = 0 TO 3 ' process a potential connect 4 test = GR(startC + i * DX(d) + iStep * DX(d), startR + i * DY(d) + iStep * DY(d)) IF test = N THEN goodF = 0: EXIT FOR 'cant get connect4 from here IF test = AI THEN cntA = cntA + 1 IF test = P THEN cntP = cntP + 1 NEXT iStep IF goodF THEN 'evaluate the Legal Connect4 we could build with c, r IF cntA = 3 THEN ' we are done! winner! AIX = c: AIY = r ' <<< this is the needed 4th cell to win tell ShowGrid last cell Grid(c, r) = AI ' <<< this is the needed 4th cell to win, add to grid this is AI move EXIT SUB ELSEIF cntP = 3 THEN 'next best move spoiler! AIX = c: AIY = r 'set the move but don't exit there might be a winner ELSEIF cntA = 0 AND cntP = 2 THEN scores(c) = scores(c) + 6 ELSEIF cntA = 2 AND cntP = 0 THEN ' very good offense or defense scores(c) = scores(c) + 5 'play this to connect 3 or prevent player from Connect 3 ELSEIF cntA = 0 AND cntP = 1 THEN scores(c) = scores(c) + 4 ELSEIF (cntA = 1 AND cntP = 0) OR (cntA = 0 AND cntP = 1) THEN 'good offense or defense scores(c) = scores(c) + 3 ' play this to connect 2 or prevent player from Connect 2 ELSEIF (cntA = 0 AND cntP = 0) THEN ' OK it's not a wasted move as it has potential for connect4 scores(c) = scores(c) + 1 ' this is good move because this can still be a Connect 4 END IF END IF ' in the board NEXT i NEXT d IF Stupid(c, r) THEN scores(c) = -1000 + scores(c) ' poison because if played the human can win END IF NEXT IF AIX <> -1 THEN ' we found a spoiler so move there since we haven't found a winner Grid(AIX, AIY) = AI ' make move on grid and done! EXIT SUB ELSE bestScore = -1000 ' a negative score indicates that the player can beat AI with their next move FOR c = 0 TO NM1 r = openRow(c) IF r <> N THEN IF scores(c) > bestScore THEN bestScore = scores(c): AIY = r: AIX = c END IF NEXT IF AIX <> -1 THEN Grid(AIX, AIY) = AI ' make first best score move we found ELSE 'We have trouble! Oh but it could be there are no moves!!! ' checkWin is run after every move by AI or Player if there were no legal moves left it should have caught that. ' Just in case it didn't here is an error stop!
'note: LOCATE here is Row, Column the reverse of Just Basic BEEP: LOCATE 4, 2: PRINT "AI has failed to find a proper move, pess any to end..." SLEEP ' <<< pause until user presses a key END END IF END IF END SUB BTW, this is complete rewrite from code I posted earlier here is the EXE.
|
|
|
Post by B+ on Dec 14, 2020 5:32:25 GMT
Maybe this is what I have to settle for?
|
|
|
Post by Rod on Dec 14, 2020 14:08:42 GMT
I see AI and P used as if they are set global but dont see where that is happening or where they are set? function stupid.
|
|
|
Post by B+ on Dec 14, 2020 14:52:59 GMT
I see AI and P used as if they are set global but dont see where that is happening or where they are set? function stupid. I just have the brain ready to transplant into JB interface, notes covering the transplant are included at the beginning of the Sub comments which actually say AI and P are Global (Constants actually equal -1 and 1 respectively). I should get to building the interface and translating this sub to JB later today. The hard part is done and working.
|
|
|
Post by Rod on Dec 14, 2020 18:56:17 GMT
Still can't see that in the code you posted. I was trying to convert to a 7x6 grid. Never mind will wait for your interface.
|
|
|
Post by B+ on Dec 14, 2020 21:42:03 GMT
Almost there! (a little rusty and spoiled by another IDE)
Big Game tonight, hope to finish before it starts ;-))
|
|
|
Post by B+ on Dec 14, 2020 23:48:46 GMT
Just finishing up, but they are finding cracks in the AI at another forum.
|
|
|
Post by B+ on Dec 15, 2020 0:38:43 GMT
OK got it before the Game! ' 8x8 Connect 4 by B+ port to JB 2020-12-14
global SQ, SW, SH, N, NM1, P, AI, XO, YO ' True CONSTants global GameOn, GoFirst, Turn, AIX, AIY, WinX, WinY, WinD ' for sharing with subroutines global Kee$, MouseX, MouseY ' input from keyboard or left mouse click SQ = 60 ' square or grid cell N = 8 ' number of rows and columns OK now works for 7x7 or 8x8 SW = SQ * (N + 2) ' screen width SH = SQ * (N + 3) ' screen height NM1 = N - 1 ' N minus 1 P = 1 ' Player is 1 on grid AI = -1 ' AI is -1 on grid XO = SQ ' x offset for grid YO = 2 * SQ ' y offset for grid
dim Grid(NM1, NM1) ' = this is the game board array dim DX(7), DY(7) ' Direction Changes in x, y or col, row DX(0) = 1: DY(0) = 0 ' : DString$(0) = "East" DX(1) = 1: DY(1) = 1 ' : DString$(1) = "South East" DX(2) = 0: DY(2) = 1 ' : DString$(2) = "South" DX(3) = -1: DY(3) = 1 ' : DString$(3) = "South West" DX(4) = -1: DY(4) = 0 ' : DString$(4) = "West" DX(5) = -1: DY(5) = -1 ' : DString$(5) = "North West" DX(6) = 0: DY(6) = -1 ' : DString$(6) = "North" DX(7) = 1: DY(7) = -1 ' : DString$(7) = "North East"
' Needed for AImove processing dim openRow(NM1) ' find open rows once dim Scores(NM1) ' evaluate each column's potential
nomainwin WindowWidth = SW + 8 WindowHeight = SH + 32 UpperLeftX = (1200 - SW) / 2 UpperLeftY = (700 - SH) / 2 open "8x8 Connect 4 with AI by B+ " for graphics_nsb_nf as #gr #gr "setfocus" #gr "trapclose Quit" #gr "when leftButtonUp LButtonUp" #gr "when characterInput CharIn" #gr "down" #gr "fill black"
GameOn = -1: GoFirst = AI: Turn = AI: MouseX = -1 : MouseY = -1 call ShowGrid while GameOn if Turn = P then if MouseX <> -1 then 'must have clicked the screen if (MouseX - XO) > 0 and (MouseY - YO) > 0 then row = INT((MouseY - YO) / SQ): col = INT((MouseX - XO) / SQ) if col >= 0 and col <= NM1 and row >= 0 and row <= NM1 then r = GetOpenRow(col) 'find next space open on board if r <> N then Grid(col, r) = P Turn = AI end if end if end if MouseX = -1 : MouseY = -1 end if else call AIMove Turn = P end if call ShowGrid call Pause 200 wend wait
sub AIMove ' What this sub does in English: ' This sub assigns the value to playing each column, then plays the best value with following caveats: ' + If it finds a winning move, it will play that immediately. ' + If it finds a spoiler move, it will play that if no winning move was found. ' + It will poisen the column's scoring, if opponent can play a winning move if AI plays this column, ' but it might be the only legal move left. We will have to play it if no better score was found. dim Scores(NM1) AIX = -1: AIY = -1 ' set these when AI makes move, they are signal to display procedure AI's move. for c = 0 to NM1 openRow(c) = GetOpenRow(c) r = openRow(c) if r <> N then for d = 0 to 3 ' 4 directions to build connect 4's that use cell c, r startC = c + -3 * DX(d): startR = r + -3 * DY(d) for i = 0 to 3 ' here we backup from the potential connect 4 in opposite build direction of c, r cntA = 0: cntP = 0: goodF = -1 ' reset counts and flag for good connect 4 'from this start position run 4 steps forward to count all connects involving cell c, r for iStep = 0 to 3 ' process a potential connect 4 test = GR(startC + i * DX(d) + iStep * DX(d), startR + i * DY(d) + iStep * DY(d)) if test = N then goodF = 0: exit for 'cant get connect4 from here if test = AI then cntA = cntA + 1 if test = P then cntP = cntP + 1 next iStep if goodF then 'evaluate the Legal Connect4 we could build with c, r if cntA = 3 then ' we are done! winner! AIX = c: AIY = r ' <<< this is the needed 4th cell to win tell ShowGrid last cell Grid(c, r) = AI ' <<< this is the needed 4th cell to win, add to grid this is AI move exit sub else if cntP = 3 then 'next best move spoiler! AIX = c: AIY = r 'set the move but don't exit there might be a winner else if cntA = 0 and cntP = 2 then Scores(c) = Scores(c) + 8 else if cntA = 2 and cntP = 0 then ' very good offense or defense Scores(c) = Scores(c) + 6 'play this to connect 3 or prevent player from Connect 3 else if cntA = 0 and cntP = 1 then Scores(c) = Scores(c) + 7 else if cntA = 1 and cntP = 0 then 'good offense or defense Scores(c) = Scores(c) + 5 ' play this to connect 2 or prevent player from Connect 2 else if (cntA = 0 and cntP = 0) then ' OK it's not a wasted move as it has potential for connect4 Scores(c) = Scores(c) + 1 ' this is good move because this can still be a Connect 4 end if end if end if end if end if end if end if end if ' in the board next i next d if Stupid(c, r) then Scores(c) = -1000 + Scores(c) ' poison because if played the human can win end if next if AIX <> -1 then ' we found a spoiler so move there since we haven't found a winner Grid(AIX, AIY) = AI ' make move on grid and done! exit sub else bestScore = -1000 ' a negative score indicates that the player can beat AI with their next move for c = 0 to NM1 r = openRow(c) if r <> N then if Scores(c) > bestScore then bestScore = Scores(c): AIY = r: AIX = c end if next if AIX <> -1 then Grid(AIX, AIY) = AI ' make first best score move we found else 'We have trouble! Oh but it could be there are no moves!!! ' checkWin is run after every move by AI or Player if there were no legal moves left it should have caught that. ' Just in case it didn't here is an error stop! notice "sub AImove failed to find a move to make. Calling it quits." call Quit "gr" end if end if end sub
function GetOpenRow (forCol) ' if the forCol has an empty row, this returns the lowest, if filled it returns N. GetOpenRow = N 'assume none open if forCol < 0 or forCol > NM1 then exit function for i = NM1 to 0 STEP -1 if Grid(forCol, i) = 0 then GetOpenRow = i: exit function next end function
function GR (c, r) ' if c, r are out of bounds returns N else returns grid(c, r) ' need to check the grid(c, r) but only if c, r is on the board, if NOT return N if c < 0 or c > NM1 or r < 0 or r > NM1 then GR = N else GR = Grid(c, r) end function
function Stupid (c, r) ' returns -1 (true) if this (c, r) is played by AI it would be STUPID because opponent can win! Grid(c, r) = AI pr = GetOpenRow(c) if pr <> N then Grid(c, pr) = P if CheckWin() = 4 then Stupid = -1 Grid(c, pr) = 0 end if Grid(c, r) = 0 end function
function CheckWin() 'return WinX, WinY, WinD along with +/- score, returns N if grid full, 0 if no win and grid not full gridFull = N for r = NM1 to 0 STEP -1 'bottom to top for c = 0 to NM1 if Grid(c, r) then 'check if c starts a row if c < NM1 - 2 then s = 0 for i = 0 to 3 s = s + Grid(c + i, r) next if s = 4 or s = -4 then WinX = c: WinY = r: WinD = 0 CheckWin = s: exit function end if end if if r > 2 then 'check if c starts a col s = 0 for i = 0 to 3 s = s + Grid(c, r - i) next if s = 4 or s = -4 then WinX = c: WinY = r: WinD = 6 'north CheckWin = s: exit function end if end if if r > 2 and c < NM1 - 2 then 'check if c starts diagonal up to right s = 0 for i = 0 to 3 s = s + Grid(c + i, r - i) next if s = 4 or s = -4 then ' north east WinX = c: WinY = r: WinD = 7 CheckWin = s: exit function end if end if if r > 2 and c > 2 then 'check if c starts a diagonal up to left s = 0 for i = 0 to 3 s = s + Grid(c - i, r - i) next if s = 4 or s = -4 then ' north west WinX = c: WinY = r: WinD = 5 CheckWin = s: exit function end if end if else gridFull = 0 ' at least one empty cell left end if 'grid is something next next CheckWin = gridFull end function
sub ShowGrid #gr "fill black" #gr "color green" for i = 0 to N 'grid #gr "line ";SQ * i + XO;" ";YO;" ";SQ * i + XO;" ";YO + N * SQ #gr "line ";XO;" ";SQ * i + YO;" ";XO + N * SQ;" ";SQ * i + YO next for r = NM1 to 0 STEP -1 'plays for c = 0 to NM1 if Grid(c, r) = P then #gr "color red" #gr "backcolor red" else if Grid(c, r) = AI then if c = AIX and r = AIY then 'highlite last AI move #gr "color ";125;" ";125;" ";255 #gr "backcolor ";125;" ";125;" ";255 else #gr "color blue" #gr "backcolor blue" end if end if end if if Grid(c, r) then #gr "place ";c * SQ + XO + 3;" ";r * SQ + YO + 3 #gr "boxfilled ";c * SQ + XO + 3 + SQ - 6;" ";r * SQ + YO + 3 + SQ - 6 end if next next check = CheckWin() if check then 'report end of round and see if want to play again if check = 4 or check = -4 then #gr "color white" for i = 0 to 3 #gr "place ";(WinX + i * DX(WinD)) * SQ + XO + 5;" ";(WinY + i * DY(WinD)) * SQ + YO + 5 #gr "box ";(WinX + i * DX(WinD)) * SQ + XO + 5 + SQ - 10;" ";(WinY + i * DY(WinD)) * SQ + YO + 5 + SQ - 10 next end if if check = -4 then s$ = "AI is Winner!" else if check = 4 then s$ = "Human is Winner!" else if check = N then s$ = "Board is full, no winner." ' keep Turn the same end if end if end if #gr "color white" #gr "backcolor black" call CText 32, s$ Kee$ = "" call CText 64, "Play again? just press spacebar, quit with any other." while len(Kee$) = 0 : scan : wend if Kee$ = " " then dim Grid(NM1, NM1) if GoFirst = P then GoFirst = AI else GoFirst = P Turn = GoFirst else GameOn = 0 notice "Goodbye!" end if end if end sub
sub Text x, y, message$ 'note: have to reset fore or back color after ink #gr "place ";x;" ";y;";|";message$ end sub
sub CText y, message$ ' center text on lower left? y pixel call Text (SW - len(message$) * 7) / 2, y, message$ end sub
sub LButtonUp H$, mx, my 'must have handle and mouse x,y MouseX = mx : MouseY = my end sub
sub CharIn H$, c$ Kee$ = c$ end sub
sub Quit H$ close #gr end end sub
sub Pause mil 'tsh version has scan built-in t0 = time$("ms") while time$("ms") < t0 + mil : scan : wend end sub
EDIT: for both 7x7 and 8x8 or 9x9 or more or less
|
|
|
Post by B+ on Dec 15, 2020 6:18:00 GMT
OK now that I have it ported, here is a mod with NumRows and NumCols so you can do any size board within reason. ' Connect 4 NumRows & Cols by B+ port to JB 2020-12-15
global SQ, SW, SH, NumCols, NCM1, NumRows, NRM1, P, AI, XO, YO ' True CONSTants global GameOn, GoFirst, Turn, AIX, AIY, WinX, WinY, WinD ' for sharing with subroutines global Kee$, MouseX, MouseY ' input from keyboard or left mouse click SQ = 60 ' square or grid cell pixel size NumCols = 7 NumRows = 6 SW = SQ * (NumCols + 2) ' screen width SH = SQ * (NumRows + 3) ' screen height NCM1 = NumCols - 1 ' NumCols minus 1 NRM1 = NumRows - 1 ' NumRows minus 1 P = 1 ' Player is 1 on grid AI = -1 ' AI is -1 on grid XO = SQ ' x offset for grid YO = 2 * SQ ' y offset for grid
dim Grid(NCM1, NRM1) ' = this is the game board array dim DX(7), DY(7) ' Direction Changes in x, y or col, row DX(0) = 1: DY(0) = 0 ' : DString$(0) = "East" DX(1) = 1: DY(1) = 1 ' : DString$(1) = "South East" DX(2) = 0: DY(2) = 1 ' : DString$(2) = "South" DX(3) = -1: DY(3) = 1 ' : DString$(3) = "South West" DX(4) = -1: DY(4) = 0 ' : DString$(4) = "West" DX(5) = -1: DY(5) = -1 ' : DString$(5) = "North West" DX(6) = 0: DY(6) = -1 ' : DString$(6) = "North" DX(7) = 1: DY(7) = -1 ' : DString$(7) = "North East"
' Needed for AImove processing dim openRow(NCM1) ' find open rows once dim Scores(NCM1) ' evaluate each column's potential
nomainwin WindowWidth = SW + 8 WindowHeight = SH + 32 UpperLeftX = (1200 - SW) / 2 UpperLeftY = (700 - SH) / 2 open "Connect 4: ";NumCols;"x";NumRows;" with AI by B+ " for graphics_nsb_nf as #gr #gr "setfocus" #gr "trapclose Quit" #gr "when leftButtonUp LButtonUp" #gr "when characterInput CharIn" #gr "down" #gr "fill black"
GameOn = -1: GoFirst = AI: Turn = AI: MouseX = -1 : MouseY = -1 call ShowGrid while GameOn if Turn = P then if MouseX <> -1 then 'must have clicked the screen if (MouseX - XO) > 0 and (MouseY - YO) > 0 then row = INT((MouseY - YO) / SQ): col = INT((MouseX - XO) / SQ) if col >= 0 and col <= NCM1 and row >= 0 and row <= NRM1 then r = GetOpenRow(col) 'find next space open on board if r <> NumRows then Grid(col, r) = P Turn = AI end if end if end if MouseX = -1 : MouseY = -1 end if else call AIMove Turn = P end if call ShowGrid call Pause 200 wend wait
sub AIMove ' What this sub does in English: ' This sub assigns the value to playing each column, then plays the best value with following caveats: ' + If it finds a winning move, it will play that immediately. ' + If it finds a spoiler move, it will play that if no winning move was found. ' + It will poisen the column's scoring, if opponent can play a winning move if AI plays this column, ' but it might be the only legal move left. We will have to play it if no better score was found. dim Scores(NCM1) AIX = -1: AIY = -1 ' set these when AI makes move, they are signal to display procedure AI's move. for c = 0 to NCM1 openRow(c) = GetOpenRow(c) r = openRow(c) if r <> NumRows then for d = 0 to 3 ' 4 directions to build connect 4's that use cell c, r startC = c + -3 * DX(d): startR = r + -3 * DY(d) for i = 0 to 3 ' here we backup from the potential connect 4 in opposite build direction of c, r cntA = 0: cntP = 0: goodF = -1 ' reset counts and flag for good connect 4 'from this start position run 4 steps forward to count all connects involving cell c, r for iStep = 0 to 3 ' process a potential connect 4 test = GR(startC + i * DX(d) + iStep * DX(d), startR + i * DY(d) + iStep * DY(d)) if test = NumRows then goodF = 0: exit for 'cant get connect4 from here if test = AI then cntA = cntA + 1 if test = P then cntP = cntP + 1 next iStep if goodF then 'evaluate the Legal Connect4 we could build with c, r if cntA = 3 then ' we are done! winner! AIX = c: AIY = r ' <<< this is the needed 4th cell to win tell ShowGrid last cell Grid(c, r) = AI ' <<< this is the needed 4th cell to win, add to grid this is AI move exit sub else if cntP = 3 then 'next best move spoiler! AIX = c: AIY = r 'set the move but don't exit there might be a winner else if cntA = 0 and cntP = 2 then Scores(c) = Scores(c) + 8 else if cntA = 2 and cntP = 0 then ' very good offense or defense Scores(c) = Scores(c) + 6 'play this to connect 3 or prevent player from Connect 3 else if cntA = 0 and cntP = 1 then Scores(c) = Scores(c) + 7 else if cntA = 1 and cntP = 0 then 'good offense or defense Scores(c) = Scores(c) + 5 ' play this to connect 2 or prevent player from Connect 2 else if (cntA = 0 and cntP = 0) then ' OK it's not a wasted move as it has potential for connect4 Scores(c) = Scores(c) + 1 ' this is good move because this can still be a Connect 4 end if end if end if end if end if end if end if end if ' in the board next i next d if Stupid(c, r) then Scores(c) = -1000 + Scores(c) ' poison because if played the human can win end if next if AIX <> -1 then ' we found a spoiler so move there since we haven't found a winner Grid(AIX, AIY) = AI ' make move on grid and done! exit sub else bestScore = -1000 ' a negative score indicates that the player can beat AI with their next move for c = 0 to NCM1 r = openRow(c) if r <> NumRows then if Scores(c) > bestScore then bestScore = Scores(c): AIY = r: AIX = c end if next if AIX <> -1 then Grid(AIX, AIY) = AI ' make first best score move we found else 'We have trouble! Oh but it could be there are no moves!!! ' checkWin is run after every move by AI or Player if there were no legal moves left it should have caught that. ' Just in case it didn't here is an error stop! notice "sub AImove failed to find a move to make. Calling it quits." call Quit "gr" end if end if end sub
function GetOpenRow (forCol) ' if the forCol has an empty row, this returns the lowest, if filled it returns N. GetOpenRow = NumRows 'assume none open if forCol < 0 or forCol > NCM1 then exit function for i = NRM1 to 0 STEP -1 if Grid(forCol, i) = 0 then GetOpenRow = i: exit function next end function
function GR (c, r) ' if c, r are out of bounds returns N else returns grid(c, r) ' need to check the grid(c, r) but only if c, r is on the board, if NOT return N if c < 0 or c > NCM1 or r < 0 or r > NRM1 then GR = NumRows else GR = Grid(c, r) end function
function Stupid (c, r) ' returns -1 (true) if this (c, r) is played by AI it would be STUPID because opponent can win! Grid(c, r) = AI pr = GetOpenRow(c) if pr <> NumRows then Grid(c, pr) = P if CheckWin() = 4 then Stupid = -1 Grid(c, pr) = 0 end if Grid(c, r) = 0 end function
function CheckWin() 'return WinX, WinY, WinD along with +/- score, returns N if grid full, 0 if no win and grid not full gridFull = NumRows for r = NRM1 to 0 STEP -1 'bottom to top for c = 0 to NCM1 if Grid(c, r) then 'check if c starts a row if c < NCM1 - 2 then s = 0 for i = 0 to 3 s = s + Grid(c + i, r) next if s = 4 or s = -4 then WinX = c: WinY = r: WinD = 0 CheckWin = s: exit function end if end if if r > 2 then 'check if c starts a col s = 0 for i = 0 to 3 s = s + Grid(c, r - i) next if s = 4 or s = -4 then WinX = c: WinY = r: WinD = 6 'north CheckWin = s: exit function end if end if if r > 2 and c < NCM1 - 2 then 'check if c starts diagonal up to right s = 0 for i = 0 to 3 s = s + Grid(c + i, r - i) next if s = 4 or s = -4 then ' north east WinX = c: WinY = r: WinD = 7 CheckWin = s: exit function end if end if if r > 2 and c > 2 then 'check if c starts a diagonal up to left s = 0 for i = 0 to 3 s = s + Grid(c - i, r - i) next if s = 4 or s = -4 then ' north west WinX = c: WinY = r: WinD = 5 CheckWin = s: exit function end if end if else gridFull = 0 ' at least one empty cell left end if 'grid is something next next CheckWin = gridFull end function
sub ShowGrid #gr "fill black" #gr "color green" for i = 0 to NumCols 'grid #gr "line ";SQ * i + XO;" ";YO;" ";SQ * i + XO;" ";YO + NumRows * SQ next for i = 0 to NumRows #gr "line ";XO;" ";SQ * i + YO;" ";XO + NumCols * SQ;" ";SQ * i + YO next for r = NRM1 to 0 STEP -1 'plays for c = 0 to NCM1 if Grid(c, r) = P then #gr "color red" #gr "backcolor red" else if Grid(c, r) = AI then if c = AIX and r = AIY then 'highlite last AI move #gr "color ";125;" ";125;" ";255 #gr "backcolor ";125;" ";125;" ";255 else #gr "color blue" #gr "backcolor blue" end if end if end if if Grid(c, r) then #gr "place ";c * SQ + XO + 3;" ";r * SQ + YO + 3 #gr "boxfilled ";c * SQ + XO + 3 + SQ - 6;" ";r * SQ + YO + 3 + SQ - 6 end if next next check = CheckWin() if check then 'report end of round and see if want to play again if check = 4 or check = -4 then #gr "color white" for i = 0 to 3 #gr "place ";(WinX + i * DX(WinD)) * SQ + XO + 5;" ";(WinY + i * DY(WinD)) * SQ + YO + 5 #gr "box ";(WinX + i * DX(WinD)) * SQ + XO + 5 + SQ - 10;" ";(WinY + i * DY(WinD)) * SQ + YO + 5 + SQ - 10 next end if if check = -4 then s$ = "AI is Winner!" else if check = 4 then s$ = "Human is Winner!" else if check = N then s$ = "Board is full, no winner." ' keep Turn the same end if end if end if #gr "color white" #gr "backcolor black" call CText 32, s$ Kee$ = "" call CText 64, "Play again? just press spacebar, quit with any other." while len(Kee$) = 0 : scan : wend if Kee$ = " " then dim Grid(NCM1, NRM1) if GoFirst = P then GoFirst = AI else GoFirst = P Turn = GoFirst else GameOn = 0 notice "Goodbye!" end if end if end sub
sub Text x, y, message$ 'note: have to reset fore or back color after ink #gr "place ";x;" ";y;";|";message$ end sub
sub CText y, message$ ' center text on lower left? y pixel call Text (SW - len(message$) * 7) / 2, y, message$ end sub
sub LButtonUp H$, mx, my 'must have handle and mouse x,y MouseX = mx : MouseY = my end sub
sub CharIn H$, c$ Kee$ = c$ end sub
sub Quit H$ close #gr end end sub
sub Pause mil 'tsh version has scan built-in t0 = time$("ms") while time$("ms") < t0 + mil : scan : wend end sub
When I was testing this the AI made a "Stupid" move (see FUNCTION Stupid, I always wanted to write a Stupid FUNCTION ) which means there may be trouble in Emerald City. I hadn't seen any Stupid moves made by AI in the square grids so there may be a subtle error maybe in the CheckWin FUNCTION which helps decide a Stupid move. CheckWin was definitely based on a square grid. PS really great game 47 to 42 too bad Browns lost, but dang! what a game.
|
|
|
Post by B+ on Dec 17, 2020 8:23:12 GMT
An update access for assessing how the AI is scoring, a move by move accounting at the end of a game and of course prettier colors ;-)) This might suggest ideas for tweaking the AI to make it stronger. ' Connect 4 Any Rows and Cols 2020-12-17.txt B+
global SQ, SW, SH, NumCols, NCM1, NumRows, NRM1, P, AI, XO, YO ' True CONSTants global GameOn, GoFirst, Turn, AIX, AIY, WinX, WinY, WinD ' for sharing with subroutines global Kee$, MouseX, MouseY global PlayerLastMoveCol, PlayerLastMoveRow, MoveNum, LastMoveNum SQ = 60 ' square or grid cell pixel size NumCols = 8 NumRows = 8 SW = SQ * (NumCols + 2) ' screen width SH = SQ * (NumRows + 3) ' screen height NCM1 = NumCols - 1 ' NumCols minus 1 NRM1 = NumRows - 1 ' NumRows minus 1 P = 1 ' Player is 1 on grid AI = -1 ' AI is -1 on grid XO = SQ ' x offset for grid YO = 2 * SQ ' y offset for grid
dim Grid(NCM1, NRM1) ' = this is the game board array dim DX(7), DY(7) ' Direction Changes in x, y or col, row DX(0) = 1: DY(0) = 0 ' : DString$(0) = "East" DX(1) = 1: DY(1) = 1 ' : DString$(1) = "South East" DX(2) = 0: DY(2) = 1 ' : DString$(2) = "South" DX(3) = -1: DY(3) = 1 ' : DString$(3) = "South West" DX(4) = -1: DY(4) = 0 ' : DString$(4) = "West" DX(5) = -1: DY(5) = -1 ' : DString$(5) = "North West" DX(6) = 0: DY(6) = -1 ' : DString$(6) = "North" DX(7) = 1: DY(7) = -1 ' : DString$(7) = "North East"
' Needed for AImove processing dim OpenRow(NCM1) ' find open rows once dim Scores(NCM1) ' evaluate each column's potential dim Record$(NCM1, NRM1) ' forrecord moves
nomainwin WindowWidth = SW + 8 WindowHeight = SH + 32 UpperLeftX = (1200 - SW) / 2 UpperLeftY = (700 - SH) / 2 open "Connect 4: ";NumCols;"x";NumRows;" with AI by B+ " for graphics_nsb_nf as #gr #gr "setfocus" #gr "trapclose Quit" #gr "when leftButtonUp LButtonUp" #gr "when characterInput CharIn" #gr "down" #gr "fill black"
GameOn = -1: GoFirst = AI: Turn = AI: MouseX = -1 : MouseY = -1 call ShowGrid while GameOn if Turn = P then if MouseX <> -1 then 'must have clicked the screen if (MouseX - XO) > 0 and (MouseY - YO) > 0 then row = INT((MouseY - YO) / SQ): col = INT((MouseX - XO) / SQ) if col >= 0 and col <= NCM1 and row >= 0 and row <= NRM1 then r = GetOpenRow(col) 'find next space open on board if r <> NumRows then Grid(col, r) = P: Turn = AI: PlayerLastMoveCol = col: PlayerLastMoveRow = r: MoveNum = MoveNum + 1 end if end if end if MouseX = -1 : MouseY = -1 end if else call AIMove Turn = P: MoveNum = MoveNum + 1 end if call ShowGrid call Pause 200 wend wait
sub AIMove ' What this sub does in English: ' This sub assigns the value to playing each column, then plays the best value with following caveats: ' + If it finds a winning move, it will play that immediately. ' + If it finds a spoiler move, it will play that if no winning move was found. ' + It will poisen the column's scoring, if opponent can play a winning move if AI plays this column, ' but it might be the only legal move left. We will have to play it if no better score was found. dim Scores(NCM1) AIX = -1: AIY = -1 ' set these when AI makes move, they are signal to display procedure AI's move. for c = 0 to NCM1 OpenRow(c) = GetOpenRow(c) r = OpenRow(c) if r <> NumRows then for d = 0 to 3 ' 4 directions to build connect 4's that use cell c, r startC = c + -3 * DX(d): startR = r + -3 * DY(d) for i = 0 to 3 ' here we backup from the potential connect 4 in opposite build direction of c, r cntA = 0: cntP = 0: goodF = -1 ' reset counts and flag for good connect 4 'from this start position run 4 steps forward to count all connects involving cell c, r for iStep = 0 to 3 ' process a potential connect 4 test = GR(startC + i * DX(d) + iStep * DX(d), startR + i * DY(d) + iStep * DY(d)) if test = NumRows then goodF = 0: exit for 'cant get connect4 from here if test = AI then cntA = cntA + 1 if test = P then cntP = cntP + 1 next iStep if goodF then 'evaluate the Legal Connect4 we could build with c, r if cntA = 3 then ' we are done! winner! AIX = c: AIY = r ' <<< this is the needed 4th cell to win tell ShowGrid last cell Grid(c, r) = AI ' <<< this is the needed 4th cell to win, add to grid this is AI move Scores(c) = Scores(c) + 1000 exit sub else if cntP = 3 then 'next best move spoiler! AIX = c: AIY = r 'set the move but don't exit there might be a winner Scores(c) = Scores(c) + 900 else if cntA = 0 and cntP = 2 then Scores(c) = Scores(c) + 8 else if cntA = 2 and cntP = 0 then ' very good offense or defense Scores(c) = Scores(c) + 4 'play this to connect 3 or prevent player from Connect 3 else if cntA = 0 and cntP = 1 then Scores(c) = Scores(c) + 4 else if cntA = 1 and cntP = 0 then 'good offense or defense Scores(c) = Scores(c) + 2 ' play this to connect 2 or prevent player from Connect 2 else if cntA = 0 and cntP = 0 then ' OK it's not a wasted move as it has potential for connect4 Scores(c) = Scores(c) + 1 ' this is good move because this can still be a Connect 4 end if end if end if end if end if end if end if end if ' in the board next i next d if Stupid(c, r) then Scores(c) = -1000 + Scores(c) ' poison because if played the human can win end if next if AIX <> -1 then ' we found a spoiler so move there since we haven't found a winner Grid(AIX, AIY) = AI ' make move on grid and done! exit sub else if GetOpenRow(PlayerLastMoveCol) < NumRows then 'all things being equal play on top of player's last move bestScore = Scores(PlayerLastMoveCol): AIY = PlayerLastMoveRow - 1: AIX = PlayerLastMoveCol else bestScore = -1000 ' a negative score indicates that the player can beat AI with their next move end if for c = 0 to NCM1 r = OpenRow(c) if r <> NumRows then if Scores(c) > bestScore then bestScore = Scores(c): AIY = r: AIX = c end if next if AIX <> -1 then Grid(AIX, AIY) = AI ' make first best score move we found else 'We have trouble! Oh but it could be there are no moves!!! ' checkWin is run after every move by AI or Player if there were no legal moves left it should have caught that. ' Just in case it didn't here is an error stop! notice "sub AImove failed to find a move to make. Calling it quits." call Quit "gr" end if end if end sub
function GetOpenRow (forCol) ' if the forCol has an empty row, this returns the lowest, if filled it returns N. GetOpenRow = NumRows 'assume none open if forCol < 0 or forCol > NCM1 then exit function for i = NRM1 to 0 STEP -1 if Grid(forCol, i) = 0 then GetOpenRow = i: exit function next end function
function GR (c, r) ' if c, r are out of bounds returns N else returns grid(c, r) ' need to check the grid(c, r) but only if c, r is on the board, if NOT return N if c < 0 or c > NCM1 or r < 0 or r > NRM1 then GR = NumRows else GR = Grid(c, r) end function
function Stupid (c, r) ' returns -1 (true) if this (c, r) is played by AI it would be STUPID because opponent can win! Grid(c, r) = AI pr = GetOpenRow(c) if pr <> NumRows then Grid(c, pr) = P if CheckWin() = 4 then Stupid = -1 Grid(c, pr) = 0 end if Grid(c, r) = 0 end function
function CheckWin() 'return WinX, WinY, WinD along with +/- score, returns N if grid full, 0 if no win and grid not full gridFull = NumRows for r = NRM1 to 0 STEP -1 'bottom to top for c = 0 to NCM1 if Grid(c, r) then 'check if c starts a row if c < NCM1 - 2 then s = 0 for i = 0 to 3 'east s = s + Grid(c + i, r) next if s = 4 or s = -4 then WinX = c: WinY = r: WinD = 0: CheckWin = s: exit function end if if r > 2 then 'check if c starts a col s = 0 for i = 0 to 3 'north s = s + Grid(c, r - i) next if s = 4 or s = -4 then WinX = c: WinY = r: WinD = 6: CheckWin = s: exit function end if if r > 2 and c < NCM1 - 2 then 'check if c starts diagonal up to right s = 0 for i = 0 to 3 ' north east s = s + Grid(c + i, r - i) next if s = 4 or s = -4 then WinX = c: WinY = r: WinD = 7: CheckWin = s: exit function end if if r > 2 and c > 2 then 'check if c starts a diagonal up to left s = 0 for i = 0 to 3 ' north west s = s + Grid(c - i, r - i) next if s = 4 or s = -4 then WinX = c: WinY = r: WinD = 5: CheckWin = s: exit function end if else gridFull = 0 ' at least one empty cell left end if 'grid is something next next CheckWin = gridFull end function
sub ShowGrid Browns$ = 255;" ";34;" ";0 Ravens$ = 57;" ";0;" ";39 if MoveNum <> lastMoveNum then ' file newest move if MoveNum = 1 then dim Record$(NCM1, NRM1) if Turn = -1 then Record$(PlayerLastMoveCol, PlayerLastMoveRow) = MoveNum;" ";"P" else Record$(AIX, AIY) = MoveNum;" ";"A" end if lastMoveNum = MoveNum end if #gr "fill black" #gr "color ";0;" ";68;" ";0 #gr "backcolor ";0;" ";68;" ";0 #gr "place ";XO;" ";YO #gr "boxfilled ";NumCols * SQ + XO;" ";NumRows * SQ + YO #gr "color white" for i = 0 to NumCols 'grid #gr "line ";SQ * i + XO;" ";YO;" ";SQ * i + XO;" ";YO + NumRows * SQ next for i = 0 to NumRows #gr "line ";XO;" ";SQ * i + YO;" ";XO + NumCols * SQ;" ";SQ * i + YO next for r = NRM1 to 0 STEP -1 'plays for c = 0 to NCM1 if Grid(c, r) = P then #gr "color ";Browns$ #gr "backcolor ";Browns$ else if Grid(c, r) = AI then if c = AIX and r = AIY then 'highlite last AI move #gr "color ";104;" ";0;" ";68 #gr "backcolor ";104;" ";0;" ";68 else #gr "color ";Ravens$ #gr "backcolor ";Ravens$ end if end if end if if Grid(c, r) then #gr "place ";c * SQ + XO + 3;" ";r * SQ + YO + 3 #gr "boxfilled ";c * SQ + XO + 3 + SQ - 6;" ";r * SQ + YO + 3 + SQ - 6 end if s$ = Str$(Scores(c)) #gr "color white" #gr "backcolor black" call Text XO + c * SQ + (60 - LEN(s$) * 7) / 2, YO + SQ * NumRows + 38, str$(Scores(c)) next next check = CheckWin() if check then 'report end of round and see if want to play again if check = 4 or check = -4 then #gr "color white" for i = 0 to 3 #gr "place ";(WinX + i * DX(WinD)) * SQ + XO + 5;" ";(WinY + i * DY(WinD)) * SQ + YO + 5 #gr "box ";(WinX + i * DX(WinD)) * SQ + XO + 5 + SQ - 10;" ";(WinY + i * DY(WinD)) * SQ + YO + 5 + SQ - 10 next end if for r = 0 to NRM1 for c = 0 to NCM1 if Record$(c, r) <> "" then s$ = mid$(Record$(c, r), 1, instr(Record$(c, r), " ") - 1) if right$(Record$(c, r), 1) = "A" then #gr "backcolor ";Ravens$ else #gr "backcolor ";Browns$ end if call Text SQ * c + XO + (SQ - LEN(s$) * 7) / 2, SQ * r + YO + 38, s$ end if next next if check = -4 then s$ = "AI is Winner!" else if check = 4 then s$ = "Human is Winner!" else if check = N then s$ = "Board is full, no winner." ' keep Turn the same end if end if end if #gr "color white" #gr "backcolor black" call CText 32, s$ Kee$ = "" call CText 64, "Play again? just press spacebar, quit with any other." while len(Kee$) = 0 : scan : wend if Kee$ = " " then dim Grid(NCM1, NRM1) if GoFirst = P then GoFirst = AI else GoFirst = P Turn = GoFirst: MoveNum = 0 else GameOn = 0 notice "Goodbye!" end if end if end sub
sub Text x, y, message$ 'note: have to reset fore or back color after ink #gr "place ";x;" ";y;";|";message$ end sub
sub CText y, message$ ' center text on lower left? y pixel call Text (SW - len(message$) * 7) / 2, y, message$ end sub
sub LButtonUp H$, mx, my 'must have handle and mouse x,y MouseX = mx : MouseY = my end sub
sub CharIn H$, c$ Kee$ = c$ end sub
sub Quit H$ close #gr end end sub
sub Pause mil 'tsh version has scan built-in t0 = time$("ms") while time$("ms") < t0 + mil : scan : wend end sub
Yes, there is still hope for Human players
|
|
|
Post by tsh73 on Dec 18, 2020 19:29:06 GMT
fixed major flicker on my netbook by moving ShowGrid [/b] MouseX = -1 : MouseY = -1 call ShowGrid end if else call AIMove Turn = P: MoveNum = MoveNum + 1 call ShowGrid end if call Pause 200 wend
Summary (resume) by me and my 10-year child: AI is definitely evil and impossible to beat! $)
|
|