Files
Gwendolyn/gwendolyn/funcs/games/connect_four.py
2021-08-17 18:05:41 +02:00

1100 lines
41 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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 # 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])
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.database_funcs = self.bot.database_funcs
# 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
}
self.REACTIONS = ["1", "2", "3", "4", "5", "6", "7"]
# 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)
user = f"#{ctx.author.id}"
channel = str(ctx.channel_id)
game = self.bot.database["connect 4 games"].find_one({"_id": channel})
started_game = False
can_start = True
if game is not None:
send_message = self.bot.long_strings["Connect 4 going on"]
log_message = "There was already a game going on"
can_start = False
elif isinstance(opponent, int):
# Opponent is Gwendolyn
if opponent in range(1, 6):
difficulty = int(opponent)
difficulty_text = f" with difficulty {difficulty}"
opponent = f"#{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 = f"#{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 = f"#{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)
new_game = {
"_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(new_game)
self.draw.draw_image(channel)
gwendolyn_turn = (players[0] == f"#{self.bot.user.id}")
started_game = True
opponent_name = self.database_funcs.get_name(opponent)
turn_name = self.database_funcs.get_name(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)
await ctx.send(send_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"
old_image = await ctx.channel.send(file=discord.File(file_path))
old_images_path = "gwendolyn/resources/games/old_images/"
old_image_path = f"{old_images_path}connect_four{ctx.channel_id}"
with open(old_image_path, "w") as file_pointer:
file_pointer.write(str(old_image.id))
if gwendolyn_turn:
await self._connect_four_ai(ctx)
else:
for reaction in self.REACTIONS:
await old_image.add_reaction(reaction)
async def place_piece(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)
connect_four_games = self.bot.database["connect 4 games"]
game = connect_four_games.find_one({"_id": channel})
player_number = game["players"].index(user)+1
user_name = self.database_funcs.get_name(user)
placed_piece = False
if game is None:
send_message = "There's no game in this channel"
log_message = "There was no game in the channel"
else:
board = game["board"]
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:
updater = {"$set": {"board": board}}
connect_four_games.update_one({"_id": channel}, updater)
turn = (game["turn"]+1) % 2
updater = {"$set": {"turn": turn}}
connect_four_games.update_one({"_id": channel}, updater)
self.bot.log("Checking for win")
won, win_direction, win_coordinates = self._is_won(board)
if won != 0:
game_won = True
updater = {"$set": {"winner": won}}
connect_four_games.update_one({"_id": channel}, updater)
updater = {"$set": {"win direction": win_direction}}
connect_four_games.update_one({"_id": channel}, updater)
updater = {"$set": {"win coordinates": win_coordinates}}
connect_four_games.update_one({"_id": channel}, updater)
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 = int(game["difficulty"])**2+5
if game["players"][won-1] != f"#{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 = game["players"][turn]
other_user_name = self.database_funcs.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 = (
game["players"][turn] == f"#{self.bot.user.id}")
placed_piece = True
await ctx.channel.send(send_message)
self.bot.log(log_message)
if placed_piece:
self.draw.draw_image(channel)
old_images_path = "gwendolyn/resources/games/old_images/"
old_image_path = old_images_path + f"connect_four{channel}"
with open(old_image_path, "r") as file_pointer:
old_image_id = int(file_pointer.read())
old_image = await ctx.channel.fetch_message(old_image_id)
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
boards_path = "gwendolyn/resources/games/connect_four_boards/"
file_path = boards_path + f"board{channel}.png"
old_image = await ctx.channel.send(file=discord.File(file_path))
if game_won:
self._end_game(channel)
else:
with open(old_image_path, "w") as file_pointer:
file_pointer.write(str(old_image.id))
if gwendolyn_turn:
await self._connect_four_ai(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"]:
loser_index = game["players"].index(f"#{ctx.author.id}")
winner_index = (loser_index+1) % 2
winner_id = game["players"][winner_index]
winner_name = self.database_funcs.get_name(winner_id)
send_message = f"{ctx.author.display_name} surrenders."
send_message += f" This means {winner_name} is the winner."
if winner_id != f"#{self.bot.user.id}":
difficulty = int(game["difficulty"])
reward = difficulty**2 + 5
send_message += f" Adding {reward} to their account"
await ctx.send(send_message)
old_images_path = "gwendolyn/resources/games/old_images/"
old_image_path = old_images_path + f"connect_four{channel}"
with open(old_image_path, "r") as file_pointer:
old_image_id = int(file_pointer.read())
old_image = await ctx.channel.fetch_message(old_image_id)
if old_image is not None:
await old_image.delete()
else:
self.bot.log("The old image was already deleted")
self._end_game(channel)
else:
await ctx.send("You can't surrender when you're not a player")
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, 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.database_funcs.delete_game("connect 4 games", channel)
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: SlashContext):
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
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):
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, f"#{self.bot.user.id}", placement)
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.database_funcs = self.bot.database_funcs
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):
"""
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"]
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 game["winner"] != 0:
self._draw_win(background, game)
self._draw_footer(drawer, game)
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, 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
start_x = start + self.PIECEGRIDSIZE[0]*coordinates[1]
start_y = start + self.PIECEGRIDSIZE[1]*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 game["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 game["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 game["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 game["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, game: dict):
if game["players"][0] == "Gwendolyn":
player1 = "Gwendolyn"
else:
player1 = self.database_funcs.get_name(game["players"][0])
if game["players"][1] == "Gwendolyn":
player2 = "Gwendolyn"
else:
player2 = self.database_funcs.get_name(game["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))