1164 lines
42 KiB
Python
1164 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)
|
|
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"
|
|
image_message = await ctx.send(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
|
|
)
|
|
|
|
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 = 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)
|
|
await ctx.channel.send(send_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(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
|
|
)
|
|
|
|
|
|
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))
|