class CombatUnits(): def __init__(self, units: Units, knowledge: 'Knowledge'): self.knowledge = knowledge self.unit_values = knowledge.unit_values self.units = units self.center = sc2math.unit_geometric_median(units) self.ground_units = self.units.not_flying if self.ground_units: self.center = self.ground_units.closest_to((self.center)).position self.power = ExtendedPower(self.unit_values) self.power.add_units(self.units) self.debug_index = 0 self._total_distance: Optional[float] = None self._area_by_circles: float = 0 def is_too_spread_out(self) -> bool: if self._total_distance is None: self._total_distance = 0 self._area_by_circles = 5 for unit in self.units: d = unit.distance_to(self.center) self._total_distance += d self._area_by_circles += unit.radius ** 2 # self.knowledge.print(f"spread: {self._total_distance} d to {self._total_radius} r") return (self._total_distance / len(self.units)) ** 2 > self._area_by_circles * 2 def is_in_combat(self, closest_enemies: 'CombatUnits') -> bool: if closest_enemies is None: return False distance = self.center.distance_to_point2(closest_enemies.center) if distance > 17: return False if distance < 10 \ or self.knowledge.unit_cache.enemy_in_range(self.center, 10).exclude_type(self.unit_values.combat_ignore): return True engaged_power = 0 total_power = 0 for unit in self.units: # type: Unit power = self.unit_values.power(unit) total_power += power for enemy_near in closest_enemies.units: d = enemy_near.distance_to(unit) if d < self.unit_values.real_range(unit, enemy_near, self.knowledge): engaged_power += power break return engaged_power > total_power * 0.15
def calc_needs_evacuation(self): """ Checks if the zone needs evacuation for the workers mining there. This is a method because it is quite CPU heavy. """ enemies: Units = self.cache.enemy_in_range(self.mineral_line_center, 10) power = ExtendedPower(self.unit_values) power.add_units(enemies) if power.ground_power > 3 and enemies.exclude_type( self.unit_values.worker_types): self.needs_evacuation = True else: self.needs_evacuation = False
def solve_combat(self, goal: CombatGoal, command: CombatAction, enemies: EnemyData) -> List[CombatAction]: oracle = goal.unit if not oracle.has_buff(BuffId.ORACLEWEAPON): goal.ready_to_shoot = False air_shooter_enemies = Units([], self.ai) enemy: Unit power = ExtendedPower(self.unit_values) for enemy in enemies.close_enemies: if self.unit_values.air_range(enemy) < enemy.distance_to(oracle) + 1: air_shooter_enemies.append(enemy) power.add_unit(enemy) if self.unit_values.is_static_air_defense(enemy): power.add(5) # can't beat turrets with oracle enemy_center = enemies.close_enemies.center for air_shooter in air_shooter_enemies: # type: Unit if air_shooter.is_light and not air_shooter.is_flying: power.add_units(air_shooter_enemies) else: power.add_units(air_shooter_enemies * 2) time = self.knowledge.ai.time if goal.move_type == MoveType.PanicRetreat and oracle.has_buff(BuffId.ORACLEWEAPON): return self.disable_beam(oracle) possible_targets = enemies.close_enemies.filter(lambda u: not u.is_flying and not u.is_structure and u.is_light) if possible_targets.exists: if oracle.energy > 50 and possible_targets.closest_distance_to(oracle) < 5 and not oracle.has_buff(BuffId.ORACLEWEAPON): return self.enable_beam(oracle) if power.air_power > 0 and power.air_power <= 3: target = air_shooter_enemies.closest_to(oracle) if target.is_light or target.health_percentage < 0.5: if not oracle.has_buff(BuffId.ORACLEWEAPON): return self.enable_beam(oracle) # Kill the target return [CombatAction(oracle, target, True)] #target_pos = self.knowledge.pathing_manager.find_weak_influence_air(goal.target, 7) #move_step = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, target_pos) return [CombatAction(oracle, target.position, True)] elif goal.ready_to_shoot and possible_targets: return [CombatAction(oracle, possible_targets.closest_to(oracle), True)] elif power.air_power > 12: # Panic retreat to whatever direction if goal.move_type in offensive: new_target: Point2 = self.knowledge.pathing_manager.find_weak_influence_air(goal.target, 7) step = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, new_target) # backstep: Point2 = self.knowledge.pathing_manager.find_weak_influence_air(oracle.position, 7) move_action = CombatAction(oracle, step, False) else: backstep = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, goal.target) move_action = CombatAction(oracle, backstep, False) # Todo disable beam? return [move_action] elif power.air_power > 3: # Try kiting while killing the target target = self.knowledge.pathing_manager.find_weak_influence_air(goal.target, 7) backstep = self.knowledge.pathing_manager.find_influence_air_path(oracle.position, target) if goal.move_type in offensive: move_action = CombatAction(oracle, backstep, False) else: move_action = CombatAction(oracle, backstep, False) if oracle.has_buff(BuffId.ORACLEWEAPON): if possible_targets: closest = possible_targets.closest_to(oracle) if closest.distance_to(oracle) < 5: return [CombatAction(oracle, closest, True)] return [CombatAction(oracle, command.target, True), move_action] else: return [move_action] if possible_targets.exists: return [CombatAction(oracle, command.target, True)] else: return [CombatAction(oracle, command.target, False)]
def enemy_static_power(self) -> ExtendedPower: """Returns power of enemy static defenses.""" power = ExtendedPower(self.unit_values) power.add_units(self.enemy_static_defenses) return power
class GroupCombatManager(ManagerBase): def __init__(self): super().__init__() async def start(self, knowledge: 'Knowledge'): await super().start(knowledge) self.cache: UnitCacheManager = self.knowledge.unit_cache self.pather: PathingManager = self.knowledge.pathing_manager self.tags: List[int] = [] self.unit_micros: Dict[UnitTypeId, MicroStep] = dict() self.all_enemy_power = ExtendedPower(self.unit_values) # Micro controllers / handlers self.unit_micros[UnitTypeId.DRONE] = MicroWorkers(knowledge) self.unit_micros[UnitTypeId.PROBE] = MicroWorkers(knowledge) self.unit_micros[UnitTypeId.SCV] = MicroWorkers(knowledge) # Protoss self.unit_micros[UnitTypeId.ARCHON] = NoMicro(knowledge) self.unit_micros[UnitTypeId.ADEPT] = MicroAdepts(knowledge) self.unit_micros[UnitTypeId.CARRIER] = MicroCarriers(knowledge) self.unit_micros[UnitTypeId.COLOSSUS] = MicroColossi(knowledge) self.unit_micros[UnitTypeId.DARKTEMPLAR] = MicroZerglings(knowledge) self.unit_micros[UnitTypeId.DISRUPTOR] = MicroDisruptor(knowledge) self.unit_micros[UnitTypeId.DISRUPTORPHASED] = MicroPurificationNova( knowledge) self.unit_micros[UnitTypeId.HIGHTEMPLAR] = MicroHighTemplars(knowledge) self.unit_micros[UnitTypeId.OBSERVER] = MicroObservers(knowledge) self.unit_micros[UnitTypeId.ORACLE] = MicroOracles(knowledge) self.unit_micros[UnitTypeId.PHOENIX] = MicroPhoenixes(knowledge) self.unit_micros[UnitTypeId.SENTRY] = MicroSentries(knowledge) self.unit_micros[UnitTypeId.STALKER] = MicroStalkers(knowledge) self.unit_micros[UnitTypeId.WARPPRISM] = MicroWarpPrism(knowledge) self.unit_micros[UnitTypeId.VOIDRAY] = MicroVoidrays(knowledge) self.unit_micros[UnitTypeId.ZEALOT] = MicroZealots(knowledge) # Zerg self.unit_micros[UnitTypeId.ZERGLING] = MicroZerglings(knowledge) self.unit_micros[UnitTypeId.ULTRALISK] = NoMicro(knowledge) self.unit_micros[UnitTypeId.OVERSEER] = MicroOverseers(knowledge) self.unit_micros[UnitTypeId.QUEEN] = MicroQueens(knowledge) # Terran self.unit_micros[UnitTypeId.HELLIONTANK] = NoMicro(knowledge) self.unit_micros[UnitTypeId.SIEGETANK] = MicroTanks(knowledge) self.unit_micros[UnitTypeId.VIKINGFIGHTER] = MicroVikings(knowledge) self.unit_micros[UnitTypeId.MARINE] = MicroBio(knowledge) self.unit_micros[UnitTypeId.MARAUDER] = MicroBio(knowledge) self.generic_micro = GenericMicro(knowledge) self.regroup_threshold = 0.75 async def update(self): self.enemy_groups: List[CombatUnits] = self.group_enemy_units() self.all_enemy_power.clear() for group in self.enemy_groups: # type: CombatUnits self.all_enemy_power.add_units(group.units) async def post_update(self): pass @property def debug(self): return self._debug and self.knowledge.debug def add_unit(self, unit: Unit): if unit.type_id in ignored: # Just no return self.tags.append(unit.tag) def add_units(self, units: Units): for unit in units: self.add_unit(unit) def get_all_units(self) -> Units: units = Units([], self.ai) for tag in self.tags: unit = self.cache.by_tag(tag) if unit: units.append(unit) return units def execute(self, target: Point2, move_type=MoveType.Assault): our_units = self.get_all_units() if len(our_units) < 1: return self.own_groups: List[CombatUnits] = self.group_own_units( our_units, 12) total_power = ExtendedPower(self.unit_values) for group in self.own_groups: total_power.add_power(group.power) if self.debug: fn = lambda group: group.center.distance_to(self.ai.start_location) sorted_list = sorted(self.own_groups, key=fn) for i in range(0, len(sorted_list)): sorted_list[i].debug_index = i for group in self.own_groups: center = group.center closest_enemies = self.closest_group(center, self.enemy_groups) if closest_enemies is None: if move_type == MoveType.PanicRetreat: self.move_to(group, target, move_type) else: self.attack_to(group, target, move_type) else: power = group.power enemy_power = ExtendedPower(closest_enemies) enemy_center = closest_enemies.center is_in_combat = group.is_in_combat(closest_enemies) # pseudocode for attack if move_type == MoveType.DefensiveRetreat or move_type == MoveType.PanicRetreat: self.move_to(group, target, move_type) break if power.power > self.regroup_threshold * total_power.power: # Most of the army is here if (group.is_too_spread_out() and not is_in_combat): self.regroup(group, group.center) else: self.attack_to(group, target, move_type) elif is_in_combat: if not power.is_enough_for(enemy_power, 0.75): # Regroup if possible own_closest_group = self.closest_group( center, self.own_groups) if own_closest_group: self.move_to(group, own_closest_group.center, MoveType.ReGroup) else: # fight to bitter end self.attack_to(group, closest_enemies.center, move_type) else: self.attack_to(group, closest_enemies.center, move_type) else: if group.power.is_enough_for(self.all_enemy_power, 0.85): # We have enough units here to crush everything the enemy has self.attack_to(group, closest_enemies.center, move_type) else: # Regroup if possible if move_type == MoveType.Assault: # Group towards attack target own_closest_group = self.closest_group( target, self.own_groups) else: # Group up with closest group own_closest_group = self.closest_group( center, self.own_groups) if own_closest_group: self.move_to(group, own_closest_group.center, MoveType.ReGroup) else: # fight to bitter end self.attack_to(group, closest_enemies.center, move_type) # if move_type == MoveType.SearchAndDestroy: # enemy_closest_group = self.closest_group(center, self.enemy_groups) # if enemy_closest_group: # self.attack_to(actions, group, closest_enemies.center, move_type) # else: # self.attack_to(actions, group, target, move_type) # elif power.is_enough_for(enemy_power, 0.75): # if move_type == MoveType.PanicRetreat: # self.move_to(actions, group, target, move_type) # else: # self.attack_to(actions, group, closest_enemies.center, move_type) # # elif distance > 12: # own_closest_group = self.closest_group(center, self.own_groups) # # if own_closest_group: # self.move_to(actions, group, own_closest_group.center, MoveType.ReGroup) # # elif move_type == MoveType.PanicRetreat: # self.move_to(actions, group, target, move_type) # else: # self.attack_to(actions, group, enemy_center, move_type) # # elif enemy_power.is_enough_for(power, 0.75): # own_closest_group = self.closest_group(center, self.own_groups) # # if own_closest_group: # self.move_to(actions, group, own_closest_group.center, MoveType.ReGroup) # # elif move_type == MoveType.PanicRetreat: # self.move_to(actions, group, target, move_type) # else: # self.attack_to(actions, group, target, move_type) self.tags.clear() def regroup(self, group: CombatUnits, target: Union[Unit, Point2]): if isinstance(target, Unit): target = self.pather.find_path(group.center, target.position, 1) else: target = self.pather.find_path(group.center, target, 3) self.move_to(group, target, MoveType.Push) def move_to(self, group: CombatUnits, target, move_type: MoveType): self.action_to(group, target, move_type, False) def attack_to(self, group: CombatUnits, target, move_type: MoveType): self.action_to(group, target, move_type, True) def action_to(self, group: CombatUnits, target, move_type: MoveType, is_attack: bool): if isinstance(target, Point2) and group.ground_units: if move_type in {MoveType.DefensiveRetreat, MoveType.PanicRetreat}: target = self.pather.find_influence_ground_path( group.center, target, 14) else: target = self.pather.find_path(group.center, target, 14) own_unit_cache: Dict[UnitTypeId, Units] = {} for unit in group.units: units = own_unit_cache.get(unit.type_id, Units([], self.ai)) if units.amount == 0: real_type = self.unit_values.real_type(unit.type_id) own_unit_cache[real_type] = units units.append(unit) for type_id, type_units in own_unit_cache.items(): micro: MicroStep = self.unit_micros.get(type_id, self.generic_micro) micro.init_group(group, type_units, self.enemy_groups, move_type) group_action = micro.group_solve_combat(type_units, Action(target, is_attack)) for unit in type_units: action = micro.unit_solve_combat(unit, group_action) order = action.to_commmand(unit) if order: self.ai.do(order) if self.debug: if action.debug_comment: status = action.debug_comment elif action.ability: status = action.ability.name elif action.is_attack: status = "Attack" else: status = "Move" if action.target is not None: if isinstance(action.target, Unit): status += f": {action.target.type_id.name}" else: status += f": {action.target}" status += f" G: {group.debug_index}" status += f"\n{move_type.name}" pos3d: Point3 = unit.position3d pos3d = Point3((pos3d.x, pos3d.y, pos3d.z + 2)) self.ai._client.debug_text_world(status, pos3d, size=10) def closest_group( self, start: Point2, combat_groups: List[CombatUnits]) -> Optional[CombatUnits]: group = None best_distance = 50 # doesn't find enemy groups closer than this for combat_group in combat_groups: center = combat_group.center if center == start: continue # it's the same group! distance = start.distance_to(center) if distance < best_distance: best_distance = distance group = combat_group return group def group_own_units(self, our_units: Units, lookup_distance: float = 7) -> List[CombatUnits]: groups: List[Units] = [] assigned: Dict[int, int] = dict() for unit in our_units: if unit.tag in assigned: continue units = Units([unit], self.ai) index = len(groups) assigned[unit.tag] = index groups.append(units) self.include_own_units(unit, units, lookup_distance, index, assigned) return [CombatUnits(u, self.knowledge) for u in groups] def include_own_units(self, unit: Unit, units: Units, lookup_distance: float, index: int, assigned: Dict[int, int]): units_close_by = self.cache.own_in_range(unit.position, lookup_distance) for unit_close in units_close_by: if unit_close.tag in assigned or unit_close.tag not in self.tags: continue assigned[unit_close.tag] = index units.append(unit_close) self.include_own_units(unit_close, units, lookup_distance, index, assigned) def group_enemy_units(self) -> List[CombatUnits]: groups: List[Units] = [] assigned: Dict[int, int] = dict() lookup_distance = 7 for unit in self.knowledge.known_enemy_units_mobile: if unit.tag in assigned or unit.type_id in self.unit_values.combat_ignore or not unit.can_be_attacked: continue units = Units([unit], self.ai) index = len(groups) assigned[unit.tag] = index groups.append(units) self.include_enemy_units(unit, units, lookup_distance, index, assigned) return [CombatUnits(u, self.knowledge) for u in groups] def include_enemy_units(self, unit: Unit, units: Units, lookup_distance: float, index: int, assigned: Dict[int, int]): units_close_by = self.cache.enemy_in_range(unit.position, lookup_distance) for unit_close in units_close_by: if unit_close.tag in assigned or unit_close.tag not in self.tags or not unit.can_be_attacked: continue assigned[unit_close.tag] = index units.append(unit_close) self.include_enemy_units(unit_close, units, lookup_distance, index, assigned)
class MicroStep(ABC): def __init__(self, knowledge): self.knowledge: 'Knowledge' = knowledge self.ai: sc2.BotAI = knowledge.ai self.unit_values: UnitValue = knowledge.unit_values self.cd_manager: CooldownManager = knowledge.cooldown_manager self.pather: PathingManager = knowledge.pathing_manager self.cache: UnitCacheManager = knowledge.unit_cache self.delay_to_shoot = self.ai._client.game_step + 1.5 self.enemy_groups: List[CombatUnits] = [] self.ready_to_attack_ratio: float = 0.0 self.center: Point2 = Point2((0, 0)) self.group: CombatUnits self.engage_ratio = 0 self.can_engage_ratio = 0 self.closest_group: CombatUnits self.engaged: Dict[int, List[int]] = dict() self.engaged_power = ExtendedPower(knowledge.unit_values) self.our_power = ExtendedPower(knowledge.unit_values) self.closest_units: Dict[int, Optional[Unit]] = dict() self.move_type = MoveType.Assault def init_group(self, group: CombatUnits, units: Units, enemy_groups: List[CombatUnits], move_type: MoveType): self.group = group self.move_type = move_type ready_to_attack = 0 self.our_power = group.power self.closest_units.clear() self.engaged_power.clear() self.closest_group = None self.closest_group_distance = 1000000 for enemy_group in enemy_groups: d = enemy_group.center.distance_to(group.center) if d < self.closest_group_distance: self.closest_group_distance = d self.closest_group = enemy_group self.enemy_groups = enemy_groups self.center = units.center self.enemies_near_by: Units = self.knowledge.unit_cache.enemy_in_range(self.center, 15 + len(group.units) * 0.1) self.engaged_power.add_units(self.enemies_near_by) engage_count = 0 can_engage_count = 0 for unit in units: closest_distance = 1000 if self.ready_to_shoot(unit): ready_to_attack += 1 engage_added = False can_engage_added = False for enemy_near in self.enemies_near_by: # type: Unit d = enemy_near.distance_to(unit) if d < closest_distance: self.closest_units[unit.tag] = enemy_near closest_distance = d if not engage_added and d < self.unit_values.real_range(enemy_near, unit, self.knowledge): engage_count += 1 engage_added = True if not can_engage_added and d < self.unit_values.real_range(unit, enemy_near, self.knowledge): can_engage_count += 1 can_engage_added = True self.ready_to_attack_ratio = ready_to_attack / len(units) self.engage_ratio = engage_count / len(units) self.can_engage_ratio = can_engage_count / len(units) def ready_to_shoot(self, unit: Unit) -> bool: if unit.type_id == UnitTypeId.CYCLONE: # if knowledge.cooldown_manager.is_ready(self.unit.tag, AbilityId.LOCKON_LOCKON): # self.ready_to_shoot = True # return if self.cd_manager.is_ready(unit.tag, AbilityId.CANCEL_LOCKON): return False if unit.type_id == UnitTypeId.DISRUPTOR: return self.cd_manager.is_ready(unit.tag, AbilityId.EFFECT_PURIFICATIONNOVA) if unit.type_id == UnitTypeId.ORACLE: tick = self.ai.state.game_loop % 16 return tick < 8 if unit.type_id == UnitTypeId.CARRIER: tick = self.ai.state.game_loop % 32 return tick < 8 return unit.weapon_cooldown <= self.delay_to_shoot @abstractmethod def group_solve_combat(self, units: Units, current_command: Action) -> Action: pass @abstractmethod def unit_solve_combat(self, unit: Unit, current_command: Action) -> Action: pass def focus_fire(self, unit: Unit, current_command: Action, prio: Optional[Dict[UnitTypeId, int]]) -> Action: shoot_air = self.unit_values.can_shoot_air(unit) shoot_ground = self.unit_values.can_shoot_ground(unit, self.knowledge) air_range = self.unit_values.air_range(unit) ground_range = self.unit_values.ground_range(unit, self.knowledge) lookup = min(air_range + 3, ground_range + 3) enemies = self.cache.enemy_in_range(unit.position, lookup) last_target = self.last_targeted(unit) if not enemies: # No enemies to shoot at return current_command value_func: Callable[[Unit], float] = None if prio: value_func = lambda u: 1 if u.type_id in changelings else prio.get(u.type_id, -1) \ * (1 - u.shield_health_percentage) else: value_func = lambda u: 1 if u.type_id in changelings else 2 \ * self.unit_values.power_by_type(u.type_id, 1 - u.shield_health_percentage) best_target: Optional[Unit] = None best_score: float = 0 for enemy in enemies: # type: Unit if not enemy.is_target: continue if not shoot_air and enemy.is_flying: continue if not shoot_ground and not enemy.is_flying: continue pos: Point2 = enemy.position score = value_func(enemy) + (1 - pos.distance_to(unit) / lookup) if enemy.tag == last_target: score += 3 if score > best_score: best_target = enemy best_score = score if best_target: return Action(best_target, True) return current_command def melee_focus_fire(self, unit: Unit, current_command: Action) -> Action: ground_range = self.unit_values.ground_range(unit, self.knowledge) lookup = ground_range + 3 enemies = self.cache.enemy_in_range(unit.position, lookup) last_target = self.last_targeted(unit) if not enemies: # No enemies to shoot at return current_command def melee_value(u: Unit): val = 1 - u.shield_health_percentage range = self.unit_values.real_range(unit, u, self.knowledge) if unit.distance_to(u) < range: val += 1 return val value_func = melee_value close_enemies = self.cache.enemy_in_range(unit.position, lookup) best_target: Optional[Unit] = None best_score: float = 0 for enemy in close_enemies: # type: Unit if enemy.is_flying: continue pos: Point2 = enemy.position score = value_func(enemy) + (1 - pos.distance_to(unit) / lookup) if enemy.tag == last_target: score += 3 if score > best_score: best_target = enemy best_score = score if best_target: return Action(best_target, True) return current_command def last_targeted(self, unit: Unit) -> Optional[int]: if unit.orders: # action: UnitCommand # current_action: UnitOrder current_action = unit.orders[0] # targeting unit if isinstance(current_action.target, int): # tag found return current_action.target return None def is_locked_on(self, unit: Unit) -> bool: if unit.has_buff(BuffId.LOCKON): return True return False
class GameAnalyzer(ManagerBase): def __init__(self): super().__init__() self._enemy_air_percentage = 0 self._our_income_advantage = 0 self._our_predicted_army_advantage = 0 self._our_predicted_tech_advantage = 0 self.enemy_gas_income = 0 self.enemy_mineral_income = 0 self.our_zones = 0 self.enemy_zones = 0 self.our_power: ExtendedPower = None self.enemy_power: ExtendedPower = None self.enemy_predict_power: ExtendedPower = None self.predicted_defeat_time = 0.0 self.minerals_left: List[int] = [] self.vespene_left: List[int] = [] self.resource_updater: IntervalFunc = None self._last_income: Advantage = Advantage.Even self._last_army: Advantage = Advantage.Even self._last_predict: Advantage = Advantage.Even async def start(self, knowledge: 'Knowledge'): await super().start(knowledge) self.enemy_predicter: EnemyArmyPredicter = knowledge.enemy_army_predicter self.our_power = ExtendedPower(self.unit_values) self.enemy_power: ExtendedPower = ExtendedPower(self.unit_values) self.enemy_predict_power: ExtendedPower = ExtendedPower( self.unit_values) self.resource_updater = IntervalFunc(self.ai, self.save_resources_status, 1) self.resource_updater.execute() def save_resources_status(self): self.minerals_left.append(self.ai.minerals) self.vespene_left.append(self.ai.vespene) async def update(self): self.resource_updater.execute() self.our_power.clear() self.our_zones = 0 self.enemy_zones = 0 our_income = self.knowledge.income_calculator.mineral_income + self.knowledge.income_calculator.gas_income if self.knowledge.my_worker_type is None: # random and we haven't seen enemy race yat enemy_workers = 12 else: enemy_workers = self.knowledge.enemy_units_manager.enemy_worker_count if not self.knowledge.enemy_main_zone.is_scouted_at_least_once: enemy_workers += 12 mineral_fields = 0 for zone in self.knowledge.zone_manager.expansion_zones: # type: Zone if zone.is_enemys: self.enemy_zones += 1 mineral_fields += len(zone.mineral_fields) if zone.is_ours: self.our_zones += 1 built_vespene = len(self.cache.enemy(self.unit_values.gas_miners)) self._enemy_gas_income = min(enemy_workers, built_vespene * 3) * GAS_MINE_RATE workers_on_minerals = min(mineral_fields * 2, enemy_workers - built_vespene * 3) workers_on_minerals = max(0, workers_on_minerals) self.enemy_mineral_income = workers_on_minerals enemy_income = self.enemy_mineral_income + self._enemy_gas_income self._our_income_advantage = our_income - enemy_income self.our_power.add_units( self.ai.units.filter(lambda u: u.is_ready and u.type_id != self. knowledge.my_worker_type)) self.enemy_predict_power = self.enemy_predicter.predicted_enemy_power self.enemy_power = self.enemy_predicter.enemy_power self._enemy_air_percentage = 0 if self.enemy_predict_power.air_presence > 0: self._enemy_air_percentage = self.enemy_predict_power.air_power / self.enemy_predict_power.power being_defeated = self.predicting_defeat if being_defeated and self.predicted_defeat_time == 0.0: self.predicted_defeat_time = self.ai.time elif not being_defeated and self.predicted_defeat_time != 0.0: self.predicted_defeat_time = 0 income = self._calc_our_income_advantage() army = self._calc_our_army_advantage() predict = self._calc_our_army_predict() if self._last_income != income: self.print(f'Income advantage is now {income.name}') if self._last_army != army: self.print(f'Known army advantage is now {army.name}') if self._last_predict != predict: self.print(f'Predicted army advantage is now {predict.name}') self._last_income = income self._last_army = army self._last_predict = predict async def post_update(self): if self.debug: msg = f"Our income: {self.knowledge.income_calculator.mineral_income} / {round(self.knowledge.income_calculator.gas_income)}" msg += f"\nEnemy income: {self.enemy_mineral_income} / {round(self.enemy_gas_income)}" msg += f"\nResources: {round(self._our_income_advantage)}+{self.our_zones - self.enemy_zones}" \ f" ({self.our_income_advantage.name})" msg += f"\nArmy: {round(self.our_power.power)} vs" \ f" {round(self.enemy_power.power)} ({self.our_army_advantage.name})" msg += f"\nArmy predict: {round(self.our_power.power)} vs" \ f" {round(self.enemy_predict_power.power)} ({self.our_army_predict.name})" msg += f"\nEnemy air: {self.enemy_air.name}" self.client.debug_text_2d(msg, Point2((0.4, 0.15)), None, 14) @property def our_income_advantage(self) -> Advantage: return self._last_income def _calc_our_income_advantage(self) -> Advantage: number = self._our_income_advantage + (self.our_zones - self.enemy_zones) * 10 if number > 40: return Advantage.OverwhelmingAdvantage if number < -40: return Advantage.OverwhelmingDisadvantage if number > 20: return Advantage.ClearAdvantage if number < -20: return Advantage.ClearDisadvantage if number > 10: return Advantage.SmallAdvantage if number < -10: return Advantage.SmallDisadvantage if number > 5: return Advantage.SlightAdvantage if number < -5: return Advantage.SlightDisadvantage return Advantage.Even @property def army_at_least_clear_disadvantage(self) -> bool: return self.our_army_predict in at_least_clear_disadvantage @property def army_at_least_small_disadvantage(self) -> bool: return self.our_army_predict in at_least_small_disadvantage @property def army_at_least_clear_advantage(self) -> bool: return self.our_army_predict in at_least_clear_advantage @property def army_at_least_small_advantage(self) -> bool: return self.our_army_predict in at_least_small_advantage @property def army_at_least_advantage(self) -> bool: return self.our_army_predict in at_least_advantage @property def army_can_survive(self) -> bool: return self.our_army_predict not in at_least_small_disadvantage @property def predicting_victory(self) -> bool: return (self.our_army_predict == Advantage.OverwhelmingAdvantage and self.our_income_advantage == Advantage.OverwhelmingAdvantage) @property def bean_predicting_defeat_for(self) -> float: if self.predicted_defeat_time == 0: return 0 return self.ai.time - self.predicted_defeat_time @property def predicting_defeat(self) -> bool: return (self.our_army_predict == Advantage.OverwhelmingDisadvantage and (self.ai.supply_workers < 5 or self.our_income_advantage == Advantage.OverwhelmingDisadvantage)) @property def our_army_predict(self) -> Advantage: return self._last_predict def _calc_our_army_predict(self) -> Advantage: if self.our_power.is_enough_for(self.enemy_predict_power, our_percentage=1 / 1.1): if self.our_power.power > 20 and self.our_power.is_enough_for( self.enemy_predict_power, our_percentage=1 / 3): return Advantage.OverwhelmingAdvantage if self.our_power.power > 10 and self.our_power.is_enough_for( self.enemy_predict_power, our_percentage=1 / 2): return Advantage.ClearAdvantage if self.our_power.power > 5 and self.our_power.is_enough_for( self.enemy_predict_power, our_percentage=1 / 1.4): return Advantage.SmallAdvantage return Advantage.SlightAdvantage if self.enemy_predict_power.is_enough_for(self.our_power, our_percentage=1 / 1.1): if self.enemy_predict_power.power > 20 and self.enemy_predict_power.is_enough_for( self.our_power, our_percentage=1 / 3): return Advantage.OverwhelmingDisadvantage if self.enemy_predict_power.power > 10 and self.enemy_predict_power.is_enough_for( self.our_power, our_percentage=1 / 2): return Advantage.ClearDisadvantage if self.enemy_predict_power.power > 5 and self.enemy_predict_power.is_enough_for( self.our_power, our_percentage=1 / 1.4): return Advantage.SmallDisadvantage return Advantage.SlightDisadvantage return Advantage.Even @property def our_army_advantage(self) -> Advantage: return self._last_army def _calc_our_army_advantage(self) -> Advantage: if self.our_power.is_enough_for(self.enemy_power, our_percentage=1 / 1.1): if self.our_power.power > 20 and self.our_power.is_enough_for( self.enemy_power, our_percentage=1 / 3): return Advantage.OverwhelmingAdvantage if self.our_power.power > 10 and self.our_power.is_enough_for( self.enemy_power, our_percentage=1 / 2): return Advantage.ClearAdvantage if self.our_power.power > 5 and self.our_power.is_enough_for( self.enemy_power, our_percentage=1 / 1.4): return Advantage.SmallAdvantage return Advantage.SlightAdvantage if self.enemy_power.is_enough_for(self.our_power, our_percentage=1 / 1.1): if self.enemy_power.power > 20 and self.enemy_power.is_enough_for( self.our_power, our_percentage=1 / 3): return Advantage.OverwhelmingDisadvantage if self.enemy_power.power > 10 and self.enemy_power.is_enough_for( self.our_power, our_percentage=1 / 2): return Advantage.ClearDisadvantage if self.enemy_power.power > 5 and self.enemy_power.is_enough_for( self.our_power, our_percentage=1 / 1.4): return Advantage.SmallDisadvantage return Advantage.SlightDisadvantage return Advantage.Even @property def enemy_air(self) -> AirArmy: if self._enemy_air_percentage > 0.90: return AirArmy.AllAir if self._enemy_air_percentage > 0.65: return AirArmy.AlmostAllAir if self._enemy_air_percentage > 0.35: return AirArmy.Mixed if self._enemy_air_percentage > 0: return AirArmy.SomeAir return AirArmy.NoAir async def on_end(self, game_result: Result): own_types: List[UnitTypeId] = [] own_types_left: Dict[UnitTypeId, int] = {} enemy_types: List[UnitTypeId] = [] enemy_types_left: Dict[UnitTypeId, int] = {} lost_data = self.knowledge.lost_units_manager.get_own_enemy_lost_units( ) own_lost: Dict[UnitTypeId, List[Unit]] = lost_data[0] enemy_lost: Dict[UnitTypeId, List[Unit]] = lost_data[1] for unit_type, units in self.cache.own_unit_cache.items( ): # type: (UnitTypeId, Units) type_id = self.unit_values.real_type(unit_type) if type_id not in own_types: own_types.append(type_id) val = own_types_left.get(type_id, 0) own_types_left[type_id] = val + units.amount for unit_count in self.knowledge.enemy_units_manager.enemy_composition: # type: UnitCount unit_type = unit_count.enemy_type if unit_type not in enemy_types: enemy_types.append(unit_type) val = enemy_types_left.get(unit_type, 0) enemy_types_left[unit_type] = val + unit_count.count for unit_type, units in own_lost.items( ): # type: (UnitTypeId, List[Unit]) if unit_type not in own_types: own_types.append(unit_type) for unit_type, units in enemy_lost.items( ): # type: (UnitTypeId, List[Unit]) if unit_type not in enemy_types: enemy_types.append(unit_type) self.print_end("Own units:") self._print_by_type(own_types, own_lost, own_types_left) self.print_end("Enemy units:") self._print_by_type(enemy_types, enemy_lost, enemy_types_left) maxed_minerals = max(self.minerals_left) avg_minerals = sum(self.minerals_left) / len(self.minerals_left) maxed_gas = max(self.vespene_left) avg_gas = sum(self.vespene_left) / len(self.vespene_left) self.print_end( f'Minerals max {maxed_minerals} Average {round(avg_minerals)}') self.print_end(f'Vespene max {maxed_gas} Average {round(avg_gas)}') def _print_by_type(self, types: List[UnitTypeId], lost_units: Dict[UnitTypeId, List[Unit]], left_units: Dict[UnitTypeId, int]): def get_counts(unit_type: UnitTypeId) -> tuple: dead = len(lost_units.get(unit_type, [])) alive = left_units.get(unit_type, 0) total = dead + alive return total, alive, dead # Sort types by total count types = sorted(types, key=lambda t: get_counts(t)[0], reverse=True) for unit_type in types: counts = get_counts(unit_type) self.print_end(f"{str(unit_type.name).ljust(17)} " f"total: {str(counts[0]).rjust(3)} " f"alive: {str(counts[1]).rjust(3)} " f"dead: {str(counts[2]).rjust(3)} ") def print_end(self, msg: str): self.knowledge.print(msg, "GameAnalyzerEnd", stats=False)