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
This commit is contained in:
Dan Albert 2021-09-03 15:09:48 -07:00
parent 94fb0d8c66
commit ab2bb6814e
6 changed files with 76 additions and 132 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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