Rank aircraft purchase preferences.

Rather than randomly selecting compatible aircraft for missions, perfer
the *best* aircraft for the job. This removes the "preferred" lists in
favor of sorting the capable lists in priority order. To maintain some
amount of variety the procurer has a 50/50 chance of buying when it
finds a match.

Fixes https://github.com/Khopa/dcs_liberation/issues/510
This commit is contained in:
Dan Albert 2020-12-25 18:19:10 -08:00
parent 993e59413a
commit b5f8e6925b
4 changed files with 171 additions and 409 deletions

View File

@ -7,6 +7,7 @@ Saves from 2.3 are not compatible with 2.4.
* **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats. * **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats.
* **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical. * **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical.
* **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy. * **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy.
* **[Campaign AI]** Auto-purchase now prefers the best aircraft for the task, but will attempt to maintain some variety.
* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior. * **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior.
* **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany. * **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany.

View File

@ -12,10 +12,7 @@ from game import db
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget
from game.utils import Distance from game.utils import Distance
from gen.flights.ai_flight_planner_db import ( from gen.flights.ai_flight_planner_db import aircraft_for_task
capable_aircraft_for_task,
preferred_aircraft_for_task,
)
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
@ -124,25 +121,26 @@ class ProcurementAi:
def _affordable_aircraft_of_types( def _affordable_aircraft_of_types(
self, types: List[Type[FlyingType]], airbase: ControlPoint, self, types: List[Type[FlyingType]], airbase: ControlPoint,
number: int, max_price: int) -> Optional[Type[FlyingType]]: number: int, max_price: int) -> Optional[Type[FlyingType]]:
unit_pool = [u for u in self.faction.aircrafts if u in types] best_choice: Optional[Type[FlyingType]] = None
affordable_units = [ for unit in [u for u in self.faction.aircrafts if u in types]:
u for u in unit_pool if db.PRICES[unit] * number > max_price:
if db.PRICES[u] * number <= max_price and airbase.can_operate(u) continue
] if not airbase.can_operate(unit):
if not affordable_units: continue
return None
return random.choice(affordable_units) # Affordable and compatible. To keep some variety, skip with a 50/50
# chance. Might be a good idea to have the chance to skip based on
# the price compared to the rest of the choices.
best_choice = unit
if random.choice([True, False]):
break
return best_choice
def affordable_aircraft_for( def affordable_aircraft_for(
self, request: AircraftProcurementRequest, self, request: AircraftProcurementRequest,
airbase: ControlPoint, budget: int) -> Optional[Type[FlyingType]]: airbase: ControlPoint, budget: int) -> Optional[Type[FlyingType]]:
aircraft = self._affordable_aircraft_of_types(
preferred_aircraft_for_task(request.task_capability),
airbase, request.number, budget)
if aircraft is not None:
return aircraft
return self._affordable_aircraft_of_types( return self._affordable_aircraft_of_types(
capable_aircraft_for_task(request.task_capability), aircraft_for_task(request.task_capability),
airbase, request.number, budget) airbase, request.number, budget)
def purchase_aircraft( def purchase_aircraft(

View File

@ -32,18 +32,15 @@ from game.theater import (
SamGroundObject, SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
) )
# Avoid importing some types that cause circular imports unless type checking.
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
EwrGroundObject, EwrGroundObject,
NavalGroundObject, VehicleGroupGroundObject, NavalGroundObject,
VehicleGroupGroundObject,
) )
from game.utils import Distance, nautical_miles from game.utils import Distance, nautical_miles
from gen import Conflict from gen import Conflict
from gen.ato import Package from gen.ato import Package
from gen.flights.ai_flight_planner_db import ( from gen.flights.ai_flight_planner_db import aircraft_for_task
capable_aircraft_for_task,
preferred_aircraft_for_task,
)
from gen.flights.closestairfields import ( from gen.flights.closestairfields import (
ClosestAirfields, ClosestAirfields,
ObjectiveDistanceCache, ObjectiveDistanceCache,
@ -55,6 +52,7 @@ from gen.flights.flight import (
from gen.flights.flightplan import FlightPlanBuilder from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator from gen.flights.traveltime import TotEstimator
# Avoid importing some types that cause circular imports unless type checking.
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.inventory import GlobalAircraftInventory from game.inventory import GlobalAircraftInventory
@ -144,13 +142,8 @@ class AircraftAllocator:
on subsequent calls. If the found aircraft are not used, the caller is on subsequent calls. If the found aircraft are not used, the caller is
responsible for returning them to the inventory. responsible for returning them to the inventory.
""" """
result = self.find_aircraft_of_type(
flight, preferred_aircraft_for_task(flight.task)
)
if result is not None:
return result
return self.find_aircraft_of_type( return self.find_aircraft_of_type(
flight, capable_aircraft_for_task(flight.task) flight, aircraft_for_task(flight.task)
) )
def find_aircraft_of_type( def find_aircraft_of_type(

View File

@ -93,442 +93,236 @@ from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
from pydcs_extensions.su57.su57 import Su_57 from pydcs_extensions.su57.su57 import Su_57
# All aircraft lists are in priority order. Aircraft higher in the list will be
# preferred over those lower in the list.
# TODO: These lists really ought to be era (faction) dependent. # TODO: These lists really ought to be era (faction) dependent.
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but # Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
# factions that also have F-4s should not. # factions that also have F-4s should not.
# Interceptor are the aircraft prioritized for interception tasks
# If none is available, the AI will use regular CAP-capable aircraft instead
INTERCEPT_CAPABLE = [
MiG_21Bis,
MiG_25PD,
MiG_31,
MiG_29S,
MiG_29A,
MiG_29G,
MiG_29K,
JF_17,
J_11A,
Su_27,
Su_30,
Su_33,
M_2000C,
Mirage_2000_5,
Rafale_M,
F_14A_135_GR,
F_14B,
F_15C,
F_16A,
F_16C_50,
FA_18C_hornet,
]
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available # Used for CAP, Escort, and intercept if there is not a specialised aircraft available
CAP_CAPABLE = [ CAP_CAPABLE = [
Su_57,
MiG_15bis, F_22A,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_29A,
MiG_29G,
MiG_29S,
MiG_31, MiG_31,
F_14B,
F_14A_135_GR,
MiG_25PD,
Rafale_M,
Su_33,
Su_30,
Su_27, Su_27,
J_11A, J_11A,
JF_17,
Su_30,
Su_33,
Su_57,
M_2000C,
Mirage_2000_5,
F_86F_Sabre,
F_4E,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15C, F_15C,
F_15E, MiG_29S,
F_16A, MiG_29K,
MiG_29G,
MiG_29A,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
F_22A, F_15E,
F_16A,
F_4E,
JF_17,
MiG_23MLD,
MiG_21Bis,
Mirage_2000_5,
M_2000C,
F_5E_3,
MiG_19P,
A_4E_C,
F_86F_Sabre,
MiG_15bis,
C_101CC, C_101CC,
L_39ZA, L_39ZA,
P_51D_30_NA, P_51D_30_NA,
P_51D, P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
P_47D_30, P_47D_30,
P_47D_30bl1, P_47D_30bl1,
P_47D_40, P_47D_40,
I_16, I_16,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
A_4E_C,
Rafale_M,
] ]
CAP_PREFERRED = [
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_29A,
MiG_29G,
MiG_29S,
Su_27,
J_11A,
JF_17,
Su_30,
Su_33,
Su_57,
M_2000C,
Mirage_2000_5,
F_86F_Sabre,
F_14A_135_GR,
F_14B,
F_15C,
F_16C_50,
F_22A,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
I_16,
Bf_109K_4,
FW_190D9,
FW_190A8,
Rafale_M,
]
# Used for CAS (Close air support) and BAI (Battlefield Interdiction) # Used for CAS (Close air support) and BAI (Battlefield Interdiction)
CAS_CAPABLE = [ CAS_CAPABLE = [
MiG_15bis,
MiG_29A,
MiG_27K,
MiG_29S,
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_25TM,
Su_30,
Su_34,
JF_17,
M_2000C,
A_10A,
A_10C,
A_10C_2, A_10C_2,
AV8BNA, A_10C,
Su_25TM,
F_86F_Sabre, Su_25T,
F_5E_3, Su_25,
F_15E,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
F_15E, Rafale_A_S,
F_22A, Rafale_B,
Tornado_IDS,
Tornado_GR4, Tornado_GR4,
Tornado_IDS,
JF_17,
A_10A,
A_4E_C,
AJS37,
Su_24MR,
Su_24M,
Su_17M4,
AV8BNA,
Su_34,
Su_30,
MiG_29S,
MiG_27K,
MiG_29A,
AH_64D,
AH_64A,
AH_1W,
OH_58D,
SA342M,
SA342L,
Ka_50,
Mi_28N,
Mi_24V,
Mi_8MT,
UH_1H,
MiG_15bis,
M_2000C,
F_5E_3,
F_86F_Sabre,
C_101CC, C_101CC,
MB_339PAN, MB_339PAN,
L_39ZA, L_39ZA,
AJS37, A_20G,
P_47D_40,
SA342M, P_47D_30bl1,
SA342L, P_47D_30,
OH_58D,
AH_64A,
AH_64D,
AH_1W,
UH_1H,
Mi_8MT,
Mi_28N,
Mi_24V,
Ka_50,
P_51D_30_NA, P_51D_30_NA,
P_51D, P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
SpitfireLFMkIX, SpitfireLFMkIX,
I_16, I_16,
Bf_109K_4, Bf_109K_4,
FW_190D9, FW_190D9,
FW_190A8, FW_190A8,
A_4E_C,
Rafale_A_S,
Rafale_B,
WingLoong_I, WingLoong_I,
MQ_9_Reaper, MQ_9_Reaper,
RQ_1A_Predator RQ_1A_Predator,
] ]
CAS_PREFERRED = [
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_25TM,
Su_30,
Su_34,
A_10A,
A_10C,
A_10C_2,
AV8BNA,
Tornado_GR4,
C_101CC,
MB_339PAN,
L_39ZA,
AJS37,
SA342M,
SA342L,
OH_58D,
AH_64A,
AH_64D,
AH_1W,
Mi_28N,
Mi_24V,
Ka_50,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
I_16,
A_4E_C,
Rafale_A_S,
Rafale_B,
WingLoong_I,
MQ_9_Reaper,
RQ_1A_Predator
]
# Aircraft used for SEAD / DEAD tasks # Aircraft used for SEAD / DEAD tasks
SEAD_CAPABLE = [ SEAD_CAPABLE = [
F_4E,
FA_18C_hornet,
F_16C_50,
AV8BNA,
JF_17, JF_17,
Su_24M,
Su_25T,
Su_25TM,
Su_17M4,
Su_30,
Su_34,
MiG_27K,
Tornado_IDS,
Tornado_GR4,
A_4E_C,
Rafale_A_S,
Rafale_B
]
SEAD_PREFERRED = [
F_4E,
Su_25T,
Su_25TM,
Tornado_IDS,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
Su_30, Tornado_IDS,
Su_34, Su_25T,
Su_25TM,
Rafale_A_S,
Rafale_B,
F_4E,
A_4E_C,
AV8BNA,
Su_24M, Su_24M,
Su_17M4,
Su_34,
Su_30,
MiG_27K,
Tornado_GR4,
] ]
# Aircraft used for Strike mission # Aircraft used for Strike mission
STRIKE_CAPABLE = [ STRIKE_CAPABLE = [
MiG_15bis, F_117A,
MiG_21Bis,
MiG_27K,
MB_339PAN,
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_25TM,
Su_27,
Su_33,
Su_30,
Su_34,
MiG_29A,
MiG_29G,
MiG_29K,
MiG_29S,
Tu_160,
Tu_22M3,
Tu_95MS,
JF_17,
M_2000C,
A_10C,
A_10C_2,
AV8BNA,
F_86F_Sabre,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15E,
F_16A,
F_16C_50,
FA_18C_hornet,
B_1B, B_1B,
B_52H, B_52H,
F_117A, Tu_160,
Tu_95MS,
Tornado_IDS, Tu_22M3,
F_15E,
AJS37,
Rafale_A_S,
Rafale_B,
Tornado_GR4, Tornado_GR4,
F_16C_50,
FA_18C_hornet,
F_16A,
F_14B,
F_14A_135_GR,
Tornado_IDS,
Su_17M4,
Su_24MR,
Su_24M,
Su_25TM,
Su_25T,
Su_25,
Su_34,
Su_33,
Su_30,
Su_27,
MiG_29S,
MiG_29K,
MiG_29G,
MiG_29A,
JF_17,
A_10C_2,
A_10C,
AV8BNA,
A_4E_C,
M_2000C,
MiG_27K,
MiG_21Bis,
MiG_15bis,
F_5E_3,
F_86F_Sabre,
MB_339PAN,
C_101CC, C_101CC,
L_39ZA, L_39ZA,
AJS37, B_17G,
A_20G,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA, P_51D_30_NA,
P_51D, P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
B_17G,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
SpitfireLFMkIX, SpitfireLFMkIX,
Bf_109K_4, Bf_109K_4,
FW_190D9, FW_190D9,
FW_190A8, FW_190A8,
A_4E_C,
Rafale_A_S,
Rafale_B
] ]
STRIKE_PREFERRED = [
AJS37,
A_20G,
B_17G,
B_1B,
B_52H,
F_117A,
F_15E,
Su_24M,
Su_30,
Su_34,
Tornado_IDS,
Tornado_GR4,
Tu_160,
Tu_22M3,
Tu_95MS,
]
ANTISHIP_CAPABLE = [ ANTISHIP_CAPABLE = [
AJS37, AJS37,
C_101CC,
Su_24M,
Su_17M4,
FA_18C_hornet,
AV8BNA,
JF_17,
Su_30,
Su_34,
Tu_22M3, Tu_22M3,
Tornado_IDS,
Tornado_GR4,
Ju_88A4,
Rafale_A_S,
Rafale_B
]
ANTISHIP_PREFERRED = [
AJS37,
C_101CC,
FA_18C_hornet, FA_18C_hornet,
JF_17,
Rafale_A_S, Rafale_A_S,
Rafale_B, Rafale_B,
Su_24M, Su_24M,
Su_30, Su_17M4,
Su_34,
Tu_22M3,
Ju_88A4
]
RUNWAY_ATTACK_PREFERRED = [
JF_17, JF_17,
Su_30,
Su_34, Su_34,
Su_30,
Tornado_IDS, Tornado_IDS,
Tornado_GR4,
AV8BNA,
Ju_88A4,
C_101CC,
] ]
RUNWAY_ATTACK_CAPABLE = STRIKE_CAPABLE
# Duplicates some list entries but that's fine.
RUNWAY_ATTACK_CAPABLE = [
JF_17,
Su_34,
Su_30,
Tornado_IDS,
] + STRIKE_CAPABLE
DRONES = [ DRONES = [
MQ_9_Reaper, MQ_9_Reaper,
@ -537,31 +331,7 @@ DRONES = [
] ]
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions:
return CAP_PREFERRED
elif task == FlightType.ANTISHIP:
return ANTISHIP_PREFERRED
elif task == FlightType.BAI:
return CAS_CAPABLE
elif task == FlightType.CAS:
return CAS_PREFERRED
elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_PREFERRED
elif task == FlightType.OCA_AIRCRAFT:
return CAS_PREFERRED
elif task == FlightType.OCA_RUNWAY:
return RUNWAY_ATTACK_PREFERRED
elif task == FlightType.STRIKE:
return STRIKE_PREFERRED
elif task == FlightType.ESCORT:
return CAP_PREFERRED
else:
return []
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP) cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions: if task in cap_missions:
return CAP_CAPABLE return CAP_CAPABLE