From ab2bb6814e13485d9dc011259742df31412fb3fb Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 3 Sep 2021 15:09:48 -0700 Subject: [PATCH] Clean up aircraft allocation and procurement. This also does improve the over-purchase problems, though I can't spot the behavior change that's causing that. Presumably the old implementation had a bug I can't spot and in rewriting it I solved the problem... Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1582 --- game/coalition.py | 2 +- game/commander/aircraftallocator.py | 69 ----------------------------- game/commander/packagebuilder.py | 31 +++++++------ game/procurement.py | 41 ++++------------- game/squadrons/airwing.py | 51 ++++++++++++++------- game/squadrons/squadron.py | 14 +++++- 6 files changed, 76 insertions(+), 132 deletions(-) delete mode 100644 game/commander/aircraftallocator.py diff --git a/game/coalition.py b/game/coalition.py index 4ccc9128..8fb2ef18 100644 --- a/game/coalition.py +++ b/game/coalition.py @@ -40,7 +40,7 @@ class Coalition: self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet() self.bullseye = Bullseye(Point(0, 0)) self.faker = Faker(self.faction.locales) - self.air_wing = AirWing(game) + self.air_wing = AirWing(player) self.transfers = PendingTransfers(game, player) # Late initialized because the two coalitions in the game are mutually diff --git a/game/commander/aircraftallocator.py b/game/commander/aircraftallocator.py deleted file mode 100644 index 0339ff27..00000000 --- a/game/commander/aircraftallocator.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import Optional, Tuple - -from game.commander.missionproposals import ProposedFlight -from game.squadrons.airwing import AirWing -from game.squadrons.squadron import Squadron -from game.theater import ControlPoint, MissionTarget -from game.utils import meters -from gen.flights.ai_flight_planner_db import aircraft_for_task -from gen.flights.closestairfields import ClosestAirfields -from gen.flights.flight import FlightType - - -class AircraftAllocator: - """Finds suitable aircraft for proposed missions.""" - - def __init__( - self, air_wing: AirWing, closest_airfields: ClosestAirfields, is_player: bool - ) -> None: - self.air_wing = air_wing - self.closest_airfields = closest_airfields - self.is_player = is_player - - def find_squadron_for_flight( - self, target: MissionTarget, flight: ProposedFlight - ) -> Optional[Tuple[ControlPoint, Squadron]]: - """Finds aircraft suitable for the given mission. - - Searches for aircraft capable of performing the given mission within the - maximum allowed range. If insufficient aircraft are available for the - mission, None is returned. - - Airfields are searched ordered nearest to farthest from the target and - searched twice. The first search looks for aircraft which prefer the - mission type, and the second search looks for any aircraft which are - capable of the mission type. For example, an F-14 from a nearby carrier - will be preferred for the CAP of an airfield that has only F-16s, but if - the carrier has only F/A-18s the F-16s will be used for CAP instead. - - Note that aircraft *will* be removed from the global inventory on - success. This is to ensure that the same aircraft are not matched twice - on subsequent calls. If the found aircraft are not used, the caller is - responsible for returning them to the inventory. - """ - return self.find_aircraft_for_task(target, flight, flight.task) - - def find_aircraft_for_task( - self, target: MissionTarget, flight: ProposedFlight, task: FlightType - ) -> Optional[Tuple[ControlPoint, Squadron]]: - types = aircraft_for_task(task) - for airfield in self.closest_airfields.operational_airfields: - if not airfield.is_friendly(self.is_player): - continue - for aircraft in types: - if not airfield.can_operate(aircraft): - continue - distance_to_target = meters(target.distance_to(airfield)) - if distance_to_target > aircraft.max_mission_range: - continue - # Valid location with enough aircraft available. Find a squadron to fit - # the role. - squadrons = self.air_wing.auto_assignable_for_task_with_type( - aircraft, task, airfield - ) - for squadron in squadrons: - if squadron.operates_from(airfield) and squadron.can_fulfill_flight( - flight.num_aircraft - ): - return airfield, squadron - return None diff --git a/game/commander/packagebuilder.py b/game/commander/packagebuilder.py index 0f84b69a..1600d03d 100644 --- a/game/commander/packagebuilder.py +++ b/game/commander/packagebuilder.py @@ -1,16 +1,20 @@ -from typing import Optional +from __future__ import annotations + +from typing import Optional, TYPE_CHECKING -from game.commander.aircraftallocator import AircraftAllocator -from game.commander.missionproposals import ProposedFlight -from game.dcs.aircrafttype import AircraftType -from game.squadrons.airwing import AirWing -from game.theater import MissionTarget, OffMapSpawn, ControlPoint from game.utils import nautical_miles from gen.ato import Package -from gen.flights.closestairfields import ClosestAirfields +from game.theater import MissionTarget, OffMapSpawn, ControlPoint from gen.flights.flight import Flight +if TYPE_CHECKING: + from game.dcs.aircrafttype import AircraftType + from game.squadrons.airwing import AirWing + from gen.flights.closestairfields import ClosestAirfields + from .missionproposals import ProposedFlight + + class PackageBuilder: """Builds a Package for the flights it receives.""" @@ -28,7 +32,7 @@ class PackageBuilder: self.is_player = is_player self.package_country = package_country self.package = Package(location, auto_asap=asap) - self.allocator = AircraftAllocator(air_wing, closest_airfields, is_player) + self.air_wing = air_wing self.start_type = start_type def plan_flight(self, plan: ProposedFlight) -> bool: @@ -39,11 +43,12 @@ class PackageBuilder: caller should return any previously planned flights to the inventory using release_planned_aircraft. """ - assignment = self.allocator.find_squadron_for_flight(self.package.target, plan) - if assignment is None: + squadron = self.air_wing.best_squadron_for( + self.package.target, plan.task, plan.num_aircraft, this_turn=True + ) + if squadron is None: return False - airfield, squadron = assignment - start_type = airfield.required_aircraft_start_type + start_type = squadron.location.required_aircraft_start_type if start_type is None: start_type = self.start_type @@ -54,7 +59,7 @@ class PackageBuilder: plan.num_aircraft, plan.task, start_type, - divert=self.find_divert_field(squadron.aircraft, airfield), + divert=self.find_divert_field(squadron.aircraft, squadron.location), ) self.package.add_flight(flight) return True diff --git a/game/procurement.py b/game/procurement.py index 094e78c7..f51b21c8 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -221,45 +221,22 @@ class ProcurementAi: else: return self.game.theater.enemy_points() - @staticmethod - def squadron_rank_for_task(squadron: Squadron, task: FlightType) -> int: - return aircraft_for_task(task).index(squadron.aircraft) - - def compatible_squadrons_at_airbase( - self, airbase: ControlPoint, request: AircraftProcurementRequest - ) -> Iterator[Squadron]: - compatible: list[Squadron] = [] - for squadron in airbase.squadrons: - if not squadron.can_auto_assign(request.task_capability): - continue - if not squadron.can_provide_pilots(request.number): - continue - - distance_to_target = meters(request.near.distance_to(airbase)) - if distance_to_target > squadron.aircraft.max_mission_range: - continue - compatible.append(squadron) - yield from sorted( - compatible, - key=lambda s: self.squadron_rank_for_task(s, request.task_capability), - ) - def best_squadrons_for( self, request: AircraftProcurementRequest ) -> Iterator[Squadron]: - distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near) threatened = [] - for cp in distance_cache.operational_airfields: - if not cp.is_friendly(self.is_player): + for squadron in self.air_wing.best_squadrons_for( + request.near, request.task_capability, request.number, this_turn=False + ): + if not squadron.can_provide_pilots(request.number): continue - if cp.unclaimed_parking() < request.number: + if squadron.location.unclaimed_parking() < request.number: continue - if self.threat_zones.threatened(cp.position): - threatened.append(cp) + if self.threat_zones.threatened(squadron.location.position): + threatened.append(squadron) continue - yield from self.compatible_squadrons_at_airbase(cp, request) - for threatened_base in threatened: - yield from self.compatible_squadrons_at_airbase(threatened_base, request) + yield squadron + yield from threatened def ground_reinforcement_candidate(self) -> Optional[ControlPoint]: worst_supply = math.inf diff --git a/game/squadrons/airwing.py b/game/squadrons/airwing.py index 9d01e65c..4032702a 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -2,20 +2,21 @@ from __future__ import annotations import itertools from collections import defaultdict -from typing import Sequence, Iterator, TYPE_CHECKING +from typing import Sequence, Iterator, TYPE_CHECKING, Optional from game.dcs.aircrafttype import AircraftType +from gen.flights.ai_flight_planner_db import aircraft_for_task +from gen.flights.closestairfields import ObjectiveDistanceCache from .squadron import Squadron -from ..theater import ControlPoint +from ..theater import ControlPoint, MissionTarget if TYPE_CHECKING: - from game import Game from gen.flights.flight import FlightType class AirWing: - def __init__(self, game: Game) -> None: - self.game = game + def __init__(self, player: bool) -> None: + self.player = player self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) def add_squadron(self, squadron: Squadron) -> None: @@ -31,6 +32,35 @@ class AirWing: except StopIteration: return False + def best_squadrons_for( + self, location: MissionTarget, task: FlightType, size: int, this_turn: bool + ) -> list[Squadron]: + airfield_cache = ObjectiveDistanceCache.get_closest_airfields(location) + best_aircraft = aircraft_for_task(task) + ordered: list[Squadron] = [] + for control_point in airfield_cache.operational_airfields: + if control_point.captured != self.player: + continue + capable_at_base = [] + for squadron in control_point.squadrons: + if squadron.can_auto_assign_mission(location, task, size, this_turn): + capable_at_base.append(squadron) + + ordered.extend( + sorted( + capable_at_base, + key=lambda s: best_aircraft.index(s.aircraft), + ) + ) + return ordered + + def best_squadron_for( + self, location: MissionTarget, task: FlightType, size: int, this_turn: bool + ) -> Optional[Squadron]: + for squadron in self.best_squadrons_for(location, task, size, this_turn): + return squadron + return None + @property def available_aircraft_types(self) -> Iterator[AircraftType]: for aircraft, squadrons in self.squadrons.items(): @@ -51,17 +81,6 @@ class AirWing: if squadron.can_auto_assign(task) and squadron.location == base: yield squadron - def auto_assignable_for_task_with_type( - self, aircraft: AircraftType, task: FlightType, base: ControlPoint - ) -> Iterator[Squadron]: - for squadron in self.squadrons_for(aircraft): - if ( - squadron.location == base - and squadron.can_auto_assign(task) - and squadron.has_available_pilots - ): - yield squadron - def squadron_for(self, aircraft: AircraftType) -> Squadron: return self.squadrons_for(aircraft)[0] diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 607a8d8a..33106740 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -16,12 +16,13 @@ from gen.ato import Package from gen.flights.flight import FlightType, Flight from gen.flights.flightplan import FlightPlanBuilder from .pilot import Pilot, PilotStatus +from ..utils import meters if TYPE_CHECKING: from game import Game from game.coalition import Coalition from game.dcs.aircrafttype import AircraftType - from game.theater import ControlPoint, ConflictTheater + from game.theater import ControlPoint, ConflictTheater, MissionTarget from .operatingbases import OperatingBases from .squadrondef import SquadronDef @@ -252,6 +253,17 @@ class Squadron: def can_auto_assign(self, task: FlightType) -> bool: return task in self.auto_assignable_mission_types + def can_auto_assign_mission( + self, location: MissionTarget, task: FlightType, size: int, this_turn: bool + ) -> bool: + if not self.can_auto_assign(task): + return False + if this_turn and not self.can_fulfill_flight(size): + return False + + distance_to_target = meters(location.distance_to(self.location)) + return distance_to_target <= self.aircraft.max_mission_range + def operates_from(self, control_point: ControlPoint) -> bool: if control_point.is_carrier: return self.operating_bases.carrier