Reimplement aircraft retreats for captured bases.

This commit is contained in:
Dan Albert 2021-08-28 18:02:11 -07:00
parent 5fae178081
commit 469b1e5efe
2 changed files with 81 additions and 18 deletions

View File

@ -263,14 +263,32 @@ class Squadron:
def can_fulfill_flight(self, count: int) -> bool: def can_fulfill_flight(self, count: int) -> bool:
return self.can_provide_pilots(count) and self.untasked_aircraft >= count return self.can_provide_pilots(count) and self.untasked_aircraft >= count
def refund_orders(self) -> None: def refund_orders(self, count: Optional[int] = None) -> None:
self.coalition.adjust_budget(self.aircraft.price * self.pending_deliveries) if count is None:
self.pending_deliveries = 0 count = self.pending_deliveries
self.coalition.adjust_budget(self.aircraft.price * count)
self.pending_deliveries -= count
def deliver_orders(self) -> None: def deliver_orders(self) -> None:
self.cancel_overflow_orders()
self.owned_aircraft += self.pending_deliveries self.owned_aircraft += self.pending_deliveries
self.pending_deliveries = 0 self.pending_deliveries = 0
def relocate_to(self, destination: ControlPoint) -> None:
self.location = destination
def cancel_overflow_orders(self) -> None:
if self.pending_deliveries <= 0:
return
overflow = -self.location.unclaimed_parking()
if overflow > 0:
sell_count = min(overflow, self.pending_deliveries)
logging.debug(
f"{self.location} is overfull by {overflow} aircraft. Cancelling "
f"orders for {sell_count} aircraft to make room."
)
self.refund_orders(sell_count)
@property @property
def max_fulfillable_aircraft(self) -> int: def max_fulfillable_aircraft(self) -> int:
return max(self.number_of_available_pilots, self.untasked_aircraft) return max(self.number_of_available_pilots, self.untasked_aircraft)

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import heapq import heapq
import itertools import itertools
import logging import logging
import math
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -576,36 +577,82 @@ class ControlPoint(MissionTarget, ABC):
value = airframe.price * count value = airframe.price * count
game.adjust_budget(value, player=not self.captured) game.adjust_budget(value, player=not self.captured)
game.message( game.message(
f"No valid retreat destination in range of {self.name} for {airframe}" f"No valid retreat destination in range of {self.name} for {airframe} "
f"{count} aircraft have been captured and sold for ${value}M." f"{count} aircraft have been captured and sold for ${value}M."
) )
def aircraft_retreat_destination( def aircraft_retreat_destination(
self, airframe: AircraftType self, squadron: Squadron
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
closest = ObjectiveDistanceCache.get_closest_airfields(self) closest = ObjectiveDistanceCache.get_closest_airfields(self)
# TODO: Should be airframe dependent. max_retreat_distance = squadron.aircraft.max_mission_range
max_retreat_distance = nautical_miles(200)
# Skip the first airbase because that's the airbase we're retreating # Skip the first airbase because that's the airbase we're retreating
# from. # from.
airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:] airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:]
not_preferred: Optional[ControlPoint] = None
overfull: list[ControlPoint] = []
for airbase in airfields: for airbase in airfields:
if not airbase.can_operate(airframe):
continue
if airbase.captured != self.captured: if airbase.captured != self.captured:
continue continue
if airbase.unclaimed_parking() > 0:
return airbase
return None
@staticmethod if airbase.unclaimed_parking() < squadron.owned_aircraft:
def _retreat_squadron(squadron: Squadron) -> None: if airbase.can_operate(squadron.aircraft):
logging.error("Air unit retreat not currently implemented") overfull.append(airbase)
continue
if squadron.operates_from(airbase):
# Has room, is a preferred base type for this squadron, and is the
# closest choice. No need to keep looking.
return airbase
if not_preferred is None and airbase.can_operate(squadron.aircraft):
# Has room and is capable of operating from this base, but it isn't
# preferred. Remember this option and use it if we can't find a
# preferred base type with room.
not_preferred = airbase
if not_preferred is not None:
# It's not our best choice but the other choices don't have room for the
# squadron and would lead to aircraft being captured.
return not_preferred
# No base was available with enough room. Find whichever base has the most room
# available so we lose as little as possible. The overfull list is already
# sorted by distance, and filtered for appropriate destinations.
base_for_fewest_losses: Optional[ControlPoint] = None
loss_count = math.inf
for airbase in overfull:
overflow = -(
airbase.unclaimed_parking()
- squadron.owned_aircraft
- squadron.pending_deliveries
)
if overflow < loss_count:
loss_count = overflow
base_for_fewest_losses = airbase
return base_for_fewest_losses
def _retreat_squadron(self, game: Game, squadron: Squadron) -> None:
destination = self.aircraft_retreat_destination(squadron)
if destination is None:
squadron.refund_orders()
self.capture_aircraft(game, squadron.aircraft, squadron.owned_aircraft)
return
logging.debug(f"{squadron} retreating to {destination} from {self}")
squadron.relocate_to(destination)
squadron.cancel_overflow_orders()
overflow = -destination.unclaimed_parking()
if overflow > 0:
logging.debug(
f"Not enough room for {squadron} at {destination}. Capturing "
f"{overflow} aircraft."
)
self.capture_aircraft(game, squadron.aircraft, overflow)
squadron.owned_aircraft -= overflow
def retreat_air_units(self, game: Game) -> None: def retreat_air_units(self, game: Game) -> None:
# TODO: Capture in order of price to retain maximum value? # TODO: Capture in order of price to retain maximum value?
for squadron in self.squadrons: for squadron in self.squadrons:
self._retreat_squadron(squadron) self._retreat_squadron(game, squadron)
def depopulate_uncapturable_tgos(self) -> None: def depopulate_uncapturable_tgos(self) -> None:
for tgo in self.connected_objectives: for tgo in self.connected_objectives:
@ -616,8 +663,6 @@ class ControlPoint(MissionTarget, ABC):
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
new_coalition = game.coalition_for(for_player) new_coalition = game.coalition_for(for_player)
self.ground_unit_orders.refund_all(self.coalition) self.ground_unit_orders.refund_all(self.coalition)
for squadron in self.squadrons:
squadron.refund_orders()
self.retreat_ground_units(game) self.retreat_ground_units(game)
self.retreat_air_units(game) self.retreat_air_units(game)
self.depopulate_uncapturable_tgos() self.depopulate_uncapturable_tgos()