def set_state_construction(self): """set the state of the building to 'construction', also changes its texture""" self.building_state = BuildingState.UNDER_CONSTRUCTION if self.has_texture_construction(): super().set_active_texture(self.__idx_texture_construction) else: error("Building does not have a construction texture!")
def set_state_destruction(self): """set the state of the building to 'destruction', also changes its texture""" self.building_state = BuildingState.DESTROYED if self.has_texture_destruction(): super().set_active_texture(self.__idx_texture_destruction) else: error("Building does not have a destruction texture!")
def evasive_movement(current_tile: Tile, target_tile: Tile, domain: List[Tile]) -> Tuple[Optional[Tile], int]: """ This movement calculates the next step which maximizes the distance to the target_tile Use with method to avoid collision between the entity placed on the current_tile, with a potentially hostile entity located on the target_tile :param current_tile: tile of the evading entity :param target_tile: tile of the hostile entity :param domain: the search domain, typically a subset of the walkable tiles :return: None, if no path is found, otherwise the next step and the distance of the resulting distance to target """ if not current_tile: error("current tile is None") return None, -1 if not target_tile: error("target tile is None") return None, -1 longest_path: Tuple[int, Optional[Tile]] = (-1, None) for nei in essentials.get_neighbours_on_set(current_tile, domain): step, dist = next_step_to_target(nei, target_tile, domain) if step: if dist > longest_path[0]: longest_path = (dist, nei) return longest_path[1], longest_path[0]
def query_ai(self, query, arg, player_id) -> str: if query == "diplo": return str(self.dict_of_ais[player_id].diplomacy.get_diplomatic_value_of_player(arg)) elif query == "state": return self.dict_of_ais[player_id].get_state_as_str() else: error("WRONG QUERY")
def __compare_to_ac(self, army: AI_Army, ac: ArmyConstellation) -> UnitType: off_merc = ac.ac[0] - (army.mercenaries / army.population) off_knight = ac.ac[1] - (army.knights / army.population) if off_merc < 0: # we have too many mercs return UnitType.KNIGHT if off_knight < 0: # too many knights return UnitType.MERCENARY error("This should not happen")
def bilinear_interpolation(a: (int, int), b: (int, int), t_start: float, t_end: float, t: float) -> Tuple[int, int]: if t > t_end: error("Animator: t > t_end") w = (t - t_start) / (t_end - t_start) x_pos = a[0] + w * (b[0] - a[0]) y_pos = a[1] + w * (b[1] - a[1]) if x_pos < 0 or y_pos < 0: error(f"Animator: {x_pos}|{y_pos}") return int(x_pos), int(y_pos)
def get_tile_from_ai_obj(obj: AI_OBJ) -> Optional[Tile]: """ :param obj: :return: """ tile: Optional[Tile] = None if type(obj) is Tile: tile = obj else: tile = obj.base_tile if tile is None: # is None error("Unable to get Tile from AI_Obj") return tile
def update(self, time): tpl = Animator.bilinear_interpolation( self.source, self.destination, self.start_time_ms, self.start_time_ms + self.time_ms, time) if len(tpl) != 2: error( "Animator: serious error, we left the 2d space! len(tpl): " + str(len(tpl))) hint(str(self.source)) hint(str(self.destination)) # if not (type(tpl) == Tuple): # error("Error in Animator -> bilinear interpolation output: " + str(type(tpl))) if self.valid: self.drawable.set_sprite_pos(tpl, self.camera_pos)
def remove_units_of_type(self, amount: int, ut: UnitType): tbr = [] count = 0 for u in self.__units: if count >= amount: break if u.unit_type == ut: tbr.append(u) count = count + 1 for r in tbr: self.__units.remove(r) if count != amount: error( "Army: unexpected amount of removed units. requested to remove: {}, removed: {}" .format(amount, count))
def __get_tile(self, offset_coordinates) -> Optional[Tile]: try: tile = self.map[offset_coordinates] if not tile: error("Tile not part of the map -> dict key error") return None return tile except KeyError: error( f"Caught Key error for key {offset_coordinates}, available keys are:" ) for k, _ in self.map.items(): print(str(k), end="") print("") traceback.print_exc() raise KeyError
def get_neighbours_dist(self, h: Hexagon, dist: int) -> Optional[List[Hexagon]]: if dist < 0: error("HexMap: negative distance?") return None elif dist == 0: return [h] # return a list to be consistent elif dist == 1: return self.get_neighbours(h) elif dist == 2: return self.get_neighbours_dist2(h) else: nei = [] for hex in self.map: if HexMap.hex_distance(h, hex) <= dist: nei.append(hex) return list(filter(None, nei))
def next_step_to_target(current_tile: Tile, target_tile: Tile, domain: List[Tile]) -> Tuple[Optional[Tile], int]: """ basic movement, returns next step, not the complete path :param current_tile: current tile of the walkable entity :param target_tile: target :param domain: the search domain, typically a subset of the walkable tiles :return: None, if no path is found, otherwise the next tile (step) where the army can move and the distance of the path """ if not current_tile: error("current tile is None") return None, -1 if not target_tile: error("target tile is None") return None, -1 path = essentials.a_star(current_tile, target_tile, domain) if len(path) <= 1: # no path found return None, -1 return path[1], len(path) - 1 # return next step and distance of the path
def army_vs_army(attacker: Army, defender: Army) -> BattleAfterMath: attack_value = attacker.get_attack_strength() defencive_value = defender.get_defence_strength() if attack_value == 0: error("Attack value is 0 -> will adjust it to 0.5") attack_value = 0.5 if defencive_value == 0: error("Defence value is 0 -> will adjust it to 0.5") defencive_value = 0.5 attacker_won = attack_value >= defencive_value defender_won = defencive_value >= attack_value attacker_losses = defencive_value if attacker_won else attack_value defender_losses = attack_value if defender_won else defencive_value attacker_alive_pop_ratio = (attack_value - attacker_losses) / attack_value defender_alive_pop_ratio = (defencive_value - defender_losses) / defencive_value if attacker_won: attacker_alive_pop_ratio = attacker_alive_pop_ratio + (attacker_losses/3.0) / attack_value if defender_won: defender_alive_pop_ratio = defender_alive_pop_ratio + (defender_losses/3.0) / defencive_value attacker_pop = attacker.get_population() defender_pop = defender.get_population() attacker_surviving_pop_ratio = attacker_pop * attacker_alive_pop_ratio defender_surviving_pop_ratio = defender_pop * defender_alive_pop_ratio for unit in UnitType: attacker_unit_x = attacker.get_population_by_unit(unit) attacker_kill_count_unit_x = attacker_unit_x - ((attacker_unit_x / attacker_pop) * attacker_surviving_pop_ratio) attacker.remove_units_of_type(ceil(attacker_kill_count_unit_x), unit) defender_unit_x = defender.get_population_by_unit(unit) defender_kill_count_unit_x = defender_unit_x - ((defender_unit_x / defender_pop) * defender_surviving_pop_ratio) defender.remove_units_of_type(ceil(defender_kill_count_unit_x), unit) if attacker_won and defender_won: return BattleAfterMath.DRAW elif attacker_won: return BattleAfterMath.ATTACKER_WON else: return BattleAfterMath.DEFENDER_WON
def protective_movement(current_tile: Tile, target_tile: Tile, protected_tile: Tile, domain: List[Tile]) -> Tuple[Optional[Tile], int]: """ If an army/obj chooses to use protective movement, it will stay close to the entity it is protecting It will position itself such that it intercepts the incoming hostile entity on the target tile if possible :param current_tile: tile of the entity which is protecting (friendly army for instance) :param target_tile: tile of the hostile entity :param protected_tile: tile of the entity which is to be protected :param domain: the search domain, typically a subset of the walkable tiles :return: None if there is a problem or no path is found, otherwise the next step and the size of the path """ if not (current_tile and target_tile and protected_tile): error("a tile is None") return None, -1 shortest_path: Tuple[int, Optional[Tile]] = (1000, None) for nei in essentials.get_neighbours_on_set(protected_tile, domain): step, dist = next_step_to_target(nei, target_tile, domain) if step: if dist < shortest_path[0]: shortest_path = (dist, nei) return next_step_to_target(current_tile, shortest_path[1], domain)
def get_cardinal_direction( tile: Tuple[int, int, int], base: Tuple[int, int, int]) -> List[CardinalDirection]: """get the cardinal direction of the cube coordinates of a tile with respect to a base function may return two adjacent CDs. IN This case, the tile is placed on the border""" x_normal = tile[0] - base[0] y_normal = tile[1] - base[1] z_normal = tile[2] - base[2] if x_normal + y_normal + z_normal != 0: error("error in CD") ret = [] if x_normal <= 0 and y_normal > 0 and z_normal <= 0: ret.append(CardinalDirection.SouthWest) if x_normal >= 0 and y_normal >= 0 and z_normal < 0: ret.append(CardinalDirection.South) if x_normal > 0 and y_normal <= 0 and z_normal <= 0: ret.append(CardinalDirection.SouthEast) if x_normal >= 0 and y_normal < 0 and z_normal >= 0: ret.append(CardinalDirection.NorthEast) if x_normal <= 0 and y_normal <= 0 and z_normal > 0: ret.append(CardinalDirection.North) if x_normal < 0 and y_normal >= 0 and z_normal >= 0: ret.append(CardinalDirection.NorthWest) return ret
def smooth_map(hex_map: HexMap): x_max = hex_map.map_dim[0] y_max = hex_map.map_dim[1] adjusted_tiles: List[(Hexagon, str)] = [] for y in range(y_max): for x in range(x_max): current_hex = hex_map.get_hex_by_offset((x, y)) if current_hex.ground.ground_type in SmoothMap.ignore_list: continue inner = "" # tex_code of the "inner" texture how it is written on the map! inv = False outer_tex = '' # tex_code of the texture of the inner!!! tex # TODO this is bad! inner_tex = '' # tex_code of the texture of the outer!!! tex if current_hex.ground.tex_code == "xx": inner = "gc" outer_tex = 'lg' inner_tex = 'dg' elif current_hex.ground.tex_code == "yy": inner = "gr" # inv = False outer_tex = 'dg' inner_tex = 'lg' elif current_hex.ground.tex_code == "zz": inner = "st" outer_tex = 'st' inner_tex = 'lg' elif current_hex.ground.tex_code == "ww": # inv = False inner = "gr" outer_tex = 'lg' inner_tex = 'st' elif current_hex.ground.tex_code == "vv": inner = "gc" outer_tex = "dg" inner_tex = "st" inv = True elif current_hex.ground.tex_code == "uu": inner = "gr" outer_tex = "lg" inner_tex = "st" inv = True else: continue # gather neighbours edge = [False, False, False, False, False, False ] # true if this side of the hexagon gets smoothed out nei = [ hex_map.get_hex_northeast(current_hex).ground. tex_code, # the order is important, better do it explicitly hex_map.get_hex_east(current_hex).ground.tex_code, hex_map.get_hex_southeast(current_hex).ground.tex_code, hex_map.get_hex_southwest(current_hex).ground.tex_code, hex_map.get_hex_west(current_hex).ground.tex_code, hex_map.get_hex_northwest(current_hex).ground.tex_code ] for i in range(6): edge[i] = nei[i] == current_hex.ground.tex_code mode = -1 orientation = False if sum(edge) == 2 or sum(edge) == 1 or sum( edge ) == 0: # in-place replacement would not work -> store reference if sum(edge) == 1 or sum(edge) == 0: for i in range(6): edge[i] = nei[i] == "wd" or nei[i] == "xx" or nei[ i] == "ww" or nei[i] == "vv" or nei[i] == "uu" if edge[0] and edge[2]: # 1_3 mode = SmoothMap.__1_TO_3 orientation = (edge[1] == inner) if inv: orientation = not orientation elif edge[0] and edge[3]: # 1_4 mode = SmoothMap.__1_TO_4 if nei[1] == inner: orientation = True else: orientation = False elif edge[0] and edge[4]: # 1_5 mode = SmoothMap.__1_TO_5 orientation = nei[1] == inner elif edge[1] and edge[3]: # 2_4 mode = SmoothMap.__2_TO_4 if nei[2] == inner: orientation = False else: orientation = True elif edge[1] and edge[4]: # 2_5 mode = SmoothMap.__2_TO_5 if nei[2] == inner: orientation = False else: orientation = True elif edge[1] and edge[5]: # 2_6 mode = SmoothMap.__2_TO_6 orientation = nei[2] == inner elif edge[2] and edge[4]: # 3_5 mode = SmoothMap.__3_TO_5 if nei[3] == inner: orientation = False else: orientation = True elif edge[2] and edge[5]: # 3_6 mode = SmoothMap.__3_TO_6 if nei[3] == inner: orientation = True else: orientation = False elif edge[3] and edge[5]: # 4_6 mode = SmoothMap.__4_TO_6 if nei[4] == inner: orientation = False else: orientation = True if orientation: adjusted_tiles.append( (current_hex, "{}_{}_{}var_0".format(outer_tex, inner_tex, mode))) else: adjusted_tiles.append( (current_hex, "{}_{}_{}var_0".format(inner_tex, outer_tex, mode))) # elif sum(edge) == 1: # # this is okay if one side is water # for i in range(6) # edge[i] = nei[i] == "wd" # mode = SmoothMap.__2_TO_5 # orientation = False # if orientation: # adjusted_tiles.append((current_hex, "{}_{}_{}var_0".format(outer_tex, inner_tex, mode))) # else: # adjusted_tiles.append((current_hex, "{}_{}_{}var_0".format(inner_tex, outer_tex, mode))) else: error("Smooth Map. Lines may not split.") # set the tex_code for h, s in adjusted_tiles: h.ground.tex_code = s h.ground.ground_type = GroundType.MIXED
def weight_options(self, options: List[Option], ai_stat: AI_GameStatus, move: AI_Move): used_weights: List[str] = [] for opt in options: # --------------------- Action options ---------- opt.weighted_score = opt.score.value if opt.score == Priority.P_NO: # no option (should not depend on weights) -> contain invalid info continue for w in self.weights: if w.condition(opt, ai_stat): used_weights.append(w.condition.__name__) if DETAILED_DEBUG: self._dump(f"Weight w: {w.weight} applied on score: {opt.weighted_score} of {type(opt)} ") opt.weighted_score = opt.weighted_score + w.weight used_weights.append(" | ") for m in self.priolist_targets: # --------------------- Movement options ---------- if m.score == Priority.P_NO: continue for w in self.m_weights: if w.condition(m, ai_stat): used_weights.append(w.condition.__name__) m.weighted_score = m.weighted_score + w.weight options.sort(key=lambda x: x.weighted_score, reverse=True) self.priolist_targets.sort(key=lambda x: x.weighted_score, reverse=True) self._dump("---") for opt in options: s = f"\tOption of type: {type(opt)}, score: {opt.weighted_score}" if type(opt) == RecruitmentOption or type(opt) == BuildOption: s = s + f", type {opt.type}" if type(opt) == ScoutingOption: s = s + f", site: {opt.site}" s = s + f", former priority: {opt.score}" self._dump(s) for m in self.priolist_targets: s = f"\tAttack Target : {'army' if type(m.target) is AI_Army else 'building'}, score: {m.weighted_score}" self._dump(s) s_tmp = "" for w in used_weights: s_tmp += w + ", " self._dump(s_tmp) # translate this into move best_option: Option = options[0] if type(best_option) == WaitOption: move.move_type = MoveType.DO_NOTHING move.str_rep_of_action = "waiting" elif type(best_option) == BuildOption: move.move_type = MoveType.DO_BUILD move.loc = best_option.site move.type = best_option.type for at in best_option.associated_tiles: move.info.append(at) s_tmp = f"building a {best_option.type} at {str(move.loc)} ({str(best_option.cardinal_direction)})" if move.type == BuildingType.FARM: s_tmp += f" TL: {str(best_option.threat_level)}" move.str_rep_of_action = s_tmp elif type(best_option) == RecruitmentOption: move.move_type = MoveType.DO_RECRUIT_UNIT move.type = best_option.type move.str_rep_of_action = f"recruiting a {best_option.type}" elif type(best_option) == ScoutingOption: move.move_type = MoveType.DO_SCOUT move.loc = best_option.site move.str_rep_of_action = "scouting at" + str(move.loc) elif type(best_option) == AI_Mazedonian.RaiseArmyOption: move.move_type = MoveType.DO_RAISE_ARMY move.loc = self.get_army_spawn_loc(ai_stat) move.str_rep_of_action = "raising new army at" elif type(best_option) is UpgradeOption: move.move_type = MoveType.DO_UPGRADE_BUILDING move.loc = best_option.site move.type = best_option.type move.str_rep_of_action = "upgrading hut to villa" else: error("unexpected type") self._dump(f"DECISION: {move.str_rep_of_action}")
def set_active_texture(self, idx: int): if self.tex_counter >= idx: self.sprite.set_texture(idx) self.__active_tex = idx else: error("Drawable: No texture at index: " + str(idx))
def weight_options(self, ai_stat: AI_GameStatus, move: AI_Move, all_options: List[Union[BuildOption, RecruitmentOption, RaiseArmyOption, WaitOption]], movement_options: List[ArmyMovementOption]): used_weights: List[str] = [] for opt in all_options: # --------------------- Action options ---------- if opt.score == Priority.P_NO: continue opt.weighted_score = opt.score.value for w in self.weights: if w.condition(opt, ai_stat): used_weights.append(w.condition.__name__) opt.weighted_score = opt.weighted_score + w.weight used_weights.append(" | ") for opt in movement_options: # --------------------- Movement options ---------- if opt.score == Priority.P_NO: continue opt.weighted_score = opt.score.value for w in self.m_weights: if w.condition(opt, ai_stat): used_weights.append(w.condition.__name__) opt.weighted_score = opt.weighted_score + w.weight all_options.sort(key=lambda x: x.weighted_score, reverse=True) movement_options.sort(key=lambda x: x.weighted_score, reverse=True) if len(all_options) > 0: best_option = all_options[0] if type(best_option) == WaitOption: move.move_type = MoveType.DO_NOTHING move.str_rep_of_action = "waiting" elif type(best_option) == BuildOption: move.move_type = MoveType.DO_BUILD move.loc = best_option.site move.type = best_option.type move.str_rep_of_action = f"building a {best_option.type} at " + str( move.loc) elif type(best_option) == UpgradeOption: move.move_type = MoveType.DO_UPGRADE_BUILDING move.loc = best_option.site move.type = best_option.type move.str_rep_of_action = f"upgrading building to {move.type}" elif type(best_option) == RecruitmentOption: move.move_type = MoveType.DO_RECRUIT_UNIT move.type = best_option.type move.str_rep_of_action = f"recruiting a {best_option.type}" elif type(best_option) == RaiseArmyOption: move.move_type = MoveType.DO_RAISE_ARMY move.loc = best_option.site move.str_rep_of_action = "raising new army at" else: error("unexpected type") if len(movement_options) > 0: best_m_option = movement_options[0] if best_m_option.weighted_score >= self.properties[ 'threshold_army_movement']: move.move_army_to = best_m_option.next_step move.doMoveArmy = True for opt in all_options: s = f"Option of type {type(opt)}, score: {opt.weighted_score} ({opt.score})" if not (type(opt) == WaitOption or type(opt) == RaiseArmyOption): s = s + f" -> Type: {opt.type}" self._dump(s) for m_opt in movement_options: stmp = 'army' if type(m_opt.target) is AI_Army else '' stmp = 'building' if type(m_opt.target) is AI_Building else '' s = f"M-Option target: {type(m_opt)} target({stmp}), score: {m_opt.weighted_score} ({m_opt.score})" self._dump(s) s = f"DECISION: {move.str_rep_of_action}" if move.doMoveArmy: s += f" moving army to {move.move_army_to}" self._dump(s)