Files
Gwendolyn/funcs/games/connectFour.py
2021-04-13 18:46:57 +02:00

584 lines
26 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.

import random
import copy
import math
import discord
from PIL import Image, ImageDraw, ImageFont
class ConnectFour():
def __init__(self,bot):
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.ROWCOUNT = 6
self.COLUMNCOUNT = 7
# Starts the game
async def start(self, ctx, opponent):
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 != None:
sendMessage = "There's already a connect 4 game going on in this channel"
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)
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"
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)]
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.databaseFuncs.getName(opponent)
turnName = self.bot.databaseFuncs.getName(players[0])
startedText = f"Started game against {opponentName}{diffText}."
turnText = f"It's {turnName}'s turn"
sendMessage = f"{startedText} {turnText}"
logMessage = "They started a game"
self.bot.log(logMessage)
await ctx.send(sendMessage)
# 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))
with open(f"resources/games/oldImages/connectFour{ctx.channel_id}", "w") as f:
f.write(str(oldImage.id))
if gwendoTurn:
await self.connectFourAI(ctx)
else:
reactions = ["1","2","3","4","5","6","7"]
for reaction in 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):
channel = str(ctx.channel.id)
game = self.bot.database["connect 4 games"].find_one({"_id":channel})
playerNumber = game["players"].index(user)+1
userName = self.bot.databaseFuncs.getName(user)
placedPiece = False
if game is None:
sendMessage = "There's no game in this channel"
logMessage = "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"
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}})
self.bot.log("Checking for win")
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}})
sendMessage = f"{userName} placed a piece in column {column+1} and won."
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"
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"
logMessage = "They placed the piece"
gwendoTurn = (game["players"][turn] == f"#{self.bot.user.id}")
placedPiece = True
await ctx.channel.send(sendMessage)
self.bot.log(logMessage)
if placedPiece:
self.draw.drawImage(channel)
with open(f"resources/games/oldImages/connectFour{channel}", "r") as f:
oldImage = await ctx.channel.fetch_message(int(f.read()))
if oldImage is not None:
await oldImage.delete()
else:
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))
if gameWon:
self.endGame(channel)
else:
with open(f"resources/games/oldImages/connectFour{channel}", "w") as f:
f.write(str(oldImage.id))
if gwendoTurn:
await self.connectFourAI(ctx)
else:
reactions = ["1","2","3","4","5","6","7"]
for reaction in 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
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):
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.databaseFuncs.getName(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)
with open(f"resources/games/oldImages/connectFour{channel}", "r") as f:
oldImage = await ctx.channel.fetch_message(int(f.read()))
if oldImage is not None:
await oldImage.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")
# Checks if someone has won the game and returns the winner
def isWon(self, board):
won = 0
winDirection = ""
winCoordinates = [0,0]
for row in range(self.ROWCOUNT):
for place in range(self.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]]
else:
pieces = [0]
if all(x == piecePlayer for x in pieces):
won = piecePlayer
winDirection = "h"
winCoordinates = [row,place]
# Checks vertical
if row <= self.ROWCOUNT-4:
pieces = [board[row+1][place],board[row+2][place],board[row+3][place]]
else:
pieces = [0]
if all(x == piecePlayer for x in pieces):
won = piecePlayer
winDirection = "v"
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]]
else:
pieces = [0]
if all(x == piecePlayer for x in pieces):
won = piecePlayer
winDirection = "r"
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]]
else:
pieces = [0]
if all(x == piecePlayer for x in pieces):
won = piecePlayer
winDirection = "l"
winCoordinates = [row,place]
return won, winDirection, winCoordinates
# Plays as the AI
async def connectFourAI(self, ctx):
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(self.COLUMNCOUNT)]
for column in range(self.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)
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:
possibleScores.remove(min(possibleScores))
highest_score = random.choice(possibleScores)
bestColumns = [i for i, x in enumerate(scores) if x == highest_score]
placement = random.choice(bestColumns)
await self.placePiece(ctx, f"#{self.bot.user.id}", placement)
# Calculates points for a board
def AICalcPoints(self,board,player):
score = 0
otherPlayer = player%2+1
# Adds points for middle placement
# Checks horizontal
for row in range(self.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):
window = rowArray[place:place+4]
score += self.evaluateWindow(window,player,otherPlayer)
# Checks Vertical
for column in range(self.COLUMNCOUNT):
columnArray = [int(i[column]) for i in list(board)]
for place in range(self.ROWCOUNT-3):
window = columnArray[place:place+4]
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 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)
## Checks if anyone has won
#won = isWon(board)[0]
## Add points if AI wins
#if won == player:
# score += self.AISCORES["win"]
return score
def evaluateWindow(self, window,player,otherPlayer):
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, depth, player , originalPlayer, alpha, beta, maximizingPlayer):
#terminal = ((0 not in board[0]) or (isWon(board)[0] != 0))
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,self.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
return value
else:
value = math.inf
for column in range(0,self.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
return value
class drawConnectFour():
def __init__(self, bot):
self.bot = bot
# Draws the whole thing
def drawImage(self, channel):
self.bot.log("Drawing connect four board")
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)
fnt = ImageFont.truetype('resources/fonts/futura-bold.ttf', exampleCircles)
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)
if game["players"][0] == "Gwendolyn":
player1 = "Gwendolyn"
else:
player1 = self.bot.databaseFuncs.getName(game["players"][0])
if game["players"][1] == "Gwendolyn":
player2 = "Gwendolyn"
else:
player2 = self.bot.databaseFuncs.getName(game["players"][1])
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")