diff --git a/game/game.py b/game/game.py index ec687561..673ad30d 100644 --- a/game/game.py +++ b/game/game.py @@ -30,7 +30,7 @@ from .factions.faction import Faction from .income import Income from .infos.information import Information from .navmesh import NavMesh -from .procurement import ProcurementAi +from .procurement import AircraftProcurementRequest, ProcurementAi from .settings import Settings from .theater import ConflictTheater from .threatzones import ThreatZones @@ -117,6 +117,9 @@ class Game: self.conditions = self.generate_conditions() + self.blue_procurement_requests: List[AircraftProcurementRequest] = [] + self.red_procurement_requests: List[AircraftProcurementRequest] = [] + self.blue_ato = AirTaskingOrder() self.red_ato = AirTaskingOrder() @@ -131,13 +134,15 @@ class Game: # Turn 0 procurement. We don't actually have any missions to plan, but # the planner will tell us what it would like to plan so we can use that # to drive purchase decisions. + self.transfers.order_airlift_assets() + blue_planner = CoalitionMissionPlanner(self, is_player=True) blue_planner.plan_missions() red_planner = CoalitionMissionPlanner(self, is_player=False) red_planner.plan_missions() - self.plan_procurement(blue_planner, red_planner) + self.plan_procurement() def __getstate__(self) -> Dict[str, Any]: state = self.__dict__.copy() @@ -159,6 +164,13 @@ class Game: return self.blue_ato return self.red_ato + def procurement_requests_for( + self, player: bool + ) -> List[AircraftProcurementRequest]: + if player: + return self.blue_procurement_requests + return self.red_procurement_requests + def generate_conditions(self) -> Conditions: return Conditions.generate( self.theater, self.current_day, self.current_turn_time_of_day, self.settings @@ -337,6 +349,7 @@ class Game: self.compute_threat_zones() self.ground_planners = {} + self.transfers.order_airlift_assets() self.transfers.plan_transports() blue_planner = CoalitionMissionPlanner(self, is_player=True) @@ -351,13 +364,9 @@ class Game: gplanner.plan_groundwar() self.ground_planners[cp.id] = gplanner - self.plan_procurement(blue_planner, red_planner) + self.plan_procurement() - def plan_procurement( - self, - blue_planner: CoalitionMissionPlanner, - red_planner: CoalitionMissionPlanner, - ) -> None: + def plan_procurement(self) -> None: # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it # gets much more of the budget that turn. Otherwise budget (after # repairs) is split evenly between air and ground. For the default @@ -372,7 +381,7 @@ class Game: manage_front_line=self.settings.automate_front_line_reinforcements, manage_aircraft=self.settings.automate_aircraft_reinforcements, front_line_budget_share=ground_portion, - ).spend_budget(self.budget, blue_planner.procurement_requests) + ).spend_budget(self.budget, self.blue_procurement_requests) self.enemy_budget = ProcurementAi( self, @@ -382,7 +391,7 @@ class Game: manage_front_line=True, manage_aircraft=True, front_line_budget_share=ground_portion, - ).spend_budget(self.enemy_budget, red_planner.procurement_requests) + ).spend_budget(self.enemy_budget, self.red_procurement_requests) def message(self, text: str) -> None: self.informations.append(Information(text, turn=self.turn)) diff --git a/game/procurement.py b/game/procurement.py index 859a4b9c..97453d3e 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -72,7 +72,7 @@ class ProcurementAi: if not self.is_player: budget += self.sell_incomplete_squadrons() if self.manage_aircraft: - budget = self.purchase_aircraft(budget, aircraft_requests) + budget = self.purchase_aircraft(budget) return budget def sell_incomplete_squadrons(self) -> float: @@ -192,10 +192,8 @@ class ProcurementAi: aircraft_for_task(request.task_capability), airbase, request.number, budget ) - def purchase_aircraft( - self, budget: float, aircraft_requests: List[AircraftProcurementRequest] - ) -> float: - for request in aircraft_requests: + def purchase_aircraft(self, budget: float) -> float: + for request in self.game.procurement_requests_for(self.is_player): for airbase in self.best_airbases_for(request): unit = self.affordable_aircraft_for(request, airbase, budget) if unit is None: diff --git a/game/transfers.py b/game/transfers.py index a0387342..e2224dd7 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -9,6 +9,8 @@ from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type from dcs.mapping import Point from dcs.unittype import FlyingType, VehicleType +from game.procurement import AircraftProcurementRequest +from game.utils import nautical_miles from gen.ato import Package from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE from gen.flights.flightplan import FlightPlanBuilder @@ -409,3 +411,38 @@ class PendingTransfers: for transfer in self.pending_transfers: if transfer.transport is None: self.arrange_transport(transfer) + + def order_airlift_assets(self) -> None: + for control_point in self.game.theater.controlpoints: + self.order_airlift_assets_at(control_point) + + @staticmethod + def desired_airlift_capacity(control_point: ControlPoint) -> int: + return 4 if control_point.has_factory else 0 + + def current_airlift_capacity(self, control_point: ControlPoint) -> int: + inventory = self.game.aircraft_inventory.for_control_point(control_point) + return sum( + count + for unit_type, count in inventory.all_aircraft + if unit_type in TRANSPORT_CAPABLE + ) + + def order_airlift_assets_at(self, control_point: ControlPoint) -> None: + gap = self.desired_airlift_capacity( + control_point + ) - self.current_airlift_capacity(control_point) + + if gap <= 0: + return + + if gap % 2: + # Always buy in pairs since we're not trying to fill odd squadrons. Purely + # aesthetic. + gap += 1 + + self.game.procurement_requests_for(player=control_point.captured).append( + AircraftProcurementRequest( + control_point, nautical_miles(200), FlightType.TRANSPORT, gap + ) + ) diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 87d00a60..a82877b0 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -551,7 +551,7 @@ class CoalitionMissionPlanner: self.objective_finder = ObjectiveFinder(self.game, self.is_player) self.ato = self.game.blue_ato if is_player else self.game.red_ato self.threat_zones = self.game.threat_zone_for(not self.is_player) - self.procurement_requests: List[AircraftProcurementRequest] = [] + self.procurement_requests = self.game.procurement_requests_for(self.is_player) def critical_missions(self) -> Iterator[ProposedMission]: """Identifies the most important missions to plan this turn.