import random import copy import math import discord from PIL import Image, ImageDraw, ImageFont class ConnectFour(): def __init__(self,bot): self.bot = bot self.draw = drawConnectFour(bot) self.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 } self.ROWCOUNT = 6 self.COLUMNCOUNT = 7 # Starts the game async def start(self, ctx, opponent): await self.bot.defer(ctx) 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.member.Member: if opponent.bot: # User has challenged a bot if opponent == self.bot.user: # It was Gwendolyn difficulty = 3 diffText = f" with difficulty {difficulty}" opponent = f"#{self.bot.user.id}" else: sendMessage = "You can't challenge a bot!" logMessage = "They tried to challenge a bot" canStart = False else: # 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(self.COLUMNCOUNT)] for _ in range(self.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): await self.bot.defer(ctx) 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(self.ROWCOUNT): for place in range(self.COLUMNCOUNT): if won == 0: piecePlayer = board[row][place] if piecePlayer != 0: # Checks horizontal if place <= self.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 <= self.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 <= self.ROWCOUNT-4 and place <= self.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 <= self.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(self.COLUMNCOUNT)] for column in range(self.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(self.ROWCOUNT): if board[row][3] == player: score += self.AISCORES["middle"] rowArray = [int(i) for i in list(board[row])] for place in range(self.COLUMNCOUNT-3): window = rowArray[place:place+4] score += self.evaluateWindow(window,player,otherPlayer) # Checks Vertical for column in range(self.COLUMNCOUNT): columnArray = [int(i[column]) for i in list(board)] for place in range(self.ROWCOUNT-3): window = columnArray[place:place+4] score += self.evaluateWindow(window,player,otherPlayer) # Checks right diagonal for row in range(self.ROWCOUNT-3): for place in range(self.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,self.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 += self.AISCORES["win"] return score def evaluateWindow(self, window,player,otherPlayer): if window.count(player) == 4: return self.AISCORES["win"] elif window.count(player) == 3 and window.count(0) == 1: return self.AISCORES["three in a row"] elif window.count(player) == 2 and window.count(0) == 2: return self.AISCORES["two in a row"] elif window.count(otherPlayer) == 4: return self.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,self.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 += self.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,self.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 += self.AISCORES["avoid losing"] value = min(value,evaluation) beta = min(beta,evaluation) if beta <= alpha: break return value class drawConnectFour(): def __init__(self, bot): self.bot = bot # Draws the whole thing def drawImage(self, channel): self.bot.log("Drawing connect four board") game = self.bot.database["connect 4 games"].find_one({"_id":channel}) board = game["board"] border = 40 gridBorder = 40 cornerSize = 300 boardOutlineSize = 10 pieceOutlineSize = 10 emptyOutlineSize = 0 bottomBorder = 110 exampleCircles = 100 w, h = 2800,2400 backgroundColor = (230,230,234,255) boardOutlineColor = (0,0,0) pieceOutlineColor = (244,244,248) emptyOutlineColor = (0,0,0) player1Color = (254,74,73) player2Color = (254,215,102) boardColor = (42,183,202) placeSize = 300 white = (255,255,255,160) winBarColor = (250,250,250,255) fnt = ImageFont.truetype('resources/fonts/futura-bold.ttf', exampleCircles) boardSize = [w-(2*(border+gridBorder)),h-(2*(border+gridBorder))] placeGridSize = [math.floor(boardSize[0]/7),math.floor(boardSize[1]/6)] pieceStartx = (border+gridBorder)+math.floor(placeGridSize[0]/2)-math.floor(placeSize/2) pieceStarty = (border+gridBorder)+math.floor(placeGridSize[1]/2)-math.floor(placeSize/2) if game["players"][0] == "Gwendolyn": player1 = "Gwendolyn" else: player1 = self.bot.databaseFuncs.getName(game["players"][0]) if game["players"][1] == "Gwendolyn": player2 = "Gwendolyn" else: player2 = self.bot.databaseFuncs.getName(game["players"][1]) background = Image.new("RGB", (w,h+bottomBorder),backgroundColor) d = ImageDraw.Draw(background,"RGBA") # This whole part was the easiest way to make a rectangle with rounded corners and an outline # - Corners: d.ellipse([(border,border),(border+cornerSize,border+cornerSize)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) d.ellipse([(w-(border+cornerSize),h-(border+cornerSize)),(w-border,h-border)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) d.ellipse([(border,h-(border+cornerSize)),(border+cornerSize,h-border)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) d.ellipse([(w-(border+cornerSize),border),(w-border,border+cornerSize)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) # - Rectangle: d.rectangle([(border+math.floor(cornerSize/2),border),(w-(border+math.floor(cornerSize/2)),h-border)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) d.rectangle([(border,border+math.floor(cornerSize/2)),(w-border,h-(border+math.floor(cornerSize/2)))],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) # - Removing outline on the inside: d.rectangle([(border+math.floor(cornerSize/2),border+math.floor(cornerSize/2)),(w-(border+math.floor(cornerSize/2)),h-(border+math.floor(cornerSize/2)))],fill=boardColor) d.rectangle([(border+math.floor(cornerSize/2),border+boardOutlineSize),(w-(border+math.floor(cornerSize/2)),h-(border+boardOutlineSize))],fill=boardColor) d.rectangle([(border+boardOutlineSize,border+math.floor(cornerSize/2)),(w-(border+boardOutlineSize),h-(border+math.floor(cornerSize/2)))],fill=boardColor) for x, line in enumerate(board): for y, piece in enumerate(line): if piece == 1: pieceColor = player1Color outlineWidth = pieceOutlineSize outlineColor = pieceOutlineColor elif piece == 2: pieceColor = player2Color outlineWidth = pieceOutlineSize outlineColor = pieceOutlineColor else: pieceColor = backgroundColor outlineWidth = emptyOutlineSize outlineColor = emptyOutlineColor startx = pieceStartx + placeGridSize[0]*y starty = pieceStarty + placeGridSize[1]*x d.ellipse([(startx,starty),(startx+placeSize,starty+placeSize)],fill=pieceColor,outline=outlineColor,width=outlineWidth) if game["winner"] != 0: coordinates = game["win coordinates"] startx = border + placeGridSize[0]*coordinates[1] + gridBorder starty = border + placeGridSize[1]*coordinates[0] + gridBorder a = (placeGridSize[0]*4-gridBorder-border)**2 b = (placeGridSize[1]*4-gridBorder-border)**2 diagonalLength = (math.sqrt(a+b))/placeGridSize[0] diagonalAngle = math.degrees(math.atan(placeGridSize[1]/placeGridSize[0])) if game["win direction"] == "h": winBar = Image.new("RGBA",(placeGridSize[0]*4,placeGridSize[1]),(0,0,0,0)) winD = ImageDraw.Draw(winBar) winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) winD.ellipse([((placeGridSize[0]*3),0),(placeGridSize[0]*4,placeGridSize[1])],fill=white) winD.rectangle([(int(placeGridSize[0]*0.5),0),(int(placeGridSize[0]*3.5),placeGridSize[1])],fill=white) elif game["win direction"] == "v": winBar = Image.new("RGBA",(placeGridSize[0],placeGridSize[1]*4),(0,0,0,0)) winD = ImageDraw.Draw(winBar) winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) winD.ellipse([(0,(placeGridSize[1]*3)),(placeGridSize[0],placeGridSize[1]*4)],fill=white) winD.rectangle([0,(int(placeGridSize[1]*0.5)),(placeGridSize[0],int(placeGridSize[1]*3.5))],fill=white) elif game["win direction"] == "r": winBar = Image.new("RGBA",(int(placeGridSize[0]*diagonalLength),placeGridSize[1]),(0,0,0,0)) winD = ImageDraw.Draw(winBar) winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) winD.ellipse([((placeGridSize[0]*(diagonalLength-1)),0),(placeGridSize[0]*diagonalLength,placeGridSize[1])],fill=white) winD.rectangle([(int(placeGridSize[0]*0.5),0),(int(placeGridSize[0]*(diagonalLength-0.5)),placeGridSize[1])],fill=white) winBar = winBar.rotate(-diagonalAngle,expand=1) startx -= 90 starty -= 100 elif game["win direction"] == "l": winBar = Image.new("RGBA",(int(placeGridSize[0]*diagonalLength),placeGridSize[1]),(0,0,0,0)) winD = ImageDraw.Draw(winBar) winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) winD.ellipse([((placeGridSize[0]*(diagonalLength-1)),0),(placeGridSize[0]*diagonalLength,placeGridSize[1])],fill=white) winD.rectangle([(int(placeGridSize[0]*0.5),0),(int(placeGridSize[0]*(diagonalLength-0.5)),placeGridSize[1])],fill=white) winBar = winBar.rotate(diagonalAngle,expand=1) startx -= placeGridSize[0]*3 + 90 starty -= gridBorder + 60 mask = winBar.copy()#.convert("L") #mask.putalpha(128) #mask.save("test.png") winBarImage = Image.new("RGBA",mask.size,color=winBarColor) background.paste(winBarImage,(startx,starty),mask) # Bottom textPadding = 20 exampleHeight = h - border + int((bottomBorder+border)/2) - int(exampleCircles/2) d.ellipse([(border,exampleHeight),(border+exampleCircles),(exampleHeight+exampleCircles)],fill=player1Color,outline=boardOutlineColor,width=3) d.text((border+exampleCircles+textPadding,exampleHeight),player1,font=fnt,fill=(0,0,0)) textWidth = fnt.getsize(player2)[0] d.ellipse([(w-border-exampleCircles-textWidth-textPadding,exampleHeight),(w-border-textWidth-textPadding),(exampleHeight+exampleCircles)],fill=player2Color,outline=boardOutlineColor,width=3) d.text((w-border-textWidth,exampleHeight),player2,font=fnt,fill=(0,0,0)) background.save("resources/games/connect4Boards/board"+channel+".png")