1069 lines
40 KiB
Python
1069 lines
40 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
|
||
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))
|