diff --git a/game/commander/garrisons.py b/game/commander/garrisons.py new file mode 100644 index 00000000..27b43de2 --- /dev/null +++ b/game/commander/garrisons.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from collections import Iterator +from dataclasses import dataclass + +from game.theater import ConflictTheater +from game.theater.theatergroundobject import VehicleGroupGroundObject +from game.utils import meters, nautical_miles + + +@dataclass +class Garrisons: + blocking_capture: list[VehicleGroupGroundObject] + defending_front_line: list[VehicleGroupGroundObject] + reserves: list[VehicleGroupGroundObject] + + @property + def in_priority_order(self) -> Iterator[VehicleGroupGroundObject]: + yield from self.blocking_capture + yield from self.defending_front_line + yield from self.reserves + + def eliminate(self, garrison: VehicleGroupGroundObject) -> None: + if garrison in self.blocking_capture: + self.blocking_capture.remove(garrison) + if garrison in self.defending_front_line: + self.defending_front_line.remove(garrison) + if garrison in self.reserves: + self.reserves.remove(garrison) + + def __contains__(self, item: VehicleGroupGroundObject) -> bool: + return item in self.in_priority_order + + @classmethod + def from_theater(cls, theater: ConflictTheater, player_owned: bool) -> Garrisons: + """Categorize garrison groups based on target priority. + + Any garrisons blocking base capture are the highest priority, followed by other + garrisons at front-line bases, and finally any garrisons in reserve at other + bases. + """ + blocking = [] + defending = [] + reserves = [] + for cp in theater.control_points_for(player_owned): + garrisons = [ + tgo + for tgo in cp.ground_objects + if isinstance(tgo, VehicleGroupGroundObject) + ] + if not cp.has_active_frontline: + reserves.extend(garrisons) + continue + + for garrison in garrisons: + # Not sure what distance DCS uses, but assuming it's about 2NM since + # that's roughly the distance of the circle on the map. + if meters(garrison.distance_to(cp)) < nautical_miles(2): + blocking.append(garrison) + else: + defending.append(garrison) + + return Garrisons(blocking, defending, reserves) diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index e2bab894..a565fc86 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -65,14 +65,6 @@ class ObjectiveFinder: yield ground_object - def threatening_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]: - """Iterates over enemy vehicle groups near friendly control points. - - Groups are sorted by their closest proximity to any friendly control - point (airfield or fleet). - """ - return self._targets_by_range(self.enemy_vehicle_groups()) - def enemy_ships(self) -> Iterator[NavalGroundObject]: for cp in self.enemy_control_points(): for ground_object in cp.ground_objects: diff --git a/game/commander/tasks/compound/attackgarrisons.py b/game/commander/tasks/compound/attackgarrisons.py index f8281597..89a0943c 100644 --- a/game/commander/tasks/compound/attackgarrisons.py +++ b/game/commander/tasks/compound/attackgarrisons.py @@ -7,5 +7,5 @@ from game.htn import CompoundTask, Method class AttackGarrisons(CompoundTask[TheaterState]): def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: - for garrison in state.enemy_garrisons: + for garrison in state.enemy_garrisons.in_priority_order: yield [PlanBai(garrison)] diff --git a/game/commander/tasks/primitive/bai.py b/game/commander/tasks/primitive/bai.py index c0dc328c..bbbabb96 100644 --- a/game/commander/tasks/primitive/bai.py +++ b/game/commander/tasks/primitive/bai.py @@ -17,7 +17,7 @@ class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]): return self.target_area_preconditions_met(state) def apply_effects(self, state: TheaterState) -> None: - state.enemy_garrisons.remove(self.target) + state.enemy_garrisons.eliminate(self.target) def propose_flights(self, doctrine: Doctrine) -> None: self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive) diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index c1508e34..a206b6e7 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -1,16 +1,17 @@ from __future__ import annotations +import dataclasses import itertools from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Union +from game.commander.garrisons import Garrisons from game.commander.objectivefinder import ObjectiveFinder from game.data.doctrine import Doctrine from game.htn import WorldState from game.theater import ControlPoint, FrontLine, MissionTarget from game.theater.theatergroundobject import ( TheaterGroundObject, - VehicleGroupGroundObject, NavalGroundObject, IadsGroundObject, ) @@ -32,7 +33,7 @@ class TheaterState(WorldState["TheaterState"]): enemy_convoys: list[Convoy] enemy_shipping: list[CargoShip] enemy_ships: list[NavalGroundObject] - enemy_garrisons: list[VehicleGroupGroundObject] + enemy_garrisons: Garrisons oca_targets: list[ControlPoint] strike_targets: list[TheaterGroundObject[Any]] enemy_barcaps: list[ControlPoint] @@ -69,7 +70,7 @@ class TheaterState(WorldState["TheaterState"]): enemy_convoys=list(self.enemy_convoys), enemy_shipping=list(self.enemy_shipping), enemy_ships=list(self.enemy_ships), - enemy_garrisons=list(self.enemy_garrisons), + enemy_garrisons=dataclasses.replace(self.enemy_garrisons), oca_targets=list(self.oca_targets), strike_targets=list(self.strike_targets), enemy_barcaps=list(self.enemy_barcaps), @@ -97,7 +98,7 @@ class TheaterState(WorldState["TheaterState"]): enemy_convoys=list(finder.convoys()), enemy_shipping=list(finder.cargo_ships()), enemy_ships=list(finder.enemy_ships()), - enemy_garrisons=list(finder.threatening_vehicle_groups()), + enemy_garrisons=Garrisons.from_theater(game.theater, not player), oca_targets=list(finder.oca_targets(min_aircraft=20)), strike_targets=list(finder.strike_targets()), enemy_barcaps=list(game.theater.control_points_for(not player)),