import random import copy import math import discord from .connectFourDraw import drawConnectFour AISCORES = { "middle": 3, "two in a row": 10, "three in a row": 50, "enemy two in a row": -35, "enemy three in a row": -200, "enemy win": -10000, "win": 10000, "avoid losing": 100 } ROWCOUNT = 6 COLUMNCOUNT = 7 class connectFour(): def __init__(self,bot): self.bot = bot self.draw = drawConnectFour(bot) # Starts the game async def start(self, ctx, opponent): try: await ctx.defer() except: self.bot.log("Defer failed") user = f"#{ctx.author.id}" channel = str(ctx.channel_id) game = self.bot.database["connect 4 games"].find_one({"_id":channel}) startedGame = False canStart = True if game != None: sendMessage = "There's already a connect 4 game going on in this channel" logMessage = "There was already a game going on" canStart = False else: if type(opponent) == int: # Opponent is Gwendolyn if opponent in range(1, 6): difficulty = int(opponent) diffText = f" with difficulty {difficulty}" opponent = f"#{self.bot.user.id}" else: sendMessage = "Difficulty doesn't exist" logMessage = "They tried to play against a difficulty that doesn't exist" canStart = False elif type(opponent) == discord.user: # Opponent is another player if ctx.author != opponent: opponent = f"#{opponent.id}" difficulty = 5 diffText = "" else: sendMessage = "You can't play against yourself" logMessage = "They tried to play against themself" canStart = False if canStart: board = [[0 for _ in range(COLUMNCOUNT)] for _ in range(ROWCOUNT)] players = [user, opponent] random.shuffle(players) newGame = {"_id":channel, "board": board, "winner":0, "win direction":"", "win coordinates":[0, 0], "players":players, "turn":0, "difficulty":difficulty} self.bot.database["connect 4 games"].insert_one(newGame) self.draw.drawImage(channel) gwendoTurn = (players[0] == f"#{self.bot.user.id}") startedGame = True opponentName = self.bot.databaseFuncs.getName(opponent) turnName = self.bot.databaseFuncs.getName(players[0]) startedText = f"Started game against {opponentName}{diffText}." turnText = f"It's {turnName}'s turn" sendMessage = f"{startedText} {turnText}" logMessage = "They started a game" self.bot.log(logMessage) await ctx.send(sendMessage) # Sets the whole game in motion if startedGame: filePath = f"resources/games/connect4Boards/board{ctx.channel_id}.png" oldImage = await ctx.channel.send(file = discord.File(filePath)) with open(f"resources/games/oldImages/connectFour{ctx.channel_id}", "w") as f: f.write(str(oldImage.id)) if gwendoTurn: await self.connectFourAI(ctx) else: reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] for reaction in reactions: await oldImage.add_reaction(reaction) # Places a piece at the lowest available point in a specific column async def placePiece(self, ctx, user, column): channel = str(ctx.channel.id) game = self.bot.database["connect 4 games"].find_one({"_id":channel}) playerNumber = game["players"].index(user)+1 userName = self.bot.databaseFuncs.getName(user) placedPiece = False if game is None: sendMessage = "There's no game in this channel" logMessage = "There was no game in the channel" else: board = game["board"] board = self.placeOnBoard(board, playerNumber, column) if board is None: sendMessage = "There isn't any room in that column" logMessage = "There wasn't any room in the column" else: self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"board":board}}) turn = (game["turn"]+1)%2 self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"turn":turn}}) self.bot.log("Checking for win") won, winDirection, winCoordinates = self.isWon(board) if won != 0: gameWon = True self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"winner":won}}) self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"win direction":winDirection}}) self.bot.database["connect 4 games"].update_one({"_id":channel}, {"$set":{"win coordinates":winCoordinates}}) sendMessage = f"{userName} placed a piece in column {column+1} and won." logMessage = f"{userName} won" winAmount = int(game["difficulty"])**2+5 if game["players"][won-1] != f"#{self.bot.user.id}": sendMessage += " Adding "+str(winAmount)+" GwendoBucks to their account" elif 0 not in board[0]: gameWon = True sendMessage = "It's a draw!" logMessage = "The game ended in a draw" else: gameWon = False otherUserName = self.bot.databaseFuncs.getName(game["players"][turn]) sendMessage = f"{userName} placed a piece in column {column+1}. It's now {otherUserName}'s turn" logMessage = "They placed the piece" gwendoTurn = (game["players"][turn] == f"#{self.bot.user.id}") placedPiece = True await ctx.channel.send(sendMessage) self.bot.log(logMessage) if placedPiece: self.draw.drawImage(channel) with open(f"resources/games/oldImages/connectFour{channel}", "r") as f: oldImage = await ctx.channel.fetch_message(int(f.read())) if oldImage is not None: await oldImage.delete() else: self.bot.log("The old image was already deleted") filePath = f"resources/games/connect4Boards/board{channel}.png" oldImage = await ctx.channel.send(file = discord.File(filePath)) if gameWon: self.endGame(channel) else: with open(f"resources/games/oldImages/connectFour{channel}", "w") as f: f.write(str(oldImage.id)) if gwendoTurn: await self.connectFourAI(ctx) else: reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] for reaction in reactions: await oldImage.add_reaction(reaction) # Returns a board where a piece has been placed in the column def placeOnBoard(self, board, player, column): placementX, placementY = -1, column for x, line in enumerate(board): if line[column] == 0: placementX = x board[placementX][placementY] = player if placementX == -1: return None else: return board def endGame(self, channel): game = self.bot.database["connect 4 games"].find_one({"_id":channel}) winner = game["winner"] if winner != 0: if game["players"][winner-1] != f"#{self.bot.user.id}": difficulty = int(game["difficulty"]) reward = difficulty**2 + 5 self.bot.money.addMoney(game["players"][winner-1], reward) self.bot.databaseFuncs.deleteGame("connect 4 games", channel) # Parses command async def surrender(self, ctx): try: await ctx.defer() except: self.bot.log("Defer failed") channel = str(ctx.channel_id) game = self.bot.database["connect 4 games"].find_one({"_id":channel}) if f"#{ctx.author.id}" in game["players"]: loserIndex = game["players"].index(f"#{ctx.author.id}") winnerIndex = (loserIndex+1)%2 winnerID = game["players"][winnerIndex] winnerName = self.bot.databaseFuncs.getName(winnerID) sendMessage = f"{ctx.author.display_name} surrenders." sendMessage += f" This means {winnerName} is the winner." if winnerID != f"#{self.bot.user.id}": difficulty = int(game["difficulty"]) reward = difficulty**2 + 5 sendMessage += f" Adding {reward} to their account" await ctx.send(sendMessage) with open(f"resources/games/oldImages/connectFour{channel}", "r") as f: oldImage = await ctx.channel.fetch_message(int(f.read())) if oldImage is not None: await oldImage.delete() else: self.bot.log("The old image was already deleted") self.endGame(channel) else: await ctx.send("You can't surrender when you're not a player") # Checks if someone has won the game and returns the winner def isWon(self, board): won = 0 winDirection = "" winCoordinates = [0,0] for row in range(ROWCOUNT): for place in range(COLUMNCOUNT): 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 async def connectFourAI(self, ctx): channel = str(ctx.channel.id) self.bot.log("Figuring out best move") game = self.bot.database["connect 4 games"].find_one({"_id":channel}) board = game["board"] player = game["players"].index(f"#{self.bot.user.id}")+1 difficulty = game["difficulty"] scores = [-math.inf for _ in range(COLUMNCOUNT)] for column in range(COLUMNCOUNT): testBoard = copy.deepcopy(board) testBoard = self.placeOnBoard(testBoard,player,column) if testBoard != None: scores[column] = await self.minimax(testBoard,difficulty,player%2+1,player,-math.inf,math.inf,False) self.bot.log(f"Best score for column {column} is {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) bestColumns = [i for i, x in enumerate(scores) if x == highest_score] placement = random.choice(bestColumns) await self.placePiece(ctx, f"#{self.bot.user.id}", placement) # Calculates points for a board def AICalcPoints(self,board,player): score = 0 otherPlayer = player%2+1 # Adds points for middle placement # 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] score += self.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 += self.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 += self.evaluateWindow(window,player,otherPlayer) 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 += self.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(self, 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 async def minimax(self, 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 = self.AICalcPoints(board,originalPlayer) # The depth is how many moves ahead the computer checks. This value is the difficulty. if depth == 0 or terminal or (points > 5000 or points < -6000): return points if maximizingPlayer: value = -math.inf for column in range(0,COLUMNCOUNT): testBoard = copy.deepcopy(board) testBoard = self.placeOnBoard(testBoard,player,column) if testBoard != None: evaluation = await self.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 = self.placeOnBoard(testBoard,player,column) if testBoard != None: evaluation = await self.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