""" Contains the classes that deal with playing and drawing connect four. *Classes* --------- ConnectFour DrawConnectFour """ import random # Used to shuffle players and by the ai to pick from # similar options import copy # Used to deepcopy boards import math # Used for math.inf import discord # Used for typehints, discord.file and to check whether # the opponent in ConnectFour.start is a discord.User from PIL import Image, ImageDraw, ImageFont # Used by DrawConnectFour() from discord_slash.context import SlashContext # Used for typehints from typing import Union # Used for typehints ROWCOUNT = 6 COLUMNCOUNT = 7 class ConnectFour(): """ Deals with connect four commands and logic. *Methods* --------- start(ctx: SlashContext, opponent: Union[int, Discord.User]) placePiece(ctx: Union[SlashContext, discord.Message], user: int, column: int) surrender(ctx: SlashContext) """ def __init__(self, bot): """Initialize the class.""" 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.REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"] async def start(self, ctx: SlashContext, opponent: Union[int, discord.User]): """ Start a game of connect four. *Parameters* ------------ ctx: SlashContext The context of the slash command. opponent: Union[int, discord.User] The requested opponent. If it's an int, the opponent is Gwendolyn with a difficulty equal to the value of the int. The difficulty is equal to how many levels the AI searches when minimaxing. """ 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 is not None: sendMessage = self.bot.long_strings["Connect 4 going on"] log_message = "There was already a game going on" canStart = False elif 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" log_message = "They challenged a difficulty that doesn't exist" canStart = False elif type(opponent) == discord.User: 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!" log_message = "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" log_message = "They tried to play against themself" canStart = False if canStart: x, y = COLUMNCOUNT, ROWCOUNT board = [[0 for _ in range(x)] for _ in range(y)] 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.database_funcs.get_name(opponent) turnName = self.bot.database_funcs.get_name(players[0]) startedText = f"Started game against {opponentName}{diffText}." turnText = f"It's {turnName}'s turn" sendMessage = f"{startedText} {turnText}" log_message = "They started a game" self.bot.log(log_message) await ctx.send(sendMessage) # Sets the whole game in motion if startedGame: boardsPath = "gwendolyn/resources/games/connect4Boards/" file_path = f"{boardsPath}board{ctx.channel_id}.png" old_image = await ctx.channel.send(file=discord.File(file_path)) old_imagesPath = "gwendolyn/resources/games/old_images/" old_imagePath = f"{old_imagesPath}connect_four{ctx.channel_id}" with open(old_imagePath, "w") as f: f.write(str(old_image.id)) if gwendoTurn: await self._connect_fourAI(ctx) else: for reaction in self.REACTIONS: await old_image.add_reaction(reaction) async def placePiece(self, ctx: Union[SlashContext, discord.Message], user: int, column: int): """ Place a piece on the board. *Parameters* ------------ ctx: Union[SlashContext, discord.Message] The context of the command/reaction. ctx can only be SlashContext if the piece is placed by the bot immediately after the player starts the game with a command. Otherwise, ctx will be the message the player reacted to when placing a piece. user: int The player-number of the player placing the piece. column: int The column the player is placing the piece in. """ channel = str(ctx.channel.id) connect4Games = self.bot.database["connect 4 games"] game = connect4Games.find_one({"_id": channel}) playerNumber = game["players"].index(user)+1 user_name = self.bot.database_funcs.get_name(user) placedPiece = False if game is None: sendMessage = "There's no game in this channel" log_message = "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" log_message = "There wasn't any room in the column" else: updater = {"$set": {"board": board}} connect4Games.update_one({"_id": channel}, updater) turn = (game["turn"]+1) % 2 updater = {"$set": {"turn": turn}} connect4Games.update_one({"_id": channel}, updater) self.bot.log("Checking for win") won, winDirection, winCoordinates = self._isWon(board) if won != 0: gameWon = True updater = {"$set": {"winner": won}} connect4Games.update_one({"_id": channel}, updater) updater = {"$set": {"win direction": winDirection}} connect4Games.update_one({"_id": channel}, updater) updater = {"$set": {"win coordinates": winCoordinates}} connect4Games.update_one({"_id": channel}, updater) sendMessage = "{} placed a piece in column {} and won. " sendMessage = sendMessage.format(user_name, column+1) log_message = f"{user_name} won" winAmount = int(game["difficulty"])**2+5 if game["players"][won-1] != f"#{self.bot.user.id}": sendMessage += "Adding {} GwendoBucks to their account" sendMessage = sendMessage.format(winAmount) elif 0 not in board[0]: gameWon = True sendMessage = "It's a draw!" log_message = "The game ended in a draw" else: gameWon = False otherUserId = game["players"][turn] otherUserName = self.bot.database_funcs.get_name(otherUserId) sendMessage = self.bot.long_strings["Connect 4 placed"] formatParams = [user_name, column+1, otherUserName] sendMessage = sendMessage.format(*formatParams) log_message = "They placed the piece" gwendoTurn = (game["players"][turn] == f"#{self.bot.user.id}") placedPiece = True await ctx.channel.send(sendMessage) self.bot.log(log_message) if placedPiece: self.draw.drawImage(channel) old_imagePath = f"gwendolyn/resources/games/old_images/connect_four{channel}" with open(old_imagePath, "r") as f: old_image = await ctx.channel.fetch_message(int(f.read())) if old_image is not None: await old_image.delete() else: self.bot.log("The old image was already deleted") file_path = f"gwendolyn/resources/games/connect4Boards/board{channel}.png" old_image = await ctx.channel.send(file=discord.File(file_path)) if gameWon: self._endGame(channel) else: with open(old_imagePath, "w") as f: f.write(str(old_image.id)) if gwendoTurn: await self._connect_fourAI(ctx) else: for reaction in self.REACTIONS: await old_image.add_reaction(reaction) async def surrender(self, ctx: SlashContext): """ Surrender a connect four game. *Parameters* ------------ ctx: SlashContext The context of the command. """ 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.database_funcs.get_name(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) old_imagePath = f"gwendolyn/resources/games/old_images/connect_four{channel}" with open(old_imagePath, "r") as f: old_image = await ctx.channel.fetch_message(int(f.read())) if old_image is not None: await old_image.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") def _placeOnBoard(self, board: dict, player: int, column: int): """ Place a piece in a given board. This differs from placePiece() by not having anything to do with any actual game. It just places the piece in a game dict. *Parameters* ------------ board: dict The board to place the piece in. player: int The player who places the piece. column: int The column the piece is placed in. *Returns* --------- board: dict The board with the placed piece. """ placementX, placementY = -1, column for x, line in list(enumerate(board))[::-1]: if line[column] == 0: placementX = x break board[placementX][placementY] = player if placementX == -1: return None else: return board def _endGame(self, channel: str): 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.database_funcs.delete_game("connect 4 games", channel) def _isWon(self, board: dict): 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: pieceOne = board[row][place+1] pieceTwo = board[row][place+2] pieceThree = board[row][place+3] pieces = [pieceOne, pieceTwo, pieceThree] else: pieces = [0] if all(x == piecePlayer for x in pieces): won = piecePlayer winDirection = "h" winCoordinates = [row, place] # Checks vertical if row <= ROWCOUNT-4: pieceOne = board[row+1][place] pieceTwo = board[row+2][place] pieceThree = board[row+3][place] pieces = [pieceOne, pieceTwo, pieceThree] else: pieces = [0] if all(x == piecePlayer for x in pieces): won = piecePlayer winDirection = "v" winCoordinates = [row, place] # Checks right diagonal goodRow = (row <= ROWCOUNT-4) goodColumn = (place <= COLUMNCOUNT-4) if goodRow and goodColumn: pieceOne = board[row+1][place+1] pieceTwo = board[row+2][place+2] pieceThree = board[row+3][place+3] pieces = [pieceOne, pieceTwo, pieceThree] 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: pieceOne = board[row+1][place-1] pieceTwo = board[row+2][place-2] pieceThree = board[row+3][place-3] pieces = [pieceOne, pieceTwo, pieceThree] else: pieces = [0] if all(x == piecePlayer for x in pieces): won = piecePlayer winDirection = "l" winCoordinates = [row, place] return won, winDirection, winCoordinates async def _connect_fourAI(self, ctx: SlashContext): def outOfRange(possibleScores: list): allowedRange = max(possibleScores)*(1-0.1) moreThanOne = len(possibleScores) != 1 return (min(possibleScores) <= allowedRange) and moreThanOne 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 is not None: _minimaxParams = [ testBoard, difficulty, player % 2+1, player, -math.inf, math.inf, False ] scores[column] = await self._minimax(*_minimaxParams) self.bot.log(f"Best score for column {column} is {scores[column]}") possibleScores = scores.copy() while outOfRange(possibleScores): possibleScores.remove(min(possibleScores)) highestScore = random.choice(possibleScores) bestColumns = [i for i, x in enumerate(scores) if x == highestScore] placement = random.choice(bestColumns) await self.placePiece(ctx, f"#{self.bot.user.id}", placement) def _AICalcPoints(self, board: dict, player: int): score = 0 otherPlayer = player % 2+1 # Adds points for middle placement # Checks horizontal for row in range(ROWCOUNT): if board[row][3] == player: score += self.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): pieceOne = board[row][place] pieceTwo = board[row+1][place+1] pieceThree = board[row+2][place+2] pieceFour = board[row+3][place+3] window = [pieceOne, pieceTwo, pieceThree, pieceFour] score += self._evaluateWindow(window, player, otherPlayer) for place in range(3, COLUMNCOUNT): pieceOne = board[row][place] pieceTwo = board[row+1][place-1] pieceThree = board[row+2][place-2] pieceFour = board[row+3][place-3] window = [pieceOne, pieceTwo, pieceThree, pieceFour] score += self._evaluateWindow(window, player, otherPlayer) return score def _evaluateWindow(self, window: list, player: int, otherPlayer: int): 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: dict, depth: int, player: int, originalPlayer: int, alpha: int, beta: int, maximizingPlayer: bool): """ Evaluate the minimax value of a board. *Parameters* ------------ board: dict The board to evaluate. depth: int How many more levels to check. If 0, the current value is returned. player: int The player who can place a piece. originalPlayer: int The AI player. alpha, beta: int Used with alpha/beta pruning, in order to prune options that will never be picked. maximizingPlayer: bool Whether the current player is the one trying to maximize their score. *Returns* --------- value: int The minimax value of the board. """ 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 is not None: _minimaxParams = [ testBoard, depth-1, (player % 2)+1, originalPlayer, alpha, beta, False ] evaluation = await self._minimax(*_minimaxParams) 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, COLUMNCOUNT): testBoard = copy.deepcopy(board) testBoard = self._placeOnBoard(testBoard, player, column) if testBoard is not None: _minimaxParams = [ testBoard, depth-1, (player % 2)+1, originalPlayer, alpha, beta, True ] evaluation = await self._minimax(*_minimaxParams) if evaluation < -9000: evaluation += self.AISCORES["avoid losing"] value = min(value, evaluation) beta = min(beta, evaluation) if beta <= alpha: break return value class DrawConnectFour(): """ Functions for drawing the connect four board. *Methods* --------- drawImage(channel: str) *Attributes* ------------ BORDER: int The border around the game board. GRIDBORDER: int The border between the edge of the board and the piece furthest towards the edge. CORNERSIZE: int The size of the corner circles. BOARDOUTLINESIZE: int How big the outline of the board is. BOARDOUTLINECOLOR: RGBA tuple The color of the outline of the board. PIECEOUTLINESIZE: int How big the outline of each piece is. PIECEOUTLINECOLOR: RGB tuple The color of the outline of each piece. EMPTYOUTLINESIZE: int How big the outline of an empty place on the board is. EMPTYOUTLINECOLOR: RGB tuple The color of the outline of an empty place on the board. BOTTOMBORDER: int The border at the bottom where the names are. TEXTSIZE: int The size of the text. TEXTPADDING: int How much padding to add to the the text. WIDTH, HEIGHT: int The size of the image, minus the bottom border BACKGROUNDCOLOR: RGBA tuple The color of the background. PLAYER1COLOR, PLAYER2COLOR: RGB tuple The color of each player. BOARDCOLOR: RGB tuple The color of the board WINBARCOLOR: RGBA tuple The color of the bar placed over the winning pieces. PIECESIZE: int The size of each piece. FONT: FreeTypeFont The font to use to write text. PIECEGRIDSIZE: list The size of each grid cell on the board. PIECESTARTX, PIECESTARTY: int Where the top left piece should be placed. WINBARWHITE: RGBA tuple White, but with the alpha set to winbarAlpha. """ def __init__(self, bot): """Initialize the class.""" self.bot = bot self.BORDER = 40 self.GRIDBORDER = 40 self.CORNERSIZE = 300 self.BOARDOUTLINESIZE = 10 self.BOARDOUTLINECOLOR = (0, 0, 0) self.PIECEOUTLINESIZE = 10 self.PIECEOUTLINECOLOR = (244, 244, 248) self.EMPTYOUTLINESIZE = 0 self.EMPTYOUTLINECOLOR = (0, 0, 0) self.BOTTOMBORDER = 110 self.TEXTSIZE = 100 self.TEXTPADDING = 20 self.WIDTH, self.HEIGHT = 2800, 2400 self.BACKGROUNDCOLOR = (230, 230, 234, 255) self.PLAYER1COLOR = (254, 74, 73) self.PLAYER2COLOR = (254, 215, 102) self.BOARDCOLOR = (42, 183, 202) winbarAlpha = 160 self.WINBARCOLOR = (250, 250, 250, 255) self.PIECESIZE = 300 fontPath = "gwendolyn/resources/fonts/futura-bold.ttf" self.FONT = ImageFont.truetype(fontPath, self.TEXTSIZE) boardWidth = self.WIDTH-(2*(self.BORDER+self.GRIDBORDER)) boardHeight = self.HEIGHT-(2*(self.BORDER+self.GRIDBORDER)) boardSize = [boardWidth, boardHeight] pieceWidth = boardSize[0]//COLUMNCOUNT pieceHeight = boardSize[1]//ROWCOUNT self.PIECEGRIDSIZE = [pieceWidth, pieceHeight] pieceStart = (self.BORDER+self.GRIDBORDER)-(self.PIECESIZE//2) self.PIECESTARTX = pieceStart+(self.PIECEGRIDSIZE[0]//2) self.PIECESTARTY = pieceStart+(self.PIECEGRIDSIZE[1]//2) self.WINBARWHITE = (255, 255, 255, winbarAlpha) # Draws the whole thing def drawImage(self, channel: str): """ Draw an image of the connect four board. *Parameters* ------------ channel: str The id of the channel the game is in. """ self.bot.log("Drawing connect four board") game = self.bot.database["connect 4 games"].find_one({"_id": channel}) board = game["board"] imageSize = (self.WIDTH, self.HEIGHT+self.BOTTOMBORDER) background = Image.new("RGB", imageSize, self.BACKGROUNDCOLOR) d = ImageDraw.Draw(background, "RGBA") self._drawBoard(d) self._drawPieces(d, board) if game["winner"] != 0: self._drawWin(background, game) self._drawFooter(d, game) background.save(f"gwendolyn/resources/games/connect4Boards/board{channel}.png") def _drawBoard(self, d: ImageDraw): # This whole part was the easiest way to make a rectangle with # rounded corners and an outline drawParams = { "fill": self.BOARDCOLOR, "outline": self.BOARDOUTLINECOLOR, "width": self.BOARDOUTLINESIZE } # - Corners -: cornerPositions = [ [ ( self.BORDER, self.BORDER ), ( self.BORDER+self.CORNERSIZE, self.BORDER+self.CORNERSIZE ) ], [ ( self.WIDTH-(self.BORDER+self.CORNERSIZE), self.HEIGHT-(self.BORDER+self.CORNERSIZE) ), ( self.WIDTH-self.BORDER, self.HEIGHT-self.BORDER ) ], [ ( self.BORDER, self.HEIGHT-(self.BORDER+self.CORNERSIZE) ), ( self.BORDER+self.CORNERSIZE, self.HEIGHT-self.BORDER ) ], [ ( self.WIDTH-(self.BORDER+self.CORNERSIZE), self.BORDER ), ( self.WIDTH-self.BORDER, self.BORDER+self.CORNERSIZE ) ] ] d.ellipse(cornerPositions[0], **drawParams) d.ellipse(cornerPositions[1], **drawParams) d.ellipse(cornerPositions[2], **drawParams) d.ellipse(cornerPositions[3], **drawParams) # - Rectangle -: rectanglePositions = [ [ ( self.BORDER+(self.CORNERSIZE//2), self.BORDER ), ( self.WIDTH-(self.BORDER+(self.CORNERSIZE//2)), self.HEIGHT-self.BORDER ) ], [ ( self.BORDER, self.BORDER+(self.CORNERSIZE//2) ), ( self.WIDTH-self.BORDER, self.HEIGHT-(self.BORDER+(self.CORNERSIZE//2)) ) ] ] d.rectangle(rectanglePositions[0], **drawParams) d.rectangle(rectanglePositions[1], **drawParams) # - Removing outline on the inside -: cleanUpPositions = [ [ ( self.BORDER+(self.CORNERSIZE//2), self.BORDER+(self.CORNERSIZE//2) ), ( self.WIDTH-(self.BORDER+(self.CORNERSIZE//2)), self.HEIGHT-(self.BORDER+(self.CORNERSIZE//2)) ) ], [ ( self.BORDER+(self.CORNERSIZE/2), self.BORDER+self.BOARDOUTLINESIZE ), ( self.WIDTH-(self.BORDER+(self.CORNERSIZE//2)), self.HEIGHT-(self.BORDER+self.BOARDOUTLINESIZE) ) ], [ ( self.BORDER+self.BOARDOUTLINESIZE, self.BORDER+(self.CORNERSIZE//2) ), ( self.WIDTH-(self.BORDER+self.BOARDOUTLINESIZE), self.HEIGHT-(self.BORDER+(self.CORNERSIZE//2)) ) ] ] d.rectangle(cleanUpPositions[0], fill=self.BOARDCOLOR) d.rectangle(cleanUpPositions[1], fill=self.BOARDCOLOR) d.rectangle(cleanUpPositions[2], fill=self.BOARDCOLOR) def _drawPieces(self, d: ImageDraw, board: list): for x, line in enumerate(board): for y, piece in enumerate(line): if piece == 1: pieceColor = self.PLAYER1COLOR outlineWidth = self.PIECEOUTLINESIZE outlineColor = self.PIECEOUTLINECOLOR elif piece == 2: pieceColor = self.PLAYER2COLOR outlineWidth = self.PIECEOUTLINESIZE outlineColor = self.PIECEOUTLINECOLOR else: pieceColor = self.BACKGROUNDCOLOR outlineWidth = self.EMPTYOUTLINESIZE outlineColor = self.EMPTYOUTLINECOLOR startX = self.PIECESTARTX + self.PIECEGRIDSIZE[0]*y startY = self.PIECESTARTY + self.PIECEGRIDSIZE[1]*x position = [ (startX, startY), (startX+self.PIECESIZE, startY+self.PIECESIZE) ] pieceParams = { "fill": pieceColor, "outline": outlineColor, "width": outlineWidth } d.ellipse(position, **pieceParams) def _drawWin(self, background: Image, game: dict): """ Draw the bar that shows the winning pieces. *Parameters* ------------ background: Image The image of the board. game: dict The game data. """ coordinates = game["win coordinates"] start = self.BORDER + self.GRIDBORDER startX = start + self.PIECEGRIDSIZE[0]*coordinates[1] startY = start + self.PIECEGRIDSIZE[1]*coordinates[0] a = (self.PIECEGRIDSIZE[0]*4-self.GRIDBORDER-self.BORDER)**2 b = (self.PIECEGRIDSIZE[1]*4-self.GRIDBORDER-self.BORDER)**2 diagonalLength = (math.sqrt(a+b))/self.PIECEGRIDSIZE[0] sizeRatio = self.PIECEGRIDSIZE[1]/self.PIECEGRIDSIZE[0] diagonalAngle = math.degrees(math.atan(sizeRatio)) if game["win direction"] == "h": imageSize = (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1]) winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0)) winD = ImageDraw.Draw(winBar) drawPositions = [ [ (0, 0), (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]) ], [ ((self.PIECEGRIDSIZE[0]*3), 0), (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1]) ], [ (int(self.PIECEGRIDSIZE[0]*0.5), 0), (int(self.PIECEGRIDSIZE[0]*3.5), self.PIECEGRIDSIZE[1]) ] ] winD.ellipse(drawPositions[0], fill=self.WINBARWHITE) winD.ellipse(drawPositions[1], fill=self.WINBARWHITE) winD.rectangle(drawPositions[2], fill=self.WINBARWHITE) elif game["win direction"] == "v": imageSize = (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4) winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0)) winD = ImageDraw.Draw(winBar) drawPositions = [ [ (0, 0), (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]) ], [ (0, (self.PIECEGRIDSIZE[1]*3)), (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4) ], [ (0, (int(self.PIECEGRIDSIZE[1]*0.5))), (self.PIECEGRIDSIZE[0], int(self.PIECEGRIDSIZE[1]*3.5)) ] ] winD.ellipse(drawPositions[0], fill=self.WINBARWHITE) winD.ellipse(drawPositions[1], fill=self.WINBARWHITE) winD.rectangle(drawPositions[2], fill=self.WINBARWHITE) elif game["win direction"] == "r": imageWidth = int(self.PIECEGRIDSIZE[0]*diagonalLength) imageSize = (imageWidth, self.PIECEGRIDSIZE[1]) winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0)) winD = ImageDraw.Draw(winBar) drawPositions = [ [ ( 0, 0 ), ( self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1] ) ], [ ( (self.PIECEGRIDSIZE[0]*(diagonalLength-1)), 0 ), ( self.PIECEGRIDSIZE[0]*diagonalLength, self.PIECEGRIDSIZE[1] ) ], [ ( int(self.PIECEGRIDSIZE[0]*0.5), 0 ), ( int(self.PIECEGRIDSIZE[0]*(diagonalLength-0.5)), self.PIECEGRIDSIZE[1] ) ] ] winD.ellipse(drawPositions[0], fill=self.WINBARWHITE) winD.ellipse(drawPositions[1], fill=self.WINBARWHITE) winD.rectangle(drawPositions[2], fill=self.WINBARWHITE) winBar = winBar.rotate(-diagonalAngle, expand=1) startX -= 90 startY -= 100 elif game["win direction"] == "l": imageWidth = int(self.PIECEGRIDSIZE[0]*diagonalLength) imageSize = (imageWidth, self.PIECEGRIDSIZE[1]) winBar = Image.new("RGBA", imageSize, (0, 0, 0, 0)) winD = ImageDraw.Draw(winBar) drawPositions = [ [ (0, 0), (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]) ], [ ( (self.PIECEGRIDSIZE[0]*(diagonalLength-1)), 0 ), ( self.PIECEGRIDSIZE[0]*diagonalLength, self.PIECEGRIDSIZE[1] ) ], [ ( int(self.PIECEGRIDSIZE[0]*0.5), 0 ), ( int(self.PIECEGRIDSIZE[0]*(diagonalLength-0.5)), self.PIECEGRIDSIZE[1] ) ] ] winD.ellipse(drawPositions[0], fill=self.WINBARWHITE) winD.ellipse(drawPositions[1], fill=self.WINBARWHITE) winD.rectangle(drawPositions[2], fill=self.WINBARWHITE) winBar = winBar.rotate(diagonalAngle, expand=1) startX -= self.PIECEGRIDSIZE[0]*3 + 90 startY -= self.GRIDBORDER + 60 mask = winBar.copy() winBarImage = Image.new("RGBA", mask.size, color=self.WINBARCOLOR) background.paste(winBarImage, (startX, startY), mask) def _drawFooter(self, d: ImageDraw, game: dict): if game["players"][0] == "Gwendolyn": player1 = "Gwendolyn" else: player1 = self.bot.database_funcs.get_name(game["players"][0]) if game["players"][1] == "Gwendolyn": player2 = "Gwendolyn" else: player2 = self.bot.database_funcs.get_name(game["players"][1]) exampleHeight = self.HEIGHT - self.BORDER exampleHeight += (self.BOTTOMBORDER+self.BORDER)//2 - self.TEXTSIZE//2 textWidth = self.FONT.getsize(player2)[0] exampleRPadding = self.BORDER+self.TEXTSIZE+textWidth+self.TEXTPADDING circlePositions = [ [ ( self.BORDER, exampleHeight ), ( self.BORDER+self.TEXTSIZE, exampleHeight+self.TEXTSIZE ) ], [ ( self.WIDTH-exampleRPadding, exampleHeight ), ( self.WIDTH-self.BORDER-textWidth-self.TEXTPADDING, exampleHeight+self.TEXTSIZE ) ] ] circleParams = { "outline": self.BOARDOUTLINECOLOR, "width": 3 } d.ellipse(circlePositions[0], fill=self.PLAYER1COLOR, **circleParams) d.ellipse(circlePositions[1], fill=self.PLAYER2COLOR, **circleParams) textPositions = [ (self.BORDER+self.TEXTSIZE+self.TEXTPADDING, exampleHeight), (self.WIDTH-self.BORDER-textWidth, exampleHeight) ] d.text(textPositions[0], player1, font=self.FONT, fill=(0, 0, 0)) d.text(textPositions[1], player2, font=self.FONT, fill=(0, 0, 0))