:spakles: Database and OOP

This commit is contained in:
NikolajDanger
2020-08-13 16:31:28 +02:00
parent f431c079d1
commit 4127e537a1
31 changed files with 3674 additions and 3731 deletions

View File

@@ -1,10 +1,9 @@
import json
import random
import copy
import math
from . import hexDraw
from funcs import logThis, getName, getID
from .hexDraw import DrawHex
from funcs import logThis
BOARDWIDTH = 11
ALL_POSITIONS = [(i,j) for i in range(11) for j in range(11)]
@@ -14,368 +13,364 @@ for position in ALL_POSITIONS:
EMPTY_DIJKSTRA[position] = math.inf # an impossibly high number
HEX_DIRECTIONS = [(0,1),(-1,1),(-1,0),(0,-1),(1,-1),(1,0)]
# Parses command
def parseHex(command, channel, user):
commands = command.lower().split()
with open("resources/games/hexGames.json", "r") as f:
data = json.load(f)
if command == "" or command == " ":
return "I didn't get that. Use \"!hex start [opponent]\" to start a game.", False, False, False, False
elif commands[0] == "start":
# Starting a game
if len(commands) == 1: # if the commands is "!hex start", the opponent is Gwendolyn at difficulty 2
commands.append("2")
logThis("Starting a hex game with hexStart(). "+str(user)+" challenged "+commands[1])
return hexStart(channel,user,commands[1]) # commands[1] is the opponent
# If using a command with no game, return error
elif channel not in data:
return "There's no game in this channel", False, False, False, False
class HexGame():
def __init__(self,bot):
self.bot = bot
self.draw = DrawHex(bot)
# Stopping the game
elif commands[0] == "stop":
if user in data[channel]["players"]:
return "Ending game.", False, False, True, False
else:
return "You can't end a game where you're not a player.", False, False, False, False
# Placing a piece
elif commands[0] == "place":
try:
return placeHex(channel,commands[1], user)
except:
return "I didn't get that. To place a piece use \"!hex place [position]\". A valid position is e.g. \"E2\".", False, False, False, False
# Undo
elif commands[0] == "undo":
return undoHex(channel, user)
# Surrender
elif commands[0] == "surrender":
players = data[channel]["players"]
if user in players:
opponent = (players.index(user) + 1) % 2
data[channel]["winner"] = opponent + 1
return "{} surrendered. That means {} won! Adding 30 Gwendobucks to their account.".format(getName(user),getName(players[opponent])), False, False, True, False
else:
return "You can't surrender when you're not a player.", False, False, False, False
# Parses command
def parseHex(self, command, channel, user):
commands = command.lower().split()
game = self.bot.database["hex games"].find_one({"_id":channel})
# Swap
elif commands[0] == "swap":
if len(data[channel]["gameHistory"]) == 1: # Only after the first move
data[channel]["players"] = data[channel]["players"][::-1] # Swaps their player-number
with open("resources/games/hexGames.json", "w") as f:
json.dump(data,f,indent=4)
# Swaps the color of the hexes on the board drawing:
hexDraw.drawSwap(channel)
player2 = data[channel]["players"][1]
gwendoTurn = (player2 == "Gwendolyn")
return "The color of both players were swapped. It is now {}'s turn".format(player2), True, True, False, gwendoTurn
else:
return "You can only swap as the second player after the very first move.", False, False, False, False
if command == "" or command == " ":
return "I didn't get that. Use \"!hex start [opponent]\" to start a game.", False, False, False, False
else:
return "I didn't get that. Use \"!hex start [opponent]\" to start a game, \"!hex place [position]\" to place a piece, \"!hex undo\" to undo your last move or \"!hex stop\" to stop a current game.", False, False, False, False
elif commands[0] == "start":
# Starting a game
if len(commands) == 1: # if the commands is "!hex start", the opponent is Gwendolyn at difficulty 2
commands.append("2")
logThis("Starting a hex game with hexStart(). "+str(user)+" challenged "+commands[1])
return self.hexStart(channel,user,commands[1]) # commands[1] is the opponent
# If using a command with no game, return error
elif game == None:
return "There's no game in this channel", False, False, False, False
# Starts the game
def hexStart(channel, user, opponent):
with open("resources/games/hexGames.json", "r") as f:
data = json.load(f)
if channel not in data:
if opponent in ["1","2","3","4","5"]:
difficulty = int(opponent)
diffText = " with difficulty "+opponent
opponent = "Gwendolyn"
elif opponent.lower() == "gwendolyn":
difficulty = 2
diffText = " with difficulty 2"
opponent = "Gwendolyn"
else:
try:
int(opponent)
return "That difficulty doesn't exist", False, False, False, False
except:
opponent = getID(opponent)
if opponent == None:
return "I can't find that user", False, False, False, False
else:
# Opponent is another player
difficulty = 3
diffText = ""
# board is 11x11
board = [ [ 0 for i in range(BOARDWIDTH) ] for j in range(BOARDWIDTH) ]
players = [user,opponent]
random.shuffle(players) # random starting player
gameHistory = []
data[channel] = {"board":board, "winner":0,
"players":players, "turn":1, "difficulty":difficulty, "gameHistory":gameHistory}
with open("resources/games/hexGames.json", "w") as f:
json.dump(data,f,indent=4)
# draw the board
hexDraw.drawBoard(channel)
gwendoTurn = True if players[0] == "Gwendolyn" else False
showImage = True
return "Started Hex game against "+getName(opponent)+ diffText+". It's "+getName(players[0])+"'s turn", showImage, False, False, gwendoTurn
else:
return "There's already a hex game going on in this channel", False, False, False, False
# Places a piece at the given location and checks things afterwards
def placeHex(channel : str,position : str, user):
with open("resources/games/hexGames.json", "r") as f:
data = json.load(f)
if channel in data:
players = data[channel]["players"]
if user in players:
turn = data[channel]["turn"]
if players[0] == players[1]:
player = turn
# Stopping the game
elif commands[0] == "stop":
if user in game["players"]:
return "Ending game.", False, False, True, False
else:
player = players.index(user)+1
return "You can't end a game where you're not a player.", False, False, False, False
# Placing a piece
elif commands[0] == "place":
try:
return self.placeHex(channel,commands[1], user)
except:
return "I didn't get that. To place a piece use \"!hex place [position]\". A valid position is e.g. \"E2\".", False, False, False, False
# Undo
elif commands[0] == "undo":
return self.undoHex(channel, user)
# Surrender
elif commands[0] == "surrender":
players = game["players"]
if user in players:
opponent = (players.index(user) + 1) % 2
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"winner":opponent + 1}})
return "{} surrendered. That means {} won! Adding 30 Gwendobucks to their account.".format(self.bot.funcs.getName(user),self.bot.funcs.getName(players[opponent])), False, False, True, False
else:
return "You can't surrender when you're not a player.", False, False, False, False
# Swap
elif commands[0] == "swap":
if len(game["gameHistory"]) == 1: # Only after the first move
self.bot.database["hex games"].update_one({"_id":channel},
{"$set":{"players":game["players"][::-1]}}) # Swaps their player-number
# Swaps the color of the hexes on the board drawing:
self.draw.drawSwap(channel)
player2 = game["players"][1]
gwendoTurn = (player2 == "Gwendolyn")
return "The color of both players were swapped. It is now {}'s turn".format(player2), True, True, False, gwendoTurn
else:
return "You can only swap as the second player after the very first move.", False, False, False, False
else:
return "I didn't get that. Use \"!hex start [opponent]\" to start a game, \"!hex place [position]\" to place a piece, \"!hex undo\" to undo your last move or \"!hex stop\" to stop a current game.", False, False, False, False
# Starts the game
def hexStart(self, channel, user, opponent):
game = self.bot.database["hex games"].find_one({"_id":channel})
if game == None:
if opponent in ["1","2","3","4","5"]:
difficulty = int(opponent)
diffText = " with difficulty "+opponent
opponent = "Gwendolyn"
elif opponent.lower() == "gwendolyn":
difficulty = 2
diffText = " with difficulty 2"
opponent = "Gwendolyn"
else:
try:
int(opponent)
return "That difficulty doesn't exist", False, False, False, False
except:
opponent = self.bot.funcs.getID(opponent)
if opponent == None:
return "I can't find that user", False, False, False, False
else:
# Opponent is another player
difficulty = 3
diffText = ""
if player == turn:
board = data[channel]["board"]
# board is 11x11
board = [ [ 0 for i in range(BOARDWIDTH) ] for j in range(BOARDWIDTH) ]
players = [user,opponent]
random.shuffle(players) # random starting player
gameHistory = []
logThis("Placing a piece on the board with placeHex()")
# Places on board
board = placeOnHexBoard(board,player,position)
if isinstance(board, list):
# If the move is valid:
data[channel]["board"] = board
turn = 1 if turn == 2 else 2
data[channel]["turn"] = turn
newGame = {"_id":channel,"board":board, "winner":0,
"players":players, "turn":1, "difficulty":difficulty, "gameHistory":gameHistory}
# Checking for a win
logThis("Checking for win")
score, winner = evaluateBoard(data[channel]["board"])
self.bot.database["hex games"].insert_one(newGame)
if winner == 0: # Continue with the game.
gameWon = False
message = getName(data[channel]["players"][player-1])+" placed at "+position.upper()+". It's now "+getName(data[channel]["players"][turn-1])+"'s turn."# The score is "+str(score)
else: # Congratulations!
gameWon = True
data[channel]["winner"] = winner
message = getName(data[channel]["players"][player-1])+" placed at "+position.upper()+" and won!"
if data[channel]["players"][winner-1] != "Gwendolyn":
winAmount = data[channel]["difficulty"]*10
message += " Adding "+str(winAmount)+" GwendoBucks to their account."
data[channel]["gameHistory"].append((int(position[1])-1, ord(position[0])-97))
# Is it now Gwendolyn's turn?
gwendoTurn = False
if data[channel]["players"][turn-1] == "Gwendolyn":
logThis("It's Gwendolyn's turn")
gwendoTurn = True
# Save the data
with open("resources/games/hexGames.json", "w") as f:
json.dump(data,f,indent=4)
# Update the board
hexDraw.drawHexPlacement(channel,player,position)
# draw the board
self.draw.drawBoard(channel)
return message, True, True, gameWon, gwendoTurn
gwendoTurn = True if players[0] == "Gwendolyn" else False
showImage = True
return "Started Hex game against "+self.bot.funcs.getName(opponent)+ diffText+". It's "+self.bot.funcs.getName(players[0])+"'s turn", showImage, False, False, gwendoTurn
else:
return "There's already a hex game going on in this channel", False, False, False, False
# Places a piece at the given location and checks things afterwards
def placeHex(self, channel : str,position : str, user):
game = self.bot.database["hex games"].find_one({"_id":channel})
if game != None:
players = game["players"]
if user in players:
turn = game["turn"]
if players[0] == players[1]:
player = turn
else:
# Invalid move. "board" is the error message
message = board
player = players.index(user)+1
if player == turn:
board = game["board"]
logThis("Placing a piece on the board with placeHex()")
# Places on board
board = self.placeOnHexBoard(board,player,position)
if isinstance(board, list):
# If the move is valid:
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"board":board}})
turn = 1 if turn == 2 else 2
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"turn":turn}})
# Checking for a win
logThis("Checking for win")
winner = self.evaluateBoard(game["board"])[1]
if winner == 0: # Continue with the game.
gameWon = False
message = self.bot.funcs.getName(game["players"][player-1])+" placed at "+position.upper()+". It's now "+self.bot.funcs.getName(game["players"][turn-1])+"'s turn."# The score is "+str(score)
else: # Congratulations!
gameWon = True
self.bot.database["hex games"].update_one({"_id":channel},{"$set":{"winner":winner}})
message = self.bot.funcs.getName(game["players"][player-1])+" placed at "+position.upper()+" and won!"
if game["players"][winner-1] != "Gwendolyn":
winAmount = game["difficulty"]*10
message += " Adding "+str(winAmount)+" GwendoBucks to their account."
self.bot.database["hex games"].update_one({"_id":channel},
{"$push":{"gameHistory":(int(position[1])-1, ord(position[0])-97)}})
# Is it now Gwendolyn's turn?
gwendoTurn = False
if game["players"][turn-1] == "Gwendolyn":
logThis("It's Gwendolyn's turn")
gwendoTurn = True
# Update the board
self.draw.drawHexPlacement(channel,player,position)
return message, True, True, gameWon, gwendoTurn
else:
# Invalid move. "board" is the error message
message = board
return message, False, False, False, False
else:
# Move out of turn
message = "It isn't your turn, it is "+self.bot.funcs.getName(game["players"][turn-1])+"'s turn."
return message, False, False, False, False
else:
# Move out of turn
message = "It isn't your turn, it is "+getName(data[channel]["players"][turn-1])+"'s turn."
message = "You can't place when you're not in the game. The game's players are: "+self.bot.funcs.getName(game["players"][0])+" and "+self.bot.funcs.getName(game["players"][1])+"."
return message, False, False, False, False
else:
message = "You can't place when you're not in the game. The game's players are: "+getName(data[channel]["players"][0])+" and "+getName(data[channel]["players"][1])+"."
return message, False, False, False, False
else:
return "There's no game in this channel", False, False, False, False
return "There's no game in this channel", False, False, False, False
# Returns a board where the placement has occured
def placeOnHexBoard(board,player,position):
# Translates the position
position = position.lower()
# Error handling
try:
column = ord(position[0]) - 97 # ord() translates from letter to number
row = int(position[1:]) - 1
if column not in range(BOARDWIDTH) or row not in range(BOARDWIDTH):
logThis("Position out of bounds (error code 1533)")
return "Error. That position is out of bounds."
except:
logThis("Invalid position (error code 1531)")
return "Error. The position should be a letter followed by a number, e.g. \"e2\"."
# Place at the position
if board[row][column] == 0:
board[row][column] = player
return board
else:
logThis("Cannot place on existing piece (error code 1532)")
return "Error. You must place on an empty space."
# Returns a board where the placement has occured
def placeOnHexBoard(self, board,player,position):
# Translates the position
position = position.lower()
# Error handling
try:
column = ord(position[0]) - 97 # ord() translates from letter to number
row = int(position[1:]) - 1
if column not in range(BOARDWIDTH) or row not in range(BOARDWIDTH):
logThis("Position out of bounds (error code 1533)")
return "Error. That position is out of bounds."
except:
logThis("Invalid position (error code 1531)")
return "Error. The position should be a letter followed by a number, e.g. \"e2\"."
# Place at the position
if board[row][column] == 0:
board[row][column] = player
return board
else:
logThis("Cannot place on existing piece (error code 1532)")
return "Error. You must place on an empty space."
# After your move, you have the option to undo get your turn back #TimeTravel
def undoHex(channel, user):
with open("resources/games/hexGames.json", "r") as f:
data = json.load(f)
if user in data[channel]["players"]:
if len(data[channel]["gameHistory"]):
turn = data[channel]["turn"]
# You can only undo after your turn, which is the opponent's turn.
if user == data[channel]["players"][(turn % 2)]: # If it's not your turn
logThis("Undoing {}'s last move".format(getName(user)))
# After your move, you have the option to undo get your turn back #TimeTravel
def undoHex(self, channel, user):
game = self.bot.database["hex games"].find_one({"_id":channel})
lastMove = data[channel]["gameHistory"].pop()
data[channel]["board"][lastMove[0]][lastMove[1]] = 0
data[channel]["turn"] = turn%2 + 1
# Save the data
with open("resources/games/hexGames.json", "w") as f:
json.dump(data,f,indent=4)
# Update the board
hexDraw.drawHexPlacement(channel,0,"abcdefghijk"[lastMove[1]]+str(lastMove[0]+1)) # The zero makes the hex disappear
return "You undid your last move at {}".format(lastMove), True, True, False, False
if user in game["players"]:
if len(game["gameHistory"]):
turn = game["turn"]
# You can only undo after your turn, which is the opponent's turn.
if user == game["players"][(turn % 2)]: # If it's not your turn
logThis("Undoing {}'s last move".format(self.bot.funcs.getName(user)))
lastMove = game["gameHistory"].pop()
self.bot.database["hex games"].update_one({"_id":channel},
{"$set":{"board."+lastMove[0]+"."+lastMove[1]:0}})
self.bot.database["hex games"].update_one({"_id":channel},
{"$set":{"turn":turn%2 + 1}})
# Update the board
self.draw.drawHexPlacement(channel,0,"abcdefghijk"[lastMove[1]]+str(lastMove[0]+1)) # The zero makes the hex disappear
return "You undid your last move at {}".format(lastMove), True, True, False, False
else:
# Sassy
return "Nice try. You can't undo your opponent's move. ", False, False, False, False
else:
# Sassy
return "Nice try. You can't undo your opponent's move. ", False, False, False, False
return "Really? You undo right at the start of the game?", False, False, False, False
else:
# Sassy
return "Really? You undo right at the start of the game?", False, False, False, False
else:
return "You're not a player in the game", False, False, False, False
return "You're not a player in the game", False, False, False, False
# Plays as the AI
def hexAI(channel):
logThis("Figuring out best move")
with open("resources/games/hexGames.json", "r") as f:
data = json.load(f)
board = data[channel]["board"]
if len(data[channel]["gameHistory"]):
lastMove = data[channel]["gameHistory"][-1]
else:
lastMove = (5,5)
# These moves are the last move +- 2.
moves = [[(lastMove[0]+j-2,lastMove[1]+i-2) for i in range(5) if lastMove[1]+i-2 in range(11)] for j in range(5) if lastMove[0]+j-2 in range(11)]
moves = sum(moves,[])
movesCopy = moves.copy()
for move in movesCopy:
if board[move[0]][move[1]] != 0:
moves.remove(move)
chosenMove = random.choice(moves)
"""
GwenColor = data[channel]["players"].index("Gwendolyn") + 1 # either 1 or 2 - red or blue
if len(data[channel]["gameHistory"]) == 0:
return placeHex(channel,"F6", "Gwendolyn") # If starting, start in the middle
board = data[channel]["board"]
difficulty = data[channel]["difficulty"]
possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0]
judgements = [float('nan')]*len(possiblePlaces) # All possible moves are yet to be judged
current_score = evaluateBoard(board)[0]
for i in possiblePlaces:
testBoard = copy.deepcopy(board)
testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = GwenColor
if evaluateBoard(testBoard)[0] != current_score: # only think about a move if it improves the score (it's impossible to get worse)
# Testing a move and evaluating it
judgements[i] = minimaxHex(testBoard,difficulty,-math.inf,math.inf,GwenColor==2)
logThis("Best score for place {} is {}".format((i // BOARDWIDTH,i % BOARDWIDTH),judgements[i]))
# Plays as the AI
def hexAI(self, channel):
logThis("Figuring out best move")
game = self.bot.database["hex games"].find_one({"_id":channel})
board = game["board"]
bestScore = max(judgements) if (GwenColor == 1) else min(judgements) # this line has an error
indices = [i for i, x in enumerate(judgements) if x == bestScore] # which moves got that score?
i = random.choice(indices)
chosenMove = (i // BOARDWIDTH , i % BOARDWIDTH)
"""
placement = "abcdefghijk"[chosenMove[1]]+str(chosenMove[0]+1)
logThis("ChosenMove is {} at {}".format(chosenMove,placement))
return placeHex(channel,placement, "Gwendolyn")
def evaluateBoard(board):
scores = {1:0, 2:0}
winner = 0
# Here, I use Dijkstra's algorithm to evaluate the board, as proposed by this article: https://towardsdatascience.com/hex-creating-intelligent-adversaries-part-2-heuristics-dijkstras-algorithm-597e4dcacf93
for player in [1,2]:
Distance = copy.deepcopy(EMPTY_DIJKSTRA)
# Initialize the starting hexes. For the blue player, this is the leftmost column. For the red player, this is the tom row.
for start in (ALL_POSITIONS[::11] if player == 2 else ALL_POSITIONS[:11]):
# An empty hex adds a of distance of 1. A hex of own color add distance 0. Opposite color adds infinite distance.
Distance[start] = 1 if (board[start[0]][start[1]] == 0) else 0 if (board[start[0]][start[1]] == player) else math.inf
visited = set() # Also called sptSet, short for "shortest path tree Set"
for _ in range(BOARDWIDTH**2): # We can at most check every 121 hexes
# Find the next un-visited hex, that has the lowest distance
remainingHexes = ALL_SET.difference(visited)
A = [Distance[k] for k in remainingHexes] # Find the distance to each un-visited hex
u = list(remainingHexes)[A.index(min(A))] # Chooses the one with the lowest distance
# Find neighbors of the hex u
for di in HEX_DIRECTIONS:
v = (u[0] + di[0] , u[1] + di[1]) # v is a neighbor of u
if v[0] in range(11) and v[1] in range(11) and v not in visited:
new_dist = Distance[u] + (1 if (board[v[0]][v[1]] == 0) else 0 if (board[v[0]][v[1]] == player) else math.inf)
Distance[v] = min(Distance[v], new_dist)
# After a hex has been visited, this is noted
visited.add(u)
#logThis("Distance from player {}'s start to {} is {}".format(player,u,Distance[u]))
if u[player-1] == 10: # if the right coordinate of v is 10, it means we're at the goal
scores[player] = Distance[u] # A player's score is the shortest distance to goal. Which equals the number of remaining moves they need to win if unblocked by the opponent.
break
if len(game["gameHistory"]):
lastMove = game["gameHistory"][-1]
else:
logThis("For some reason, no path to the goal was found. ")
if scores[player] == 0:
winner = player
break # We don't need to check the other player's score, if player1 won.
return scores[2]-scores[1], winner
lastMove = (5,5)
# These moves are the last move +- 2.
moves = [[(lastMove[0]+j-2,lastMove[1]+i-2) for i in range(5) if lastMove[1]+i-2 in range(11)] for j in range(5) if lastMove[0]+j-2 in range(11)]
moves = sum(moves,[])
movesCopy = moves.copy()
for move in movesCopy:
if board[move[0]][move[1]] != 0:
moves.remove(move)
chosenMove = random.choice(moves)
def minimaxHex(board, depth, alpha, beta, maximizingPlayer):
# The depth is how many moves ahead the computer checks. This value is the difficulty.
if depth == 0 or 0 not in sum(board,[]):
score = evaluateBoard(board)[0]
return score
# if final depth is not reached, look another move ahead:
if maximizingPlayer: # red player predicts next move
maxEval = -math.inf
"""
GwenColor = data[channel]["players"].index("Gwendolyn") + 1 # either 1 or 2 - red or blue
if len(data[channel]["gameHistory"]) == 0:
return placeHex(channel,"F6", "Gwendolyn") # If starting, start in the middle
board = data[channel]["board"]
difficulty = data[channel]["difficulty"]
possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0]
#logThis("Judging a red move at depth {}".format(depth))
judgements = [float('nan')]*len(possiblePlaces) # All possible moves are yet to be judged
current_score = evaluateBoard(board)[0]
for i in possiblePlaces:
testBoard = copy.deepcopy(board)
testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 1 # because maximizingPlayer is Red which is number 1
evaluation = minimaxHex(testBoard,depth-1,alpha,beta,False)
maxEval = max(maxEval, evaluation)
alpha = max(alpha, evaluation)
if beta <= alpha:
#logThis("Just pruned something!")
break
return maxEval
else: # blue player predicts next move
minEval = math.inf
possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0]
#logThis("Judging a blue move at depth {}".format(depth))
for i in possiblePlaces:
testBoard = copy.deepcopy(board)
testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 2 # because minimizingPlayer is Blue which is number 2
evaluation = minimaxHex(testBoard,depth-1,alpha,beta,True)
minEval = min(minEval, evaluation)
beta = min(beta, evaluation)
if beta <= alpha:
#logThis("Just pruned something!")
break
return minEval
testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = GwenColor
if evaluateBoard(testBoard)[0] != current_score: # only think about a move if it improves the score (it's impossible to get worse)
# Testing a move and evaluating it
judgements[i] = minimaxHex(testBoard,difficulty,-math.inf,math.inf,GwenColor==2)
logThis("Best score for place {} is {}".format((i // BOARDWIDTH,i % BOARDWIDTH),judgements[i]))
bestScore = max(judgements) if (GwenColor == 1) else min(judgements) # this line has an error
indices = [i for i, x in enumerate(judgements) if x == bestScore] # which moves got that score?
i = random.choice(indices)
chosenMove = (i // BOARDWIDTH , i % BOARDWIDTH)
"""
placement = "abcdefghijk"[chosenMove[1]]+str(chosenMove[0]+1)
logThis("ChosenMove is {} at {}".format(chosenMove,placement))
return self.placeHex(channel,placement, "Gwendolyn")
def evaluateBoard(self, board):
scores = {1:0, 2:0}
winner = 0
# Here, I use Dijkstra's algorithm to evaluate the board, as proposed by this article: https://towardsdatascience.com/hex-creating-intelligent-adversaries-part-2-heuristics-dijkstras-algorithm-597e4dcacf93
for player in [1,2]:
Distance = copy.deepcopy(EMPTY_DIJKSTRA)
# Initialize the starting hexes. For the blue player, this is the leftmost column. For the red player, this is the tom row.
for start in (ALL_POSITIONS[::11] if player == 2 else ALL_POSITIONS[:11]):
# An empty hex adds a of distance of 1. A hex of own color add distance 0. Opposite color adds infinite distance.
Distance[start] = 1 if (board[start[0]][start[1]] == 0) else 0 if (board[start[0]][start[1]] == player) else math.inf
visited = set() # Also called sptSet, short for "shortest path tree Set"
for _ in range(BOARDWIDTH**2): # We can at most check every 121 hexes
# Find the next un-visited hex, that has the lowest distance
remainingHexes = ALL_SET.difference(visited)
A = [Distance[k] for k in remainingHexes] # Find the distance to each un-visited hex
u = list(remainingHexes)[A.index(min(A))] # Chooses the one with the lowest distance
# Find neighbors of the hex u
for di in HEX_DIRECTIONS:
v = (u[0] + di[0] , u[1] + di[1]) # v is a neighbor of u
if v[0] in range(11) and v[1] in range(11) and v not in visited:
new_dist = Distance[u] + (1 if (board[v[0]][v[1]] == 0) else 0 if (board[v[0]][v[1]] == player) else math.inf)
Distance[v] = min(Distance[v], new_dist)
# After a hex has been visited, this is noted
visited.add(u)
#logThis("Distance from player {}'s start to {} is {}".format(player,u,Distance[u]))
if u[player-1] == 10: # if the right coordinate of v is 10, it means we're at the goal
scores[player] = Distance[u] # A player's score is the shortest distance to goal. Which equals the number of remaining moves they need to win if unblocked by the opponent.
break
else:
logThis("For some reason, no path to the goal was found. ")
if scores[player] == 0:
winner = player
break # We don't need to check the other player's score, if player1 won.
return scores[2]-scores[1], winner
def minimaxHex(self, board, depth, alpha, beta, maximizingPlayer):
# The depth is how many moves ahead the computer checks. This value is the difficulty.
if depth == 0 or 0 not in sum(board,[]):
score = self.evaluateBoard(board)[0]
return score
# if final depth is not reached, look another move ahead:
if maximizingPlayer: # red player predicts next move
maxEval = -math.inf
possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0]
#logThis("Judging a red move at depth {}".format(depth))
for i in possiblePlaces:
testBoard = copy.deepcopy(board)
testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 1 # because maximizingPlayer is Red which is number 1
evaluation = self.minimaxHex(testBoard,depth-1,alpha,beta,False)
maxEval = max(maxEval, evaluation)
alpha = max(alpha, evaluation)
if beta <= alpha:
#logThis("Just pruned something!")
break
return maxEval
else: # blue player predicts next move
minEval = math.inf
possiblePlaces = [i for i,v in enumerate(sum(board,[])) if v == 0]
#logThis("Judging a blue move at depth {}".format(depth))
for i in possiblePlaces:
testBoard = copy.deepcopy(board)
testBoard[i // BOARDWIDTH][i % BOARDWIDTH] = 2 # because minimizingPlayer is Blue which is number 2
evaluation = self.minimaxHex(testBoard,depth-1,alpha,beta,True)
minEval = min(minEval, evaluation)
beta = min(beta, evaluation)
if beta <= alpha:
#logThis("Just pruned something!")
break
return minEval