""" 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 from typing import Union # Used for typehints 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, ComponentContext # Used for # typehints from discord_slash.utils.manage_components import (create_button, create_actionrow) from discord_slash.model import ButtonStyle from gwendolyn.utils import encode_id from .game_base import BoardGame ROWCOUNT = 6 COLUMNCOUNT = 7 def _encode_board_string(board: list): string = [str(i) for row in board for i in row] while len(string) > 0 and string[0] == "0": string = string[1:] if string == "": string = "0" dec = 0 for i, digit in enumerate(string[::-1]): dec += (3**i)*int(digit) return str(dec) def _decode_board_string(board_string: str): dec = int(board_string) string = [] while dec: string.append(str(dec % 3)) dec = dec // 3 while len(string) < ROWCOUNT * COLUMNCOUNT: string.append("0") string = string[::-1] board = [ [int(x) for x in string[i*COLUMNCOUNT:i*COLUMNCOUNT+COLUMNCOUNT]] for i in range(ROWCOUNT)] return board class ConnectFour(BoardGame): """ Deals with connect four commands and logic. *Methods* --------- start(ctx: SlashContext, opponent: Union[int, Discord.User]) place_piece(ctx: Union[SlashContext, discord.Message], user: int, column: int) surrender(ctx: SlashContext) """ def __init__(self, bot): """Initialize the class.""" super().__init__(bot, "connectfour", DrawConnectFour) self.get_name = self.bot.database_funcs.get_name # pylint: disable=invalid-name 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 } # pylint: enable=invalid-name 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) channel = str(ctx.channel_id) opponent, opponent_info = await self._test_opponent(ctx, opponent) if not opponent_info: return difficulty = opponent_info[0] difficulty_text = opponent_info[1] board = [[0 for _ in range(COLUMNCOUNT)] for _ in range(ROWCOUNT)] players = [ctx.author.id, opponent] random.shuffle(players) self.draw.draw_image(channel, board, players, [0, [0,0], ""]) opponent_name = self.get_name(f"#{opponent}") turn_name = self.get_name(f"#{players[0]}") started_text = "Started game against {}{}.".format( opponent_name, difficulty_text ) turn_text = f"It's {turn_name}'s turn" await ctx.send(f"{started_text} {turn_text}") self.bot.log("They started a game") # Sets the whole game in motion boards_path = "gwendolyn/resources/games/connect_four_boards/" file_path = f"{boards_path}board{ctx.channel_id}.png" image_message = await ctx.send(file=discord.File(file_path)) board_string = _encode_board_string(board) if (players[0] == self.bot.user.id): await self._connect_four_ai( ctx, board_string, players, difficulty, image_message.id ) else: buttons = [] for i in range(7): custom_id = encode_id( [ "connectfour", "place", str(players[0]), board_string, str(i), str(players[0]), str(players[1]), str(difficulty), str(image_message.id) ] ) buttons.append(create_button( style=ButtonStyle.blue, label=str(i+1), custom_id=custom_id, disabled=(board[0][i] != 0) )) custom_id = encode_id( [ "connectfour", "end", str(players[0]), str(players[1]), str(image_message.id) ] ) buttons.append(create_button( style=ButtonStyle.red, label="Surrender", custom_id=custom_id )) action_rows = [] for x in range(((len(buttons)-1)//4)+1): row_buttons = buttons[ (x*4):(min(len(buttons),x*4+4)) ] action_rows.append(create_actionrow(*row_buttons)) await image_message.edit( components=action_rows ) async def place_piece(self, ctx: ComponentContext, board_string: str, column: int, players: list[int], difficulty: int, placer: int, message_id: int): """ Place a piece on the board. *Parameters* column: int The column the player is placing the piece in. """ old_message = await ctx.channel.fetch_message(message_id) await old_message.delete() player_number = players.index(placer)+1 user_name = self.get_name(f"#{placer}") placed_piece = False board = _decode_board_string(board_string) board = self._place_on_board(board, player_number, column) if board is None: send_message = "There isn't any room in that column" log_message = "There wasn't any room in the column" else: turn = player_number % 2 self.bot.log("Checking for win") winner, win_direction, win_coordinates = self._is_won(board) if winner != 0: game_won = True send_message = "{} placed a piece in column {} and won. " send_message = send_message.format(user_name, column+1) log_message = f"{user_name} won" win_amount = difficulty**2+5 if players[winner-1] != self.bot.user.id: send_message += "Adding {} GwendoBucks to their account" send_message = send_message.format(win_amount) elif 0 not in board[0]: game_won = True send_message = "It's a draw!" log_message = "The game ended in a draw" else: game_won = False other_user_id = f"#{players[turn]}" other_user_name = self.get_name(other_user_id) send_message = self.bot.long_strings["Connect 4 placed"] format_parameters = [user_name, column+1, other_user_name] send_message = send_message.format(*format_parameters) log_message = "They placed the piece" gwendolyn_turn = (players[turn] == self.bot.user.id) placed_piece = True self.bot.log(log_message) await ctx.channel.send(send_message) if placed_piece: channel = str(ctx.channel) self.draw.draw_image( channel, board, players, [winner, win_coordinates, win_direction] ) boards_path = "gwendolyn/resources/games/connect_four_boards/" file_path = boards_path + f"board{channel}.png" image_message = await ctx.channel.send(file=discord.File(file_path)) if game_won: self._end_game(winner, players, difficulty) else: board_string = _encode_board_string(board) if gwendolyn_turn: await self._connect_four_ai( ctx, board_string, players, difficulty, image_message.id ) else: buttons = [] for i in range(7): custom_id = encode_id( [ "connectfour", "place", str(players[turn]), board_string, str(i), str(players[0]), str(players[1]), str(difficulty), str(image_message.id) ] ) buttons.append(create_button( style=ButtonStyle.blue, label=str(i+1), custom_id=custom_id, disabled=(board[0][i] != 0) )) custom_id = encode_id( [ "connectfour", "end", str(players[0]), str(players[1]), str(difficulty), str(image_message.id) ] ) buttons.append(create_button( style=ButtonStyle.red, label="Surrender", custom_id=custom_id )) action_rows = [] for x in range(((len(buttons)-1)//4)+1): row_buttons = buttons[ (x*4):(min(len(buttons),x*4+4)) ] action_rows.append(create_actionrow(*row_buttons)) await image_message.edit( components=action_rows ) async def surrender(self, ctx: ComponentContext, players: list, difficulty: int, message_id: int): """ Surrender a connect four game. *Parameters* ------------ ctx: SlashContext The context of the command. """ loser_index = players.index(ctx.author.id) winner_index = (loser_index+1) % 2 winner_id = players[winner_index] winner_name = self.get_name(f"#{winner_id}") send_message = f"{ctx.author.display_name} surrenders." send_message += f" This means {winner_name} is the winner." if winner_id != self.bot.user.id: difficulty = int(difficulty) reward = difficulty**2 + 5 send_message += f" Adding {reward} to their account" await ctx.channel.send(send_message) self._end_game(winner_index+1, players, difficulty) old_image = await ctx.channel.fetch_message(message_id) await old_image.delete() def _place_on_board(self, board: dict, player: int, column: int): """ Place a piece in a given board. This differs from place_piece() 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. """ placement_x, placement_y = -1, column for i, line in list(enumerate(board))[::-1]: if line[column] == 0: placement_x = i break board[placement_x][placement_y] = player if placement_x == -1: return None else: return board def _end_game(self, winner: int, players: list, difficulty: int): if winner != 0: if players[winner-1] != f"#{self.bot.user.id}": difficulty = int(difficulty) reward = difficulty**2 + 5 self.bot.money.addMoney(f"#{players[winner-1]}", reward) def _is_won(self, board: dict): won = 0 win_direction = "" win_coordinates = [0, 0] for row in range(ROWCOUNT): for place in range(COLUMNCOUNT): if won == 0: piece_player = board[row][place] if piece_player != 0: # Checks horizontal if place <= COLUMNCOUNT-4: piece_one = board[row][place+1] piece_two = board[row][place+2] piece_three = board[row][place+3] pieces = [piece_one, piece_two, piece_three] else: pieces = [0] if all(x == piece_player for x in pieces): won = piece_player win_direction = "h" win_coordinates = [row, place] # Checks vertical if row <= ROWCOUNT-4: piece_one = board[row+1][place] piece_two = board[row+2][place] piece_three = board[row+3][place] pieces = [piece_one, piece_two, piece_three] else: pieces = [0] if all(x == piece_player for x in pieces): won = piece_player win_direction = "v" win_coordinates = [row, place] # Checks right diagonal good_row = (row <= ROWCOUNT-4) good_column = (place <= COLUMNCOUNT-4) if good_row and good_column: piece_one = board[row+1][place+1] piece_two = board[row+2][place+2] piece_three = board[row+3][place+3] pieces = [piece_one, piece_two, piece_three] else: pieces = [0] if all(x == piece_player for x in pieces): won = piece_player win_direction = "r" win_coordinates = [row, place] # Checks left diagonal if row <= ROWCOUNT-4 and place >= 3: piece_one = board[row+1][place-1] piece_two = board[row+2][place-2] piece_three = board[row+3][place-3] pieces = [piece_one, piece_two, piece_three] else: pieces = [0] if all(i == piece_player for i in pieces): won = piece_player win_direction = "l" win_coordinates = [row, place] return won, win_direction, win_coordinates async def _connect_four_ai(self, ctx: Union[SlashContext, ComponentContext], board_string: str, players: list, difficulty: int, message_id: int): def out_of_range(possible_scores: list): allowed_range = max(possible_scores)*(1-0.1) more_than_one = len(possible_scores) != 1 return (min(possible_scores) <= allowed_range) and more_than_one self.bot.log("Figuring out best move") board = _decode_board_string(board_string) player = players.index(self.bot.user.id)+1 scores = [-math.inf for _ in range(COLUMNCOUNT)] for column in range(COLUMNCOUNT): test_board = copy.deepcopy(board) test_board = self._place_on_board(test_board, player, column) if test_board is not None: minimax_parameters = [ test_board, difficulty, player % 2+1, player, -math.inf, math.inf, False ] scores[column] = await self._minimax(*minimax_parameters) self.bot.log(f"Best score for column {column} is {scores[column]}") possible_scores = scores.copy() while out_of_range(possible_scores): possible_scores.remove(min(possible_scores)) highest_score = random.choice(possible_scores) best_columns = [i for i, x in enumerate(scores) if x == highest_score] placement = random.choice(best_columns) await self.place_piece( ctx, board_string, placement, players, difficulty, self.bot.user.id, message_id ) def _ai_calc_points(self, board: dict, player: int): score = 0 other_player = player % 2+1 # Adds points for middle placement # Checks horizontal for row in range(ROWCOUNT): if board[row][3] == player: score += self.AISCORES["middle"] row_array = [int(i) for i in list(board[row])] for place in range(COLUMNCOUNT-3): window = row_array[place:place+4] score += self._evaluate_window(window, player, other_player) # Checks Vertical for column in range(COLUMNCOUNT): column_array = [int(i[column]) for i in list(board)] for place in range(ROWCOUNT-3): window = column_array[place:place+4] score += self._evaluate_window(window, player, other_player) # Checks right diagonal for row in range(ROWCOUNT-3): for place in range(COLUMNCOUNT-3): piece_one = board[row][place] piece_two = board[row+1][place+1] piece_three = board[row+2][place+2] piece_four = board[row+3][place+3] window = [piece_one, piece_two, piece_three, piece_four] score += self._evaluate_window(window, player, other_player) for place in range(3, COLUMNCOUNT): piece_one = board[row][place] piece_two = board[row+1][place-1] piece_three = board[row+2][place-2] piece_four = board[row+3][place-3] window = [piece_one, piece_two, piece_three, piece_four] score += self._evaluate_window(window, player, other_player) return score def _evaluate_window(self, window: list, player: int, other_player: 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(other_player) == 4: return self.AISCORES["enemy win"] else: return 0 async def _minimax(self, board: dict, depth: int, player: int, original_player: int, alpha: int, beta: int, maximizing_player: 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. original_player: int The AI player. alpha, beta: int Used with alpha/beta pruning, in order to prune options that will never be picked. maximizing_player: 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._ai_calc_points(board, original_player) # 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 maximizing_player: value = -math.inf for column in range(0, COLUMNCOUNT): test_board = copy.deepcopy(board) test_board = self._place_on_board(test_board, player, column) if test_board is not None: minimax_parameters = [ test_board, depth-1, (player % 2)+1, original_player, alpha, beta, False ] evaluation = await self._minimax(*minimax_parameters) 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): test_board = copy.deepcopy(board) test_board = self._place_on_board(test_board, player, column) if test_board is not None: minimax_parameters = [ test_board, depth-1, (player % 2)+1, original_player, alpha, beta, True ] evaluation = await self._minimax(*minimax_parameters) 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* --------- draw_image(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 win_bar_alpha. """ def __init__(self, bot, game): """Initialize the class.""" self.bot = bot self.get_name = self.bot.database_funcs.get_name win_bar_alpha = 160 font_path = "gwendolyn/resources/fonts/futura-bold.ttf" # pylint: disable=invalid-name 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) self.WINBARCOLOR = (250, 250, 250, 255) self.PIECESIZE = 300 # pylint: enable=invalid-name board_width = self.WIDTH-(2*(self.BORDER+self.GRIDBORDER)) board_height = self.HEIGHT-(2*(self.BORDER+self.GRIDBORDER)) board_size = [board_width, board_height] piece_width = board_size[0]//COLUMNCOUNT piece_height = board_size[1]//ROWCOUNT piece_start = (self.BORDER+self.GRIDBORDER)-(self.PIECESIZE//2) # pylint: disable=invalid-name self.FONT = ImageFont.truetype(font_path, self.TEXTSIZE) self.PIECEGRIDSIZE = [piece_width, piece_height] self.PIECESTARTX = piece_start+(self.PIECEGRIDSIZE[0]//2) self.PIECESTARTY = piece_start+(self.PIECEGRIDSIZE[1]//2) self.WINBARWHITE = (255, 255, 255, win_bar_alpha) # pylint: enable=invalid-name # Draws the whole thing def draw_image(self, channel: str, board: list, players: list, win_info: list): """ Draw an image of the connect four board. *Parameters* ------------ """ self.bot.log("Drawing connect four board") image_size = (self.WIDTH, self.HEIGHT+self.BOTTOMBORDER) background = Image.new("RGB", image_size, self.BACKGROUNDCOLOR) drawer = ImageDraw.Draw(background, "RGBA") self._draw_board(drawer) self._draw_pieces(drawer, board) if win_info[0] != 0: self._draw_win(background, win_info[1], win_info[2]) self._draw_footer(drawer, players) boards_path = "gwendolyn/resources/games/connect_four_boards/" background.save(boards_path + f"board{channel}.png") def _draw_board(self, drawer: ImageDraw): # This whole part was the easiest way to make a rectangle with # rounded corners and an outline draw_parameters = { "fill": self.BOARDCOLOR, "outline": self.BOARDOUTLINECOLOR, "width": self.BOARDOUTLINESIZE } # - Corners -: corner_positions = [ [ ( 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 ) ] ] drawer.ellipse(corner_positions[0], **draw_parameters) drawer.ellipse(corner_positions[1], **draw_parameters) drawer.ellipse(corner_positions[2], **draw_parameters) drawer.ellipse(corner_positions[3], **draw_parameters) # - Rectangle -: rectangle_positions = [ [ ( 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)) ) ] ] drawer.rectangle(rectangle_positions[0], **draw_parameters) drawer.rectangle(rectangle_positions[1], **draw_parameters) # - Removing outline on the inside -: clean_up_positions = [ [ ( 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)) ) ] ] drawer.rectangle(clean_up_positions[0], fill=self.BOARDCOLOR) drawer.rectangle(clean_up_positions[1], fill=self.BOARDCOLOR) drawer.rectangle(clean_up_positions[2], fill=self.BOARDCOLOR) def _draw_pieces(self, drawer: ImageDraw, board: list): for piece_x, line in enumerate(board): for piece_y, piece in enumerate(line): if piece == 1: piece_color = self.PLAYER1COLOR outline_width = self.PIECEOUTLINESIZE outline_color = self.PIECEOUTLINECOLOR elif piece == 2: piece_color = self.PLAYER2COLOR outline_width = self.PIECEOUTLINESIZE outline_color = self.PIECEOUTLINECOLOR else: piece_color = self.BACKGROUNDCOLOR outline_width = self.EMPTYOUTLINESIZE outline_color = self.EMPTYOUTLINECOLOR start_x = self.PIECESTARTX + self.PIECEGRIDSIZE[0]*piece_y start_y = self.PIECESTARTY + self.PIECEGRIDSIZE[1]*piece_x position = [ (start_x, start_y), (start_x+self.PIECESIZE, start_y+self.PIECESIZE) ] piece_parameters = { "fill": piece_color, "outline": outline_color, "width": outline_width } drawer.ellipse(position, **piece_parameters) def _draw_win(self, background: Image, win_coordinates: list, win_direction: str): """ Draw the bar that shows the winning pieces. *Parameters* ------------ background: Image The image of the board. game: dict The game data. """ start = self.BORDER + self.GRIDBORDER start_x = start + self.PIECEGRIDSIZE[0]*win_coordinates[1] start_y = start + self.PIECEGRIDSIZE[1]*win_coordinates[0] diagonal_length = ( (math.sqrt( (self.PIECEGRIDSIZE[0]*4-self.GRIDBORDER-self.BORDER)**2+ (self.PIECEGRIDSIZE[1]*4-self.GRIDBORDER-self.BORDER)**2 ))/ self.PIECEGRIDSIZE[0] ) size_ratio = self.PIECEGRIDSIZE[1]/self.PIECEGRIDSIZE[0] diagonal_angle = math.degrees(math.atan(size_ratio)) if win_direction == "h": image_size = (self.PIECEGRIDSIZE[0]*4, self.PIECEGRIDSIZE[1]) win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0)) win_drawer = ImageDraw.Draw(win_bar) draw_position = [ [ (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]) ] ] win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE) win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) elif win_direction == "v": image_size = (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]*4) win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0)) win_drawer = ImageDraw.Draw(win_bar) draw_position = [ [ (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)) ] ] win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE) win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) elif win_direction == "r": image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length) image_size = (image_width, self.PIECEGRIDSIZE[1]) win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0)) win_drawer = ImageDraw.Draw(win_bar) draw_position = [ [ ( 0, 0 ), ( self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1] ) ], [ ( (self.PIECEGRIDSIZE[0]*(diagonal_length-1)), 0 ), ( self.PIECEGRIDSIZE[0]*diagonal_length, self.PIECEGRIDSIZE[1] ) ], [ ( int(self.PIECEGRIDSIZE[0]*0.5), 0 ), ( int(self.PIECEGRIDSIZE[0]*(diagonal_length-0.5)), self.PIECEGRIDSIZE[1] ) ] ] win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE) win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) win_bar = win_bar.rotate(-diagonal_angle, expand=1) start_x -= 90 start_y -= 100 elif win_direction == "l": image_width = int(self.PIECEGRIDSIZE[0]*diagonal_length) image_size = (image_width, self.PIECEGRIDSIZE[1]) win_bar = Image.new("RGBA", image_size, (0, 0, 0, 0)) win_drawer = ImageDraw.Draw(win_bar) draw_position = [ [ (0, 0), (self.PIECEGRIDSIZE[0], self.PIECEGRIDSIZE[1]) ], [ ( (self.PIECEGRIDSIZE[0]*(diagonal_length-1)), 0 ), ( self.PIECEGRIDSIZE[0]*diagonal_length, self.PIECEGRIDSIZE[1] ) ], [ ( int(self.PIECEGRIDSIZE[0]*0.5), 0 ), ( int(self.PIECEGRIDSIZE[0]*(diagonal_length-0.5)), self.PIECEGRIDSIZE[1] ) ] ] win_drawer.ellipse(draw_position[0], fill=self.WINBARWHITE) win_drawer.ellipse(draw_position[1], fill=self.WINBARWHITE) win_drawer.rectangle(draw_position[2], fill=self.WINBARWHITE) win_bar = win_bar.rotate(diagonal_angle, expand=1) start_x -= self.PIECEGRIDSIZE[0]*3 + 90 start_y -= self.GRIDBORDER + 60 mask = win_bar.copy() win_bar_image = Image.new("RGBA", mask.size, color=self.WINBARCOLOR) background.paste(win_bar_image, (start_x, start_y), mask) def _draw_footer(self, drawer: ImageDraw, players: list): if players[0] == "Gwendolyn": player1 = "Gwendolyn" else: player1 = self.get_name(f"#{players[0]}") if players[1] == "Gwendolyn": player2 = "Gwendolyn" else: player2 = self.get_name(f"#{players[1]}") circle_height = self.HEIGHT - self.BORDER circle_height += (self.BOTTOMBORDER+self.BORDER)//2 - self.TEXTSIZE//2 text_width = self.FONT.getsize(player2)[0] circle_right_padding = ( self.BORDER+self.TEXTSIZE+text_width+self.TEXTPADDING ) circle_positions = [ [ ( self.BORDER, circle_height ), ( self.BORDER+self.TEXTSIZE, circle_height+self.TEXTSIZE ) ], [ ( self.WIDTH-circle_right_padding, circle_height ), ( self.WIDTH-self.BORDER-text_width-self.TEXTPADDING, circle_height+self.TEXTSIZE ) ] ] circle_parameters = { "outline": self.BOARDOUTLINECOLOR, "width": 3 } drawer.ellipse( circle_positions[0], fill=self.PLAYER1COLOR, **circle_parameters ) drawer.ellipse( circle_positions[1], fill=self.PLAYER2COLOR, **circle_parameters ) text_positions = [ (self.BORDER+self.TEXTSIZE+self.TEXTPADDING, circle_height), (self.WIDTH-self.BORDER-text_width, circle_height) ] drawer.text(text_positions[0], player1, font=self.FONT, fill=(0, 0, 0)) drawer.text(text_positions[1], player2, font=self.FONT, fill=(0, 0, 0))