From 469b1e5efeca8f4c4848a78d27a3ef1a94a7eceb Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 28 Aug 2021 18:02:11 -0700 Subject: [PATCH] Reimplement aircraft retreats for captured bases. --- game/squadrons/squadron.py | 24 ++++++++++-- game/theater/controlpoint.py | 75 ++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 993a7c07..c7354c03 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -263,14 +263,32 @@ class Squadron: def can_fulfill_flight(self, count: int) -> bool: return self.can_provide_pilots(count) and self.untasked_aircraft >= count - def refund_orders(self) -> None: - self.coalition.adjust_budget(self.aircraft.price * self.pending_deliveries) - self.pending_deliveries = 0 + def refund_orders(self, count: Optional[int] = None) -> None: + if count is None: + count = self.pending_deliveries + self.coalition.adjust_budget(self.aircraft.price * count) + self.pending_deliveries -= count def deliver_orders(self) -> None: + self.cancel_overflow_orders() self.owned_aircraft += self.pending_deliveries 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 def max_fulfillable_aircraft(self) -> int: return max(self.number_of_available_pilots, self.untasked_aircraft) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index ecaf7e4a..0b738237 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -3,6 +3,7 @@ from __future__ import annotations import heapq import itertools import logging +import math from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass, field @@ -576,36 +577,82 @@ class ControlPoint(MissionTarget, ABC): value = airframe.price * count game.adjust_budget(value, player=not self.captured) 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." ) def aircraft_retreat_destination( - self, airframe: AircraftType + self, squadron: Squadron ) -> Optional[ControlPoint]: closest = ObjectiveDistanceCache.get_closest_airfields(self) - # TODO: Should be airframe dependent. - max_retreat_distance = nautical_miles(200) + max_retreat_distance = squadron.aircraft.max_mission_range # Skip the first airbase because that's the airbase we're retreating # from. airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:] + not_preferred: Optional[ControlPoint] = None + overfull: list[ControlPoint] = [] for airbase in airfields: - if not airbase.can_operate(airframe): - continue if airbase.captured != self.captured: continue - if airbase.unclaimed_parking() > 0: - return airbase - return None - @staticmethod - def _retreat_squadron(squadron: Squadron) -> None: - logging.error("Air unit retreat not currently implemented") + if airbase.unclaimed_parking() < squadron.owned_aircraft: + if airbase.can_operate(squadron.aircraft): + 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: # TODO: Capture in order of price to retain maximum value? for squadron in self.squadrons: - self._retreat_squadron(squadron) + self._retreat_squadron(game, squadron) def depopulate_uncapturable_tgos(self) -> None: for tgo in self.connected_objectives: @@ -616,8 +663,6 @@ class ControlPoint(MissionTarget, ABC): def capture(self, game: Game, for_player: bool) -> None: new_coalition = game.coalition_for(for_player) self.ground_unit_orders.refund_all(self.coalition) - for squadron in self.squadrons: - squadron.refund_orders() self.retreat_ground_units(game) self.retreat_air_units(game) self.depopulate_uncapturable_tgos()