From 45a2013c7fc1da495053d8287a9c5f5a3c7f3597 Mon Sep 17 00:00:00 2001 From: NikolajDanger Date: Sun, 18 Apr 2021 20:09:13 +0200 Subject: [PATCH] :red_circle: Connect four --- funcs/games/connectFour.py | 1111 ++++++++++++++++++++++++++---------- resources/longStrings.json | 4 +- 2 files changed, 801 insertions(+), 314 deletions(-) diff --git a/funcs/games/connectFour.py b/funcs/games/connectFour.py index 88a8024..c0d511d 100644 --- a/funcs/games/connectFour.py +++ b/funcs/games/connectFour.py @@ -1,14 +1,42 @@ -import random -import copy -import math -import discord +""" +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 -from PIL import Image, ImageDraw, ImageFont class ConnectFour(): - def __init__(self,bot): + """ + 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.draw = DrawConnectFour(bot) self.AISCORES = { "middle": 3, "two in a row": 10, @@ -19,67 +47,84 @@ class ConnectFour(): "win": 10000, "avoid losing": 100 } + self.REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣"] - self.ROWCOUNT = 6 - self.COLUMNCOUNT = 7 + async def start(self, ctx: SlashContext, + opponent: Union[int, discord.User]): + """ + Start a game of connect four. - # Starts the game - async def start(self, ctx, opponent): + *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}) + game = self.bot.database["connect 4 games"].find_one({"_id": channel}) startedGame = False canStart = True - if game != None: - sendMessage = "There's already a connect 4 game going on in this channel" + if game is not None: + sendMessage = self.bot.longStrings["Connect 4 going on"] logMessage = "There was already a game going on" canStart = False - else: - if type(opponent) == int: - # Opponent is Gwendolyn - if opponent in range(1, 6): - difficulty = int(opponent) + 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" + logMessage = "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 = "Difficulty doesn't exist" - logMessage = "They tried to play against a difficulty that doesn't exist" + sendMessage = "You can't challenge a bot!" + logMessage = "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" + logMessage = "They tried to play against themself" canStart = False - elif type(opponent) == discord.member.Member: - 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!" - logMessage = "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" - logMessage = "They tried to play against themself" - canStart = False - if canStart: - board = [[0 for _ in range(self.COLUMNCOUNT)] for _ in range(self.ROWCOUNT)] + 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} + 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) @@ -101,23 +146,42 @@ class ConnectFour(): # Sets the whole game in motion if startedGame: - filePath = f"resources/games/connect4Boards/board{ctx.channel_id}.png" - oldImage = await ctx.channel.send(file = discord.File(filePath)) + boardsPath = "resources/games/connect4Boards/" + filePath = f"{boardsPath}board{ctx.channel_id}.png" + oldImage = await ctx.channel.send(file=discord.File(filePath)) - with open(f"resources/games/oldImages/connectFour{ctx.channel_id}", "w") as f: + oldImagesPath = "resources/games/oldImages/" + oldImagePath = f"{oldImagesPath}connectFour{ctx.channel_id}" + with open(oldImagePath, "w") as f: f.write(str(oldImage.id)) if gwendoTurn: - await self.connectFourAI(ctx) + await self._connectFourAI(ctx) else: - reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] - for reaction in reactions: + for reaction in self.REACTIONS: await oldImage.add_reaction(reaction) - # Places a piece at the lowest available point in a specific column - async def placePiece(self, ctx, user, column): + 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) - game = self.bot.database["connect 4 games"].find_one({"_id":channel}) + connect4Games = self.bot.database["connect 4 games"] + game = connect4Games.find_one({"_id": channel}) playerNumber = game["players"].index(user)+1 userName = self.bot.databaseFuncs.getName(user) placedPiece = False @@ -127,39 +191,48 @@ class ConnectFour(): logMessage = "There was no game in the channel" else: board = game["board"] - board = self.placeOnBoard(board, playerNumber, column) + board = self._placeOnBoard(board, playerNumber, column) if board is None: sendMessage = "There isn't any room in that column" logMessage = "There wasn't any room in the column" else: - self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"board":board}}) - turn = (game["turn"]+1)%2 - self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"turn":turn}}) + 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) + won, winDirection, winCoordinates = self._isWon(board) if won != 0: gameWon = True - self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"winner":won}}) - self.bot.database["connect 4 games"].update_one({"_id":channel},{"$set":{"win direction":winDirection}}) - self.bot.database["connect 4 games"].update_one({"_id":channel}, - {"$set":{"win coordinates":winCoordinates}}) + 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 = f"{userName} placed a piece in column {column+1} and won." + sendMessage = "{} placed a piece in column {} and won. " + sendMessage = sendMessage.format(userName, column+1) logMessage = f"{userName} won" winAmount = int(game["difficulty"])**2+5 if game["players"][won-1] != f"#{self.bot.user.id}": - sendMessage += " Adding "+str(winAmount)+" GwendoBucks to their account" + sendMessage += "Adding {} GwendoBucks to their account" + sendMessage = sendMessage.format(winAmount) elif 0 not in board[0]: gameWon = True sendMessage = "It's a draw!" logMessage = "The game ended in a draw" else: gameWon = False - otherUserName = self.bot.databaseFuncs.getName(game["players"][turn]) - sendMessage = f"{userName} placed a piece in column {column+1}. It's now {otherUserName}'s turn" + otherUserId = game["players"][turn] + otherUserName = self.bot.databaseFuncs.getName(otherUserId) + sendMessage = self.bot.longStrings["Connect 4 placed"] + formatParams = [userName, column+1, otherUserName] + sendMessage = sendMessage.format(*formatParams) logMessage = "They placed the piece" gwendoTurn = (game["players"][turn] == f"#{self.bot.user.id}") @@ -172,7 +245,8 @@ class ConnectFour(): if placedPiece: self.draw.drawImage(channel) - with open(f"resources/games/oldImages/connectFour{channel}", "r") as f: + oldImagePath = f"resources/games/oldImages/connectFour{channel}" + with open(oldImagePath, "r") as f: oldImage = await ctx.channel.fetch_message(int(f.read())) if oldImage is not None: @@ -181,56 +255,35 @@ class ConnectFour(): self.bot.log("The old image was already deleted") filePath = f"resources/games/connect4Boards/board{channel}.png" - oldImage = await ctx.channel.send(file = discord.File(filePath)) + oldImage = await ctx.channel.send(file=discord.File(filePath)) if gameWon: - self.endGame(channel) + self._endGame(channel) else: - with open(f"resources/games/oldImages/connectFour{channel}", "w") as f: + with open(oldImagePath, "w") as f: f.write(str(oldImage.id)) if gwendoTurn: - await self.connectFourAI(ctx) + await self._connectFourAI(ctx) else: - reactions = ["1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣"] - for reaction in reactions: + for reaction in self.REACTIONS: await oldImage.add_reaction(reaction) - # Returns a board where a piece has been placed in the column - def placeOnBoard(self, board, player, column): - placementX, placementY = -1, column + async def surrender(self, ctx: SlashContext): + """ + Surrender a connect four game. - for x, line in enumerate(board): - if line[column] == 0: - placementX = x - - board[placementX][placementY] = player - - if placementX == -1: - return None - else: - return board - - def endGame(self, channel): - 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.databaseFuncs.deleteGame("connect 4 games", channel) - - # Parses command - async def surrender(self, ctx): + *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}) + 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 + winnerIndex = (loserIndex+1) % 2 winnerID = game["players"][winnerIndex] winnerName = self.bot.databaseFuncs.getName(winnerID) @@ -242,7 +295,8 @@ class ConnectFour(): sendMessage += f" Adding {reward} to their account" await ctx.send(sendMessage) - with open(f"resources/games/oldImages/connectFour{channel}", "r") as f: + oldImagePath = f"resources/games/oldImages/connectFour{channel}" + with open(oldImagePath, "r") as f: oldImage = await ctx.channel.fetch_message(int(f.read())) if oldImage is not None: @@ -250,142 +304,219 @@ class ConnectFour(): else: self.bot.log("The old image was already deleted") - self.endGame(channel) + self._endGame(channel) else: await ctx.send("You can't surrender when you're not a player") - # Checks if someone has won the game and returns the winner - def isWon(self, board): + 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 enumerate(board): + 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.databaseFuncs.deleteGame("connect 4 games", channel) + + def _isWon(self, board: dict): won = 0 winDirection = "" - winCoordinates = [0,0] + winCoordinates = [0, 0] - for row in range(self.ROWCOUNT): - for place in range(self.COLUMNCOUNT): + for row in range(ROWCOUNT): + for place in range(COLUMNCOUNT): if won == 0: piecePlayer = board[row][place] if piecePlayer != 0: # Checks horizontal - if place <= self.COLUMNCOUNT-4: - pieces = [board[row][place+1],board[row][place+2],board[row][place+3]] + 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] + winCoordinates = [row, place] # Checks vertical - if row <= self.ROWCOUNT-4: - pieces = [board[row+1][place],board[row+2][place],board[row+3][place]] + 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] + winCoordinates = [row, place] # Checks right diagonal - if row <= self.ROWCOUNT-4 and place <= self.COLUMNCOUNT-4: - pieces = [board[row+1][place+1],board[row+2][place+2],board[row+3][place+3]] + 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] + winCoordinates = [row, place] # Checks left diagonal - if row <= self.ROWCOUNT-4 and place >= 3: - pieces = [board[row+1][place-1],board[row+2][place-2],board[row+3][place-3]] + 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] - + winCoordinates = [row, place] return won, winDirection, winCoordinates - # Plays as the AI - async def connectFourAI(self, ctx): + async def _connectFourAI(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}) + 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(self.COLUMNCOUNT)] - for column in range(self.COLUMNCOUNT): + scores = [int(-math.inf) for _ in range(COLUMNCOUNT)] + for column in range(COLUMNCOUNT): testBoard = copy.deepcopy(board) - testBoard = self.placeOnBoard(testBoard,player,column) - if testBoard != None: - scores[column] = await self.minimax(testBoard,difficulty,player%2+1,player,-math.inf,math.inf,False) + testBoard = self._placeOnBoard(testBoard, player, column) + if testBoard is not None: + _minimaxParams = [ + testBoard, + difficulty, + player % 2+1, + player, + int(-math.inf), + int(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 (min(possibleScores) <= (max(possibleScores) - max(possibleScores)/10)) and len(possibleScores) != 1: + while outOfRange(possibleScores): possibleScores.remove(min(possibleScores)) - highest_score = random.choice(possibleScores) + highestScore = random.choice(possibleScores) - bestColumns = [i for i, x in enumerate(scores) if x == highest_score] + 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) - # Calculates points for a board - def AICalcPoints(self,board,player): + def _AICalcPoints(self, board: dict, player: int): score = 0 - otherPlayer = player%2+1 + otherPlayer = player % 2+1 # Adds points for middle placement # Checks horizontal - for row in range(self.ROWCOUNT): + 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(self.COLUMNCOUNT-3): + for place in range(COLUMNCOUNT-3): window = rowArray[place:place+4] - score += self.evaluateWindow(window,player,otherPlayer) + score += self._evaluateWindow(window, player, otherPlayer) # Checks Vertical - for column in range(self.COLUMNCOUNT): + for column in range(COLUMNCOUNT): columnArray = [int(i[column]) for i in list(board)] - for place in range(self.ROWCOUNT-3): + for place in range(ROWCOUNT-3): window = columnArray[place:place+4] - score += self.evaluateWindow(window,player,otherPlayer) + score += self._evaluateWindow(window, player, otherPlayer) # Checks right diagonal - for row in range(self.ROWCOUNT-3): - for place in range(self.COLUMNCOUNT-3): - window = [board[row][place],board[row+1][place+1],board[row+2][place+2],board[row+3][place+3]] - score += self.evaluateWindow(window,player,otherPlayer) + 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] - for place in range(3,self.COLUMNCOUNT): - window = [board[row][place],board[row+1][place-1],board[row+2][place-2],board[row+3][place-3]] - score += self.evaluateWindow(window,player,otherPlayer) + 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] - ## Checks if anyone has won - #won = isWon(board)[0] - - ## Add points if AI wins - #if won == player: - # score += self.AISCORES["win"] + window = [pieceOne, pieceTwo, pieceThree, pieceFour] + score += self._evaluateWindow(window, player, otherPlayer) return score - def evaluateWindow(self, window,player,otherPlayer): + 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: @@ -397,76 +528,498 @@ class ConnectFour(): else: return 0 - async def minimax(self, board, depth, player , originalPlayer, alpha, beta, maximizingPlayer): - #terminal = ((0 not in board[0]) or (isWon(board)[0] != 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. + 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,self.COLUMNCOUNT): + value = int(-math.inf) + for column in range(0, COLUMNCOUNT): testBoard = copy.deepcopy(board) - testBoard = self.placeOnBoard(testBoard,player,column) - if testBoard != None: - evaluation = await self.minimax(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,False) - if evaluation < -9000: evaluation += self.AISCORES["avoid losing"] - value = max(value,evaluation) - alpha = max(alpha,evaluation) - if beta <= alpha: break + 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,self.COLUMNCOUNT): + value = int(math.inf) + for column in range(0, COLUMNCOUNT): testBoard = copy.deepcopy(board) - testBoard = self.placeOnBoard(testBoard,player,column) - if testBoard != None: - evaluation = await self.minimax(testBoard,depth-1,player%2+1,originalPlayer,alpha,beta,True) - if evaluation < -9000: evaluation += self.AISCORES["avoid losing"] - value = min(value,evaluation) - beta = min(beta,evaluation) - if beta <= alpha: break + 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(): + +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 = "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): + 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}) + game = self.bot.database["connect 4 games"].find_one({"_id": channel}) board = game["board"] - border = 40 - gridBorder = 40 - cornerSize = 300 - boardOutlineSize = 10 - pieceOutlineSize = 10 - emptyOutlineSize = 0 - bottomBorder = 110 - exampleCircles = 100 - w, h = 2800,2400 - backgroundColor = (230,230,234,255) - boardOutlineColor = (0,0,0) - pieceOutlineColor = (244,244,248) - emptyOutlineColor = (0,0,0) - player1Color = (254,74,73) - player2Color = (254,215,102) - boardColor = (42,183,202) - placeSize = 300 - white = (255,255,255,160) - winBarColor = (250,250,250,255) + imageSize = (self.WIDTH, self.HEIGHT+self.BOTTOMBORDER) + background = Image.new("RGB", imageSize, self.BACKGROUNDCOLOR) + d = ImageDraw.Draw(background, "RGBA") - fnt = ImageFont.truetype('resources/fonts/futura-bold.ttf', exampleCircles) + self._drawBoard(d) - boardSize = [w-(2*(border+gridBorder)),h-(2*(border+gridBorder))] - placeGridSize = [math.floor(boardSize[0]/7),math.floor(boardSize[1]/6)] - pieceStartx = (border+gridBorder)+math.floor(placeGridSize[0]/2)-math.floor(placeSize/2) - pieceStarty = (border+gridBorder)+math.floor(placeGridSize[1]/2)-math.floor(placeSize/2) + self._drawPieces(d, board) + if game["winner"] != 0: + self._drawWin(background, game) + + self._drawFooter(d, game) + + background.save(f"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: @@ -477,107 +1030,39 @@ class drawConnectFour(): else: player2 = self.bot.databaseFuncs.getName(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) - background = Image.new("RGB", (w,h+bottomBorder),backgroundColor) - d = ImageDraw.Draw(background,"RGBA") - - # This whole part was the easiest way to make a rectangle with rounded corners and an outline - # - Corners: - d.ellipse([(border,border),(border+cornerSize,border+cornerSize)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - d.ellipse([(w-(border+cornerSize),h-(border+cornerSize)),(w-border,h-border)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - d.ellipse([(border,h-(border+cornerSize)),(border+cornerSize,h-border)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - d.ellipse([(w-(border+cornerSize),border),(w-border,border+cornerSize)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - # - Rectangle: - d.rectangle([(border+math.floor(cornerSize/2),border),(w-(border+math.floor(cornerSize/2)),h-border)],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - d.rectangle([(border,border+math.floor(cornerSize/2)),(w-border,h-(border+math.floor(cornerSize/2)))],fill=boardColor,outline=boardOutlineColor,width=boardOutlineSize) - # - Removing outline on the inside: - d.rectangle([(border+math.floor(cornerSize/2),border+math.floor(cornerSize/2)),(w-(border+math.floor(cornerSize/2)),h-(border+math.floor(cornerSize/2)))],fill=boardColor) - d.rectangle([(border+math.floor(cornerSize/2),border+boardOutlineSize),(w-(border+math.floor(cornerSize/2)),h-(border+boardOutlineSize))],fill=boardColor) - d.rectangle([(border+boardOutlineSize,border+math.floor(cornerSize/2)),(w-(border+boardOutlineSize),h-(border+math.floor(cornerSize/2)))],fill=boardColor) - - for x, line in enumerate(board): - for y, piece in enumerate(line): - - if piece == 1: - pieceColor = player1Color - outlineWidth = pieceOutlineSize - outlineColor = pieceOutlineColor - elif piece == 2: - pieceColor = player2Color - outlineWidth = pieceOutlineSize - outlineColor = pieceOutlineColor - else: - pieceColor = backgroundColor - outlineWidth = emptyOutlineSize - outlineColor = emptyOutlineColor - - startx = pieceStartx + placeGridSize[0]*y - starty = pieceStarty + placeGridSize[1]*x - - d.ellipse([(startx,starty),(startx+placeSize,starty+placeSize)],fill=pieceColor,outline=outlineColor,width=outlineWidth) - - if game["winner"] != 0: - coordinates = game["win coordinates"] - startx = border + placeGridSize[0]*coordinates[1] + gridBorder - starty = border + placeGridSize[1]*coordinates[0] + gridBorder - a = (placeGridSize[0]*4-gridBorder-border)**2 - b = (placeGridSize[1]*4-gridBorder-border)**2 - diagonalLength = (math.sqrt(a+b))/placeGridSize[0] - diagonalAngle = math.degrees(math.atan(placeGridSize[1]/placeGridSize[0])) - - if game["win direction"] == "h": - winBar = Image.new("RGBA",(placeGridSize[0]*4,placeGridSize[1]),(0,0,0,0)) - winD = ImageDraw.Draw(winBar) - winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) - winD.ellipse([((placeGridSize[0]*3),0),(placeGridSize[0]*4,placeGridSize[1])],fill=white) - winD.rectangle([(int(placeGridSize[0]*0.5),0),(int(placeGridSize[0]*3.5),placeGridSize[1])],fill=white) - - elif game["win direction"] == "v": - winBar = Image.new("RGBA",(placeGridSize[0],placeGridSize[1]*4),(0,0,0,0)) - winD = ImageDraw.Draw(winBar) - winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) - winD.ellipse([(0,(placeGridSize[1]*3)),(placeGridSize[0],placeGridSize[1]*4)],fill=white) - winD.rectangle([0,(int(placeGridSize[1]*0.5)),(placeGridSize[0],int(placeGridSize[1]*3.5))],fill=white) - - elif game["win direction"] == "r": - winBar = Image.new("RGBA",(int(placeGridSize[0]*diagonalLength),placeGridSize[1]),(0,0,0,0)) - winD = ImageDraw.Draw(winBar) - winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) - winD.ellipse([((placeGridSize[0]*(diagonalLength-1)),0),(placeGridSize[0]*diagonalLength,placeGridSize[1])],fill=white) - winD.rectangle([(int(placeGridSize[0]*0.5),0),(int(placeGridSize[0]*(diagonalLength-0.5)),placeGridSize[1])],fill=white) - winBar = winBar.rotate(-diagonalAngle,expand=1) - startx -= 90 - starty -= 100 - - elif game["win direction"] == "l": - winBar = Image.new("RGBA",(int(placeGridSize[0]*diagonalLength),placeGridSize[1]),(0,0,0,0)) - winD = ImageDraw.Draw(winBar) - winD.ellipse([(0,0),(placeGridSize[0],placeGridSize[1])],fill=white) - winD.ellipse([((placeGridSize[0]*(diagonalLength-1)),0),(placeGridSize[0]*diagonalLength,placeGridSize[1])],fill=white) - winD.rectangle([(int(placeGridSize[0]*0.5),0),(int(placeGridSize[0]*(diagonalLength-0.5)),placeGridSize[1])],fill=white) - winBar = winBar.rotate(diagonalAngle,expand=1) - startx -= placeGridSize[0]*3 + 90 - starty -= gridBorder + 60 - - - mask = winBar.copy()#.convert("L") - #mask.putalpha(128) - #mask.save("test.png") - - winBarImage = Image.new("RGBA",mask.size,color=winBarColor) - background.paste(winBarImage,(startx,starty),mask) - - # Bottom - textPadding = 20 - - exampleHeight = h - border + int((bottomBorder+border)/2) - int(exampleCircles/2) - d.ellipse([(border,exampleHeight),(border+exampleCircles),(exampleHeight+exampleCircles)],fill=player1Color,outline=boardOutlineColor,width=3) - d.text((border+exampleCircles+textPadding,exampleHeight),player1,font=fnt,fill=(0,0,0)) - - textWidth = fnt.getsize(player2)[0] - d.ellipse([(w-border-exampleCircles-textWidth-textPadding,exampleHeight),(w-border-textWidth-textPadding),(exampleHeight+exampleCircles)],fill=player2Color,outline=boardOutlineColor,width=3) - d.text((w-border-textWidth,exampleHeight),player2,font=fnt,fill=(0,0,0)) - - - background.save("resources/games/connect4Boards/board"+channel+".png") - + 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)) diff --git a/resources/longStrings.json b/resources/longStrings.json index 188fc7f..c0f1de8 100644 --- a/resources/longStrings.json +++ b/resources/longStrings.json @@ -12,5 +12,7 @@ "Stock value": "The current {} stock is valued at **{}** GwendoBucks", "Stock parameters": "You must give both a stock name and an amount of GwendoBucks you wish to spend.", "Trivia going on": "There's already a trivia question going on. Try again in like, a minute", - "Trivia time up": "Time's up! The answer was \"*{}) {}*\". Anyone who answered that has gotten 1 GwendoBuck" + "Trivia time up": "Time's up! The answer was \"*{}) {}*\". Anyone who answered that has gotten 1 GwendoBuck", + "Connect 4 going on": "There's already a connect 4 game going on in this channel", + "Connect 4 placed": "{} placed a piece in column {}. It's now {}'s turn" } \ No newline at end of file