def get_enemy_perimeter(self, enemy_units: Units, enemy_structures: Units, reference_position: Point2): perimeter = 0 pathing_grid: PixelMap = self.bot._game_info.pathing_grid for enemy_unit in enemy_units: enemies_excluding_self: Units = enemy_units.tags_not_in( {enemy_unit.tag}) pos: Point2 = enemy_unit.position positions = [ Point2((pos.x - 1, pos.y + 1)), Point2((pos.x, pos.y + 1)), Point2((pos.x + 1, pos.y + 1)), Point2((pos.x - 1, pos.y)), # [pos.x, pos.y], disregard center point Point2((pos.x + 1, pos.y)), Point2((pos.x - 1, pos.y - 1)), Point2((pos.x, pos.y - 1)), Point2((pos.x + 1, pos.y - 1)), ] if reference_position.distance_to(enemy_unit.position) > 5: positions = remove_n_furthest_points(positions, reference_position, 3) for p in positions: if pathing_grid[ math.floor(p.x), math.floor( p.y )] <= 0 and not enemies_excluding_self.closer_than( 1, p).exists and not enemy_structures.closer_than( 1, p).exists: perimeter += 1 return perimeter
def find_neighbors(self, THE_SOURCE: Unit, units: Units) -> Units: neighbors: Units = units.closer_than(3, THE_SOURCE.position) temp: Units = Units([], self.bot._game_data) for individual in neighbors: temp.__or__( self.find_neighbors(individual, units.tags_not_in(neighbors.tags))) output = neighbors.__or__(temp) if output is None: return Units([], self.bot._game_data) return neighbors.__or__(temp)
def group_army(self, army: Units) -> List[Units]: groups: List[Units] = [] already_grouped_tags = [] for unit in army: if unit.tag in already_grouped_tags: continue # TODO: fix recursive grouping # neighbors: Units = self.find_neighbors(unit, army.tags_not_in(set(already_grouped_tags))) neighbors: Units = army.closer_than(15, unit.position) groups.append(neighbors) already_grouped_tags.extend(neighbors.tags) return groups
class StrategicObjective(): def __init__(self, module, urgency, rendezvous=None): self.module = module self.bot = module.bot self.status = ObjectiveStatus.ALLOCATING self.status_since = self.bot.time self.allocated = set() self.urgency = urgency self.rendezvous = rendezvous self.units = Units([], self.bot) self.last_seen = self.bot.time self.enemies = self.find_enemies() self.log = module.log.withFields({"objective": type(self).__name__}) def __getattr__(self, name): return getattr(self.bot, name) @property def target(self): raise NotImplementedError( "You must extend this class and provide a target property") def is_complete(self): if self.time - self.last_seen > 5: self.log.info( "completed: enemies have not been seen for 5 seconds") if self.status == ObjectiveStatus.ACTIVE: # any enemies still in self.enemies should be removed from the enemy army # there probably aren't many but they are clearly interfering with us at this point self.log.info( f"objective was active; clearing {self.enemies.amount} tags from known enemies!" ) for enemy_tag in (e.tag for e in self.enemies): self.shared.known_enemy_units.pop(enemy_tag, None) return True return False def find_enemies(self): return (Units(self.shared.known_enemy_units.values(), self.bot) + self.enemy_structures) \ .filter(lambda u: u.is_visible or not self.is_visible(u.position)) def abort(self): self.status = ObjectiveStatus.RETREATING self.status_since = self.time async def tick(self): self.log.info({ "status": self.status, "num_units": self.units.amount, "num_enemies": self.enemies.amount, "game_time": self.time, }) self.enemies = self.find_enemies() if self.enemy_units.tags_in(e.tag for e in self.enemies).exists: self.last_seen = self.time if self.status >= ObjectiveStatus.ALLOCATING: self.allocate() if self.status == ObjectiveStatus.STAGING: self.stage() if self.status == ObjectiveStatus.ACTIVE: await self.micro() if self.status == ObjectiveStatus.RETREATING: await self.retreat() # retreat requires implementation in subclasses # [ Really tricky on defense. For now, victory or death! ] async def retreat(self): await self.micro() def stage(self): # override this if you want to stage units self.status = ObjectiveStatus.ACTIVE self.status_since = self.time def retreat_unit(self, unit, target): if unit.ground_range >= 5 and unit.weapon_cooldown == 0: self.do(unit.attack(target)) else: self.do(unit.move(target)) def minimum_supply(self, enemy_units): return 0 def optimum_supply(self, enemy_units): return sum( supply_cost(unit) for unit in self.bot.units.filter( lambda u: u.ground_dps + u.air_dps > 5 and not is_worker(u))) def do_attack(self, unit): self.do( unit.attack( self.enemies.closest_to(self.target.position).position if self. enemies.exists else self.target.position)) async def micro(self): self.rendezvous = None if self.units.empty: return if self.time - self.status_since > 2: self.status_since = self.time for unit in self.units: self.do_attack(unit) near_target_units = self.units.closer_than(15, self.target) cooling_down_units = near_target_units.filter( lambda u: u.weapon_cooldown > 0) if cooling_down_units.amount < near_target_units.amount / 3: for unit in cooling_down_units: self.do(unit.move(unit.position.towards(self.target, 2))) self.do(unit.attack(self.target.position, queue=True)) nearby_enemies = Units( list({ enemy_unit for (friendly_unit, enemy_unit) in itertools.product(self.units, self.enemies) if enemy_unit.position.is_closer_than(10, friendly_unit) }), self.bot) if nearby_enemies.exists: allies_center = median_position([u.position for u in self.units]) clustered_allies = self.units.closer_than(15, allies_center) if optimism(clustered_allies, self.enemies) < 0.75 and self.supply_used < 180: self.status = ObjectiveStatus.RETREATING self.status_since = self.time return def allocate(self): may_proceed = False enemy_units = self.enemies minimum_supply = int(self.minimum_supply(enemy_units)) optimum_supply = int(self.optimum_supply(enemy_units)) allocated_supply = int( sum(supply_cost(u) for u in self.units.tags_in(self.allocated))) if minimum_supply <= allocated_supply: may_proceed = True still_needed = minimum_supply - allocated_supply still_wanted = max(optimum_supply - allocated_supply, still_needed) usable_units = self.unallocated( urgency=self.urgency).filter(lambda u: not is_worker(u)) if enemy_units.filter(lambda e: not e.is_flying).empty: usable_units = usable_units.filter(lambda u: u.can_attack_air) preferred_units = usable_units.filter( lambda u: u.can_attack_both ) if usable_units.exists else usable_units adding_units = set() if sum(supply_cost(u) for u in preferred_units) >= still_needed: # still_wanted is actually supply, not units, so this will over-select for protoss and under-select for zerg adding_units = set(unit.tag for unit in preferred_units.closest_n_units( self.target.position, still_wanted)) elif sum(supply_cost(u) for u in usable_units) >= still_needed: adding_units = set(preferred_units) adding_units.update((unit.tag for unit in usable_units.closest_n_units( self.target.position, still_wanted))) self.deallocate(adding_units) self.allocated = self.allocated.union(adding_units) if sum(supply_cost(u) for u in self.units.tags_in(self.allocated)) >= minimum_supply: may_proceed = True if may_proceed: if self.status == ObjectiveStatus.ALLOCATING: self.status = ObjectiveStatus.STAGING self.status_since = self.time elif self.status == ObjectiveStatus.RETREATING and \ self.shared.optimism > 1.5 and \ self.units.exists and \ self.units.closer_than(15, median_position([u.position for u in self.units])).amount > self.units.amount / 2: self.status = ObjectiveStatus.STAGING self.status_since = self.time self.units = self.bot.units.tags_in(self.allocated) if len(adding_units) > 0: self.log.debug({ "message": "Allocating units", "quantity": len(adding_units), "now_allocated": len(self.allocated), }) # noisy, but possibly informative # self.log.info(f"{self.units.amount} units allocated for {self.enemies.amount} known enemies") return
class TestUnits(unittest.TestCase): # @classmethod # def setUpClass(cls): # pass # @classmethod # def tearDownClass(cls): # pass def setUp(self): mock_proto_data1 = MockProtoData(tag=245346374, pos=Mock(x=-5, y=6, z=50), health=35, health_max=45, orders=[ Mock(ability_id=AbilityId.ATTACK, target_unit_tag=1337, progress=1.0) ]) mock_proto_data2 = MockProtoData( tag=245346375, pos=Mock(x=-2, y=7, z=50), orders=[ Mock(ability_id=AbilityId.MOVE, target_world_space_pos=Point2((0, 0)), progress=1.0) ]) mock_proto_data3 = MockProtoData( tag=245346376, pos=Mock(x=7, y=7, z=50), ) self.mock_game_state = MockGameState() self.marine1 = Unit(mock_proto_data1, self.mock_game_state) self.marine2 = Unit(mock_proto_data2, self.mock_game_state) self.marine3 = Unit(mock_proto_data3, self.mock_game_state) self.marines = Units([self.marine1, self.marine2, self.marine3], self.mock_game_state) self.emptyUnitsGroup = Units([], self.mock_game_state) def tearDown(self): # unnecessary here del self.marine1 del self.marine2 del self.marine3 del self.marines del self.mock_game_state def test_amount(self): self.assertEqual(self.marines.amount, 3) self.assertEqual(self.emptyUnitsGroup.amount, 0) def test_empty(self): self.assertFalse(self.marines.empty) self.assertTrue(self.emptyUnitsGroup.empty) def test_exists(self): self.assertTrue(self.marines.exists) self.assertFalse(self.emptyUnitsGroup.exists) def test_find_by_tag(self): self.assertEqual(self.marines.find_by_tag(245346374), self.marine1) self.assertIsNone(self.marines.find_by_tag(245346)) def test_first(self): self.assertEqual(self.marines.first, self.marine1) def test_random(self): self.assertTrue( self.marines.random in [self.marine1, self.marine2, self.marine3]) def test_closest_distance_to(self): self.assertEqual(self.marines.closest_distance_to(Point2((10, 10))), (3**2 + 3**2)**0.5) def test_closest_to(self): self.assertEqual(self.marines.closest_to(Point2((10, 10))), self.marine3) def test_furthest_to(self): self.assertEqual(self.marines.furthest_to(Point2((10, 10))), self.marine1) def test_closer_than(self): self.assertEqual(self.marines.closer_than(20, Point2((10, 10))), self.marines) self.assertEqual(self.marines.closer_than(6, Point2((10, 10))), Units([self.marine3], self.mock_game_state)) self.assertEqual(self.marines.closer_than(2, Point2((10, 10))), self.emptyUnitsGroup) def test_tags_in(self): self.assertEqual( self.marines.tags_in({245346374, 245346375}), Units([self.marine1, self.marine2], self.mock_game_state)) self.assertEqual(self.marines.tags_in({}), self.emptyUnitsGroup) def test_tags_not_in(self): self.assertEqual(self.marines.tags_not_in({}), self.marines) self.assertEqual( self.marines.tags_not_in({245346374}), Units([self.marine2, self.marine3], self.mock_game_state)) def test_of_type(self): self.assertEqual(self.marines.of_type(UnitTypeId.MARINE), self.marines) self.assertEqual(self.marines.of_type([UnitTypeId.MARINE]), self.marines) def test_exclude_type(self): self.assertEqual(self.marines.exclude_type([UnitTypeId.MARINE]), self.emptyUnitsGroup) def test_tags(self): self.assertSetEqual(self.marines.tags, {u.tag for u in self.marines}) def test_noqueue(self): self.assertEqual(self.marines.noqueue, Units([self.marine3], self.mock_game_state)) def test_idle(self): self.assertEqual(self.marines.idle, Units([self.marine3], self.mock_game_state)) def test_owned(self): self.assertEqual(self.marines.owned, self.marines) def test_enemy(self): self.assertEqual(self.marines.enemy, self.emptyUnitsGroup)