mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Dedup purchase requests.
Since the theater commander runs once per campaign action, missions that do not have aircraft available may be checked more than once a turn. Without deduping requests this can lead to cases where the AI buys dozens of tankers on turn 0. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1470
This commit is contained in:
parent
a3e3e9046f
commit
edf95ea9fb
@ -10,7 +10,9 @@ from game.commander.missionscheduler import MissionScheduler
|
||||
from game.income import Income
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from game.navmesh import NavMesh
|
||||
from game.orderedset import OrderedSet
|
||||
from game.profiling import logged_duration, MultiEventTracer
|
||||
from game.savecompat import has_save_compat_for
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import PendingTransfers
|
||||
|
||||
@ -35,7 +37,7 @@ class Coalition:
|
||||
self.budget = budget
|
||||
self.ato = AirTaskingOrder()
|
||||
self.transit_network = TransitNetwork()
|
||||
self.procurement_requests: list[AircraftProcurementRequest] = []
|
||||
self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet()
|
||||
self.bullseye = Bullseye(Point(0, 0))
|
||||
self.faker = Faker(self.faction.locales)
|
||||
self.air_wing = AirWing(game, self)
|
||||
@ -98,7 +100,14 @@ class Coalition:
|
||||
del state["faker"]
|
||||
return state
|
||||
|
||||
@has_save_compat_for(5)
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Begin save compat
|
||||
old_procurement_requests = state["procurement_requests"]
|
||||
if isinstance(old_procurement_requests, list):
|
||||
state["procurement_requests"] = OrderedSet(old_procurement_requests)
|
||||
# End save compat
|
||||
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.on_load()
|
||||
@ -225,3 +234,6 @@ class Coalition:
|
||||
manage_front_line,
|
||||
manage_aircraft,
|
||||
).spend_budget(self.budget)
|
||||
|
||||
def add_procurement_request(self, request: AircraftProcurementRequest) -> None:
|
||||
self.procurement_requests.add(request)
|
||||
|
||||
@ -60,7 +60,7 @@ class PackageFulfiller:
|
||||
return self.coalition.opponent.threat_zone
|
||||
|
||||
def add_procurement_request(self, request: AircraftProcurementRequest) -> None:
|
||||
self.coalition.procurement_requests.append(request)
|
||||
self.coalition.add_procurement_request(request)
|
||||
|
||||
def air_wing_can_plan(self, mission_type: FlightType) -> bool:
|
||||
"""Returns True if it is possible for the air wing to plan this mission type.
|
||||
@ -78,13 +78,14 @@ class PackageFulfiller:
|
||||
flight: ProposedFlight,
|
||||
builder: PackageBuilder,
|
||||
missing_types: Set[FlightType],
|
||||
purchase_multiplier: int,
|
||||
) -> None:
|
||||
if not builder.plan_flight(flight):
|
||||
missing_types.add(flight.task)
|
||||
purchase_order = AircraftProcurementRequest(
|
||||
near=mission.location,
|
||||
task_capability=flight.task,
|
||||
number=flight.num_aircraft,
|
||||
number=flight.num_aircraft * purchase_multiplier,
|
||||
)
|
||||
# Reserves are planned for critical missions, so prioritize those orders
|
||||
# over aircraft needed for non-critical missions.
|
||||
@ -96,11 +97,14 @@ class PackageFulfiller:
|
||||
builder: PackageBuilder,
|
||||
missing_types: Set[FlightType],
|
||||
not_attempted: Iterable[ProposedFlight],
|
||||
purchase_multiplier: int,
|
||||
) -> None:
|
||||
# Try to plan the rest of the mission just so we can count the missing
|
||||
# types to buy.
|
||||
for flight in not_attempted:
|
||||
self.plan_flight(mission, flight, builder, missing_types)
|
||||
self.plan_flight(
|
||||
mission, flight, builder, missing_types, purchase_multiplier
|
||||
)
|
||||
|
||||
missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
|
||||
builder.release_planned_aircraft()
|
||||
@ -124,7 +128,10 @@ class PackageFulfiller:
|
||||
return threats
|
||||
|
||||
def plan_mission(
|
||||
self, mission: ProposedMission, tracer: MultiEventTracer
|
||||
self,
|
||||
mission: ProposedMission,
|
||||
purchase_multiplier: int,
|
||||
tracer: MultiEventTracer,
|
||||
) -> Optional[Package]:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
builder = PackageBuilder(
|
||||
@ -155,11 +162,17 @@ class PackageFulfiller:
|
||||
escorts.append(proposed_flight)
|
||||
continue
|
||||
with tracer.trace("Flight planning"):
|
||||
self.plan_flight(mission, proposed_flight, builder, missing_types)
|
||||
self.plan_flight(
|
||||
mission,
|
||||
proposed_flight,
|
||||
builder,
|
||||
missing_types,
|
||||
purchase_multiplier,
|
||||
)
|
||||
|
||||
if missing_types:
|
||||
self.scrub_mission_missing_aircraft(
|
||||
mission, builder, missing_types, escorts
|
||||
mission, builder, missing_types, escorts, purchase_multiplier
|
||||
)
|
||||
return None
|
||||
|
||||
@ -189,13 +202,15 @@ class PackageFulfiller:
|
||||
assert escort.escort_type is not None
|
||||
if needed_escorts[escort.escort_type]:
|
||||
with tracer.trace("Flight planning"):
|
||||
self.plan_flight(mission, escort, builder, missing_types)
|
||||
self.plan_flight(
|
||||
mission, escort, builder, missing_types, purchase_multiplier
|
||||
)
|
||||
|
||||
# Check again for unavailable aircraft. If the escort was required and
|
||||
# none were found, scrub the mission.
|
||||
if missing_types:
|
||||
self.scrub_mission_missing_aircraft(
|
||||
mission, builder, missing_types, escorts
|
||||
mission, builder, missing_types, escorts, purchase_multiplier
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@ -9,4 +9,4 @@ class ProtectAirSpace(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for cp, needed in state.barcaps_needed.items():
|
||||
if needed > 0:
|
||||
yield [PlanBarcap(cp)]
|
||||
yield [PlanBarcap(cp, needed)]
|
||||
|
||||
@ -74,6 +74,27 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
def asap(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchase_multiplier(self) -> int:
|
||||
"""The multiplier for aircraft quantity when missions could not be fulfilled.
|
||||
|
||||
For missions that do not schedule in rounds like BARCAPs do, this should be one
|
||||
to ensure that the we only purchase enough aircraft to plan the mission once.
|
||||
|
||||
For missions that repeat within the same turn, however, we may need to buy for
|
||||
the same mission more than once. If three rounds of BARCAP still need to be
|
||||
fulfilled, this would return 3, and we'd triplicate the purchase order.
|
||||
|
||||
There is a small misbehavior here that's not symptomatic for our current mission
|
||||
planning: multi-round, multi-flight packages will only purchase multiple sets of
|
||||
aircraft for whatever is unavailable for the *first* failed package. For
|
||||
example, if we extend this to CAS and have no CAS aircraft but enough TARCAP
|
||||
aircraft for one round, we'll order CAS for every round but will not order any
|
||||
TARCAP aircraft, since we can't know that TARCAP aircraft are needed until we
|
||||
attempt to plan the second mission *without returning the first round aircraft*.
|
||||
"""
|
||||
return 1
|
||||
|
||||
def fulfill_mission(self, state: TheaterState) -> bool:
|
||||
self.propose_flights()
|
||||
fulfiller = PackageFulfiller(
|
||||
@ -83,7 +104,9 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
state.context.settings,
|
||||
)
|
||||
self.package = fulfiller.plan_mission(
|
||||
ProposedMission(self.target, self.flights), state.context.tracer
|
||||
ProposedMission(self.target, self.flights),
|
||||
self.purchase_multiplier,
|
||||
state.context.tracer,
|
||||
)
|
||||
return self.package is not None
|
||||
|
||||
|
||||
@ -10,6 +10,8 @@ from gen.flights.flight import FlightType
|
||||
|
||||
@dataclass
|
||||
class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
||||
max_orders: int
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not state.barcaps_needed[self.target]:
|
||||
return False
|
||||
@ -20,3 +22,7 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.BARCAP, 2)
|
||||
|
||||
@property
|
||||
def purchase_multiplier(self) -> int:
|
||||
return self.max_orders
|
||||
|
||||
@ -136,11 +136,6 @@ class Game:
|
||||
def ato_for(self, player: bool) -> AirTaskingOrder:
|
||||
return self.coalition_for(player).ato
|
||||
|
||||
def procurement_requests_for(
|
||||
self, player: bool
|
||||
) -> list[AircraftProcurementRequest]:
|
||||
return self.coalition_for(player).procurement_requests
|
||||
|
||||
def transit_network_for(self, player: bool) -> TransitNetwork:
|
||||
return self.coalition_for(player).transit_network
|
||||
|
||||
|
||||
23
game/orderedset.py
Normal file
23
game/orderedset.py
Normal file
@ -0,0 +1,23 @@
|
||||
from collections import Iterator, Iterable
|
||||
from typing import Generic, TypeVar, Optional
|
||||
|
||||
ValueT = TypeVar("ValueT")
|
||||
|
||||
|
||||
class OrderedSet(Generic[ValueT]):
|
||||
def __init__(self, initial_data: Optional[Iterable[ValueT]] = None) -> None:
|
||||
if initial_data is None:
|
||||
initial_data = []
|
||||
self._data: dict[ValueT, None] = {v: None for v in initial_data}
|
||||
|
||||
def __iter__(self) -> Iterator[ValueT]:
|
||||
yield from self._data
|
||||
|
||||
def __contains__(self, item: ValueT) -> bool:
|
||||
return item in self._data
|
||||
|
||||
def add(self, item: ValueT) -> None:
|
||||
self._data[item] = None
|
||||
|
||||
def clear(self) -> None:
|
||||
self._data.clear()
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
from game import db
|
||||
@ -262,7 +262,7 @@ class ProcurementAi:
|
||||
return budget, False
|
||||
|
||||
def purchase_aircraft(self, budget: float) -> float:
|
||||
for request in self.game.procurement_requests_for(self.is_player):
|
||||
for request in self.game.coalition_for(self.is_player).procurement_requests:
|
||||
if not list(self.best_airbases_for(request)):
|
||||
# No airbases in range of this request. Skip it.
|
||||
continue
|
||||
|
||||
@ -751,6 +751,6 @@ class PendingTransfers:
|
||||
# aesthetic.
|
||||
gap += 1
|
||||
|
||||
self.game.procurement_requests_for(self.player).append(
|
||||
self.game.coalition_for(self.player).add_procurement_request(
|
||||
AircraftProcurementRequest(control_point, FlightType.TRANSPORT, gap)
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user