Files
Gwendolyn/gwendolyn/funcs/games/connect_four.py
2021-08-19 15:37:02 +02:00

1169 lines
42 KiB
Python

"""
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
ROWCOUNT = 6
COLUMNCOUNT = 7
class ConnectFour():
"""
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."""
self.bot = bot
self.draw = DrawConnectFour(bot)
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
def _encode_board_string(self, 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(self, 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
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 = ctx.author.id
channel = str(ctx.channel_id)
started_game = False
can_start = True
if isinstance(opponent, int):
# Opponent is Gwendolyn
if opponent in range(1, 6):
difficulty = int(opponent)
difficulty_text = f" with difficulty {difficulty}"
opponent = self.bot.user.id
else:
send_message = "Difficulty doesn't exist"
log_message = "They challenged a difficulty that doesn't exist"
can_start = False
elif isinstance(opponent, discord.User):
if opponent.bot:
# User has challenged a bot
if opponent == self.bot.user:
# It was Gwendolyn
difficulty = 3
difficulty_text = f" with difficulty {difficulty}"
opponent = self.bot.user.id
else:
send_message = "You can't challenge a bot!"
log_message = "They tried to challenge a bot"
can_start = False
else:
# Opponent is another player
if ctx.author != opponent:
opponent = opponent.id
difficulty = 5
difficulty_text = ""
else:
send_message = "You can't play against yourself"
log_message = "They tried to play against themself"
can_start = False
if can_start:
board = [[0 for _ in range(COLUMNCOUNT)] for _ in range(ROWCOUNT)]
players = [user, opponent]
random.shuffle(players)
self.draw.draw_image(channel, board, 0, [0,0], "", players)
gwendolyn_turn = (players[0] == self.bot.user.id)
started_game = True
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"
send_message = f"{started_text} {turn_text}"
log_message = "They started a game"
self.bot.log(log_message)
# Sets the whole game in motion
if started_game:
boards_path = "gwendolyn/resources/games/connect_four_boards/"
file_path = f"{boards_path}board{ctx.channel_id}.png"
image_message = await ctx.send(
send_message, file=discord.File(file_path)
)
board_string = self._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[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
)
else:
await ctx.send(send_message)
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.
"""
player_number = players.index(placer)+1
user_name = self.get_name(f"#{placer}")
placed_piece = False
board = self._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)
if placed_piece:
channel = str(ctx.channel)
self.draw.draw_image(
channel, board, winner, win_coordinates, win_direction, players)
boards_path = "gwendolyn/resources/games/connect_four_boards/"
file_path = boards_path + f"board{channel}.png"
image_message = await ctx.channel.send(
send_message, file=discord.File(file_path))
if game_won:
self._end_game(winner, players, difficulty)
else:
board_string = self._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
)
else:
await ctx.channel.send(send_message)
old_message = await ctx.channel.fetch_message(message_id)
await old_message.delete()
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 = self._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):
"""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, winner: int,
win_coordinates: list, win_direction: str, players: 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 winner != 0:
self._draw_win(background, win_coordinates, win_direction)
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))