From 328405150cd57751ab98019e4ed1ba7be4e68277 Mon Sep 17 00:00:00 2001 From: FredNoonienSingh Date: Wed, 25 Dec 2024 23:37:48 +0100 Subject: [PATCH] retreating seems to work --- Test | 37 ------ bot/HarstemsAunt/common.py | 2 +- bot/HarstemsAunt/main.py | 132 ++++++++++++++++---- bot/HarstemsAunt/pathing.py | 104 +++++++++------- bot/Unit_Classes/Zealots.py | 71 +++++------ bot/Unit_Classes/baseClassGround.py | 7 +- bot/army_group/army_group.py | 182 +++++++++++++++++++++++++--- bot/utils/and_or.py | 5 + 8 files changed, 369 insertions(+), 171 deletions(-) delete mode 100644 Test create mode 100644 bot/utils/and_or.py diff --git a/Test b/Test deleted file mode 100644 index aaa8a3a..0000000 --- a/Test +++ /dev/null @@ -1,37 +0,0 @@ - - for unit in units: - - - close_enemies: Units = self.bot.enemy_units.filter( - lambda u: u.position.distance_to(unit) < 15.0 - and not u.is_flying - and unit.type_id not in ATTACK_TARGET_IGNORE - ) - - target: Optional[Unit] = None - if close_enemies: - in_attack_range: Units = close_enemies.in_attack_range_of(unit) - if in_attack_range: - target = self.pick_enemy_target(in_attack_range) - else: - target = self.pick_enemy_target(close_enemies) - - if target and unit.weapon_cooldown == 0: - unit.attack(target) - continue - - if not self.pathing.is_position_safe(grid, unit.position): - self.move_to_safety(unit, grid) - continue - - if unit.distance_to(attack_target) > 5: - if close_enemies: - unit.move( - self.pathing.find_path_next_point( - unit.position, attack_target, grid - ) - ) - else: - unit.move(attack_target) - else: - unit.attack(attack_target) \ No newline at end of file diff --git a/bot/HarstemsAunt/common.py b/bot/HarstemsAunt/common.py index ea42b00..12b900f 100644 --- a/bot/HarstemsAunt/common.py +++ b/bot/HarstemsAunt/common.py @@ -386,5 +386,5 @@ } SECTORS: int = 3 -SPEEDMINING_DISTANCE: float = 1.8 MIN_SHIELD_AMOUNT: float = 0.5 +SPEEDMINING_DISTANCE: float = 1.8 \ No newline at end of file diff --git a/bot/HarstemsAunt/main.py b/bot/HarstemsAunt/main.py index 47c5cb5..91ce90e 100644 --- a/bot/HarstemsAunt/main.py +++ b/bot/HarstemsAunt/main.py @@ -1,5 +1,9 @@ """MainClass of the Bot handling""" +import os +import csv +import pickle import threading +from datetime import datetime from typing import List from itertools import chain from .common import GATEWAY_UNTIS,WORKER_IDS,SECTORS,\ @@ -14,6 +18,7 @@ """PATHING""" from HarstemsAunt.pathing import Pathing +from map_analyzer import MapData """MAP VISION""" from map_vision.map_sector import MapSector @@ -26,6 +31,9 @@ """Utils""" from utils.get_build_pos import get_build_pos +"""Army Groups""" +from army_group.army_group import ArmyGroup + """Unit Classes""" # Ground from Unit_Classes.Archon import Archons @@ -37,13 +45,14 @@ """Wrappers""" from HarstemsAunt.macro import marco +# This could either be removed or the Army_group Control could be moved in here from HarstemsAunt.micro import micro DEBUG = True class HarstemsAunt(BotAI): pathing: Pathing - + map_data: MapData # Ground Units zealots: Zealot archons: Archons @@ -55,9 +64,10 @@ class HarstemsAunt(BotAI): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.name = "HarstemsAunt" - self.version = "1.5.1" + self.version = "1.6 alpha" self.race:Race = Race.Protoss + self.start_time = None self.game_step = None self.speedmining_positions = None @@ -86,16 +96,46 @@ def __init__(self, *args, **kwargs) -> None: @property def greeting(self): - return f"I am {self.name} on Version{self.version}" + return f"Hey {self.opponent_id}\n \ + i am {self.name} on Version {self.version}\n\ + GL HF" + + @property + def match_id(self): + map_name:str = self.game_info.map_name + return f"{self.start_time}_{self.name}_{self.opponent_id}_{map_name}" + + @property + def data_path(self): + return f"data/match/{self.match_id}/" + + @property + def map_data_path(self): + map_name:str = self.game_info.map_name + return f"data/map_data/{map_name}" + + @property + def opponent_data_path(self): + opponent:str = self.opponent_id + return f"data/opponent/{opponent}" + + def create_folders(self): + for path in [self.data_path,self.map_data_path, self.opponent_data_path]: + if not os.path.exists(path): + os.makedirs(path) async def on_before_start(self) -> None: + self.start_time = datetime.now().strftime("%Y%m%d_%H%M%S") + # Create Folders to save data for analysis + self.create_folders() + + # set Edge Points top_right = Point2((self.game_info.playable_area.right, self.game_info.playable_area.top)) bottom_right = Point2((self.game_info.playable_area.right, 0)) top_left = Point2((0, self.game_info.playable_area.top)) # Create Map_sectors sector_width:int = abs(top_right.x - top_left.x)//SECTORS - for x in range(SECTORS): for y in range(SECTORS): upper_left: Point2 = Point2((0+(sector_width*x), 0+bottom_right.y+(sector_width*(y)))) @@ -105,22 +145,49 @@ async def on_before_start(self) -> None: self.map_sectors.append(sector) async def on_start(self): + + # Here to Debug the Unit Micro: + await self.client.debug_upgrade() + await self.client.debug_show_map() + + await self.client.debug_create_unit([[UnitTypeId.ZEALOT, + 5, + self._game_info.map_center.towards(self.start_location, 1), + 1]]) + await self.client.debug_create_unit([[UnitTypeId.STALKER, + 3, + self._game_info.map_center.towards(self.start_location, -1), + 1]]) + + await self.client.debug_create_unit([[UnitTypeId.MARINE, + 15, + self.enemy_start_locations[0].towards(self._game_info.map_center, 3), + 2]]) + + self.pathing = Pathing(self, DEBUG) self.stalkers = Stalkers(self, self.pathing) self.zealots = Zealot(self, self.pathing) - await self.chat_send(self.greeting) + self.expand_locs = list(self.expansion_locations) self.client.game_step = self.game_step self.speedmining_positions = get_speedmining_positions(self) + + await self.chat_send(self.greeting) + for sector in self.map_sectors: sector.build_sector() split_workers(self) - - self._client.debug_create_unit([[UnitTypeId.STALKER, 5, self._game_info.map_center, 1]]) + + initial_army_group:ArmyGroup = ArmyGroup(self, [],[],self.pathing) + self.army_groups.append(initial_army_group) async def on_step(self, iteration): - + + if self.units(UnitTypeId.ZEALOT): + await self.client.move_camera(self.units(UnitTypeId.ZEALOT).closest_to(self.enemy_start_locations[0])) + labels = ["min_step","avg_step","max_step","last_step"] for i, value in enumerate(self.step_time): @@ -129,8 +196,7 @@ async def on_step(self, iteration): else: color = (0, 255, 0) self.client.debug_text_screen(f"{labels[i]}: {value}", (0, 0.025+(i*0.025)), color=color, size=20) - - + threads: list = [] for i, sector in enumerate(self.map_sectors): t_0 = threading.Thread(target=sector.update()) @@ -142,7 +208,14 @@ async def on_step(self, iteration): for t in threads: t.join() - self.pathing.update() + self.pathing.update(iteration) + + for group in self.army_groups: + await group.update() + self.client.debug_text_screen(f"{group.name}: {group.attack_pos}", (.25, 0.025), color=(255,255,255), size=20) + + self.client.debug_text_screen(f"Supply:{self.supply_army} Enemysupply:{self.enemy_supply}", (.25, 0.05), color=(255,255,255), size=20) + if self.townhalls and self.units: self.transfer_from: List[Unit] = [] @@ -185,18 +258,17 @@ async def on_step(self, iteration): worker = self.workers.closest_to(build_pos) await self.distribute_workers() await marco(self, worker, build_pos) - await micro(self) # tie_breaker - if self.units.closer_than(10, self.enemy_start_locations[0]) and not self.enemy_units and not self.enemy_structures: + if self.units.closer_than(10, self.enemy_start_locations[0])\ + and not self.enemy_units and not self.enemy_structures: for loc in self.expand_locs: worker = self.workers.closest_to(loc) worker.move(loc) - - return + # If the Game is lost, but i want to insult the Opponent before i leave if self.last_tick == 0: await self.chat_send(f"GG, you are probably a hackcheating smurf cheat hacker anyway also \ {self.enemy_race} is IMBA") @@ -206,6 +278,7 @@ async def on_step(self, iteration): @property def get_attack_target(self) -> Point2: + # Why the 5 Minute wait time if self.time > 300.0: if enemy_units := self.enemy_units.filter( lambda u: u.type_id not in ATTACK_TARGET_IGNORE @@ -216,7 +289,6 @@ def get_attack_target(self) -> Point2: return enemy_units.closest_to(self.start_location).position elif enemy_structures := self.enemy_structures: return enemy_structures.closest_to(self.start_location).position - return self.enemy_start_locations[0] async def on_building_construction_started(self,unit): @@ -255,20 +327,17 @@ async def on_enemy_unit_entered_vision(self, unit): self.enemy_supply += self.calculate_supply_cost(unit.type_id) async def on_enemy_unit_left_vision(self, unit_tag): - unit = self.enemy_units.find_by_tag(unit_tag) - logger.info(f"{unit} left vision") + pass async def on_unit_created(self, unit): - if unit in GATEWAY_UNTIS: - print(unit) - self.last_gateway_units.append(unit.type_id) - logger.info(f"{unit} created") + if unit.type_id in GATEWAY_UNTIS: + self.army_groups[0].unit_list.append(unit.tag) async def on_unit_type_changed(self, unit, previous_type): - logger.info(f"{unit} morphed from {previous_type}") + pass async def on_unit_took_damage(self, unit, amount_damage_taken): - logger.info(f"{unit} took {amount_damage_taken} damage") + pass async def on_unit_destroyed(self, unit_tag): unit = self.enemy_units.find_by_tag(unit_tag) @@ -276,9 +345,20 @@ async def on_unit_destroyed(self, unit_tag): self.enemy_supply -= self.calculate_supply_cost(unit.type_id) async def on_upgrade_complete(self, upgrade): - logger.info(f"researched {upgrade}") self.researched.append(upgrade) async def on_end(self,game_result): - logger.info(f"game ended with result {game_result}") + logger.error("I get Called") + data:list = [self.match_id, game_result, self.version, self.time] + filename = f"{self.opponent_data_path}/results.csv" + + if not os.path.exists(filename): + with open(filename, 'w', newline='') as csvfile: + csv_writer = csv.writer(csvfile) + csv_writer.writerow(data) + else: + with open(filename, 'a', newline='') as csvfile: + csv_writer = csv.writer(csvfile) + csv_writer.writerow(data) + await self.client.leave() \ No newline at end of file diff --git a/bot/HarstemsAunt/pathing.py b/bot/HarstemsAunt/pathing.py index f8319ec..b3b540c 100644 --- a/bot/HarstemsAunt/pathing.py +++ b/bot/HarstemsAunt/pathing.py @@ -6,10 +6,12 @@ from sc2.unit import Unit from scipy import spatial +import matplotlib.pyplot as plt + from map_analyzer import MapData -from .common import ALL_STRUCTURES, INFLUENCE_COSTS +from .common import ALL_STRUCTURES, INFLUENCE_COSTS, logger -RANGE_BUFFER: float = 2.22 +RANGE_BUFFER: float = 2.00 class Pathing: def __init__(self, bot: BotAI, debug: bool) -> None: @@ -18,31 +20,36 @@ def __init__(self, bot: BotAI, debug: bool) -> None: self.map_data: MapData = MapData(bot) self.climber_grid: np.ndarray = self.map_data.get_climber_grid() + self.units_grid: np.ndarray = self.map_data.get_pyastar_grid() self.ground_grid: np.ndarray = self.map_data.get_pyastar_grid() self.air_grid: np.ndarray = self.map_data.get_clean_air_grid() - - self.influence_fade_rate: float = 0.5 + self.influence_fade_rate: float = 3 - def update(self) -> None: + def update(self, iteration) -> None: last_ground_grid:np.ndarray = self.ground_grid last_air_grid:np.ndarray = self.air_grid - - last_ground_grid[last_ground_grid != 0] *= self.influence_fade_rate - last_air_grid[last_air_grid != 0] *= self.influence_fade_rate - + + last_ground_grid[last_ground_grid != 0] /= self.influence_fade_rate + last_air_grid[last_air_grid != 0] /= self.influence_fade_rate + self.ground_grid = self.map_data.get_pyastar_grid() + last_ground_grid self.air_grid = self.map_data.get_clean_air_grid() + last_air_grid - + for unit in self.bot.all_enemy_units: if unit.type_id in ALL_STRUCTURES: self._add_structure_influence(unit) else: self._add_unit_influence(unit) + + self.add_positional_costs() + + #if not iteration%100: + #self.save_plot(iteration) def find_closest_safe_spot( - self, from_pos: Point2, grid: np.ndarray, radius: int = 15 - ) -> Point2: + self, from_pos: Point2, grid: np.ndarray, radius: int = 15 + ) -> Point2: """ @param from_pos: @param grid: @@ -60,12 +67,12 @@ def find_closest_safe_spot( return Point2(all_safe[min_index]) def find_path_next_point( - self, - start: Point2, - target: Point2, - grid: np.ndarray, - sensitivity: int = 2, - smoothing: bool = False, + self, + start: Point2, + target: Point2, + grid: np.ndarray, + sensitivity: int = 2, + smoothing: bool = False, ) -> Point2: """ Most commonly used, we need to calculate the right path for a unit @@ -88,10 +95,10 @@ def find_path_next_point( @staticmethod def is_position_safe( - grid: np.ndarray, - position: Point2, - weight_safety_limit: float = 1.0, - ) -> bool: + grid: np.ndarray, + position: Point2, + weight_safety_limit: float = 1.0, + ) -> bool: """ Checks if the current position is dangerous by comparing against default_grid_weights @@ -143,11 +150,8 @@ def _add_unit_influence(self, enemy: Unit) -> None: def _add_structure_influence(self, structure: Unit) -> None: """ Add structure influence to the relevant grid. - TODO: - Extend this to add influence to an air grid - @param enemy: - @return: """ + if not structure.is_ready: return @@ -168,14 +172,13 @@ def _add_structure_influence(self, structure: Unit) -> None: ) def _add_cost( - self, - pos: Point2, - weight: float, - unit_range: float, - grid: np.ndarray, - initial_default_weights: int = 0, - ) -> np.ndarray: - """Or add "influence", mostly used to add enemies to a grid""" + self, + pos: Point2, + weight: float, + unit_range: float, + grid: np.ndarray, + initial_default_weights: int = 0, + ) -> np.ndarray: grid = self.map_data.add_cost( position=(int(pos.x), int(pos.y)), @@ -187,13 +190,13 @@ def _add_cost( return grid def _add_cost_to_multiple_grids( - self, - pos: Point2, - weight: float, - unit_range: float, - grids: List[np.ndarray], - initial_default_weights: int = 0, - ) -> List[np.ndarray]: + self, + pos: Point2, + weight: float, + unit_range: float, + grids: List[np.ndarray], + initial_default_weights: int = 0, + ) -> List[np.ndarray]: """ Similar to method above, but add cost to multiple grids at once This is much faster then doing it one at a time @@ -208,11 +211,20 @@ def _add_cost_to_multiple_grids( ) return grids - # TODO: Add weights to points close to the edges, and points on Ramps + #TODO: Add weights to points close to the edges, and points on Ramps def add_positional_costs(self): - """ Goes over the map and adds cost to the grids""" pass - #TODO: Add function to save plots periodical to /data/... - def save_plot(self, iteration): - pass \ No newline at end of file + def save_plot(self, iteration:int): + unit_postions = [unit.position_tuple for unit in self.bot.units] + y_positions, x_positions = zip(*unit_postions) + influence_map = self.ground_grid + fig, ax = plt.subplots() + plt.imshow(influence_map, cmap='jet') + plt.scatter(x_positions, y_positions, color='green', marker='x', s=10) + plt.grid(True) + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_title('Influence Map') + plt.plot() + plt.savefig(f"{self.bot.data_path}/influence_map_at_{iteration}.png") \ No newline at end of file diff --git a/bot/Unit_Classes/Zealots.py b/bot/Unit_Classes/Zealots.py index 77195df..0f23a4f 100644 --- a/bot/Unit_Classes/Zealots.py +++ b/bot/Unit_Classes/Zealots.py @@ -10,59 +10,48 @@ from Unit_Classes.baseClassGround import BaseClassGround from HarstemsAunt.common import ATTACK_TARGET_IGNORE -MIN_SHIELD_AMOUNT = 0.5 class Zealot(BaseClassGround): # Overwritten from BaseClassGround + async def handle_attackers(self, units: Units, attack_target: Point2) -> None: grid: np.ndarray = self.pathing.ground_grid for unit in units: - if unit.shield_percentage < MIN_SHIELD_AMOUNT \ - and not self.pathing.is_position_safe(grid, unit.position): - unit.move( - self.pathing.find_path_next_point( - unit.position, self.get_recharge_spot, grid - ) + friendly_units = self.bot.units.filter(lambda u: u.tag != unit.tag) + friendly_positions = [unit.position for unit in friendly_units] + enemies = self.bot.enemy_units.closer_than(15, unit) + if not enemies.filter(lambda unit: + unit.ground_dps > 0 and + not unit.is_flying + ): + attack_pos = self.pathing.find_path_next_point( + unit.position, attack_target, grid ) - continue - - close_enemies: Units = self.bot.enemy_units.filter( - lambda u: u.position.distance_to(unit) < 15.0 - and not u.is_flying - and unit.type_id not in ATTACK_TARGET_IGNORE - ) - - target: Optional[Unit] = None - if close_enemies: - in_attack_range: Units = close_enemies.in_attack_range_of(unit) - if in_attack_range: - target = self.pick_enemy_target(in_attack_range, unit) + if attack_pos not in friendly_positions: + unit.attack(attack_pos) else: - target = self.pick_enemy_target(close_enemies, unit) - - if target: - unit.attack(target) - continue - - if not self.pathing.is_position_safe(grid, unit.position): - self.move_to_safety(unit, grid) - continue - - if unit.distance_to(attack_target) > 5: - if close_enemies: - unit.move( - self.pathing.find_path_next_point( - unit.position, attack_target, grid - ) + attack_pos = self.pathing.find_path_next_point( + friendly_units.closest_to(unit).position, attack_target, grid ) - else: - unit.move(attack_target) - else: - unit.attack(attack_target) + unit.attack(attack_pos) + return + unit.attack(self.pick_enemy_target(enemies, unit)) + return + + async def retreat(self, retreat_pos: Point2): + grid: np.ndarray = self.pathing.ground_grid + for unit in self.units: + move_to: Point2 = self.pathing.find_path_next_point( + unit.position, retreat_pos, grid + ) + unit.move(move_to) @staticmethod def pick_enemy_target(enemies: Units, unit: Unit) -> Unit: - fighting_units: Units = enemies.filter(lambda unit: unit.ground_dps > 5) + fighting_units: Units = enemies.filter(lambda unit: + unit.ground_dps > 5 and + not unit.is_flying + ) if fighting_units: return fighting_units.closest_to(unit) return enemies.closest_to(unit) diff --git a/bot/Unit_Classes/baseClassGround.py b/bot/Unit_Classes/baseClassGround.py index cedcbfb..e780055 100644 --- a/bot/Unit_Classes/baseClassGround.py +++ b/bot/Unit_Classes/baseClassGround.py @@ -11,6 +11,7 @@ from HarstemsAunt.common import ATTACK_TARGET_IGNORE, MIN_SHIELD_AMOUNT,\ ALL_STRUCTURES, PRIO_ATTACK_TARGET, logger +#TODO: Change it be a useful BaseClass class BaseClassGround: def __init__(self, bot:BotAI, pathing:Pathing): self.bot:BotAI=bot @@ -19,11 +20,11 @@ def __init__(self, bot:BotAI, pathing:Pathing): @property def get_recharge_spot(self) -> Point2: # Thats stupid, unless the recharge rate is insane - + return self.pathing.find_closest_safe_spot( self.bot.game_info.map_center, self.pathing.ground_grid ) - + async def handle_attackers(self, units: Units, attack_target: Point2) -> None: grid: np.ndarray = self.pathing.ground_grid for stalker in units: @@ -37,7 +38,7 @@ async def handle_attackers(self, units: Units, attack_target: Point2) -> None: # When enemy_units are visible if self.bot.enemy_units: - visible_units = self.bot.enemy_units.closer_than(stalker.ground_range+2, stalker) + visible_units = self.bot.enemy_units.closer_than(stalker.ground_range+10, stalker) enemy_structs = self.bot.enemy_structures.closer_than(20, stalker) # Attack if Possible diff --git a/bot/army_group/army_group.py b/bot/army_group/army_group.py index c9b6558..ce36489 100644 --- a/bot/army_group/army_group.py +++ b/bot/army_group/army_group.py @@ -1,16 +1,38 @@ from __future__ import annotations +from functools import cached_property from typing import Union +import numpy as np +from enum import Enum +"""SC2 Imports""" from sc2.unit import Unit from sc2.units import Units from sc2.ids.unit_typeid import UnitTypeId from sc2.bot_ai import BotAI from sc2.position import Point2, Point3 +"""Utils""" +from utils.and_or import and_or +from utils.can_build import can_build_unit +from utils.in_proximity import in_proximity_to_point +from HarstemsAunt.pathing import Pathing +from HarstemsAunt.common import logger + +class GroupStatus(Enum): + ATTACKING = 1 + DEFENDING = 2 + RETREATING = 3 + REGROUPING = 4 + + class ArmyGroup: - def __init__(self, bot:BotAI, unit_list:list): + def __init__(self, bot:BotAI, unit_list:list,units_in_transit:list,pathing:Pathing): self.bot:BotAI = bot - self.unit_list:list=unit_list + self.name = "Attack Group Alpha" + self.unit_list:list = unit_list + self.units_in_transit:list = units_in_transit + self.pathing:Pathing = pathing + self.status:GroupStatus = GroupStatus.ATTACKING @property def units(self) -> Units: @@ -18,7 +40,9 @@ def units(self) -> Units: @property def position(self) -> Point2: - return self.units.center + if self.units: + return self.units.center + return self.bot.game_info.map_center @property def ground_dps(self) -> float: @@ -30,10 +54,14 @@ def air_dps(self) -> float: @property def average_health_percentage(self) -> float: + if not self.units: + return 0 return 1/len(self.units)*sum([unit.health_percentage for unit in self.units]) @property def average_shield_precentage(self) -> float: + if not self.units: + return 0 return 1/len(self.units)*sum([unit.shield_percentage for unit in self.units]) @property @@ -43,50 +71,170 @@ def has_detection(self) -> float: return False @property - def attack_pos(self) -> Union[Point2,Point3,Unit]: + def attack_target(self) -> Union[Point2, Unit]: + #TODO Rework when regrouping is working as it is supposed to return self.bot.enemy_start_locations[0] - @attack_pos.setter - def attack_pos(self, new_attack_pos:Union[Point2,Point3,Unit]): + @property + def attack_pos(self) -> Union[Point2,Point3,Unit]: + return self.position.towards(self.attack_target, 10) + + @attack_target.setter + def attack_target(self, new_attack_pos:Union[Point2,Point3,Unit]): self.attack_pos = new_attack_pos @property def retreat_pos(self) -> Union[Point2,Point3,Unit]: - return self.bot.game_info.map_center + return self.bot.start_location @retreat_pos.setter def retreat_pos(self, new_retreat_pos:Union[Point2,Point3,Unit]): self.retreat_pos = new_retreat_pos - def request_unit(self) -> UnitTypeId: - # Gets called when Production is Idle + def request_unit(self, structure_type: UnitTypeId) -> UnitTypeId: + """ + + Args: + structure_type (UnitTypeId): _description_ + + Returns: + UnitTypeId: _description_ + """ pass def remove_unit(self, unit_tag:str) -> bool: + """ Removes are unit from ArmyGroup + + Args: + unit_tag (str): tag of the Unit that is going to be removed + + Returns: + bool: # Returns true if the Unit was in the ArmyGroup and has been removed + """ if unit_tag in self.unit_list: self.unit_list.remove(unit_tag) return True return False def merge_groups(self, army_group:ArmyGroup) -> bool: - # Returns true if ArmyGroups were merged + """ Merges ArmyGroup into ArmyGroup + Args: + army_group (ArmyGroup): army group that is supposed to get merged + + Returns: + bool: returns True if Groups got merged + """ if army_group in self.bot.army_groups: self.unit_list.extend(army_group.unit_list) self.bot.army_groups.remove(army_group) return True return False - def attack(self) -> None: - pass - - def move(self) -> None: - pass + async def attack(self) -> None: + stalkers: Units = self.units(UnitTypeId.STALKER) + zealots: Units = self.units(UnitTypeId.ZEALOT) + + if stalkers: + await self.bot.stalkers.handle_attackers( + self.units(UnitTypeId.STALKER), self.attack_target + ) + if zealots: + await self.bot.zealots.handle_attackers( + self.units(UnitTypeId.ZEALOT), self.attack_target + ) + + def move(self,target_pos:Union[Point2, Point3, Unit]) -> None: + """ Moves Army towards position + + Args: + target_pos (Union[Point2, Point3, Unit]): _description_ + """ + for unit in self.units: + + # This could be handled more efficient if i could overwrite the Unit move command + + grid:np.ndarray = self.pathing.air_grid if unit.is_flying \ + else self.pathing.ground_grid + unit.move( + self.pathing.find_path_next_point( + unit.position, target_pos, grid + ) + ) def retreat(self) -> None: - pass + """Moves Army back to retreat position + """ + grid:np.ndarray = self.pathing.ground_grid + + #Early return if units are safe + if all(self.pathing.is_position_safe(grid, unit.position,2) for unit in self.units): + return + + for unit in self.units: + # This could be handled more efficient if i could overwrite the Unit move command + if not in_proximity_to_point(self.bot, unit, self.retreat_pos, 15): + unit.move( + self.pathing.find_path_next_point( + unit.position, self.retreat_pos, grid + ) + ) + + def check_regroup_state(self) -> bool: + return True def regroup(self) -> None: pass + # TODO: very basic, needs to be Adjusted to account for different, Unit types def defend(self, position:Union[Point2,Point3,Unit]) -> None: - pass \ No newline at end of file + grid:np.ndarray = self.pathing.air_grid if unit.is_flying \ + else self.pathing.ground_grid + + enemy_units = self.bot.enemy_units.closer_than(25, position) + + for unit in self.units: + if not in_proximity_to_point(self.bot, unit, position, 20): + unit.move( + self.pathing.find_path_next_point( + unit.position, self.retreat_pos, grid + ) + ) + continue + if enemy_units: + unit.attack(enemy_units.closest_to(unit)) + + async def update(self): + """ Method controlling the Behavior of the Group, \ + shall be called every tick in main.py + """ + + # SAVING last Status in var + last_status: GroupStatus = self.status + + # CHECK DEFEND POSITION + for townhall in self.bot.townhalls: + enemys_in_area = self.bot.enemy_units.closer_than(30, townhall) + if enemys_in_area: + supply_in_area = sum([self.bot.calculate_supply_cost(unit) for unit in enemys_in_area]) + if supply_in_area > 10: + self.defend(townhall) + self.status = GroupStatus.DEFENDING + return + + # TODO add check if enemy_supply in Target_area > self.supply + # CHECK RETREAT CONDITIONS + shield_condition = self.average_shield_precentage < .33 + supply_condition = self.bot.supply_army <= self.bot.enemy_supply + if and_or(shield_condition, supply_condition): + self.retreat() + self.status = GroupStatus.RETREATING + return + + # ELSE ATTACK !!! + if last_status in [GroupStatus.REGROUPING]: + if not self.check_regroup_state(): + self.regroup() + return + + await self.attack() + self.status = GroupStatus.ATTACKING \ No newline at end of file diff --git a/bot/utils/and_or.py b/bot/utils/and_or.py new file mode 100644 index 0000000..f8385b2 --- /dev/null +++ b/bot/utils/and_or.py @@ -0,0 +1,5 @@ + + +def and_or(a, b): + # Does what it says on the tin + return a or b or (a and b) \ No newline at end of file