From e6b9a73d03eeffa3e4b0cb29961a25b213fdee37 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 28 May 2021 19:19:40 -0700 Subject: [PATCH] Improve AI air defense target prioritization. Target the air defenses whose *threat ranges* come closest to friendly bases rather than the closest sites themselves. In other words, the SA-10 that is 5 miles behind the SA-6 will now be the priority. This also treats EWRs a bit differently. If they are not protected by a SAM their detection range will be used for determining their "threat" range. Otherwise a heuristic is used to determine whether or not they can be safely attacked without encroaching on the covering SAM. --- changelog.md | 3 +- game/theater/theatergroundobject.py | 6 ++++ game/threatzones.py | 8 ++++- gen/flights/ai_flight_planner.py | 49 +++++++++++++++++++++-------- 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/changelog.md b/changelog.md index 466aed51..34de24eb 100644 --- a/changelog.md +++ b/changelog.md @@ -10,7 +10,8 @@ Saves from 2.5 are not compatible with 3.0. * **[Campaign]** Non-control point FOBs will no longer spawn. * **[Campaign]** Added squadrons and pilots. See https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots for more information. * **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions. -* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP. +* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP. +* **[Campaign AI]** AI now considers the range to the SAM's threat zone rather than the range to the SAM itself when determining target priorities. * **[Flight Planner]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time. * **[Flight Planner]** Flight plans now include bullseye waypoints. * **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route. diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 1d1f0ffc..2484ec71 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -168,9 +168,15 @@ class TheaterGroundObject(MissionTarget): max_range = max(max_range, meters(unit_range)) return max_range + def max_detection_range(self) -> Distance: + return max(self.detection_range(g) for g in self.groups) + def detection_range(self, group: Group) -> Distance: return self._max_range_of_type(group, "detection_range") + def max_threat_range(self) -> Distance: + return max(self.threat_range(g) for g in self.groups) + def threat_range(self, group: Group, radar_only: bool = False) -> Distance: return self._max_range_of_type(group, "threat_range") diff --git a/game/threatzones.py b/game/threatzones.py index 475517d6..c7207a74 100644 --- a/game/threatzones.py +++ b/game/threatzones.py @@ -13,7 +13,7 @@ from shapely.geometry import ( from shapely.geometry.base import BaseGeometry from shapely.ops import nearest_points, unary_union -from game.theater import ControlPoint +from game.theater import ControlPoint, MissionTarget from game.utils import Distance, meters, nautical_miles from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.flight import Flight, FlightWaypoint @@ -92,6 +92,12 @@ class ThreatZones: LineString((self.dcs_to_shapely_point(p.position) for p in flight.points)) ) + @threatened_by_air_defense.register + def _threatened_by_air_defense_mission_target(self, target: MissionTarget) -> bool: + return self.threatened_by_air_defense( + self.dcs_to_shapely_point(target.position) + ) + @singledispatchmethod def threatened_by_radar_sam(self, target) -> bool: raise NotImplementedError diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index c5d084bf..9004f5b1 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -18,6 +18,7 @@ from typing import ( Tuple, Type, TypeVar, + Union, ) from dcs.unittype import FlyingType @@ -44,7 +45,7 @@ from game.theater.theatergroundobject import ( VehicleGroupGroundObject, ) from game.transfers import CargoShip, Convoy -from game.utils import Distance, nautical_miles +from game.utils import Distance, nautical_miles, meters from gen.ato import Package from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.closestairfields import ( @@ -296,23 +297,35 @@ class ObjectiveFinder: self.game = game self.is_player = is_player - def enemy_air_defenses(self) -> Iterator[TheaterGroundObject]: + def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject, Distance]]: """Iterates over all enemy SAM sites.""" + doctrine = self.game.faction_for(self.is_player).doctrine + threat_zones = self.game.threat_zone_for(not self.is_player) for cp in self.enemy_control_points(): for ground_object in cp.ground_objects: - is_ewr = isinstance(ground_object, EwrGroundObject) - is_sam = isinstance(ground_object, SamGroundObject) - if not is_ewr and not is_sam: - continue - if ground_object.is_dead: continue - # TODO: Yield in order of most threatening. - # Need to sort in order of how close their defensive range comes - # to friendly assets. To do that we need to add effective range - # information to the database. - yield ground_object + if isinstance(ground_object, EwrGroundObject): + if threat_zones.threatened_by_air_defense(ground_object): + # This is a very weak heuristic for determining whether the EWR + # is close enough to be worth targeting before a SAM that is + # covering it. Ingress distance corresponds to the beginning of + # the attack range and is sufficient for most standoff weapons, + # so treating the ingress distance as the threat distance sorts + # these EWRs such that they will be attacked before SAMs that do + # not threaten the ingress point, but after those that do. + target_range = doctrine.ingress_egress_distance + else: + # But if the EWR isn't covered then we should only be worrying + # about its detection range. + target_range = ground_object.max_detection_range() + elif isinstance(ground_object, SamGroundObject): + target_range = ground_object.max_threat_range() + else: + continue + + yield ground_object, target_range def threatening_air_defenses(self) -> Iterator[TheaterGroundObject]: """Iterates over enemy SAMs in threat range of friendly control points. @@ -320,7 +333,17 @@ class ObjectiveFinder: SAM sites are sorted by their closest proximity to any friendly control point (airfield or fleet). """ - return self._targets_by_range(self.enemy_air_defenses()) + + target_ranges: list[tuple[TheaterGroundObject, Distance]] = [] + for target, threat_range in self.enemy_air_defenses(): + ranges: list[Distance] = [] + for cp in self.friendly_control_points(): + ranges.append(meters(target.distance_to(cp)) - threat_range) + target_ranges.append((target, min(ranges))) + + target_ranges = sorted(target_ranges, key=operator.itemgetter(1)) + for target, _range in target_ranges: + yield target def enemy_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]: """Iterates over all enemy vehicle groups."""