Files
Gwendolyn/gwendolyn/funcs/games/connect_four.py
2021-06-16 14:12:21 +02:00

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