diff --git a/Gwendolyn.py b/Gwendolyn.py index f8dec23..0cbe2dd 100644 --- a/Gwendolyn.py +++ b/Gwendolyn.py @@ -21,6 +21,79 @@ blackjackDecks = 4 # Variable for reacting to messages meanWords = ["stupid", "bitch", "fuck", "dumb", "idiot"] +# Runs Hex +async def hex(channel,command,user): + try: + response, showImage, deleteImage, gameDone, gwendoTurn = parseHex(command,str(channel),user) + except: + logThis("Error parsing command (error code 1510)") + + await channel.send(response) + logThis(response,str(channel)) + if showImage: + if deleteImage: + try: + oldImage = await deleteMessage("hex"+str(channel),channel) + except: + logThis("Error deleting old image (error code 1501)") + oldImage = await channel.send(file = discord.File("resources/games/hexBoards/board"+str(channel)+".png")) + if gameDone == False: + if gwendoTurn: + try: + response, showImage, deleteImage, gameDone, gwendoTurn = hexAI(str(channel)) + except: + logThis("AI error (error code 1420)") + await channel.send(response) + logThis(response,str(channel)) + if showImage: + if deleteImage: + await oldImage.delete() + oldImage = await channel.send(file = discord.File("resources/games/4InARowBoards/board"+str(channel)+".png")) + if gameDone == False: + with open("resources/games/oldImages/fourInARow"+str(channel), "w") as f: + f.write(str(oldImage.id)) + try: + reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] + for reaction in reactions: + await oldImage.add_reaction(reaction) + + except: + logThis("Image deleted before I could react to all of them") + + else: + with open("resources/games/oldImages/fourInARow"+str(channel), "w") as f: + f.write(str(oldImage.id)) + try: + reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] + for reaction in reactions: + await oldImage.add_reaction(reaction) + except: + logThis("Image deleted before I could react to all of them") +# else: +# with open("resources/games/oldImages/fourInARow"+str(channel), "w") as f: +# f.write(str(oldImage.id)) + + if gameDone: + with open("resources/games/games.json", "r") as f: + data = json.load(f) + + try: + with open("resources/games/oldImages/fourInARow"+str(channel), "r") as f: + oldImage = await channel.fetch_message(int(f.read())) + + await oldImage.delete() + except: + logThis("The old image was already deleted") + + winner = data["4 in a row games"][str(channel)]["winner"] + if winner != 0: + addMoney(data["4 in a row games"][str(channel)]["players"][winner-1].lower(),20) + with open("resources/games/games.json", "r") as f: + data = json.load(f) + + deleteGame("4 in a row games",str(channel)) + + # Runs Four in a Row async def fiar(channel,command,user): try: @@ -673,8 +746,14 @@ async def parseCommands(message,content): await fiar(message.channel,command,message.author.display_name) except: logThis("Something went wrong (error code 1400)") - - + + # Runs a game of Hex + elif content.startswith("hex"): + try: + command = content.replace("hex","") + await hex(message.channel,command,message.author.display_name) + except: + logThis("Something went wrong (error code 1500)") # Not a command else: diff --git a/funcs/games/fourInARow.py b/funcs/games/fourInARow.py index db2f24a..ac668a4 100644 --- a/funcs/games/fourInARow.py +++ b/funcs/games/fourInARow.py @@ -28,16 +28,22 @@ def fourInARowStart(channel, user, opponent): if user.lower() != opponent.lower(): if channel not in data["4 in a row games"]: - - if opponent.lower() in ["gwendolyn",""," "]: + + if opponent in ["1","2","3","4","5"]: + difficulty = int(opponent) opponent = "Gwendolyn" + elif opponent in ["0","6","7","8","9","10","69","100","420"]: + return "That difficulty doesn't exist", False, False, False, False + else: + # Opponent is another player + difficulty = "NA" board = [ [ 0 for i in range(columnCount) ] for j in range(rowCount) ] players = [user,opponent] random.shuffle(players) data["4 in a row games"][channel] = {"board": board,"winner":0,"win direction":"", - "win coordinates":[0,0],"players":players,"turn":0} + "win coordinates":[0,0],"players":players,"turn":0,"difficulty":difficulty} with open("resources/games/games.json", "w") as f: json.dump(data,f,indent=4) @@ -126,15 +132,15 @@ def placeOnBoard(board,player,column): # Parses command def parseFourInARow(command, channel, user): + commands = command.split() if command == "" or command == " ": - return "I didn't get that. \"Use !fourinarow start [opponent]\" to start a game.", False, False, False, False + return "I didn't get that. Use \"!fourinarow start [opponent]\" to start a game. To play against the computer, use difficulty 1 through 5 as the [opponent].", False, False, False, False + elif commands[0] == "start": + # Starting a game + return fourInARowStart(channel,user,commands[1]) # commands[1] is the opponent - - elif command.startswith(" start ") or command == (" start"): - command = command.replace(" start ","").replace(" start","") - return fourInARowStart(channel,user,command) - - elif command.startswith(" stop"): + # Stopping the game + elif commands[0] == "stop": with open("resources/games/games.json", "r") as f: data = json.load(f) @@ -142,14 +148,15 @@ def parseFourInARow(command, channel, user): return "Ending game.", False, False, True, False else: return "You can't end a game where you're not a player.", False, False, False, False - elif command.startswith(" place"): - commands = command.split(" ") - #try: - return placePiece(channel,int(commands[2]),int(commands[3])-1) - #except: - # return "I didn't quite get that", False, False, False + + # Placing manually + elif commands[0] == "place": + try: + return placePiece(channel,int(commands[1]),int(commands[2])-1) + except: + return "I didn't get that. To place a piece use \"!fourinarow place [player number] [column]\" or press the corresponding message-reaction beneath the board.", False, False, False, False else: - return "I didn't get that", False, False, False, False + return "I didn't get that. Use \"!fourinarow start [opponent]\" to start a game. To play against the computer, use difficulty 1 through 5 as the [opponent].", False, False, False, False # Checks if someone has won the game and returns the winner def isWon(board): @@ -217,13 +224,14 @@ def fourInARowAI(channel): board = data["4 in a row games"][channel]["board"] player = data["4 in a row games"][channel]["players"].index("Gwendolyn")+1 + difficulty = data["4 in a row games"][channel]["difficulty"] scores = [-math.inf,-math.inf,-math.inf,-math.inf,-math.inf,-math.inf,-math.inf] for column in range(0,columnCount): testBoard = copy.deepcopy(board) testBoard = placeOnBoard(testBoard,player,column) if testBoard != None: - scores[column] = minimax(testBoard,5,player%2+1,player,-math.inf,math.inf,False) + scores[column] = 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() @@ -298,6 +306,7 @@ def evaluateWindow(window,player,otherPlayer): def minimax(board, depth, player , originalPlayer, alpha, beta, maximizingPlayer): terminal = ((isWon(board)[0] != 0) or (0 not in board[0])) + # The depth is how many moves ahead the computer checks. This value is the difficulty. if depth == 0 or terminal: points = AICalcPoints(board,originalPlayer) return points diff --git a/funcs/games/hex.py b/funcs/games/hex.py new file mode 100644 index 0000000..e4ea18d --- /dev/null +++ b/funcs/games/hex.py @@ -0,0 +1,330 @@ +import json +import random +import copy +import math + +from . import hexDraw +from funcs import logThis + +# This is totally copied from the four in a row game. Just modified + +# Parses command +def parseHex(command, channel, user): + commands = command.split() + if command == "" or command == " ": + return "I didn't get that. Use \"!hex start [opponent]\" to start a game.", False, False, False, False + + elif commands[0] == "start": + # Starting a game + if len(commands) == 1: # if the commands is "!hex start", the opponent is Gwendolyn + commands.append("Gwendolyn") + return hexStart(channel,user,commands[1]) # commands[1] is the opponent + + # Stopping the game + elif commands[0] == "stop": + with open("resources/games/games.json", "r") as f: + data = json.load(f) + + if user in data["hex games"][channel]["players"]: + return "Ending game.", False, False, True, False + else: + return "You can't end a game where you're not a player.", False, False, False, False + + # Placing a piece + elif commands[0] == "place": + try: + return placePiece(channel,int(commands[1]),commands[2]) + except: + return "I didn't get that. To place a piece use \"!hex place [player number] [position]\". A valid position is e.g. \"e2\".", False, False, False, False + else: + return "I didn't get that. Use \"!hex start [opponent]\" to start a game or \"!hex stop\" to stop a current game.", False, False, False, False + + +# Starts the game +def hexStart(channel, user, opponent): + with open("resources/games/games.json", "r") as f: + data = json.load(f) + + if user.lower() != opponent.lower(): + if channel not in data["4 in a row games"]: + + if opponent in ["1","2","3","4","5"]: + difficulty = int(opponent) + opponent = "Gwendolyn" + elif opponent in ["0","6","7","8","9","10","69","100","420"]: + return "That difficulty doesn't exist", False, False, False, False + else: + # Opponent is another player + difficulty = "NA" + + board = [ [ 0 for i in range(columnCount) ] for j in range(rowCount) ] + players = [user,opponent] + random.shuffle(players) + + data["4 in a row games"][channel] = {"board": board,"winner":0,"win direction":"", + "win coordinates":[0,0],"players":players,"turn":0,"difficulty":difficulty} + + with open("resources/games/games.json", "w") as f: + json.dump(data,f,indent=4) + + fourInARowDraw.drawImage(channel) + + gwendoTurn = False + + if players[0] == "Gwendolyn": + gwendoTurn = True + + return "Started hex game against "+opponent+". It's "+players[0]+"'s turn", True, False, False, gwendoTurn + else: + return "There's already a hex game going on in this channel", False, False, False, False + else: + return "You can't play against yourself", False, False, False, False + +# Places a piece at the lowest available point in a specific column +def placePiece(channel : str,player : int,column : int): + with open("resources/games/games.json", "r") as f: + data = json.load(f) + + if channel in data["4 in a row games"]: + board = data["4 in a row games"][channel]["board"] + + board = placeOnBoard(board,player,column) + + + if board != None: + data["4 in a row games"][channel]["board"] = board + turn = (data["4 in a row games"][channel]["turn"]+1)%2 + data["4 in a row games"][channel]["turn"] = turn + + with open("resources/games/games.json", "w") as f: + json.dump(data,f,indent=4) + + logThis("Checking for win") + won, winDirection, winCoordinates = isWon(data["4 in a row games"][channel]["board"]) + + if won != 0: + gameWon = True + data["4 in a row games"][channel]["winner"] = won + data["4 in a row games"][channel]["win direction"] = winDirection + data["4 in a row games"][channel]["win coordinates"] = winCoordinates + + message = data["4 in a row games"][channel]["players"][won-1]+" won." + if data["4 in a row games"][channel]["players"][won-1] != "Gwendolyn": + message += " Adding 20 GwendoBucks to their account." + elif 0 not in board[0]: + gameWon = True + message = "It's a draw!" + else: + gameWon = False + message = data["4 in a row games"][channel]["players"][player-1]+" placed a piece in column "+str(column+1)+". It's now "+data["4 in a row games"][channel]["players"][turn]+"'s turn." + + with open("resources/games/games.json", "w") as f: + json.dump(data,f,indent=4) + + gwendoTurn = False + + if data["4 in a row games"][channel]["players"][turn] == "Gwendolyn": + logThis("It's Gwendolyn's turn") + gwendoTurn = True + + fourInARowDraw.drawImage(channel) + return message, True, True, gameWon, gwendoTurn + else: + return "There isn't any room in that column", True, True, False, False + else: + return "There's no game in this channel", False, False, False, False + +# Returns a board where a piece has been placed in the column +def placeOnBoard(board,player,column): + placementx, placementy = -1, column + + for x in range(len(board)): + if board[x][column] == 0: + placementx = x + + board[placementx][placementy] = player + + if placementx == -1: + return None + else: + return board + +# Checks if someone has won the game and returns the winner +def isWon(board): + won = 0 + winDirection = "" + winCoordinates = [0,0] + + for row in range(len(board)): + for place in range(len(board[row])): + if won == 0: + piecePlayer = board[row][place] + if piecePlayer != 0: + # Checks horizontal + if place <= columnCount-4: + pieces = [board[row][place+1],board[row][place+2],board[row][place+3]] + else: + pieces = [0] + + if all(x == piecePlayer for x in pieces): + won = piecePlayer + winDirection = "h" + winCoordinates = [row,place] + + # Checks vertical + if row <= rowCount-4: + pieces = [board[row+1][place],board[row+2][place],board[row+3][place]] + else: + pieces = [0] + + if all(x == piecePlayer for x in pieces): + won = piecePlayer + winDirection = "v" + winCoordinates = [row,place] + + # Checks right diagonal + if row <= rowCount-4 and place <= columnCount-4: + pieces = [board[row+1][place+1],board[row+2][place+2],board[row+3][place+3]] + else: + pieces = [0] + + if all(x == piecePlayer for x in pieces): + won = piecePlayer + winDirection = "r" + winCoordinates = [row,place] + + # Checks left diagonal + if row <= rowCount-4 and place >= 3: + pieces = [board[row+1][place-1],board[row+2][place-2],board[row+3][place-3]] + else: + pieces = [0] + + if all(x == piecePlayer for x in pieces): + won = piecePlayer + winDirection = "l" + winCoordinates = [row,place] + + + return won, winDirection, winCoordinates + +# Plays as the AI +def fourInARowAI(channel): + logThis("Figuring out best move") + with open("resources/games/games.json", "r") as f: + data = json.load(f) + + board = data["4 in a row games"][channel]["board"] + player = data["4 in a row games"][channel]["players"].index("Gwendolyn")+1 + difficulty = data["4 in a row games"][channel]["difficulty"] + + scores = [-math.inf,-math.inf,-math.inf,-math.inf,-math.inf,-math.inf,-math.inf] + for column in range(0,columnCount): + 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) + logThis("Best score for column "+str(column)+" is "+str(scores[column])) + + possibleScores = scores.copy() + + while (min(possibleScores) <= (max(possibleScores) - max(possibleScores)/10)) and len(possibleScores) != 1: + possibleScores.remove(min(possibleScores)) + + highest_score = random.choice(possibleScores) + + indices = [i for i, x in enumerate(scores) if x == highest_score] + placement = random.choice(indices) + return placePiece(channel,player,placement) + +# Calculates points for a board +def AICalcPoints(board,player): + score = 0 + 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): + rowArray = [int(i) for i in list(board[row])] + for place in range(columnCount-3): + window = rowArray[place:place+4] + score += evaluateWindow(window,player,otherPlayer) + + # Checks Vertical + for column in range(columnCount): + columnArray = [int(i[column]) for i in list(board)] + for place in range(rowCount-3): + window = columnArray[place:place+4] + score += evaluateWindow(window,player,otherPlayer) + + # Checks right diagonal + for row in range(rowCount-3): + for place in range(columnCount-3): + 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) + + + ## Checks if anyone has won + #won = isWon(board)[0] + + ## Add points if AI wins + #if won == player: + # score += AIScores["win"] + + return score + + +def evaluateWindow(window,player,otherPlayer): + if window.count(player) == 4: + return AIScores["win"] + elif window.count(player) == 3 and window.count(0) == 1: + return AIScores["three in a row"] + elif window.count(player) == 2 and window.count(0) == 2: + return AIScores["two in a row"] + elif window.count(otherPlayer) == 4: + return AIScores["enemy win"] + else: + return 0 + +def minimax(board, depth, player , originalPlayer, alpha, beta, maximizingPlayer): + terminal = ((isWon(board)[0] != 0) or (0 not in board[0])) + # The depth is how many moves ahead the computer checks. This value is the difficulty. + if depth == 0 or terminal: + points = AICalcPoints(board,originalPlayer) + return points + if maximizingPlayer: + value = -math.inf + for column in range(0,columnCount): + testBoard = copy.deepcopy(board) + testBoard = placeOnBoard(testBoard,player,column) + if testBoard != None: + evaluation = 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 + return value + else: + value = math.inf + for column in range(0,columnCount): + testBoard = copy.deepcopy(board) + testBoard = placeOnBoard(testBoard,player,column) + if testBoard != None: + evaluation = 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 + return value + diff --git a/funcs/games/hexDraw.py b/funcs/games/hexDraw.py new file mode 100644 index 0000000..31035bc --- /dev/null +++ b/funcs/games/hexDraw.py @@ -0,0 +1,2 @@ +# haha good joke as if I can do this manually +# just find a picture online \ No newline at end of file diff --git a/funcs/miscFuncs.py b/funcs/miscFuncs.py index 1576482..e8cfea2 100644 --- a/funcs/miscFuncs.py +++ b/funcs/miscFuncs.py @@ -191,6 +191,11 @@ def makeFiles(): os.makedirs("resources/games/4InARowBoards") logThis("The 4 in a row boards directory didn't exist") + # Creates the hexBoards foulder if it doesn't exist + if os.path.isdir("resources/games/hexBoards") == False: + os.makedirs("resources/games/hexBoards") + logThis("The Hex boards directory didn't exist") + # Creates the oldImages foulder if it doesn't exist if os.path.isdir("resources/games/oldImages") == False: os.makedirs("resources/games/oldImages") diff --git a/resources/errorCodes.txt b/resources/errorCodes.txt index e1061c1..6635067 100644 --- a/resources/errorCodes.txt +++ b/resources/errorCodes.txt @@ -90,4 +90,10 @@ 1400 - Unspecified 1401 - Error deleting old image 1410 - Unspecified parsing error -1420 - Unspecified AI error \ No newline at end of file +1420 - Unspecified AI error + +15 - Hex +1500 - Unspecified +1501 - Error deleting old image +1510 - Unspecified parsing error +1520 - Unspecified AI error