def area_descriptor(area: Area, board: Board): neighborhood = area.get_adjacent_areas() player_name = area.get_owner_name() enemy_areas = [ adj for adj in neighborhood if board.get_area(adj).get_owner_name() != player_name ] owned_areas = [ adj for adj in neighborhood if board.get_area(adj).get_owner_name() == player_name ] unique_enemies = { board.get_area(adj).get_owner_name() for adj in neighborhood } if player_name in unique_enemies: unique_enemies.remove(player_name) max_region_size = max( len(r) for r in board.get_players_regions(player_name)) feature_vector = [ survival_prob(area, board), rel_area_power(area, board), len(enemy_areas), len(owned_areas), len(unique_enemies), max_region_size / len(board.areas) ] return np.asarray(feature_vector, dtype=np.float32)
def _heuristic(self, players: List[int], board: Board) -> Dict[int, float]: """ Computes the heuristic function for all the given players. :param players: Names of players. :param board: The game board. :return: A dictionary where keys are names of given players and values are values of the heuristic function for these players in a current game board. """ h = {} for player in players: h[player] = board.get_player_dice(player) players_regions = board.get_players_regions(player) players_regions_sizes = [] for region in players_regions: region_size = len(region) h[player] += self.__REG_HEURISTIC_WEIGHT * region_size players_regions_sizes.append(region_size) h[player] += \ self.__LARGEST_REG_HEURISTIC_WEIGHT * max(players_regions_sizes) return h
def possible_attacks(board: Board, player_name: int): for source in board.get_player_border(player_name): if not source.can_attack(): continue for adj in source.get_adjacent_areas(): target = board.get_area(adj) if target.get_owner_name() != player_name: succ_prob = ATTACK_SUCC_PROBS[source.get_dice()][ target.get_dice()] yield source, target, succ_prob
def possible_attacks(board: Board, player_name: int) -> Iterator[Tuple[int, int]]: for area in board.get_player_border(player_name): if not area.can_attack(): continue neighbours = area.get_adjacent_areas() for adj in neighbours: adjacent_area = board.get_area(adj) if adjacent_area.get_owner_name() != player_name: yield (area, adjacent_area)
def path_heuristics(board: Board, path: list) -> float: """ Heuristika = součet všech dílčích heuristik v cestě """ if len(path) < 2: return 0 with simulate_battle(board.get_area(path[0]), board.get_area(path[1])): h = path_heuristics(board, path[1:]) h += battle_heuristic(board, board.get_area(path[0]), board.get_area(path[1])) return h
def game_descriptor(board: Board, player_name: int, players: list): areas = board.get_player_areas(player_name) regions = board.get_players_regions(player_name) border = board.get_player_border(player_name) max_region_size = max(len(r) for r in regions) rel_border_size_1 = len(border) / sum( len(board.get_player_border(name)) for name in players) rel_border_size_2 = sum(a.get_dice() for a in border) / sum(a.get_dice() for a in areas) rel_area_size = len(areas) / sum( len(board.get_player_areas(name)) for name in players) best_border = [] for r in regions: if len(r) == max_region_size: for area in map(lambda a: board.get_area(a), r): if board.is_at_border(area): best_border.append(area) feature_vector = [ max_region_size / len(board.areas), rel_border_size_1, rel_border_size_2, rel_area_size, np.mean([survival_prob(a, board) for a in best_border]), np.mean([rel_area_power(a, board) for a in areas]) ] return np.asarray(feature_vector, dtype=np.float32)
def get_attackable(board: Board, active_area: Area): neighbors = active_area.get_adjacent_areas() attackable = [] for nb in neighbors: a = board.get_area(nb) if active_area.get_owner_name() != a.get_owner_name(): attackable.append(a) return attackable
def survival_prob(target: Area, board: Board): prob = 1. for adj in target.get_adjacent_areas(): source = board.get_area(adj) if source.get_owner_name() == target.get_owner_name( ) or not source.can_attack(): continue prob *= 1. - ATTACK_SUCC_PROBS[source.get_dice()][target.get_dice()] return prob
def rel_area_power(area: Area, board: Board): player_power = area.get_dice() total_power = player_power for adj in area.get_adjacent_areas(): adj_area = board.get_area(adj) dice = adj_area.get_dice() if adj_area.get_owner_name() == area.get_owner_name(): player_power += dice total_power += dice return player_power / total_power
def add_dice_to_player(board: Board, player_name: int): """Vytvoří dočasné herní pole s náhodně přidělenými kostkami pro vybraného hráče. Základní logika předělování kostek je převzatá z implemenetace hry viz dicewars/server/game.py line:230. Funkce se používá stejně jako funkce simulate_battle. Args ---- board (Board): Aktuální stav herního pole player_name (int): Jméno hráče, kterému se mají přiřadit kostky """ affected_areas: Dict[int, int] = {} dice = 0 regions = board.get_players_regions(player_name) for region in regions: dice = max(dice, len(region)) areas: List[Area] = [] for area in board.get_player_areas(player_name): areas.append(area) if dice > 64: dice = 64 while dice and areas: area = random.choice(areas) if area.dice >= 8: areas.remove(area) else: if area.name not in affected_areas: affected_areas[area.name] = area.dice area.dice += 1 dice -= 1 try: yield finally: # Vrátit herní pole do původního stavu for name in affected_areas: board.get_area(name).dice = affected_areas[name]
def __largest_region(player_name: int, board: Board) -> List[int]: """ Returns the largest region of a given player. :param player_name: A name (ID) of the associated player. :param board: The game board. :return: The largest region of a given player. (A list of area names (IDs) in the largest region.) """ players_regions = board.get_players_regions(player_name) max_region_size = max(len(r) for r in players_regions) return [r for r in players_regions if len(r) == max_region_size][0]
def __perform_atack(board: Board, a_name: int, b_name: int) -> None: """ Performes a successful atack. :param board: The game board. :param a_name: A name of an atacker area. :param b_name: A name of a defender area. """ a = board.get_area(a_name) b = board.get_area(b_name) b.set_dice(a.get_dice() - 1) a.set_dice(1) b.set_owner(a_name)
def player_heuristic(board: Board, player_name: int) -> int: """Výpočet heuristiky pro hráče podle aktuálního stavu herní plochy. koeficient = počet_polí_hlavního_území Args ---- board (Board): Aktuální stav herní plochy player_name (int): Jméno hráče Returns ------- int: Výsledná hodnota heuristiky. Větší znamená lepší """ score = 0 regions = board.get_players_regions(player_name) for region in regions: score = max(score, len(region)) return score
def __possible_turns(self, player_name: int, board: Board) -> List[Tuple[int, int]]: """ Returns possible turns with higher hold probability than a trashold. (The single turn expectiminimax.) :param player_name: A name (ID) of the associated player. :param board: The game board. :return: Possible turns with higher hold probability than a trashold. (A list of tuples (atacker area; defender area) sorted by a weight in descending order.) """ if board.nb_players_alive() > self.__ATK_PLAYERS_LIMIT_2: atk_prob_treshold = self.__ATK_PROB_TRESHOLD_2 atk_from_larges_reg_weight = self.__ATK_FROM_LARGEST_REG_WEIGHT_2 else: atk_prob_treshold = self.__ATK_PROB_TRESHOLD atk_from_larges_reg_weight = self.__ATK_FROM_LARGEST_REG_WEIGHT turns = [] largest_region = self.__largest_region(player_name, board) for a, b in possible_attacks(board, player_name): a_name = a.get_name() b_name = b.get_name() atk_power = a.get_dice() p = probability_of_successful_attack(board, a_name, b_name) p *= probability_of_holding_area(board, b_name, atk_power - 1, player_name) if p >= atk_prob_treshold or atk_power == self.__MAX_DICE: if a_name in largest_region: p *= atk_from_larges_reg_weight turns.append((a_name, b_name, p)) turns = sorted(turns, key=lambda t: t[2], reverse=True) turns = list(map(lambda t: t[:2], turns)) return turns
def __init__(self, player_name: int, board: Board, players_order: List[int]) -> None: """ Constructs the AI agent for the Dice Wars game. :param player_name: A name (ID) of the associated player. :param board: A copy of the game board. :param players_order: An order in which the players take turn. (A list of players IDs.) """ super().__init__() self.__player_name = player_name self.__board = board self.__players_order = players_order self.__loger = getLogger(self.__LOGGER_NAME) self.__model = self.__load_model() nb_players = board.nb_players_alive() self.__loger.debug( f'An AI agent for the {nb_players}-player game is set up.' f' player_name: {player_name}; players_order: {players_order}')
def __maxn_rec( self, board: Board, players_names: Deque[int], depth: int, ) -> Tuple[Union[Tuple[int, int], None], Dict[int, float]]: """ The Max^n recursive algorithm. It uses the single turn expectiminimax for the computation of the best moves in the individual turns. :param board: A copy of the game board. :param players_names: A queue of players (names of players, i.e., IDs) to be considered in this algorithm. The player on the top of the queue is a currently associated player. After a player is processed, he is removed from the queue. :param depth: A depth of the algorithm. It declinines with recursive calls of this method. :return: A tuple where the first item is a tuple (atacker area, defender area) with the best turn, or None in case there is no suitable turn. And the second item of the tuple is a dictionary where keys are names of players and values are values of the heuristic function for these players in a current game board. """ # there are no more players if not players_names: # just evaluate the heuristic function for the AI's player return None, self._heuristic([self.__player_name], board) player_name = players_names[-1] # current player is not alive => skip him if not board.get_player_areas(player_name): players_names.pop() return self.__maxn_rec(deepcopy(board), players_names, depth) # depth is not 0 => expand lower nodes if depth: # expand some lower suitable nodes, according to the single turn # expectiminimax, the number of nodes to be expanded is decreasing # with a depth turns = self.__possible_turns(player_name, board)[:depth] if not turns: # no further moves possible => return just the heuristic # in a leaf node _, h = self.__maxn_rec(deepcopy(board), players_names, 0) return None, h h = {} turn = 0, 0 for a, b in turns: board_copy = deepcopy(board) self.__perform_atack(board_copy, a, b) # simulate an atack _, new_h = self.__maxn_rec(board_copy, players_names, depth - 1) # maximise the heuristic for a current player if player_name in new_h: if player_name not in h \ or new_h[player_name] > h[player_name]: h = deepcopy(new_h) turn = a, b return turn if h else None, h # depth is 0 => advance to further players or compute the heuristic players_names.pop() # there are furthe players if players_names: return self.__maxn_rec(deepcopy(board), players_names, self.__MAXN_DEPTH) # compute the heuristic function for all living players living_players = set(a.get_owner_name() for a in board.areas.values()) h = self._heuristic(list(living_players), board) return None, h
def add_dice_to_player_worst_case(board: Board, player_name: int, is_yourself: bool, logger): affected_areas: Dict[int, int] = {} dice = 0 regions = board.get_players_regions(player_name) for region in regions: dice = max(dice, len(region)) if is_yourself: areas = board.get_player_areas(player_name) b_a = board.get_player_border(player_name) areas = list(set(areas)-set(b_a)) if areas is None: areas = b_a else: areas = board.get_player_border(player_name) arr = [a.get_name() for a in areas] logger.info("PLayer: {} with areas: {}".format(player_name, arr)) if dice > 64: dice = 64 while dice and areas: if is_yourself: area = random.choice(areas) while area.dice < 8: area.dice += 1 dice -= 1 else: area = areas.pop(0) areas.append(area) if area.dice >= 8: areas.remove(area) elif not is_yourself: if area.name not in affected_areas: affected_areas[area.name] = area.dice area.dice += 1 dice -= 1 if dice > 0: if is_yourself: areas = board.get_player_border(player_name) else: areas = board.get_player_areas(player_name) while dice and areas: area = random.choice(areas) if area.dice >= 8: areas.remove(area) else: if area.name not in affected_areas: affected_areas[area.name] = area.dice area.dice += 1 dice -= 1 try: yield finally: # Vrátit herní pole do původního stavu for name in affected_areas: board.get_area(name).dice = affected_areas[name]