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]** 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 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.
* **[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.theater import ControlPoint, MissionTarget
from game.utils import Distance
from gen.flights.ai_flight_planner_db import (
capable_aircraft_for_task,
preferred_aircraft_for_task,
)
from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
@ -124,25 +121,26 @@ class ProcurementAi:
def _affordable_aircraft_of_types(
self, types: List[Type[FlyingType]], airbase: ControlPoint,
number: int, max_price: int) -> Optional[Type[FlyingType]]:
unit_pool = [u for u in self.faction.aircrafts if u in types]
affordable_units = [
u for u in unit_pool
if db.PRICES[u] * number <= max_price and airbase.can_operate(u)
]
if not affordable_units:
return None
return random.choice(affordable_units)
best_choice: Optional[Type[FlyingType]] = None
for unit in [u for u in self.faction.aircrafts if u in types]:
if db.PRICES[unit] * number > max_price:
continue
if not airbase.can_operate(unit):
continue
# 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(
self, request: AircraftProcurementRequest,
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(
capable_aircraft_for_task(request.task_capability),
aircraft_for_task(request.task_capability),
airbase, request.number, budget)
def purchase_aircraft(

View File

@ -32,18 +32,15 @@ from game.theater import (
SamGroundObject,
TheaterGroundObject,
)
# Avoid importing some types that cause circular imports unless type checking.
from game.theater.theatergroundobject import (
EwrGroundObject,
NavalGroundObject, VehicleGroupGroundObject,
NavalGroundObject,
VehicleGroupGroundObject,
)
from game.utils import Distance, nautical_miles
from gen import Conflict
from gen.ato import Package
from gen.flights.ai_flight_planner_db import (
capable_aircraft_for_task,
preferred_aircraft_for_task,
)
from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import (
ClosestAirfields,
ObjectiveDistanceCache,
@ -55,6 +52,7 @@ from gen.flights.flight import (
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator
# Avoid importing some types that cause circular imports unless type checking.
if TYPE_CHECKING:
from game import Game
from game.inventory import GlobalAircraftInventory
@ -144,13 +142,8 @@ class AircraftAllocator:
on subsequent calls. If the found aircraft are not used, the caller is
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(
flight, capable_aircraft_for_task(flight.task)
flight, aircraft_for_task(flight.task)
)
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.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.
# 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.
# 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
CAP_CAPABLE = [
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_29A,
MiG_29G,
MiG_29S,
Su_57,
F_22A,
MiG_31,
F_14B,
F_14A_135_GR,
MiG_25PD,
Rafale_M,
Su_33,
Su_30,
Su_27,
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_15E,
F_16A,
MiG_29S,
MiG_29K,
MiG_29G,
MiG_29A,
F_16C_50,
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,
L_39ZA,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
P_47D_30,
P_47D_30bl1,
P_47D_40,
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)
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,
AV8BNA,
F_86F_Sabre,
F_5E_3,
A_10C,
Su_25TM,
Su_25T,
Su_25,
F_15E,
F_16C_50,
FA_18C_hornet,
F_15E,
F_22A,
Tornado_IDS,
Rafale_A_S,
Rafale_B,
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,
MB_339PAN,
L_39ZA,
AJS37,
SA342M,
SA342L,
OH_58D,
AH_64A,
AH_64D,
AH_1W,
UH_1H,
Mi_8MT,
Mi_28N,
Mi_24V,
Ka_50,
A_20G,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
I_16,
Bf_109K_4,
FW_190D9,
FW_190A8,
A_4E_C,
Rafale_A_S,
Rafale_B,
WingLoong_I,
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
SEAD_CAPABLE = [
F_4E,
FA_18C_hornet,
F_16C_50,
AV8BNA,
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,
FA_18C_hornet,
Su_30,
Su_34,
Tornado_IDS,
Su_25T,
Su_25TM,
Rafale_A_S,
Rafale_B,
F_4E,
A_4E_C,
AV8BNA,
Su_24M,
Su_17M4,
Su_34,
Su_30,
MiG_27K,
Tornado_GR4,
]
# Aircraft used for Strike mission
STRIKE_CAPABLE = [
MiG_15bis,
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,
F_117A,
B_1B,
B_52H,
F_117A,
Tornado_IDS,
Tu_160,
Tu_95MS,
Tu_22M3,
F_15E,
AJS37,
Rafale_A_S,
Rafale_B,
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,
L_39ZA,
AJS37,
B_17G,
A_20G,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
B_17G,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
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 = [
AJS37,
C_101CC,
Su_24M,
Su_17M4,
FA_18C_hornet,
AV8BNA,
JF_17,
Su_30,
Su_34,
Tu_22M3,
Tornado_IDS,
Tornado_GR4,
Ju_88A4,
Rafale_A_S,
Rafale_B
]
ANTISHIP_PREFERRED = [
AJS37,
C_101CC,
FA_18C_hornet,
JF_17,
Rafale_A_S,
Rafale_B,
Su_24M,
Su_30,
Su_34,
Tu_22M3,
Ju_88A4
]
RUNWAY_ATTACK_PREFERRED = [
Su_17M4,
JF_17,
Su_30,
Su_34,
Su_30,
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 = [
MQ_9_Reaper,
@ -537,31 +331,7 @@ DRONES = [
]
def preferred_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]]:
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions:
return CAP_CAPABLE