diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index 9333bebd..5369b5dd 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -14,7 +14,6 @@ from game.theater import ( Airfield, ) from game.theater.theatergroundobject import ( - VehicleGroupGroundObject, NavalGroundObject, BuildingGroundObject, IadsGroundObject, @@ -50,18 +49,6 @@ class ObjectiveFinder: if isinstance(ground_object, IadsGroundObject): yield ground_object - def enemy_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]: - """Iterates over all enemy vehicle groups.""" - for cp in self.enemy_control_points(): - for ground_object in cp.ground_objects: - if not isinstance(ground_object, VehicleGroupGroundObject): - continue - - if ground_object.is_dead: - continue - - yield ground_object - 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/degradeiads.py b/game/commander/tasks/compound/degradeiads.py index ab50d5b8..21ddd02e 100644 --- a/game/commander/tasks/compound/degradeiads.py +++ b/game/commander/tasks/compound/degradeiads.py @@ -1,16 +1,24 @@ from collections import Iterator +from typing import Union from game.commander.tasks.primitive.antiship import PlanAntiShip from game.commander.tasks.primitive.dead import PlanDead from game.commander.theaterstate import TheaterState from game.htn import CompoundTask, Method -from game.theater.theatergroundobject import IadsGroundObject +from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject class DegradeIads(CompoundTask[TheaterState]): def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: for air_defense in state.threatening_air_defenses: - if isinstance(air_defense, IadsGroundObject): - yield [PlanDead(air_defense)] - else: - yield [PlanAntiShip(air_defense)] + yield [self.plan_against(air_defense)] + for detector in state.detecting_air_defenses: + yield [self.plan_against(detector)] + + @staticmethod + def plan_against( + target: Union[IadsGroundObject, NavalGroundObject] + ) -> Union[PlanDead, PlanAntiShip]: + if isinstance(target, IadsGroundObject): + return PlanDead(target) + return PlanAntiShip(target) diff --git a/game/commander/tasks/packageplanningtask.py b/game/commander/tasks/packageplanningtask.py index 5013fde7..6d310abb 100644 --- a/game/commander/tasks/packageplanningtask.py +++ b/game/commander/tasks/packageplanningtask.py @@ -4,6 +4,7 @@ import itertools import operator from abc import abstractmethod from dataclasses import dataclass, field +from enum import unique, IntEnum, auto from typing import TYPE_CHECKING, Optional, Generic, TypeVar, Iterator, Union from game.commander.missionproposals import ProposedFlight, EscortType, ProposedMission @@ -23,6 +24,12 @@ if TYPE_CHECKING: MissionTargetT = TypeVar("MissionTargetT", bound=MissionTarget) +@unique +class RangeType(IntEnum): + Detection = auto() + Threat = auto() + + # TODO: Refactor so that we don't need to call up to the mission planner. # Bypass type checker due to https://github.com/python/mypy/issues/5374 @dataclass # type: ignore @@ -75,8 +82,8 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]): EscortType.AirToAir, ) - def iter_iads_threats( - self, state: TheaterState + def iter_iads_ranges( + self, state: TheaterState, range_type: RangeType ) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]: target_ranges: list[ tuple[Union[IadsGroundObject, NavalGroundObject], Distance] @@ -86,15 +93,21 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]): ] = itertools.chain(state.enemy_air_defenses, state.enemy_ships) for target in all_iads: distance = meters(target.distance_to(self.target)) - threat_range = target.max_threat_range() - if not threat_range: + if range_type is RangeType.Detection: + target_range = target.max_detection_range() + elif range_type is RangeType.Threat: + target_range = target.max_threat_range() + else: + raise ValueError(f"Unknown RangeType: {range_type}") + if not target_range: continue + # IADS out of range of our target area will have a positive # distance_to_threat and should be pruned. The rest have a decreasing # distance_to_threat as overlap increases. The most negative distance has # the greatest coverage of the target and should be treated as the highest # priority threat. - distance_to_threat = distance - threat_range + distance_to_threat = distance - target_range if distance_to_threat > meters(0): continue target_ranges.append((target, distance_to_threat)) @@ -104,11 +117,27 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]): for target, _range in target_ranges: yield target + def iter_detecting_iads( + self, state: TheaterState + ) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]: + return self.iter_iads_ranges(state, RangeType.Detection) + + def iter_iads_threats( + self, state: TheaterState + ) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]: + return self.iter_iads_ranges(state, RangeType.Threat) + def target_area_preconditions_met( self, state: TheaterState, ignore_iads: bool = False ) -> bool: """Checks if the target area has been cleared of threats.""" threatened = False + + # Non-blocking, but analyzed so we can pick detectors worth eliminating. + for detector in self.iter_detecting_iads(state): + if detector not in state.detecting_air_defenses: + state.detecting_air_defenses.append(detector) + if not ignore_iads: for iads_threat in self.iter_iads_threats(state): threatened = True diff --git a/game/commander/tasks/primitive/dead.py b/game/commander/tasks/primitive/dead.py index 87c48b34..730eb832 100644 --- a/game/commander/tasks/primitive/dead.py +++ b/game/commander/tasks/primitive/dead.py @@ -13,7 +13,10 @@ from gen.flights.flight import FlightType @dataclass class PlanDead(PackagePlanningTask[IadsGroundObject]): def preconditions_met(self, state: TheaterState) -> bool: - if self.target not in state.threatening_air_defenses: + if ( + self.target not in state.threatening_air_defenses + and self.target not in state.detecting_air_defenses + ): return False return self.target_area_preconditions_met(state, ignore_iads=True) diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index a206b6e7..bc168f04 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -30,6 +30,7 @@ class TheaterState(WorldState["TheaterState"]): refueling_targets: list[MissionTarget] enemy_air_defenses: list[IadsGroundObject] threatening_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]] + detecting_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]] enemy_convoys: list[Convoy] enemy_shipping: list[CargoShip] enemy_ships: list[NavalGroundObject] @@ -49,12 +50,18 @@ class TheaterState(WorldState["TheaterState"]): ) def eliminate_air_defense(self, target: IadsGroundObject) -> None: - self.threatening_air_defenses.remove(target) + if target in self.threatening_air_defenses: + self.threatening_air_defenses.remove(target) + if target in self.detecting_air_defenses: + self.detecting_air_defenses.remove(target) self.enemy_air_defenses.remove(target) self._rebuild_threat_zones() def eliminate_ship(self, target: NavalGroundObject) -> None: - self.threatening_air_defenses.remove(target) + if target in self.threatening_air_defenses: + self.threatening_air_defenses.remove(target) + if target in self.detecting_air_defenses: + self.detecting_air_defenses.remove(target) self.enemy_ships.remove(target) self._rebuild_threat_zones() @@ -83,6 +90,7 @@ class TheaterState(WorldState["TheaterState"]): # would add the IADS that prevented it from being planned to the list of # IADS threats so that DegradeIads will consider it a threat later. threatening_air_defenses=self.threatening_air_defenses, + detecting_air_defenses=self.detecting_air_defenses, ) @classmethod @@ -95,6 +103,7 @@ class TheaterState(WorldState["TheaterState"]): refueling_targets=[finder.closest_friendly_control_point()], enemy_air_defenses=list(finder.enemy_air_defenses()), threatening_air_defenses=[], + detecting_air_defenses=[], enemy_convoys=list(finder.convoys()), enemy_shipping=list(finder.cargo_ships()), enemy_ships=list(finder.enemy_ships()),