def get_alpha_beta_value(board: Board, depth: int, alpha: float, beta: float, color: PlayerColor, parameters: List[float]) -> float: if (depth == 0 or board.phase == GamePhase.FINISHED): return Player.get_heuristic_value(board, color, parameters) if (color == PlayerColor.WHITE): # Maximizer v: float = -999999 deltas: List[Delta] = board.get_all_possible_deltas(color) for delta in deltas: v = max( v, Player.get_alpha_beta_value(board.get_next_board(delta), depth - 1, alpha, beta, color.opposite(), parameters)) alpha = max(alpha, v) if (beta <= alpha): break return v else: # Minimizer v = 999999 deltas: List[Delta] = board.get_all_possible_deltas(color) for delta in deltas: v = min( v, Player.get_alpha_beta_value(board.get_next_board(delta), depth - 1, alpha, beta, color.opposite(), parameters)) beta = min(beta, v) if (beta <= alpha): break return v
def get_best_delta(board: Board, player: PlayerColor, depth: int, recent_board_history: List[str]) \ -> Tuple[Delta, List[float]]: """ Returns the highest-rated (or best) move from the current board for the given player, exploring 'depth' number of levels to determine the best move. recent_board_history helps avoid repeating board states. Along with the delta object for the best move, also returns a list of floats, containing the ratings for the series of moves used to rate the returned delta. This list is more thoroughly explained in the docs for 'get_board_ratings'. """ # Evaluate all of the possible moves from this board. deltas: List[Delta] = board.get_all_possible_deltas(player) # Shuffling the calculated deltas can assist in avoiding endless loops, # particularly if board states are being repeated. random.shuffle(deltas) # Iterate through every valid move, rating them and keeping track of the # best move along the way. best_delta: Tuple[Delta, List[float]] = (None, [-999999]) for delta in deltas: delta_ratings: List[float] = \ IDSAgent.get_board_ratings(board.get_next_board(delta), depth - 1, recent_board_history) # This "max" criteria defined by the lambda looks a bit complex, so # let's explain. Keep in mind that floats further to the left in a # given list represents the rating of a board further down the game # three. When finding the best move sequence (from the current # board's perspective), we prioritize moves that result in the best # score at its furthest down board state i.e. [5 1 1] is better than # [1 9 9], because three moves down, it will have a board rated 5 vs # the other's board which is rated 1. In the event of this first # value being equal, we prefer the shortest list i.e. [5 2] is # better than [5 3 2]. This is because the shorter list means that # the game will be over sooner (but still have a board rating as # good as the longer list). This will help the algorithm execute # killing moves in 'Massacre' and not put them off by doing an # inconsequential move first. Finally, if the lengths are the same, # we prioritize the list with the highest rating at any given index # i.e. [5 3 3] is better than [5 3 2] because it means that we're # doing the moves that will keep the board's rating as high as # possible (again, only if the comparison gets to this point). # Sorted example according to this criteria: # [4 3 2] > [3 1] > [3 2 2] > [3 2 1] > [-999]. best_delta = max([best_delta, (delta, delta_ratings)], key=lambda x: (x[1][0], -len(x[1]), x[1])) return best_delta
def alphabeta(board: Board, node: Node, depth: int, alpha: float, beta: float, is_maximizer: bool) -> float: if (depth == 0 or board.phase == GamePhase.FINISHED): return AlphaBetaAgent.get_heuristic_value(board) if (is_maximizer): v: float = -999999 deltas: List[Delta] = board.get_all_possible_deltas(Utils.get_player(board.round_num)) for delta in deltas: child_node: Node = Node(node, delta) v = max(v, AlphaBetaAgent.alphabeta(board.get_next_board(delta), child_node, depth - 1, alpha, beta, False)) alpha = max(alpha, v) if (beta <= alpha): break return v else: v = 999999 deltas: List[Delta] = board.get_all_possible_deltas(Utils.get_player(board.round_num)) for delta in deltas: child_node: Node = Node(node, delta) v = min(v, AlphaBetaAgent.alphabeta(board.get_next_board(delta), child_node, depth - 1, alpha, beta, True)) beta = min(beta, v) if beta <= alpha: break return v
class AlphaBetaAgent(): _board: Board _node: Node _init_node: Node = Node(None, None) def __init__(self, start_board: Board = None, seed: int = random.randint(0, 999999)): if (start_board == None): self._board = Board(None, 1, GamePhase.PLACEMENT) else: self._board = start_board self._node = self._init_node random.seed(seed) def run(self): print(self._board) is_maximizer: bool = False while (self._board.phase != GamePhase.FINISHED): deltas: List[Delta] = self._board.get_all_possible_deltas(Utils.get_player(self._board.round_num)) delta_scores: List[Tuple[Delta, float]] = [] for delta in deltas: delta_scores.append((delta, AlphaBetaAgent.alphabeta(self._board.get_next_board(delta), Node(self._node, delta), 2, -9999, 9999, is_maximizer))) if (len(set([delta_score[1] for delta_score in delta_scores])) == 1): best_delta: Tuple[Delta, float] = random.choice(delta_scores) elif not is_maximizer: best_delta: Tuple[Delta, float] = max(delta_scores, key=lambda x:x[1]) elif is_maximizer: best_delta: Tuple[Delta, float] = min(delta_scores, key=lambda x:x[1]) self._board = self._board.get_next_board(best_delta[0]) self._node = Node(self._node, best_delta[0]) is_maximizer = not is_maximizer print("{:3}: {} ({})".format(self._board.round_num - 1, best_delta[0], best_delta[1])) print(self._board) @staticmethod def alphabeta(board: Board, node: Node, depth: int, alpha: float, beta: float, is_maximizer: bool) -> float: if (depth == 0 or board.phase == GamePhase.FINISHED): return AlphaBetaAgent.get_heuristic_value(board) if (is_maximizer): v: float = -999999 deltas: List[Delta] = board.get_all_possible_deltas(Utils.get_player(board.round_num)) for delta in deltas: child_node: Node = Node(node, delta) v = max(v, AlphaBetaAgent.alphabeta(board.get_next_board(delta), child_node, depth - 1, alpha, beta, False)) alpha = max(alpha, v) if (beta <= alpha): break return v else: v = 999999 deltas: List[Delta] = board.get_all_possible_deltas(Utils.get_player(board.round_num)) for delta in deltas: child_node: Node = Node(node, delta) v = min(v, AlphaBetaAgent.alphabeta(board.get_next_board(delta), child_node, depth - 1, alpha, beta, True)) beta = min(beta, v) if beta <= alpha: break return v @staticmethod def get_heuristic_value(board: Board): num_white_pieces: int = len(board._get_player_squares(PlayerColor.WHITE)) num_black_pieces: int = len(board._get_player_squares(PlayerColor.BLACK)) return num_white_pieces - num_black_pieces
class Player(): # --- Heuristic Weights --- # TODO: Consider if we should weigh own player's pieces higher than enemies's. _OWN_PIECE_WEIGHT: float _OPPONENT_PIECE_WEIGHT: float # Don't want to prioritize mobility over pieces, so it's much smaller. _OWN_MOBILITY_WEIGHT: float _OPPONENT_MOBILITY_WEIGHT: float # TODO: How to balance cohesiveness and mobility? They're opposing, in a way. _OWN_DIVIDED_WEIGHT: float # Bad to be divided. Want to be cohesive! _OPPONENT_DIVIDED_WEIGHT: float # Good for opponent to be divided. _OWN_NON_CENTRALITY_WEIGHT: float _OPPONENT_NON_CENTRALITY_WEIGHT: float # Heuristic score decimal place rounding. Used to prevent floating point # imprecision from interfering with move decisions. _RATING_NUM_ROUNDING: int = 10 _ALPHA_START_VALUE: int = -9999 _BETA_START_VALUE: int = 9999 _SEED: int = 13373 # A reference to the current board that the agent is on. _board: Board _color: PlayerColor # The depth to go in each iteration of the iterative-deepening search # algorithm i.e. number of moves to look ahead. _depth: int = 1 def __init__(self, color: str, parameters: List[float]): """ TODO This method is called by the referee once at the beginning of the game to initialise your player. You should use this opportunity to set up your own internal representation of the board, and any other state you would like to maintain for the duration of the game. The input parameter colour is a string representing the piece colour your program will control for this game. It can take one of only two values: the string 'white' (if you are the White player for this game) or the string 'black' (if you are the Black player for this game). """ self.parameters = parameters self._board = Board(None, 0, GamePhase.PLACEMENT) if (color.lower() == "white"): self._color = PlayerColor.WHITE else: self._color = PlayerColor.BLACK random.seed(Player._SEED) def action(self, turns) -> Union[str, None]: """ This method is called by the referee to request an action by your player. The input parameter turns is an integer representing the number of turns that have taken place since the start of the current game phase. For example, if White player has already made 11 moves in the moving phase, and Black player has made 10 moves (and the referee is asking for its 11th move), then the value of turns would be 21. Based on the current state of the board, your player should select its next action and return it. Your player should represent this action based on the instructions below, in the ‘Representing actions’ section. """ deltas: List[Delta] = self._board.get_all_possible_deltas(self._color) if (len(deltas) == 0): return None delta_scores: Dict[Delta, float] = {} for delta in deltas: delta_scores[delta] = \ Player.get_alpha_beta_value( self._board.get_next_board(delta), Player._depth - 1, Player._ALPHA_START_VALUE, Player._BETA_START_VALUE, self._color, self.parameters) if self._board.round_num > 0 and \ self._board.phase == GamePhase.PLACEMENT: test = { k: v for k, v in delta_scores.items() if (not self._board.is_suicide(k)) } delta_scores = test best_deltas: List[Delta] = Utils.get_best_deltas( delta_scores, self._color) best_delta: Tuple[Delta, float] if (len(best_deltas) > 1): # There are more than one "best" deltas. Pick a random one. best_delta = random.choice(list(best_deltas)) else: best_delta = best_deltas[0] self._board = self._board.get_next_board(best_delta[0]) # if self._color == PlayerColor.WHITE and self.parameters == [1, -1, 0.01, -0.01]: # print([(str(delta), score) for delta, score in delta_scores.items()]) # print("{} {} DOES {} [{}]".format(self.parameters, self._color, best_delta[0], best_delta[1])) return best_delta[0].get_referee_form() def update(self, action: Tuple[Union[int, Tuple[int]]]): """ This method is called by the referee to inform your player about the opponent’s most recent move, so that you can maintain your internal board configuration. The input parameter action is a representation of the opponent’s recent action based on the instructions below, in the ‘Representing actions’ section. This method should not return anything. Note: update() is only called to notify your player about the opponent’s actions. Your player will not be notified about its own actions. - To represent the action of placing a piece on square (x,y), use a tuple (x,y). - To represent the action of moving a piece from square (a,b) to square (c,d), use a nested tuple ((a,b),(c,d)). - To represent a forfeited turn, use the value None. """ # TODO # Easiest way to generate a Delta from 'action' seems to be to use # board.get_valid_movements or board.get_valid_placements and then # "getting" the Delta being made by matching the Pos2Ds. if (action is None): # Opponent forfeited turn. self._board.round_num += 1 self._board._update_game_phase() return positions: List[Pos2D] if (type(action[0]) == int): positions = [Pos2D(action[0], action[1])] else: positions = [Pos2D(x, y) for x, y in action] opponent_delta: Delta = None deltas: List[Delta] if (len(positions) == 1): # Placement assert (self._board.phase == GamePhase.PLACEMENT) deltas = self._board.get_possible_placements( self._color.opposite()) for delta in deltas: if delta.move_target.pos == positions[0]: opponent_delta = delta break elif (len(positions) == 2): # Movement. try: assert (self._board.phase == GamePhase.MOVEMENT) except AssertionError: print( "WARNING: 'assert(self._board.phase == GamePhase.MOVEMENT)' FAILED.'" ) print("SETTING PHASE = GAMEPHASE.MOVEMENT.") self._board.phase = GamePhase.MOVEMENT deltas = self._board.get_possible_moves(positions[0]) for delta in deltas: if delta.move_target.pos == positions[1]: opponent_delta = delta break assert (opponent_delta is not None) self._board = self._board.get_next_board(opponent_delta) @staticmethod def get_alpha_beta_value(board: Board, depth: int, alpha: float, beta: float, color: PlayerColor, parameters: List[float]) -> float: if (depth == 0 or board.phase == GamePhase.FINISHED): return Player.get_heuristic_value(board, color, parameters) if (color == PlayerColor.WHITE): # Maximizer v: float = -999999 deltas: List[Delta] = board.get_all_possible_deltas(color) for delta in deltas: v = max( v, Player.get_alpha_beta_value(board.get_next_board(delta), depth - 1, alpha, beta, color.opposite(), parameters)) alpha = max(alpha, v) if (beta <= alpha): break return v else: # Minimizer v = 999999 deltas: List[Delta] = board.get_all_possible_deltas(color) for delta in deltas: v = min( v, Player.get_alpha_beta_value(board.get_next_board(delta), depth - 1, alpha, beta, color.opposite(), parameters)) beta = min(beta, v) if (beta <= alpha): break return v @staticmethod def get_heuristic_value(board: Board, player: PlayerColor, parameters: List[float]): """ Given a board, calculates and returns its rating based on heuristics. """ player_squares: List[Square] = board.get_player_squares(player) opponent_squares: List[Square] = board.get_player_squares( player.opposite()) # -- Num pieces -- # Calculate the number of white and black pieces. This is a very # important heuristic that will help prioritize preserving white's own # pieces and killing the enemy's black pieces. num_own_pieces: int = len(player_squares) num_opponent_pieces: int = len(opponent_squares) # -- Mobility -- # Calculate the mobility for both white and black i.e. the number of # possible moves they can make. own_mobility: int = board.get_num_moves(player) opponent_mobility: int = board.get_num_moves(player.opposite()) # -- Cohesiveness -- own_total_distance: int = 0 opponent_total_distance: int = 0 displacement: Pos2D for idx, square in enumerate(player_squares): for square2 in player_squares[idx + 1:]: displacement = square.pos - square2.pos own_total_distance += abs(displacement.x) + abs(displacement.y) for idx, square in enumerate(opponent_squares): for square2 in opponent_squares[idx + 1:]: displacement = square.pos - square2.pos opponent_total_distance += abs(displacement.x) + abs( displacement.y) own_avg_allied_distance: float = own_total_distance / (num_own_pieces + 1) opponent_avg_allied_distance: float = opponent_total_distance / ( num_opponent_pieces + 1) # -- Centrality -- own_total_distance: int = 0 opponent_total_distance: int = 0 x_displacement: float y_displacement: float for square in player_squares: x_displacement = 3.5 - square.pos.x y_displacement = 3.5 - square.pos.y own_total_distance += abs(x_displacement) + abs(y_displacement) for square in opponent_squares: x_displacement = 3.5 - square.pos.x y_displacement = 3.5 - square.pos.y opponent_total_distance += abs(x_displacement) + abs( y_displacement) own_avg_center_distance: float = own_total_distance / (num_own_pieces + 1) opponent_avg_center_distance: float = opponent_total_distance / ( num_opponent_pieces + 1) # Calculate the heuristic score/rating. rounded_heuristic_score: float = round( parameters[0] * num_own_pieces + parameters[1] * num_opponent_pieces + parameters[2] * own_mobility + parameters[3] * opponent_mobility + parameters[4] * own_avg_allied_distance + parameters[5] * opponent_avg_allied_distance + parameters[6] * own_avg_center_distance + parameters[7] * opponent_avg_center_distance, Player._RATING_NUM_ROUNDING) # Return the score as is or negate, depending on the player. # For white, return as is. For black, negate. return rounded_heuristic_score if player == PlayerColor.WHITE \ else -rounded_heuristic_score
class Player(): # --- Heuristic Weights --- # TODO: Consider if we should weigh own player's pieces higher than enemies's. _OWN_PIECE_WEIGHT: float = 1 _OPPONENT_PIECE_WEIGHT: float = -1 # Don't want to prioritize mobility over pieces, so it's much smaller. _OWN_MOBILITY_WEIGHT: float = 0.01 _OPPONENT_MOBILITY_WEIGHT: float = -0.01 # TODO: How to balance cohesiveness and mobility? They're opposing, in a way. _OWN_DIVIDED_WEIGHT: float = -0.001 # Bad to be divided. Want to be cohesive! _OPPONENT_DIVIDED_WEIGHT: float = 0.001 # Good for opponent to be divided. _OWN_NON_CENTRALITY_WEIGHT: float = -0.005 _OPPONENT_NON_CENTRALITY_WEIGHT: float = 0.005 # Heuristic score decimal place rounding. Used to prevent floating point # imprecision from interfering with move decisions. _RATING_NUM_ROUNDING: int = 10 # --- Timer parameters --- # The total time for the player in the game. _TIME_LIMIT: float = 120.0 # The remaining amount of time remaining at which point the AI will start # picking moves completely randomly so as to not run out of time. _PANIC_MODE_REMAINING_TIME: float = 2.0 # The amount of rounds expected to be played. Includes placement rounds and # all rounds until around 2nd deathzone. _NUM_EXPECTED_ROUNDS: int = 24 + 194 _DEPTH_TWO_EXPECTED_TURN_TIME: float = 200 # --- Other parameters --- _ALPHA_START_VALUE: int = -9999 _BETA_START_VALUE: int = 9999 _SEED: int = 1337 # --- Instance variables --- # A reference to the current board that the agent is on. _timer: Timer _board: Board _color: PlayerColor def __init__(self, color: str): """ TODO This method is called by the referee once at the beginning of the game to initialise your player. You should use this opportunity to set up your own internal representation of the board, and any other state you would like to maintain for the duration of the game. The input parameter colour is a string representing the piece colour your program will control for this game. It can take one of only two values: the string 'white' (if you are the White player for this game) or the string 'black' (if you are the Black player for this game). """ self._board = Board(None, 0, GamePhase.PLACEMENT) if (color.lower() == "white"): self._color = PlayerColor.WHITE else: self._color = PlayerColor.BLACK random.seed(Player._SEED) self._timer = Timer(Player._TIME_LIMIT) def action(self, turns) -> Union[str, None]: """ This method is called by the referee to request an action by your player. The input parameter turns is an integer representing the number of turns that have taken place since the start of the current game phase. For example, if White player has already made 11 moves in the moving phase, and Black player has made 10 moves (and the referee is asking for its 11th move), then the value of turns would be 21. Based on the current state of the board, your player should select its next action and return it. Your player should represent this action based on the instructions below, in the ‘Representing actions’ section. """ with(self._timer): deltas: List[Delta] = self._board.get_all_possible_deltas(self._color) if (len(deltas) == 0): return None remaining_time: float = self._timer.limit - self._timer.clock if (remaining_time < Player._PANIC_MODE_REMAINING_TIME): # AHH! Not much time remaining - pick a random move. print(self._color, "PANIC") random_delta: Delta = random.choice(deltas) self._board = self._board.get_next_board(random_delta) return random_delta.get_referee_form() # Determine the depth based on the amount of time remaining. depth: int remaining_expected_rounds: int = \ Player._NUM_EXPECTED_ROUNDS - self._board.round_num remaining_expected_time_per_round: float = \ remaining_time / (remaining_expected_rounds + 10) if (remaining_expected_time_per_round > Player._DEPTH_TWO_EXPECTED_TURN_TIME): depth = 2 else: depth = 1 print("Looking {} moves ahead!".format(depth)) delta_scores: Dict[Delta, float] = {} for delta in deltas: delta_scores[delta] = \ Player.get_alpha_beta_value( self._board.get_next_board(delta), depth - 1, Player._ALPHA_START_VALUE, Player._BETA_START_VALUE, self._color) best_deltas: List[Delta] = Utils.get_best_deltas(delta_scores, self._color) best_delta: Tuple[Delta, float] if (len(best_deltas) > 1): # There are more than one "best" deltas. Pick a random one. best_delta = random.choice(list(best_deltas)) else: best_delta = best_deltas[0] self._board = self._board.get_next_board(best_delta[0]) print(self._color, "DOES", best_delta[0], "[{}]".format(best_delta[1])) return best_delta[0].get_referee_form() def update(self, action: Tuple[Union[int, Tuple[int]]]): """ This method is called by the referee to inform your player about the opponent’s most recent move, so that you can maintain your internal board configuration. The input parameter action is a representation of the opponent’s recent action based on the instructions below, in the ‘Representing actions’ section. This method should not return anything. Note: update() is only called to notify your player about the opponent’s actions. Your player will not be notified about its own actions. - To represent the action of placing a piece on square (x,y), use a tuple (x,y). - To represent the action of moving a piece from square (a,b) to square (c,d), use a nested tuple ((a,b),(c,d)). - To represent a forfeited turn, use the value None. """ # TODO # Easiest way to generate a Delta from 'action' seems to be to use # board.get_valid_movements or board.get_valid_placements and then # "getting" the Delta being made by matching the Pos2Ds. with self._timer: print(self._color, "SEES", action) if (action is None): # Opponent forfeited turn. self._board.round_num += 1 self._board._update_game_phase() positions: List[Pos2D] if (type(action[0]) == int): positions = [Pos2D(action[0], action[1])] else: positions = [Pos2D(x, y) for x, y in action] opponent_delta: Delta = None deltas: List[Delta] if (len(positions) == 1): # Placement assert(self._board.phase == GamePhase.PLACEMENT) deltas = self._board.get_possible_placements(self._color.opposite()) for delta in deltas: if delta.move_target.pos == positions[0]: opponent_delta = delta break elif (len(positions) == 2): # Movement. assert(self._board.phase == GamePhase.MOVEMENT) deltas = self._board.get_possible_moves(positions[0]) for delta in deltas: if delta.move_target.pos == positions[1]: opponent_delta = delta break assert(opponent_delta is not None) self._board = self._board.get_next_board(opponent_delta) @staticmethod def get_alpha_beta_value(board: Board, depth: int, alpha: float, beta: float, color: PlayerColor) -> float: if (depth == 0 or board.phase == GamePhase.FINISHED): return Player.get_heuristic_value(board, color) if (color == PlayerColor.WHITE): # Maximizer v: float = Player._ALPHA_START_VALUE deltas: List[Delta] = board.get_all_possible_deltas(color) for delta in deltas: v = max(v, Player.get_alpha_beta_value(board.get_next_board(delta), depth - 1, alpha, beta, color.opposite())) alpha = max(alpha, v) if (beta <= alpha): break return v else: # Minimizer v = Player._BETA_START_VALUE deltas: List[Delta] = board.get_all_possible_deltas(color) for delta in deltas: v = min(v, Player.get_alpha_beta_value(board.get_next_board(delta), depth - 1, alpha, beta, color.opposite())) beta = min(beta, v) if (beta <= alpha): break return v @staticmethod def get_heuristic_value(board: Board, player: PlayerColor): """ Given a board, calculates and returns its rating based on heuristics. """ player_squares: List[Square] = board.get_player_squares(player) opponent_squares: List[Square] = board.get_player_squares(player.opposite()) # -- Num pieces -- # Calculate the number of white and black pieces. This is a very # important heuristic that will help prioritize preserving white's own # pieces and killing the enemy's black pieces. num_own_pieces: int = len(player_squares) num_opponent_pieces: int = len(opponent_squares) # -- Mobility -- # Calculate the mobility for both white and black i.e. the number of # possible moves they can make. own_mobility: int = board.get_num_moves(player) opponent_mobility: int = board.get_num_moves(player.opposite()) # -- Cohesiveness -- own_total_distance: int = 0 opponent_total_distance: int = 0 displacement: Pos2D for idx, square1 in enumerate(player_squares): for square2 in player_squares[idx + 1:]: displacement = square1.pos - square2.pos own_total_distance += abs(displacement.x) + abs(displacement.y) for idx, square1 in enumerate(opponent_squares): for square2 in opponent_squares[idx + 1:]: displacement = square1.pos - square2.pos opponent_total_distance += abs(displacement.x) + abs(displacement.y) own_avg_allied_distance: float = own_total_distance / ( num_own_pieces + 1) opponent_avg_allied_distance: float = opponent_total_distance / ( num_opponent_pieces + 1) # -- Centrality -- own_total_distance: int = 0 opponent_total_distance: int = 0 x_displacement: float y_displacement: float for square in player_squares: x_displacement = 3.5 - square.pos.x y_displacement = 3.5 - square.pos.y own_total_distance += abs(x_displacement) + abs(y_displacement) for square in opponent_squares: x_displacement = 3.5 - square.pos.x y_displacement = 3.5 - square.pos.y opponent_total_distance += abs(x_displacement) + abs(y_displacement) own_avg_center_distance: float = own_total_distance / ( num_own_pieces + 1) opponent_avg_center_distance: float = opponent_total_distance / ( num_opponent_pieces + 1) # Calculate the heuristic score/rating. rounded_heuristic_score: float = round( Player._OWN_PIECE_WEIGHT * num_own_pieces + Player._OPPONENT_PIECE_WEIGHT * num_opponent_pieces + Player._OWN_MOBILITY_WEIGHT * own_mobility + Player._OPPONENT_MOBILITY_WEIGHT * opponent_mobility + Player._OWN_DIVIDED_WEIGHT * own_avg_allied_distance + Player._OPPONENT_DIVIDED_WEIGHT * opponent_avg_allied_distance + Player._OWN_NON_CENTRALITY_WEIGHT * own_avg_center_distance + Player._OPPONENT_NON_CENTRALITY_WEIGHT * opponent_avg_center_distance, Player._RATING_NUM_ROUNDING) # Return the score as is or negate, depending on the player. # For white, return as is. For black, negate. return rounded_heuristic_score if player == PlayerColor.WHITE \ else -rounded_heuristic_score