diff --git a/game/commander/garrisons.py b/game/commander/garrisons.py index ac685e24..8ed43843 100644 --- a/game/commander/garrisons.py +++ b/game/commander/garrisons.py @@ -3,59 +3,50 @@ from __future__ import annotations from collections import Iterator from dataclasses import dataclass -from game.theater import ConflictTheater, ControlPoint +from game.theater import ControlPoint from game.theater.theatergroundobject import VehicleGroupGroundObject -from game.utils import meters, nautical_miles +from game.utils import meters @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: + def for_control_point(cls, control_point: ControlPoint) -> 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. + Any garrisons blocking base capture are the highest priority. """ blocking = [] defending = [] - reserves = [] - for cp in theater.control_points_for(player_owned): - garrisons = [ - tgo - for tgo in cp.ground_objects - if isinstance(tgo, VehicleGroupGroundObject) and not tgo.is_dead - ] - if not cp.has_active_frontline: - reserves.extend(garrisons) - continue + garrisons = [ + tgo + for tgo in control_point.ground_objects + if isinstance(tgo, VehicleGroupGroundObject) and not tgo.is_dead + ] + for garrison in garrisons: + if ( + meters(garrison.distance_to(control_point)) + < ControlPoint.CAPTURE_DISTANCE + ): + blocking.append(garrison) + else: + defending.append(garrison) - for garrison in garrisons: - if meters(garrison.distance_to(cp)) < ControlPoint.CAPTURE_DISTANCE: - blocking.append(garrison) - else: - defending.append(garrison) - - return Garrisons(blocking, defending, reserves) + return Garrisons(blocking, defending) diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index 5369b5dd..34db8730 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -223,17 +223,18 @@ class ObjectiveFinder: if not c.is_friendly(self.is_player) ) - def all_possible_targets(self) -> Iterator[MissionTarget]: - """Iterates over all possible mission targets in the theater. - - Valid mission targets are control points (airfields and carriers), front - lines, and ground objects (SAM sites, factories, resource extraction - sites, etc). - """ - for cp in self.game.theater.controlpoints: - yield cp - yield from cp.ground_objects - yield from self.front_lines() + def prioritized_unisolated_points(self) -> list[ControlPoint]: + prioritized = [] + capturable_later = [] + for cp in self.game.theater.control_points_for(not self.is_player): + if cp.is_isolated: + continue + if cp.has_active_frontline: + prioritized.append(cp) + else: + capturable_later.append(cp) + prioritized.extend(self._targets_by_range(capturable_later)) + return prioritized @staticmethod def closest_airfields_to(location: MissionTarget) -> ClosestAirfields: diff --git a/game/commander/tasks/compound/attackgarrisons.py b/game/commander/tasks/compound/attackgarrisons.py index 89a0943c..479bcc71 100644 --- a/game/commander/tasks/compound/attackgarrisons.py +++ b/game/commander/tasks/compound/attackgarrisons.py @@ -7,5 +7,6 @@ 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.in_priority_order: - yield [PlanBai(garrison)] + for garrisons in state.enemy_garrisons.values(): + for garrison in garrisons.in_priority_order: + yield [PlanBai(garrison)] diff --git a/game/commander/tasks/primitive/bai.py b/game/commander/tasks/primitive/bai.py index 1b03ce53..352aa0b4 100644 --- a/game/commander/tasks/primitive/bai.py +++ b/game/commander/tasks/primitive/bai.py @@ -14,12 +14,12 @@ class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]): def preconditions_met(self, state: TheaterState) -> bool: if not super().preconditions_met(state): return False - if self.target not in state.enemy_garrisons: + if not state.has_garrison(self.target): return False return self.target_area_preconditions_met(state) def apply_effects(self, state: TheaterState) -> None: - state.enemy_garrisons.eliminate(self.target) + state.eliminate_garrison(self.target) def propose_flights(self, doctrine: Doctrine) -> None: self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive) diff --git a/game/commander/tasks/primitive/breakthroughattack.py b/game/commander/tasks/primitive/breakthroughattack.py index 6657bf23..eb17b5ac 100644 --- a/game/commander/tasks/primitive/breakthroughattack.py +++ b/game/commander/tasks/primitive/breakthroughattack.py @@ -2,9 +2,6 @@ from __future__ import annotations from game.commander.tasks.frontlinestancetask import FrontLineStanceTask from game.commander.theaterstate import TheaterState -from game.theater import ControlPoint -from game.theater.theatergroundobject import VehicleGroupGroundObject -from game.utils import meters from gen.ground_forces.combat_stance import CombatStance @@ -17,20 +14,14 @@ class BreakthroughAttack(FrontLineStanceTask): def have_sufficient_front_line_advantage(self) -> bool: return self.ground_force_balance >= 2.0 - @property - def opposing_garrisons_eliminated(self) -> bool: - # TODO: Should operate on TheaterState to account for BAIs planned this turn. - for tgo in self.enemy_cp.ground_objects: - if not isinstance(tgo, VehicleGroupGroundObject): - continue - if meters(tgo.distance_to(self.enemy_cp)) < ControlPoint.CAPTURE_DISTANCE: - return False - return True + def opposing_garrisons_eliminated(self, state: TheaterState) -> bool: + garrisons = state.enemy_garrisons[self.enemy_cp] + return not bool(garrisons.blocking_capture) def preconditions_met(self, state: TheaterState) -> bool: if not super().preconditions_met(state): return False - return self.opposing_garrisons_eliminated + return self.opposing_garrisons_eliminated(state) def apply_effects(self, state: TheaterState) -> None: super().apply_effects(state) diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index d80e38e9..f737a2aa 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -15,6 +15,7 @@ from game.theater.theatergroundobject import ( TheaterGroundObject, NavalGroundObject, IadsGroundObject, + VehicleGroupGroundObject, ) from game.threatzones import ThreatZones from game.transfers import Convoy, CargoShip @@ -41,7 +42,7 @@ class TheaterState(WorldState["TheaterState"]): enemy_convoys: list[Convoy] enemy_shipping: list[CargoShip] enemy_ships: list[NavalGroundObject] - enemy_garrisons: Garrisons + enemy_garrisons: dict[ControlPoint, Garrisons] oca_targets: list[ControlPoint] strike_targets: list[TheaterGroundObject[Any]] enemy_barcaps: list[ControlPoint] @@ -72,6 +73,12 @@ class TheaterState(WorldState["TheaterState"]): self.enemy_ships.remove(target) self._rebuild_threat_zones() + def has_garrison(self, target: VehicleGroupGroundObject) -> bool: + return target in self.enemy_garrisons[target.control_point] + + def eliminate_garrison(self, target: VehicleGroupGroundObject) -> None: + self.enemy_garrisons[target.control_point].eliminate(target) + def clone(self) -> TheaterState: # Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly # expensive. @@ -89,7 +96,9 @@ class TheaterState(WorldState["TheaterState"]): enemy_convoys=list(self.enemy_convoys), enemy_shipping=list(self.enemy_shipping), enemy_ships=list(self.enemy_ships), - enemy_garrisons=dataclasses.replace(self.enemy_garrisons), + enemy_garrisons={ + cp: dataclasses.replace(g) for cp, g in self.enemy_garrisons.items() + }, oca_targets=list(self.oca_targets), strike_targets=list(self.strike_targets), enemy_barcaps=list(self.enemy_barcaps), @@ -110,6 +119,7 @@ class TheaterState(WorldState["TheaterState"]): finder = ObjectiveFinder(game, player) auto_stance = game.settings.automate_front_line_stance auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled + ordered_capturable_points = finder.prioritized_unisolated_points() return TheaterState( player=player, stance_automation_enabled=auto_stance, @@ -126,7 +136,9 @@ class TheaterState(WorldState["TheaterState"]): enemy_convoys=list(finder.convoys()), enemy_shipping=list(finder.cargo_ships()), enemy_ships=list(finder.enemy_ships()), - enemy_garrisons=Garrisons.from_theater(game.theater, not player), + enemy_garrisons={ + cp: Garrisons.for_control_point(cp) for cp in ordered_capturable_points + }, 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)), diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 8fb111f5..e1333dfc 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -342,9 +342,13 @@ class ControlPoint(MissionTarget, ABC): return self.name @property - def is_global(self) -> bool: + def is_isolated(self) -> bool: return not self.connected_points + @property + def is_global(self) -> bool: + return self.is_isolated + def transitive_connected_friendly_points( self, seen: Optional[Set[ControlPoint]] = None ) -> List[ControlPoint]: