From 47502710d90c6b12ed4e322482d7e94c936e21ef Mon Sep 17 00:00:00 2001 From: jona605a Date: Sun, 9 Aug 2020 22:48:09 +0200 Subject: [PATCH 01/13] :sparkles: Hex is winable! --- funcs/games/gameLoops.py | 3 +- funcs/games/hex.py | 62 +++++++++++----------------------- resources/games/hex-board.jpg | Bin 82395 -> 0 bytes 3 files changed, 22 insertions(+), 43 deletions(-) delete mode 100644 resources/games/hex-board.jpg diff --git a/funcs/games/gameLoops.py b/funcs/games/gameLoops.py index f1d2f38..d1fcb9e 100644 --- a/funcs/games/gameLoops.py +++ b/funcs/games/gameLoops.py @@ -62,7 +62,8 @@ async def runHex(channel,command,user): data = json.load(f) winner = data[str(channel.id)]["winner"] if winner != 0: - addMoney(data[str(channel.id)]["players"][winner-1].lower(),50) + winnings = data[str(channel.id)]["difficulty"]*10 + addMoney(data[str(channel.id)]["players"][winner-1].lower(),winnings) #deleteGame("hex games",str(channel.id)) with open("resources/games/hexGames.json", "r") as f: diff --git a/funcs/games/hex.py b/funcs/games/hex.py index 32259a3..5afc07a 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -6,15 +6,6 @@ import math from . import hexDraw from funcs import logThis, getName, getID -# This is totally copied from the four in a row game. Just modified - -AIScoresHex = { - "lol, dunno": 3, - "enemy win": -10000, - "win": 1000, - "avoid losing": 100 -} - BOARDWIDTH = 11 ALL_POSITIONS = [(i,j) for i in range(11) for j in range(11)] ALL_SET = set(ALL_POSITIONS) @@ -23,7 +14,6 @@ for position in ALL_POSITIONS: EMPTY_DIJKSTRA[position] = math.inf # an impossibly high number HEX_DIRECTIONS = [(0,1),(-1,1),(-1,0),(0,-1),(1,-1),(1,0)] - # Parses command def parseHex(command, channel, user): commands = command.lower().split() @@ -119,7 +109,6 @@ def hexStart(channel, user, opponent): # draw the board hexDraw.drawBoard(channel) - gwendoTurn = True if players[0] == "Gwendolyn" else False showImage = True return "Started Hex game against "+getName(opponent)+ diffText+". It's "+getName(players[0])+"'s turn", showImage, False, False, gwendoTurn @@ -149,32 +138,23 @@ def placeHex(channel : str,position : str, user): data[channel]["turn"] = turn - """ - with open("resources/games/hexGames.json", "w") as f: - json.dump(data,f,indent=4) - - # Checking for a win logThis("Checking for win") - won, winningPieces = isHexWon(data[channel]["board"]) + score, winner = evaluateBoard(data[channel]["board"]) - if won != 0: - gameWon = True - data[channel]["winner"] = won - data[channel]["winningPieces"] = winningPieces + if winner == 0: # Continue with the game. + gameWon = False + message = getName(data[channel]["players"][player-1])+" placed at "+position.upper()+". It's now "+getName(data[channel]["players"][turn-1])+"'s turn. The score is "+str(score) - - message = data[channel]["players"][won-1]+" won!" - if data[channel]["players"][won-1] != "Gwendolyn": - winAmount = data[channel]["difficulty"]^2+5 + else: # Congratulations! + gameWon = True + data[channel]["winner"] = winner + message = getName(data[channel]["players"][player-1])+" placed at "+position.upper()+" and won!" + if data[channel]["players"][winner-1] != "Gwendolyn": + winAmount = data[channel]["difficulty"]*10 message += " Adding "+str(winAmount)+" GwendoBucks to their account." - else:""" - - gameWon = False - message = getName(data[channel]["players"][player-1])+" placed at "+position.upper()+". It's now "+getName(data[channel]["players"][turn-1])+"'s turn." + data[channel]["lastMove"] = (int(position[1])-1, ord(position[0])-97) - with open("resources/games/hexGames.json", "w") as f: - json.dump(data,f,indent=4) # Is it now Gwendolyn's turn? gwendoTurn = False @@ -182,6 +162,10 @@ def placeHex(channel : str,position : str, user): logThis("It's Gwendolyn's turn") gwendoTurn = True + # Save the data + with open("resources/games/hexGames.json", "w") as f: + json.dump(data,f,indent=4) + # Update the board hexDraw.drawHexPlacement(channel,player,position) @@ -312,7 +296,7 @@ def evaluateBoard(board): # Initialize the starting hexes. For the blue player, this is the leftmost column. For the red player, this is the tom row. for start in (ALL_POSITIONS[::11] if player == 2 else ALL_POSITIONS[:11]): # An empty hex adds a of distance of 1. A hex of own color add distance 0. Opposite color adds infinite distance. - Distance[start] = 1 if (board[v[0]][v[1]] == 0) else 0 if (board[v[0]][v[1]] == player) else math.inf + Distance[start] = 1 if (board[start[0]][start[1]] == 0) else 0 if (board[start[0]][start[1]] == player) else math.inf visited = set() # Also called sptSet, short for "shortest path tree Set" for _ in range(BOARDWIDTH**2): # We can at most check every 121 hexes # Find the next un-visited hex, that has the lowest distance @@ -326,17 +310,12 @@ def evaluateBoard(board): if v[0] in range(11) and v[1] in range(11) and v not in visited: new_dist = Distance[u] + (1 if (board[v[0]][v[1]] == 0) else 0 if (board[v[0]][v[1]] == player) else math.inf) Distance[v] = min(Distance[v], new_dist) - - # If at the goal, we've found the shortest distance - if v[player-1] == 10: # if the right coordinate of v is 10, it means we're at the goal - atGoal = True - break - if atGoal: - score[player] = Distance[v] # A player's score is the shortest distance to goal. Which equals the number of remaining moves they need to win if unblocked by the opponent. - break # After a hex has been visited, this is noted visited.add(u) - logThis("Distance from player {}'s start to {} is {}".format(player,u,Distance[u])) + #logThis("Distance from player {}'s start to {} is {}".format(player,u,Distance[u])) + if u[player-1] == 10: # if the right coordinate of v is 10, it means we're at the goal + score[player] = Distance[u] # A player's score is the shortest distance to goal. Which equals the number of remaining moves they need to win if unblocked by the opponent. + break else: logThis("For some reason, no path to the goal was found. ") if score[player] == 0: @@ -346,7 +325,6 @@ def evaluateBoard(board): return score, winner - def minimaxHex(board, depth, player , originalPlayer, alpha, beta, maximizingPlayer): # The depth is how many moves ahead the computer checks. This value is the difficulty. if depth == 0 or 0 not in sum(board,[0]): diff --git a/resources/games/hex-board.jpg b/resources/games/hex-board.jpg deleted file mode 100644 index 1274168f68821718ad31c6aad3c7d725b2f1193b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82395 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 From 437c3e76685edac9e6030b852dd6b4ef1606302f Mon Sep 17 00:00:00 2001 From: jona605a Date: Sun, 9 Aug 2020 22:52:33 +0200 Subject: [PATCH 02/13] :bug: Log --- funcs/miscFuncs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/funcs/miscFuncs.py b/funcs/miscFuncs.py index da1d3d2..1780cdd 100644 --- a/funcs/miscFuncs.py +++ b/funcs/miscFuncs.py @@ -322,7 +322,7 @@ def getName(userID): if userID in data: return data[userID]["user name"] else: - logThis("Couldn't find user") + logThis("Couldn't find user "+userID) return userID except: logThis("Error getting name") From bc11c5dd24a19cae1b3eb91ec0f5d0583f38ad8f Mon Sep 17 00:00:00 2001 From: jona605a Date: Sun, 9 Aug 2020 23:26:18 +0200 Subject: [PATCH 03/13] :pencil: Replaced "lastMove" with "gameHistory", so you can now undo several times in a row --- funcs/games/hex.py | 20 +++++++++++--------- funcs/games/hexDraw.py | 24 ++---------------------- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index 5afc07a..7f9ac1b 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -97,11 +97,10 @@ def hexStart(channel, user, opponent): board = [ [ 0 for i in range(BOARDWIDTH) ] for j in range(BOARDWIDTH) ] players = [user,opponent] random.shuffle(players) # random starting player - winningPieces = [[""],[""],[""]] # etc. - lastMove = (-1,-1) + gameHistory = [] data[channel] = {"board":board, "winner":0, - "players":players, "winningPieces":winningPieces, "turn":1, "difficulty":difficulty, "lastMove":lastMove} + "players":players, "turn":1, "difficulty":difficulty, "gameHistory":gameHistory} with open("resources/games/hexGames.json", "w") as f: json.dump(data,f,indent=4) @@ -154,7 +153,7 @@ def placeHex(channel : str,position : str, user): winAmount = data[channel]["difficulty"]*10 message += " Adding "+str(winAmount)+" GwendoBucks to their account." - data[channel]["lastMove"] = (int(position[1])-1, ord(position[0])-97) + data[channel]["gameHistory"].append((int(position[1])-1, ord(position[0])-97)) # Is it now Gwendolyn's turn? gwendoTurn = False @@ -189,6 +188,7 @@ def placeHex(channel : str,position : str, user): def placeOnHexBoard(board,player,position): # Translates the position position = position.lower() + # Error handling try: column = ord(position[0]) - 97 # ord() translates from letter to number row = int(position[1:]) - 1 @@ -198,7 +198,6 @@ def placeOnHexBoard(board,player,position): except: logThis("Invalid position (error code 1531)") return "Error. The position should be a letter followed by a number, e.g. \"e2\"." - # Place at the position if board[row][column] == 0: board[row][column] = player @@ -213,16 +212,19 @@ def undoHex(channel, user): with open("resources/games/hexGames.json", "r") as f: data = json.load(f) if user in data[channel]["players"]: - if data[channel]["lastMove"] != (-1,-1): + if len(data[channel]["gameHistory"]): turn = data[channel]["turn"] # You can only undo after your turn, which is the opponent's turn. if user == data[channel]["players"][(turn % 2)]: # If it's not your turn logThis("Undoing {}'s last move".format(getName(user))) - lastMove = data[channel]["lastMove"] + lastMove = data[channel]["gameHistory"].pop() data[channel]["board"][lastMove[0]][lastMove[1]] = 0 data[channel]["turn"] = turn%2 + 1 - + # Save the data + with open("resources/games/hexGames.json", "w") as f: + json.dump(data,f,indent=4) + # Update the board hexDraw.drawHexPlacement(channel,0,"abcdefghijk"[lastMove[1]]+str(lastMove[0]+1)) # The zero makes the hex disappear return "You undid", True, True, False, False @@ -246,7 +248,7 @@ def hexAI(channel): board = data[channel]["board"] player = data[channel]["players"].index("Gwendolyn")+1 #difficulty = data[channel]["difficulty"] - lastMove = data[channel]["lastMove"] + lastMove = data[channel]["gameHistory"][-1] # These moves are the last move +- 2. moves = [[(lastMove[0]+j-2,lastMove[1]+i-2) for i in range(5) if lastMove[1]+i-2 in range(11)] for j in range(5) if lastMove[0]+j-2 in range(11)] diff --git a/funcs/games/hexDraw.py b/funcs/games/hexDraw.py index a8d85ae..48df046 100644 --- a/funcs/games/hexDraw.py +++ b/funcs/games/hexDraw.py @@ -21,7 +21,7 @@ LINETHICKNESS = 15 HEXTHICKNESS = 6 # This is half the width of the background lining between every hex X_THICKNESS = HEXTHICKNESS * math.cos(math.pi/6) Y_THICKNESS = HEXTHICKNESS * math.sin(math.pi/6) -BACKGROUND_COLOR = (230,230,230) +BACKGROUND_COLOR = (235,235,235) BETWEEN_COLOR = BACKGROUND_COLOR BLANK_COLOR = "lightgrey" PIECECOLOR = {1:(237,41,57),2:(0,165,255),0:BLANK_COLOR} # player1 is red, player2 is blue @@ -41,16 +41,12 @@ SMOL_SIDELENGTH = SIDELENGTH * 0.6 def drawBoard(channel): logThis("Drawing empty Hex board") - - # Creates the empty image im = Image.new('RGB', size=(CANVAS_WIDTH, CANVAS_HEIGHT),color = BACKGROUND_COLOR) # 'd' is a shortcut to drawing on the image d = ImageDraw.Draw(im,"RGBA") - - - # The board is indexed with [row][column] + # Drawing all the hexagons for column in BOARDCOORDINATES: for startingPoint in column: x = startingPoint[0] @@ -98,22 +94,6 @@ def drawBoard(channel): d.text( (X_OFFSET + HEXAGONWIDTH*(i/2+11) + 30 , Y_OFFSET + 6 + i*HEXAGONHEIGHT) , str(i+1), font=fnt, fill=TEXTCOLOR) # Write player names and color - """ - with open("resources/games/hexGames.json", "r") as f: - data = json.load(f) - - board = data[channel]["board"] - # Getting player names - players = data[channel]["players"] - if players[0] == "Gwendolyn": - player1 = "Gwendolyn" - else: - player1 = getName(players[0]) - if players[1] == "Gwendolyn": - player2 = "Gwendolyn" - else: - player2 = getName(players[1]) -""" with open("resources/games/hexGames.json", "r") as f: data = json.load(f) for p in [1,2]: From 0f368f77317ce5cf20ed09c2ca9fefb21bbb93ff Mon Sep 17 00:00:00 2001 From: jona605a Date: Mon, 10 Aug 2020 00:57:24 +0200 Subject: [PATCH 04/13] :robot: HexAI! --- funcs/games/hex.py | 102 ++++++++++++++++++++--------------------- funcs/games/hexDraw.py | 4 +- 2 files changed, 52 insertions(+), 54 deletions(-) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index 7f9ac1b..f85cb17 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -246,9 +246,13 @@ def hexAI(channel): data = json.load(f) board = data[channel]["board"] - player = data[channel]["players"].index("Gwendolyn")+1 - #difficulty = data[channel]["difficulty"] - lastMove = data[channel]["gameHistory"][-1] + player = (data[channel]["players"].index("Gwendolyn")+1) % 2 + difficulty = data[channel]["difficulty"] + """ + if len(data[channel]["gameHistory"]): + lastMove = data[channel]["gameHistory"][-1] + else: + lastMove = (5,5) # These moves are the last move +- 2. moves = [[(lastMove[0]+j-2,lastMove[1]+i-2) for i in range(5) if lastMove[1]+i-2 in range(11)] for j in range(5) if lastMove[0]+j-2 in range(11)] @@ -263,33 +267,29 @@ def hexAI(channel): if board[candidate[0]][candidate[1]] == 0: chosenMove = candidate logThis("Last move was "+str(lastMove)) - logThis("Chosen move is "+str(chosenMove)) - """ - scores = [-math.inf,-math.inf,-math.inf,-math.inf,-math.inf,-math.inf,-math.inf] - for column in range(0,BOARDWIDTH): + logThis("Chosen move is "+str(chosenMove)) """ + + possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] + judgements = [-math.inf]*len(possiblePlaces) # All possible moves are yet to be judged + + + for i in possiblePlaces: testBoard = copy.deepcopy(board) - # Testing a move - testBoard = placeOnHexBoard(testBoard,player,column) - # Evaluating that move - if testBoard != None: - scores[column] = minimaxHex(testBoard,difficulty,player%2+1,player,-math.inf,math.inf,False) - logThis("Best score for column "+str(column)+" is "+str(scores[column])) + testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 1 + # Testing a move and evaluating it + judgements[i] = minimaxHex(testBoard,difficulty,-math.inf,math.inf,False) + logThis("Best score for place {} is {}".format((i // BOARDWIDTH,i % BOARDWIDTH),judgements[i])) - possibleScores = scores.copy() - - while (min(possibleScores) < (max(possibleScores)*0.9)): - possibleScores.remove(min(possibleScores)) - - highest_score = random.choice(possibleScores) - - indices = [i for i, x in enumerate(scores) if x == highest_score] - """ + bestScore = max(judgements) # the value of the best score(s) + indices = [i for i, x in enumerate(judgements) if x == bestScore] # which moves got that score? + i = random.choice(indices) + chosenMove = (i // BOARDWIDTH , i % BOARDWIDTH) placement = "abcdefghijk"[chosenMove[1]]+str(chosenMove[0]+1) return placeHex(channel,placement, "Gwendolyn") def evaluateBoard(board): - score = {1:0, 2:0} + scores = {1:0, 2:0} winner = 0 # Here, I use Dijkstra's algorithm to evaluate the board, as proposed by this article: https://towardsdatascience.com/hex-creating-intelligent-adversaries-part-2-heuristics-dijkstras-algorithm-597e4dcacf93 for player in [1,2]: @@ -316,47 +316,45 @@ def evaluateBoard(board): visited.add(u) #logThis("Distance from player {}'s start to {} is {}".format(player,u,Distance[u])) if u[player-1] == 10: # if the right coordinate of v is 10, it means we're at the goal - score[player] = Distance[u] # A player's score is the shortest distance to goal. Which equals the number of remaining moves they need to win if unblocked by the opponent. + scores[player] = Distance[u] # A player's score is the shortest distance to goal. Which equals the number of remaining moves they need to win if unblocked by the opponent. break else: logThis("For some reason, no path to the goal was found. ") - if score[player] == 0: + if scores[player] == 0: winner = player break # We don't need to check the other player's score, if player1 won. - return score, winner + return scores[2]-scores[1], winner -def minimaxHex(board, depth, player , originalPlayer, alpha, beta, maximizingPlayer): +def minimaxHex(board, depth, alpha, beta, maximizingPlayer): # The depth is how many moves ahead the computer checks. This value is the difficulty. - if depth == 0 or 0 not in sum(board,[0]): + if depth == 0 or 0 not in sum(board,[]): score = evaluateBoard(board) return score # if final depth is not reached, look another move ahead: - if maximizingPlayer: - value = -math.inf - for column in range(0,BOARDWIDTH): + if maximizingPlayer: # red player predicts next move + maxEval = -math.inf + possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] + for i in possiblePlaces: testBoard = copy.deepcopy(board) - testBoard = placeOnHexBoard(testBoard,player,column) - if testBoard != None: - evaluation = minimaxHex(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,False) - if evaluation < -9000: evaluation += AIScoresHex["avoid losing"] - value = max(value,evaluation) - alpha = max(alpha,evaluation) - if beta <= alpha: - break - return value - else: - value = math.inf - for column in range(0,BOARDWIDTH): + testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 1 # because maximizingPlayer is Red which is number 1 + evaluation = minimaxHex(testBoard,depth-1,alpha,beta,False) + maxEval = max(maxEval, evaluation) + alpha = max(alpha, evaluation) + if beta <= alpha: + break + return maxEval + else: # blue player predicts next move + minEval = math.inf + possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] + for i in possiblePlaces: testBoard = copy.deepcopy(board) - testBoard = placeOnHexBoard(testBoard,player,column) - if testBoard != None: - evaluation = minimaxHex(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,True) - if evaluation < -9000: evaluation += AIScoresHex["avoid losing"] - value = min(value,evaluation) - beta = min(beta,evaluation) - if beta <= alpha: - break - return value + testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 2 # because minimizingPlayer is Blue which is number 2 + evaluation = minimaxHex(testBoard,depth-1,alpha,beta,True) + minEval = min(minEval, evaluation) + beta = min(beta, evaluation) + if beta <= alpha: + break + return minEval diff --git a/funcs/games/hexDraw.py b/funcs/games/hexDraw.py index 48df046..d765872 100644 --- a/funcs/games/hexDraw.py +++ b/funcs/games/hexDraw.py @@ -21,8 +21,8 @@ LINETHICKNESS = 15 HEXTHICKNESS = 6 # This is half the width of the background lining between every hex X_THICKNESS = HEXTHICKNESS * math.cos(math.pi/6) Y_THICKNESS = HEXTHICKNESS * math.sin(math.pi/6) -BACKGROUND_COLOR = (235,235,235) -BETWEEN_COLOR = BACKGROUND_COLOR +BACKGROUND_COLOR = (230,230,230) +BETWEEN_COLOR = (231,231,231) BLANK_COLOR = "lightgrey" PIECECOLOR = {1:(237,41,57),2:(0,165,255),0:BLANK_COLOR} # player1 is red, player2 is blue BOARDCOORDINATES = [ [(X_OFFSET + HEXAGONWIDTH*(column + row/2),Y_OFFSET + HEXAGONHEIGHT*row) for column in range(11)] for row in range(11)] # These are the coordinates for the upperleft corner of every hex From cd4013aaf6683d3cb526e82af90c0aee8a29445f Mon Sep 17 00:00:00 2001 From: jona605a Date: Mon, 10 Aug 2020 01:00:39 +0200 Subject: [PATCH 05/13] :bug: --- funcs/games/hex.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index f85cb17..97357bc 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -246,7 +246,6 @@ def hexAI(channel): data = json.load(f) board = data[channel]["board"] - player = (data[channel]["players"].index("Gwendolyn")+1) % 2 difficulty = data[channel]["difficulty"] """ if len(data[channel]["gameHistory"]): @@ -271,7 +270,6 @@ def hexAI(channel): possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] judgements = [-math.inf]*len(possiblePlaces) # All possible moves are yet to be judged - for i in possiblePlaces: testBoard = copy.deepcopy(board) @@ -280,7 +278,8 @@ def hexAI(channel): judgements[i] = minimaxHex(testBoard,difficulty,-math.inf,math.inf,False) logThis("Best score for place {} is {}".format((i // BOARDWIDTH,i % BOARDWIDTH),judgements[i])) - bestScore = max(judgements) # the value of the best score(s) + GwenColor = data[channel]["players"].index("Gwendolyn") + 1 + bestScore = max(judgements) if (GwenColor == 1) else min(judgements) indices = [i for i, x in enumerate(judgements) if x == bestScore] # which moves got that score? i = random.choice(indices) chosenMove = (i // BOARDWIDTH , i % BOARDWIDTH) From 3ccb1587f31aee53fb93f88b40456cfaabf808e2 Mon Sep 17 00:00:00 2001 From: jona605a Date: Mon, 10 Aug 2020 01:41:09 +0200 Subject: [PATCH 06/13] :bug: --- funcs/games/hex.py | 39 +++++++++++---------------------------- funcs/games/hexDraw.py | 2 +- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index 97357bc..bbeeac4 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -247,43 +247,25 @@ def hexAI(channel): board = data[channel]["board"] difficulty = data[channel]["difficulty"] - """ - if len(data[channel]["gameHistory"]): - lastMove = data[channel]["gameHistory"][-1] - else: - lastMove = (5,5) - - # These moves are the last move +- 2. - moves = [[(lastMove[0]+j-2,lastMove[1]+i-2) for i in range(5) if lastMove[1]+i-2 in range(11)] for j in range(5) if lastMove[0]+j-2 in range(11)] - moves = sum(moves,[]) - chosenMove = None - safety = 0 - while chosenMove == None: - safety += 1 - if safety > 1000: - break - candidate = random.choice(moves) - if board[candidate[0]][candidate[1]] == 0: - chosenMove = candidate - logThis("Last move was "+str(lastMove)) - logThis("Chosen move is "+str(chosenMove)) """ - possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] judgements = [-math.inf]*len(possiblePlaces) # All possible moves are yet to be judged - + GwenColor = data[channel]["players"].index("Gwendolyn") + 1 # either 1 or 2 - red or blue + + current_score = evaluateBoard(board)[0] for i in possiblePlaces: testBoard = copy.deepcopy(board) testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 1 - # Testing a move and evaluating it - judgements[i] = minimaxHex(testBoard,difficulty,-math.inf,math.inf,False) - logThis("Best score for place {} is {}".format((i // BOARDWIDTH,i % BOARDWIDTH),judgements[i])) + if evaluateBoard(testBoard)[0] != current_score: # only think about a move if it improves the score (it's impossible to get worse) + # Testing a move and evaluating it + judgements[i] = minimaxHex(testBoard,difficulty,-math.inf,math.inf,GwenColor==2) + logThis("Best score for place {} is {}".format((i // BOARDWIDTH,i % BOARDWIDTH),judgements[i])) - GwenColor = data[channel]["players"].index("Gwendolyn") + 1 bestScore = max(judgements) if (GwenColor == 1) else min(judgements) indices = [i for i, x in enumerate(judgements) if x == bestScore] # which moves got that score? i = random.choice(indices) chosenMove = (i // BOARDWIDTH , i % BOARDWIDTH) placement = "abcdefghijk"[chosenMove[1]]+str(chosenMove[0]+1) + logThis("ChosenMove is {} at {}".format(chosenMove,placement)) return placeHex(channel,placement, "Gwendolyn") @@ -292,7 +274,6 @@ def evaluateBoard(board): winner = 0 # Here, I use Dijkstra's algorithm to evaluate the board, as proposed by this article: https://towardsdatascience.com/hex-creating-intelligent-adversaries-part-2-heuristics-dijkstras-algorithm-597e4dcacf93 for player in [1,2]: - logThis("Running Dijkstra for player "+str(player)) Distance = copy.deepcopy(EMPTY_DIJKSTRA) # Initialize the starting hexes. For the blue player, this is the leftmost column. For the red player, this is the tom row. for start in (ALL_POSITIONS[::11] if player == 2 else ALL_POSITIONS[:11]): @@ -329,12 +310,13 @@ def evaluateBoard(board): def minimaxHex(board, depth, alpha, beta, maximizingPlayer): # The depth is how many moves ahead the computer checks. This value is the difficulty. if depth == 0 or 0 not in sum(board,[]): - score = evaluateBoard(board) + score = evaluateBoard(board)[0] return score # if final depth is not reached, look another move ahead: if maximizingPlayer: # red player predicts next move maxEval = -math.inf possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] + #logThis("Judging a red move at depth {}".format(depth)) for i in possiblePlaces: testBoard = copy.deepcopy(board) testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 1 # because maximizingPlayer is Red which is number 1 @@ -347,6 +329,7 @@ def minimaxHex(board, depth, alpha, beta, maximizingPlayer): else: # blue player predicts next move minEval = math.inf possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] + #logThis("Judging a blue move at depth {}".format(depth)) for i in possiblePlaces: testBoard = copy.deepcopy(board) testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 2 # because minimizingPlayer is Blue which is number 2 diff --git a/funcs/games/hexDraw.py b/funcs/games/hexDraw.py index d765872..36772de 100644 --- a/funcs/games/hexDraw.py +++ b/funcs/games/hexDraw.py @@ -121,7 +121,7 @@ def drawBoard(channel): def drawHexPlacement(channel,player,position): FILEPATH = "resources/games/hexBoards/board"+channel+".png" - logThis("Drawing a newly placed hex. Filepath:"+FILEPATH) + logThis("Drawing a newly placed hex. Filepath: "+FILEPATH) # Translates position # We don't need to error-check, because the position is already checked in placeOnHexBoard() From 8b74e529896b041c09a73426d3a8b529eb3caab4 Mon Sep 17 00:00:00 2001 From: jona605a Date: Tue, 11 Aug 2020 18:08:05 +0200 Subject: [PATCH 07/13] :pencil: HexAI now bad again. But works --- funcs/games/gameLoops.py | 1 + funcs/games/hex.py | 34 +++++++++++++++++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/funcs/games/gameLoops.py b/funcs/games/gameLoops.py index d1fcb9e..db6564b 100644 --- a/funcs/games/gameLoops.py +++ b/funcs/games/gameLoops.py @@ -45,6 +45,7 @@ async def runHex(channel,command,user): try: response, showImage, deleteImage, gameDone, gwendoTurn = hexAI(str(channel.id)) except: + response, showImage, deleteImage, gameDone, gwendoTurn = "An AI error occured",False,False,False,False logThis("AI error (error code 1520)") await channel.send(response) logThis(response,str(channel.id)) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index bbeeac4..f005fbc 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -227,7 +227,7 @@ def undoHex(channel, user): # Update the board hexDraw.drawHexPlacement(channel,0,"abcdefghijk"[lastMove[1]]+str(lastMove[0]+1)) # The zero makes the hex disappear - return "You undid", True, True, False, False + return "You undid your last move at {}".format(lastMove), True, True, False, False else: # Sassy return "Nice try. You can't undo your opponent's move. ", False, False, False, False @@ -245,25 +245,44 @@ def hexAI(channel): with open("resources/games/hexGames.json", "r") as f: data = json.load(f) + + if len(data[channel]["gameHistory"]): + lastMove = data[channel]["gameHistory"][-1] + else: + lastMove = (5,5) + + # These moves are the last move +- 2. + moves = [[(lastMove[0]+j-2,lastMove[1]+i-2) for i in range(5) if lastMove[1]+i-2 in range(11)] for j in range(5) if lastMove[0]+j-2 in range(11)] + moves = sum(moves,[]) + movesCopy = moves.copy() + for move in movesCopy: + if board[move[0]][move[1]] != 0: + moves.remove(move) + chosenMove = random.choice(moves) + + """ + GwenColor = data[channel]["players"].index("Gwendolyn") + 1 # either 1 or 2 - red or blue + if len(data[channel]["gameHistory"]) == 0: + return placeHex(channel,"F6", "Gwendolyn") # If starting, start in the middle board = data[channel]["board"] difficulty = data[channel]["difficulty"] possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0] - judgements = [-math.inf]*len(possiblePlaces) # All possible moves are yet to be judged - GwenColor = data[channel]["players"].index("Gwendolyn") + 1 # either 1 or 2 - red or blue - + judgements = [float('nan')]*len(possiblePlaces) # All possible moves are yet to be judged + current_score = evaluateBoard(board)[0] for i in possiblePlaces: testBoard = copy.deepcopy(board) - testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 1 + testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = GwenColor if evaluateBoard(testBoard)[0] != current_score: # only think about a move if it improves the score (it's impossible to get worse) # Testing a move and evaluating it judgements[i] = minimaxHex(testBoard,difficulty,-math.inf,math.inf,GwenColor==2) logThis("Best score for place {} is {}".format((i // BOARDWIDTH,i % BOARDWIDTH),judgements[i])) - bestScore = max(judgements) if (GwenColor == 1) else min(judgements) + bestScore = max(judgements) if (GwenColor == 1) else min(judgements) # this line has an error indices = [i for i, x in enumerate(judgements) if x == bestScore] # which moves got that score? i = random.choice(indices) chosenMove = (i // BOARDWIDTH , i % BOARDWIDTH) + """ placement = "abcdefghijk"[chosenMove[1]]+str(chosenMove[0]+1) logThis("ChosenMove is {} at {}".format(chosenMove,placement)) return placeHex(channel,placement, "Gwendolyn") @@ -303,7 +322,6 @@ def evaluateBoard(board): if scores[player] == 0: winner = player break # We don't need to check the other player's score, if player1 won. - return scores[2]-scores[1], winner @@ -324,6 +342,7 @@ def minimaxHex(board, depth, alpha, beta, maximizingPlayer): maxEval = max(maxEval, evaluation) alpha = max(alpha, evaluation) if beta <= alpha: + logThis("Just pruned something!") break return maxEval else: # blue player predicts next move @@ -337,6 +356,7 @@ def minimaxHex(board, depth, alpha, beta, maximizingPlayer): minEval = min(minEval, evaluation) beta = min(beta, evaluation) if beta <= alpha: + logThis("Just pruned something!") break return minEval From 1d9831d77300a98dfe5082cadacda1ef63f826fd Mon Sep 17 00:00:00 2001 From: jona605a Date: Tue, 11 Aug 2020 18:18:24 +0200 Subject: [PATCH 08/13] :robot: --- funcs/games/hex.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index f005fbc..29211db 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -55,8 +55,8 @@ def parseHex(command, channel, user): players = data[channel]["players"] if user in players: opponent = (players.index(user) + 1) % 2 - data[channel]["winner"] = opponent - return "{} surrendered. Ending game.".format(getName(user)), False, False, True, False + data[channel]["winner"] = opponent + 1 + return "{} surrendered. That means {} won! ".format(getName(user),getName(players[opponent])), False, False, True, False else: return "You can't surrender when you're not a player.", False, False, False, False @@ -244,7 +244,7 @@ def hexAI(channel): logThis("Figuring out best move") with open("resources/games/hexGames.json", "r") as f: data = json.load(f) - + board = data[channel]["board"] if len(data[channel]["gameHistory"]): lastMove = data[channel]["gameHistory"][-1] @@ -342,7 +342,7 @@ def minimaxHex(board, depth, alpha, beta, maximizingPlayer): maxEval = max(maxEval, evaluation) alpha = max(alpha, evaluation) if beta <= alpha: - logThis("Just pruned something!") + #logThis("Just pruned something!") break return maxEval else: # blue player predicts next move @@ -356,7 +356,7 @@ def minimaxHex(board, depth, alpha, beta, maximizingPlayer): minEval = min(minEval, evaluation) beta = min(beta, evaluation) if beta <= alpha: - logThis("Just pruned something!") + #logThis("Just pruned something!") break return minEval From 71467bbe16d240734b9392a08d211aa30ed11818 Mon Sep 17 00:00:00 2001 From: jona605a Date: Tue, 11 Aug 2020 21:27:46 +0200 Subject: [PATCH 09/13] :sparkles: !hex swap --- funcs/games/hex.py | 16 ++++++++++++++-- funcs/games/hexDraw.py | 37 +++++++++++++++++++++++++++++++++++++ resources/errorCodes.txt | 1 + 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index 29211db..ee5360b 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -56,10 +56,22 @@ def parseHex(command, channel, user): if user in players: opponent = (players.index(user) + 1) % 2 data[channel]["winner"] = opponent + 1 - return "{} surrendered. That means {} won! ".format(getName(user),getName(players[opponent])), False, False, True, False + return "{} surrendered. That means {} won! Adding 30 Gwendobucks to their account.".format(getName(user),getName(players[opponent])), False, False, True, False else: return "You can't surrender when you're not a player.", False, False, False, False + # Swap + elif commands[0] == "swap": + with open("resources/games/hexGames.json", "r") as f: + data = json.load(f) + if len(data[channel]["gameHistory"]) == 1: # Only after the first move + data[channel]["players"] = data[channel]["players"][::-1] # Swaps their player-number + # Swaps the color of the hexes on the board drawing: + hexDraw.drawSwap(channel) + return "The color of both players were swapped.", True, True, False, False + else: + return "You can only swap as the second player after the very first move.", False, False, False, False + else: return "I didn't get that. Use \"!hex start [opponent]\" to start a game, \"!hex place [position]\" to place a piece, \"!hex undo\" to undo your last move or \"!hex stop\" to stop a current game.", False, False, False, False @@ -90,7 +102,7 @@ def hexStart(channel, user, opponent): return "I can't find that user", False, False, False, False else: # Opponent is another player - difficulty = 5 + difficulty = 3 diffText = "" # board is 11x11 diff --git a/funcs/games/hexDraw.py b/funcs/games/hexDraw.py index 36772de..1a6d360 100644 --- a/funcs/games/hexDraw.py +++ b/funcs/games/hexDraw.py @@ -150,3 +150,40 @@ def drawHexPlacement(channel,player,position): im.save(FILEPATH) except: logThis("Error drawing new hex on board (error code 1541") + +def drawSwap(channel): + FILEPATH = "resources/games/hexBoards/board"+channel+".png" + with open("resources/games/hexGames.json", "r") as f: + data = json.load(f) + # Opens the image + try: + with Image.open(FILEPATH) as im: + d = ImageDraw.Draw(im,"RGBA") + + # Write player names and color + for p in [1,2]: + playername = getName(data[channel]["players"][p-1]) + + x = X_NAME[p] + x -= NAME_fnt.getsize(playername)[0] if p==2 else 0 # player2's name is right-aligned + y = Y_NAME[p] + + # Draw a half-size Hexagon to indicate the player's color + x -= NAMEHEXPADDING # To the left of both names + d.polygon([ + (x, y), + (x+SMOL_WIDTH/2, y-SMOL_SIDELENGTH/2), + (x+SMOL_WIDTH, y), + (x+SMOL_WIDTH, y+SMOL_SIDELENGTH), + (x+SMOL_WIDTH/2, y+SMOL_SIDELENGTH*3/2), + (x, y+SMOL_SIDELENGTH), + ],fill = PIECECOLOR[p % 2 + 1]) + + # Save + im.save(FILEPATH) + except: + logThis("Error drawing swap (error code 1542)") + + + + \ No newline at end of file diff --git a/resources/errorCodes.txt b/resources/errorCodes.txt index cc76bb8..cbfec1c 100644 --- a/resources/errorCodes.txt +++ b/resources/errorCodes.txt @@ -114,6 +114,7 @@ 1532 - Cannot place on existing piece 1533 - Position out of bounds 1541 - Error loading board-image +1542 - Error swapping 16 - Monopoly 1600 - Unspecified error From 5623accd47350b132bb6b5f709dd15ba499431ec Mon Sep 17 00:00:00 2001 From: jona605a Date: Tue, 11 Aug 2020 21:31:50 +0200 Subject: [PATCH 10/13] :bug: --- funcs/games/hex.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index ee5360b..1c7cc12 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -17,6 +17,9 @@ HEX_DIRECTIONS = [(0,1),(-1,1),(-1,0),(0,-1),(1,-1),(1,0)] # Parses command def parseHex(command, channel, user): commands = command.lower().split() + with open("resources/games/hexGames.json", "r") as f: + data = json.load(f) + if command == "" or command == " ": return "I didn't get that. Use \"!hex start [opponent]\" to start a game.", False, False, False, False @@ -27,11 +30,12 @@ def parseHex(command, channel, user): logThis("Starting a hex game with hexStart(). "+str(user)+" challenged "+commands[1]) return hexStart(channel,user,commands[1]) # commands[1] is the opponent + # If using a command with no game, return error + elif channel not in data: + return "There's no game in this channel", False, False, False, False + # Stopping the game elif commands[0] == "stop": - with open("resources/games/hexGames.json", "r") as f: - data = json.load(f) - if user in data[channel]["players"]: return "Ending game.", False, False, True, False else: @@ -50,8 +54,6 @@ def parseHex(command, channel, user): # Surrender elif commands[0] == "surrender": - with open("resources/games/hexGames.json", "r") as f: - data = json.load(f) players = data[channel]["players"] if user in players: opponent = (players.index(user) + 1) % 2 @@ -62,8 +64,6 @@ def parseHex(command, channel, user): # Swap elif commands[0] == "swap": - with open("resources/games/hexGames.json", "r") as f: - data = json.load(f) if len(data[channel]["gameHistory"]) == 1: # Only after the first move data[channel]["players"] = data[channel]["players"][::-1] # Swaps their player-number # Swaps the color of the hexes on the board drawing: From f0044bcfb183cb28d78536a68b858a98d5553845 Mon Sep 17 00:00:00 2001 From: jona605a Date: Tue, 11 Aug 2020 21:57:17 +0200 Subject: [PATCH 11/13] :pencil: You can now play against yourself --- funcs/games/gameLoops.py | 2 +- funcs/games/hex.py | 21 ++++++++++++++------- funcs/games/hexDraw.py | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/funcs/games/gameLoops.py b/funcs/games/gameLoops.py index db6564b..1a9527c 100644 --- a/funcs/games/gameLoops.py +++ b/funcs/games/gameLoops.py @@ -62,7 +62,7 @@ async def runHex(channel,command,user): with open("resources/games/hexGames.json", "r") as f: data = json.load(f) winner = data[str(channel.id)]["winner"] - if winner != 0: + if winner != 0 and data[channel]["players"][0] != data[channel]["players"][1]: # player1 != player2 winnings = data[str(channel.id)]["difficulty"]*10 addMoney(data[str(channel.id)]["players"][winner-1].lower(),winnings) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index 1c7cc12..4295fc0 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -66,9 +66,13 @@ def parseHex(command, channel, user): elif commands[0] == "swap": if len(data[channel]["gameHistory"]) == 1: # Only after the first move data[channel]["players"] = data[channel]["players"][::-1] # Swaps their player-number + with open("resources/games/hexGames.json", "w") as f: + json.dump(data,f,indent=4) # Swaps the color of the hexes on the board drawing: hexDraw.drawSwap(channel) - return "The color of both players were swapped.", True, True, False, False + player2 = data[channel]["players"][1] + gwendoTurn = (player2 == "Gwendolyn") + return "The color of both players were swapped. It is now {}'s turn".format(player2), True, True, False, gwendoTurn else: return "You can only swap as the second player after the very first move.", False, False, False, False @@ -96,15 +100,13 @@ def hexStart(channel, user, opponent): return "That difficulty doesn't exist", False, False, False, False except: opponent = getID(opponent) - if opponent == user: - return "You can't play against yourself", False, False, False, False - elif opponent == None: + if opponent == None: return "I can't find that user", False, False, False, False else: # Opponent is another player difficulty = 3 diffText = "" - + # board is 11x11 board = [ [ 0 for i in range(BOARDWIDTH) ] for j in range(BOARDWIDTH) ] players = [user,opponent] @@ -132,9 +134,14 @@ def placeHex(channel : str,position : str, user): data = json.load(f) if channel in data: - if user in data[channel]["players"]: + players = data[channel]["players"] + if user in players: turn = data[channel]["turn"] - player = data[channel]["players"].index(user)+1 + if players[0] == players[1]: + player = turn + else: + player = players.index(user)+1 + if player == turn: board = data[channel]["board"] diff --git a/funcs/games/hexDraw.py b/funcs/games/hexDraw.py index 1a6d360..f457a01 100644 --- a/funcs/games/hexDraw.py +++ b/funcs/games/hexDraw.py @@ -162,7 +162,7 @@ def drawSwap(channel): # Write player names and color for p in [1,2]: - playername = getName(data[channel]["players"][p-1]) + playername = getName(data[channel]["players"][p%2]) x = X_NAME[p] x -= NAME_fnt.getsize(playername)[0] if p==2 else 0 # player2's name is right-aligned From e9e08ef0d6806bb82d2da1b7e50ec7cbafe750ae Mon Sep 17 00:00:00 2001 From: jona605a Date: Tue, 11 Aug 2020 22:02:40 +0200 Subject: [PATCH 12/13] :bug: --- funcs/games/hex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/funcs/games/hex.py b/funcs/games/hex.py index 4295fc0..205f1b4 100644 --- a/funcs/games/hex.py +++ b/funcs/games/hex.py @@ -162,7 +162,7 @@ def placeHex(channel : str,position : str, user): if winner == 0: # Continue with the game. gameWon = False - message = getName(data[channel]["players"][player-1])+" placed at "+position.upper()+". It's now "+getName(data[channel]["players"][turn-1])+"'s turn. The score is "+str(score) + message = getName(data[channel]["players"][player-1])+" placed at "+position.upper()+". It's now "+getName(data[channel]["players"][turn-1])+"'s turn."# The score is "+str(score) else: # Congratulations! gameWon = True From 25da2cc3eb94d3d28c3e0c580e2bcb78f7fcba58 Mon Sep 17 00:00:00 2001 From: Nikolaj Danger Date: Wed, 12 Aug 2020 13:45:05 +0200 Subject: [PATCH 13/13] :broom: --- funcs/games/fourInARow.py | 34 +++++++++++++++------------------- funcs/games/gameLoops.py | 2 +- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/funcs/games/fourInARow.py b/funcs/games/fourInARow.py index 3947681..cae9a17 100644 --- a/funcs/games/fourInARow.py +++ b/funcs/games/fourInARow.py @@ -2,6 +2,7 @@ import json import random import copy import math +import asyncio from . import fourInARowDraw from funcs import logThis, getName, getID @@ -13,7 +14,7 @@ AIScores = { "enemy two in a row": -35, "enemy three in a row": -200, "enemy win": -10000, - "win": 1000, + "win": 10000, "avoid losing": 100 } @@ -232,7 +233,7 @@ def isWon(board): return won, winDirection, winCoordinates # Plays as the AI -def fourInARowAI(channel): +async def fourInARowAI(channel): logThis("Figuring out best move") with open("resources/games/games.json", "r") as f: data = json.load(f) @@ -246,7 +247,7 @@ def fourInARowAI(channel): testBoard = copy.deepcopy(board) testBoard = placeOnBoard(testBoard,player,column) if testBoard != None: - scores[column] = minimax(testBoard,difficulty,player%2+1,player,-math.inf,math.inf,False) + scores[column] = await minimax(testBoard,difficulty,player%2+1,player,-math.inf,math.inf,False) logThis("Best score for column "+str(column)+" is "+str(scores[column])) possibleScores = scores.copy() @@ -266,12 +267,10 @@ def AICalcPoints(board,player): otherPlayer = player%2+1 # Adds points for middle placement - for row in range(len(board)): - if board[row][3] == player: - score += AIScores["middle"] - # Checks horizontal for row in range(rowCount): + if board[row][3] == player: + score += AIScores["middle"] rowArray = [int(i) for i in list(board[row])] for place in range(columnCount-3): window = rowArray[place:place+4] @@ -290,8 +289,6 @@ def AICalcPoints(board,player): window = [board[row][place],board[row+1][place+1],board[row+2][place+2],board[row+3][place+3]] score += evaluateWindow(window,player,otherPlayer) - # Checks left diagonal - for row in range(rowCount-3): for place in range(3,columnCount): window = [board[row][place],board[row+1][place-1],board[row+2][place-2],board[row+3][place-3]] score += evaluateWindow(window,player,otherPlayer) @@ -319,11 +316,12 @@ def evaluateWindow(window,player,otherPlayer): else: return 0 -def minimax(board, depth, player , originalPlayer, alpha, beta, maximizingPlayer): - terminal = ((isWon(board)[0] != 0) or (0 not in board[0])) +async def minimax(board, depth, player , originalPlayer, alpha, beta, maximizingPlayer): + #terminal = ((0 not in board[0]) or (isWon(board)[0] != 0)) + terminal = 0 not in board[0] + points = AICalcPoints(board,originalPlayer) # The depth is how many moves ahead the computer checks. This value is the difficulty. - if depth == 0 or terminal: - points = AICalcPoints(board,originalPlayer) + if depth == 0 or terminal or (points > 5000 or points < -6000): return points if maximizingPlayer: value = -math.inf @@ -331,12 +329,11 @@ def minimax(board, depth, player , originalPlayer, alpha, beta, maximizingPlayer testBoard = copy.deepcopy(board) testBoard = placeOnBoard(testBoard,player,column) if testBoard != None: - evaluation = minimax(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,False) + evaluation = await minimax(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,False) if evaluation < -9000: evaluation += AIScores["avoid losing"] value = max(value,evaluation) alpha = max(alpha,evaluation) - if beta <= alpha: - break + if beta <= alpha: break return value else: value = math.inf @@ -344,10 +341,9 @@ def minimax(board, depth, player , originalPlayer, alpha, beta, maximizingPlayer testBoard = copy.deepcopy(board) testBoard = placeOnBoard(testBoard,player,column) if testBoard != None: - evaluation = minimax(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,True) + evaluation = await minimax(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,True) if evaluation < -9000: evaluation += AIScores["avoid losing"] value = min(value,evaluation) beta = min(beta,evaluation) - if beta <= alpha: - break + if beta <= alpha: break return value diff --git a/funcs/games/gameLoops.py b/funcs/games/gameLoops.py index 1a9527c..4b3c143 100644 --- a/funcs/games/gameLoops.py +++ b/funcs/games/gameLoops.py @@ -94,7 +94,7 @@ async def fiar(channel,command,user): if gameDone == False: if gwendoTurn: try: - response, showImage, deleteImage, gameDone, gwendoTurn = fourInARowAI(str(channel.id)) + response, showImage, deleteImage, gameDone, gwendoTurn = await fourInARowAI(str(channel.id)) except: logThis("AI error (error code 1420)") await channel.send(response)