mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +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.income import Income
|
||||||
from game.inventory import GlobalAircraftInventory
|
from game.inventory import GlobalAircraftInventory
|
||||||
from game.navmesh import NavMesh
|
from game.navmesh import NavMesh
|
||||||
|
from game.orderedset import OrderedSet
|
||||||
from game.profiling import logged_duration, MultiEventTracer
|
from game.profiling import logged_duration, MultiEventTracer
|
||||||
|
from game.savecompat import has_save_compat_for
|
||||||
from game.threatzones import ThreatZones
|
from game.threatzones import ThreatZones
|
||||||
from game.transfers import PendingTransfers
|
from game.transfers import PendingTransfers
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ class Coalition:
|
|||||||
self.budget = budget
|
self.budget = budget
|
||||||
self.ato = AirTaskingOrder()
|
self.ato = AirTaskingOrder()
|
||||||
self.transit_network = TransitNetwork()
|
self.transit_network = TransitNetwork()
|
||||||
self.procurement_requests: list[AircraftProcurementRequest] = []
|
self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet()
|
||||||
self.bullseye = Bullseye(Point(0, 0))
|
self.bullseye = Bullseye(Point(0, 0))
|
||||||
self.faker = Faker(self.faction.locales)
|
self.faker = Faker(self.faction.locales)
|
||||||
self.air_wing = AirWing(game, self)
|
self.air_wing = AirWing(game, self)
|
||||||
@ -98,7 +100,14 @@ class Coalition:
|
|||||||
del state["faker"]
|
del state["faker"]
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
@has_save_compat_for(5)
|
||||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
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)
|
self.__dict__.update(state)
|
||||||
# Regenerate any state that was not persisted.
|
# Regenerate any state that was not persisted.
|
||||||
self.on_load()
|
self.on_load()
|
||||||
@ -225,3 +234,6 @@ class Coalition:
|
|||||||
manage_front_line,
|
manage_front_line,
|
||||||
manage_aircraft,
|
manage_aircraft,
|
||||||
).spend_budget(self.budget)
|
).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
|
return self.coalition.opponent.threat_zone
|
||||||
|
|
||||||
def add_procurement_request(self, request: AircraftProcurementRequest) -> None:
|
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:
|
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.
|
"""Returns True if it is possible for the air wing to plan this mission type.
|
||||||
@ -78,13 +78,14 @@ class PackageFulfiller:
|
|||||||
flight: ProposedFlight,
|
flight: ProposedFlight,
|
||||||
builder: PackageBuilder,
|
builder: PackageBuilder,
|
||||||
missing_types: Set[FlightType],
|
missing_types: Set[FlightType],
|
||||||
|
purchase_multiplier: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not builder.plan_flight(flight):
|
if not builder.plan_flight(flight):
|
||||||
missing_types.add(flight.task)
|
missing_types.add(flight.task)
|
||||||
purchase_order = AircraftProcurementRequest(
|
purchase_order = AircraftProcurementRequest(
|
||||||
near=mission.location,
|
near=mission.location,
|
||||||
task_capability=flight.task,
|
task_capability=flight.task,
|
||||||
number=flight.num_aircraft,
|
number=flight.num_aircraft * purchase_multiplier,
|
||||||
)
|
)
|
||||||
# Reserves are planned for critical missions, so prioritize those orders
|
# Reserves are planned for critical missions, so prioritize those orders
|
||||||
# over aircraft needed for non-critical missions.
|
# over aircraft needed for non-critical missions.
|
||||||
@ -96,11 +97,14 @@ class PackageFulfiller:
|
|||||||
builder: PackageBuilder,
|
builder: PackageBuilder,
|
||||||
missing_types: Set[FlightType],
|
missing_types: Set[FlightType],
|
||||||
not_attempted: Iterable[ProposedFlight],
|
not_attempted: Iterable[ProposedFlight],
|
||||||
|
purchase_multiplier: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Try to plan the rest of the mission just so we can count the missing
|
# Try to plan the rest of the mission just so we can count the missing
|
||||||
# types to buy.
|
# types to buy.
|
||||||
for flight in not_attempted:
|
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]))
|
missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
|
||||||
builder.release_planned_aircraft()
|
builder.release_planned_aircraft()
|
||||||
@ -124,7 +128,10 @@ class PackageFulfiller:
|
|||||||
return threats
|
return threats
|
||||||
|
|
||||||
def plan_mission(
|
def plan_mission(
|
||||||
self, mission: ProposedMission, tracer: MultiEventTracer
|
self,
|
||||||
|
mission: ProposedMission,
|
||||||
|
purchase_multiplier: int,
|
||||||
|
tracer: MultiEventTracer,
|
||||||
) -> Optional[Package]:
|
) -> Optional[Package]:
|
||||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||||
builder = PackageBuilder(
|
builder = PackageBuilder(
|
||||||
@ -155,11 +162,17 @@ class PackageFulfiller:
|
|||||||
escorts.append(proposed_flight)
|
escorts.append(proposed_flight)
|
||||||
continue
|
continue
|
||||||
with tracer.trace("Flight planning"):
|
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:
|
if missing_types:
|
||||||
self.scrub_mission_missing_aircraft(
|
self.scrub_mission_missing_aircraft(
|
||||||
mission, builder, missing_types, escorts
|
mission, builder, missing_types, escorts, purchase_multiplier
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -189,13 +202,15 @@ class PackageFulfiller:
|
|||||||
assert escort.escort_type is not None
|
assert escort.escort_type is not None
|
||||||
if needed_escorts[escort.escort_type]:
|
if needed_escorts[escort.escort_type]:
|
||||||
with tracer.trace("Flight planning"):
|
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
|
# Check again for unavailable aircraft. If the escort was required and
|
||||||
# none were found, scrub the mission.
|
# none were found, scrub the mission.
|
||||||
if missing_types:
|
if missing_types:
|
||||||
self.scrub_mission_missing_aircraft(
|
self.scrub_mission_missing_aircraft(
|
||||||
mission, builder, missing_types, escorts
|
mission, builder, missing_types, escorts, purchase_multiplier
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@ -9,4 +9,4 @@ class ProtectAirSpace(CompoundTask[TheaterState]):
|
|||||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
for cp, needed in state.barcaps_needed.items():
|
for cp, needed in state.barcaps_needed.items():
|
||||||
if needed > 0:
|
if needed > 0:
|
||||||
yield [PlanBarcap(cp)]
|
yield [PlanBarcap(cp, needed)]
|
||||||
|
|||||||
@ -74,6 +74,27 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
def asap(self) -> bool:
|
def asap(self) -> bool:
|
||||||
return False
|
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:
|
def fulfill_mission(self, state: TheaterState) -> bool:
|
||||||
self.propose_flights()
|
self.propose_flights()
|
||||||
fulfiller = PackageFulfiller(
|
fulfiller = PackageFulfiller(
|
||||||
@ -83,7 +104,9 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
state.context.settings,
|
state.context.settings,
|
||||||
)
|
)
|
||||||
self.package = fulfiller.plan_mission(
|
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
|
return self.package is not None
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@ from gen.flights.flight import FlightType
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
||||||
|
max_orders: int
|
||||||
|
|
||||||
def preconditions_met(self, state: TheaterState) -> bool:
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
if not state.barcaps_needed[self.target]:
|
if not state.barcaps_needed[self.target]:
|
||||||
return False
|
return False
|
||||||
@ -20,3 +22,7 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
|||||||
|
|
||||||
def propose_flights(self) -> None:
|
def propose_flights(self) -> None:
|
||||||
self.propose_flight(FlightType.BARCAP, 2)
|
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:
|
def ato_for(self, player: bool) -> AirTaskingOrder:
|
||||||
return self.coalition_for(player).ato
|
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:
|
def transit_network_for(self, player: bool) -> TransitNetwork:
|
||||||
return self.coalition_for(player).transit_network
|
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 math
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
@ -262,7 +262,7 @@ class ProcurementAi:
|
|||||||
return budget, False
|
return budget, False
|
||||||
|
|
||||||
def purchase_aircraft(self, budget: float) -> float:
|
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)):
|
if not list(self.best_airbases_for(request)):
|
||||||
# No airbases in range of this request. Skip it.
|
# No airbases in range of this request. Skip it.
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -751,6 +751,6 @@ class PendingTransfers:
|
|||||||
# aesthetic.
|
# aesthetic.
|
||||||
gap += 1
|
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)
|
AircraftProcurementRequest(control_point, FlightType.TRANSPORT, gap)
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user