mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Maintain composition when buying ground units.
Unit composition is defined by the doctrine. The most understaffed CP will now get the most underrepresented unit type. Previously a random understaffed CP would get a random unit type. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1057.
This commit is contained in:
parent
d4fe893539
commit
d61382f4e2
@ -12,6 +12,7 @@ Saves from 2.5 are not compatible with 3.0.
|
||||
* **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions.
|
||||
* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP.
|
||||
* **[Campaign AI]** AI now considers the range to the SAM's threat zone rather than the range to the SAM itself when determining target priorities.
|
||||
* **[Campaign AI]** Auto purchase of ground units will now maintain unit composition instead of buying randomly. The unit composition is predefined.
|
||||
* **[Flight Planner]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time.
|
||||
* **[Flight Planner]** Flight plans now include bullseye waypoints.
|
||||
* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route.
|
||||
|
||||
0
game/data/__init__.py
Normal file
0
game/data/__init__.py
Normal file
@ -2,6 +2,18 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from game.utils import Distance, feet, nautical_miles
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundUnitProcurementRatios:
|
||||
ratios: dict[GroundUnitClass, float]
|
||||
|
||||
def for_unit_class(self, unit_class: GroundUnitClass) -> float:
|
||||
try:
|
||||
return self.ratios[unit_class] / sum(self.ratios.values())
|
||||
except KeyError:
|
||||
return 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -50,6 +62,8 @@ class Doctrine:
|
||||
|
||||
sweep_distance: Distance
|
||||
|
||||
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
@ -76,6 +90,16 @@ MODERN_DOCTRINE = Doctrine(
|
||||
cap_engagement_range=nautical_miles(50),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(60),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 4,
|
||||
GroundUnitClass.Atgm: 1,
|
||||
GroundUnitClass.Apc: 2,
|
||||
GroundUnitClass.Ifv: 3,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 2,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
@ -103,6 +127,16 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
cap_engagement_range=nautical_miles(35),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(40),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 4,
|
||||
GroundUnitClass.Atgm: 1,
|
||||
GroundUnitClass.Apc: 2,
|
||||
GroundUnitClass.Ifv: 3,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 2,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
@ -130,4 +164,14 @@ WWII_DOCTRINE = Doctrine(
|
||||
cap_engagement_range=nautical_miles(20),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(10),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 4,
|
||||
GroundUnitClass.Atgm: 1,
|
||||
GroundUnitClass.Apc: 2,
|
||||
GroundUnitClass.Ifv: 3,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 2,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
229
game/data/groundunitclass.py
Normal file
229
game/data/groundunitclass.py
Normal file
@ -0,0 +1,229 @@
|
||||
from enum import unique, Enum
|
||||
from typing import Type
|
||||
|
||||
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from pydcs_extensions.frenchpack import frenchpack
|
||||
|
||||
|
||||
@unique
|
||||
class GroundUnitClass(Enum):
|
||||
Tank = (
|
||||
"Tank",
|
||||
(
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_72B3,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_Leopard_2A4,
|
||||
Armor.MBT_Leopard_2A4_Trs,
|
||||
Armor.MBT_Leopard_2A5,
|
||||
Armor.MBT_Leopard_2A6M,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_Chieftain_Mk_3,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_IV,
|
||||
Armor.ZTZ_96B,
|
||||
Armor.LT_PT_76,
|
||||
# WW2
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.Tk_PzIV_H,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
Armor.Tk_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.SPG_StuG_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
Armor.SPG_Sturmpanzer_IV_Brummbar,
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
),
|
||||
)
|
||||
|
||||
Atgm = (
|
||||
"ATGM",
|
||||
(
|
||||
Armor.ATGM_HMMWV,
|
||||
Armor.ATGM_VAB_Mephisto,
|
||||
Armor.ATGM_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
# WW2 (Tank Destroyers)
|
||||
Unarmed.Carrier_M30_Cargo,
|
||||
Armor.SPG_Jagdpanzer_IV,
|
||||
Armor.SPG_Jagdpanther_G1,
|
||||
Armor.SPG_M10_GMC,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
),
|
||||
)
|
||||
|
||||
Ifv = (
|
||||
"IFV",
|
||||
(
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_Warrior,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.SPG_Stryker_MGS,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
# WW2
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.Car_M8_Greyhound_Armored,
|
||||
Armor.Car_Daimler_Armored,
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13,
|
||||
),
|
||||
)
|
||||
|
||||
Apc = (
|
||||
"APC",
|
||||
(
|
||||
Armor.Scout_HMMWV,
|
||||
Armor.IFV_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.IFV_BTR_82A,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.Scout_Cobra,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
Armor.APC_AAV_7_Amphibious,
|
||||
Armor.APC_TPz_Fuchs,
|
||||
Armor.Scout_BRDM_2,
|
||||
Armor.APC_BTR_RD,
|
||||
Artillery.Grad_MRL_FDDM__FC,
|
||||
# WW2
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
),
|
||||
)
|
||||
|
||||
Artillery = (
|
||||
"Artillery",
|
||||
(
|
||||
Artillery.MLRS_9A52_Smerch_HE_300mm,
|
||||
Artillery.SPH_2S1_Gvozdika_122mm,
|
||||
Artillery.SPH_2S3_Akatsia_152mm,
|
||||
Artillery.MLRS_BM_21_Grad_122mm,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
|
||||
Artillery.SPH_M109_Paladin_155mm,
|
||||
Artillery.MLRS_M270_227mm,
|
||||
Artillery.SPM_2S9_Nona_120mm_M,
|
||||
Artillery.SPH_Dana_vz77_152mm,
|
||||
Artillery.SPH_T155_Firtina_155mm,
|
||||
Artillery.PLZ_05,
|
||||
Artillery.SPH_2S19_Msta_152mm,
|
||||
Artillery.MLRS_9A52_Smerch_CM_300mm,
|
||||
# WW2
|
||||
Artillery.SPG_M12_GMC_155mm,
|
||||
),
|
||||
)
|
||||
|
||||
Logistics = (
|
||||
"Logistics",
|
||||
(
|
||||
Unarmed.Truck_M818_6x6,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
Unarmed.Truck_Ural_375,
|
||||
Unarmed.Truck_GAZ_66,
|
||||
Unarmed.Truck_GAZ_3307,
|
||||
Unarmed.Truck_GAZ_3308,
|
||||
Unarmed.Truck_Ural_4320_31_Arm_d,
|
||||
Unarmed.Truck_Ural_4320T,
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||
Unarmed.LUV_Kettenrad,
|
||||
Unarmed.Car_Willys_Jeep,
|
||||
Unarmed.LUV_Land_Rover_109,
|
||||
Unarmed.Truck_Land_Rover_101_FC,
|
||||
# Mods
|
||||
frenchpack.VBL,
|
||||
frenchpack.VAB,
|
||||
),
|
||||
)
|
||||
|
||||
Infantry = (
|
||||
"Infantry",
|
||||
(
|
||||
Infantry.Insurgent_AK_74,
|
||||
Infantry.Infantry_AK_74,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Infantry_M4_Georgia,
|
||||
Infantry.Infantry_AK_74_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Infantry_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Infantry_RPG,
|
||||
),
|
||||
)
|
||||
|
||||
Shorads = (
|
||||
"SHORADS",
|
||||
(
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker___Bradley_M6,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Avenger__Stinger,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
AirDefence.AAA_QF_3_7,
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, class_name: str, unit_list: tuple[Type[VehicleType], ...]
|
||||
) -> None:
|
||||
self.class_name = class_name
|
||||
self.unit_list = unit_list
|
||||
|
||||
def __contains__(self, unit_type: Type[VehicleType]) -> bool:
|
||||
return unit_type in self.unit_list
|
||||
@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
@ -133,6 +134,16 @@ class Faction:
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
def has_access_to_unittype(self, unitclass: GroundUnitClass) -> bool:
|
||||
has_access = False
|
||||
for vehicle in unitclass.unit_list:
|
||||
if vehicle in self.frontline_units:
|
||||
return True
|
||||
if vehicle in self.artillery_units:
|
||||
return True
|
||||
|
||||
return has_access
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
faction = Faction(locales=json.get("locales"))
|
||||
|
||||
@ -14,7 +14,8 @@ from game.utils import Distance
|
||||
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
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -114,28 +115,14 @@ class ProcurementAi:
|
||||
)
|
||||
return budget
|
||||
|
||||
def random_affordable_ground_unit(
|
||||
self, budget: float, cp: ControlPoint
|
||||
def affordable_ground_unit_of_class(
|
||||
self, budget: float, unit_class: GroundUnitClass
|
||||
) -> Optional[Type[VehicleType]]:
|
||||
affordable_units = [
|
||||
u
|
||||
for u in self.faction.frontline_units + self.faction.artillery_units
|
||||
if db.PRICES[u] <= budget
|
||||
]
|
||||
|
||||
total_number_aa = (
|
||||
cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
|
||||
faction_units = set(self.faction.frontline_units) | set(
|
||||
self.faction.artillery_units
|
||||
)
|
||||
total_non_aa = (
|
||||
cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
|
||||
)
|
||||
max_aa = math.ceil(total_non_aa / 8)
|
||||
|
||||
# Limit the number of AA units the AI will buy
|
||||
if not total_number_aa < max_aa:
|
||||
for unit in [u for u in affordable_units if u in TYPE_SHORAD]:
|
||||
affordable_units.remove(unit)
|
||||
|
||||
of_class = set(unit_class.unit_list) & faction_units
|
||||
affordable_units = [u for u in of_class if db.PRICES[u] <= budget]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
@ -147,12 +134,12 @@ class ProcurementAi:
|
||||
# TODO: Attempt to transfer from reserves.
|
||||
|
||||
while budget > 0:
|
||||
candidates = self.front_line_candidates()
|
||||
if not candidates:
|
||||
cp = self.ground_reinforcement_candidate()
|
||||
if cp is None:
|
||||
break
|
||||
|
||||
cp = random.choice(candidates)
|
||||
unit = self.random_affordable_ground_unit(budget, cp)
|
||||
most_needed_type = self.most_needed_unit_class(cp)
|
||||
unit = self.affordable_ground_unit_of_class(budget, most_needed_type)
|
||||
if unit is None:
|
||||
# Can't afford any more units.
|
||||
break
|
||||
@ -162,6 +149,31 @@ class ProcurementAi:
|
||||
|
||||
return budget
|
||||
|
||||
def most_needed_unit_class(self, cp: ControlPoint) -> GroundUnitClass:
|
||||
worst_balanced: Optional[GroundUnitClass] = None
|
||||
worst_fulfillment = math.inf
|
||||
for unit_class in GroundUnitClass:
|
||||
if not self.faction.has_access_to_unittype(unit_class):
|
||||
continue
|
||||
|
||||
current_ratio = self.cost_ratio_of_ground_unit(cp, unit_class)
|
||||
desired_ratio = (
|
||||
self.faction.doctrine.ground_unit_procurement_ratios.for_unit_class(
|
||||
unit_class
|
||||
)
|
||||
)
|
||||
if not desired_ratio:
|
||||
continue
|
||||
if current_ratio >= desired_ratio:
|
||||
continue
|
||||
fulfillment = current_ratio / desired_ratio
|
||||
if fulfillment < worst_fulfillment:
|
||||
worst_fulfillment = fulfillment
|
||||
worst_balanced = unit_class
|
||||
if worst_balanced is None:
|
||||
return GroundUnitClass.Tank
|
||||
return worst_balanced
|
||||
|
||||
def _affordable_aircraft_for_task(
|
||||
self,
|
||||
task: FlightType,
|
||||
@ -256,56 +268,71 @@ class ProcurementAi:
|
||||
yield cp
|
||||
yield from threatened
|
||||
|
||||
def front_line_candidates(self) -> List[ControlPoint]:
|
||||
candidates = []
|
||||
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
|
||||
worst_supply = math.inf
|
||||
understaffed: Optional[ControlPoint] = None
|
||||
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
for cp in self.owned_points:
|
||||
|
||||
total_ground_units_allocated_to_this_control_point = (
|
||||
self.total_ground_units_allocated_to(cp)
|
||||
)
|
||||
|
||||
if not cp.has_ground_unit_source(self.game):
|
||||
if not cp.has_active_frontline:
|
||||
continue
|
||||
|
||||
if not cp.has_ground_unit_source(self.game):
|
||||
# No source of ground units, so can't buy anything.
|
||||
continue
|
||||
|
||||
allocated = cp.allocated_ground_units(self.game.transfers)
|
||||
if (
|
||||
total_ground_units_allocated_to_this_control_point >= 50
|
||||
or total_ground_units_allocated_to_this_control_point
|
||||
>= cp.frontline_unit_count_limit
|
||||
allocated.total >= self.game.settings.front_line_procurement_target
|
||||
or allocated.total >= cp.frontline_unit_count_limit
|
||||
):
|
||||
# Control point is already sufficiently defended.
|
||||
continue
|
||||
for connected in cp.connected_points:
|
||||
if not connected.is_friendly(to_player=self.is_player):
|
||||
candidates.append(cp)
|
||||
if allocated.total < worst_supply:
|
||||
worst_supply = allocated.total
|
||||
understaffed = cp
|
||||
|
||||
if not candidates:
|
||||
# Otherwise buy reserves, but don't exceed 10 reserve units per CP.
|
||||
# These units do not exist in the world until the CP becomes
|
||||
# connected to an active front line, at which point all these units
|
||||
# will suddenly appear at the gates of the newly captured CP.
|
||||
#
|
||||
# To avoid sudden overwhelming numbers of units we avoid buying
|
||||
# many.
|
||||
#
|
||||
# Also, do not bother buying units at bases that will never connect
|
||||
# to a front line.
|
||||
for cp in self.owned_points:
|
||||
if not cp.can_recruit_ground_units(self.game):
|
||||
continue
|
||||
if self.total_ground_units_allocated_to(cp) >= 10:
|
||||
continue
|
||||
if cp.is_global:
|
||||
continue
|
||||
candidates.append(cp)
|
||||
if understaffed is not None:
|
||||
return understaffed
|
||||
|
||||
return candidates
|
||||
# Otherwise buy reserves, but don't exceed the amount defined in the settings.
|
||||
# These units do not exist in the world until the CP becomes
|
||||
# connected to an active front line, at which point all these units
|
||||
# will suddenly appear at the gates of the newly captured CP.
|
||||
#
|
||||
# To avoid sudden overwhelming numbers of units we avoid buying
|
||||
# many.
|
||||
#
|
||||
# Also, do not bother buying units at bases that will never connect
|
||||
# to a front line.
|
||||
for cp in self.owned_points:
|
||||
if cp.is_global:
|
||||
continue
|
||||
if not cp.can_recruit_ground_units(self.game):
|
||||
continue
|
||||
|
||||
def total_ground_units_allocated_to(self, control_point: ControlPoint) -> int:
|
||||
total = control_point.expected_ground_units_next_turn.total
|
||||
for transfer in self.game.transfers:
|
||||
if transfer.destination == control_point:
|
||||
total += sum(transfer.units.values())
|
||||
return total
|
||||
allocated = cp.allocated_ground_units(self.game.transfers)
|
||||
if allocated.total >= self.game.settings.reserves_procurement_target:
|
||||
continue
|
||||
|
||||
if allocated.total < worst_supply:
|
||||
worst_supply = allocated.total
|
||||
understaffed = cp
|
||||
|
||||
return understaffed
|
||||
|
||||
def cost_ratio_of_ground_unit(
|
||||
self, control_point: ControlPoint, unit_class: GroundUnitClass
|
||||
) -> float:
|
||||
allocations = control_point.allocated_ground_units(self.game.transfers)
|
||||
class_cost = 0
|
||||
total_cost = 0
|
||||
for unit_type, count in allocations.all.items():
|
||||
cost = db.PRICES[unit_type] * count
|
||||
total_cost += cost
|
||||
if unit_type in unit_class:
|
||||
class_cost += cost
|
||||
if not total_cost:
|
||||
return 0
|
||||
return class_cost / total_cost
|
||||
|
||||
@ -57,6 +57,8 @@ class Settings:
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
front_line_procurement_target: int = 50
|
||||
reserves_procurement_target: int = 10
|
||||
|
||||
# Performance culling
|
||||
perf_culling: bool = False
|
||||
|
||||
@ -10,7 +10,6 @@ from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from game import db
|
||||
from game.db import PRICES
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
|
||||
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
||||
PLANES_SCRAMBLE_MIN_BASE = 2
|
||||
@ -25,6 +24,7 @@ class Base:
|
||||
def __init__(self):
|
||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||
self.armor: Dict[Type[VehicleType], int] = {}
|
||||
# TODO: Appears unused.
|
||||
self.aa: Dict[AirDefence, int] = {}
|
||||
self.commision_points: Dict[Type, float] = {}
|
||||
self.strength = 1
|
||||
@ -47,10 +47,6 @@ class Base:
|
||||
logging.exception(f"No price found for {unit_type.id}")
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_frontline_aa(self) -> int:
|
||||
return sum([v for k, v in self.armor.items() if k in TYPE_SHORAD])
|
||||
|
||||
@property
|
||||
def total_aa(self) -> int:
|
||||
return sum(self.aa.values())
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
from __future__ import annotations
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
|
||||
import heapq
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from functools import total_ordering
|
||||
from functools import total_ordering, cached_property
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
@ -32,13 +34,12 @@ from dcs.ships import (
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unit import Unit
|
||||
from dcs.unittype import FlyingType
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.scenery_group import SceneryGroup
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from gen.runways import RunwayAssigner, RunwayData
|
||||
from .base import Base
|
||||
@ -58,6 +59,7 @@ from ..weather import Conditions
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
from ..transfers import PendingTransfers
|
||||
|
||||
FREE_FRONTLINE_UNIT_SUPPLY: int = 15
|
||||
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
|
||||
@ -222,6 +224,30 @@ class PendingOccupancy:
|
||||
return self.present + self.ordered + self.transferring
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitAllocations:
|
||||
present: dict[Type[VehicleType], int]
|
||||
ordered: dict[Type[VehicleType], int]
|
||||
transferring: dict[Type[VehicleType], int]
|
||||
|
||||
@property
|
||||
def all(self) -> dict[Type[VehicleType], int]:
|
||||
combined: dict[Type[VehicleType], int] = defaultdict(int)
|
||||
for unit_type, count in itertools.chain(
|
||||
self.present.items(), self.ordered.items(), self.transferring.items()
|
||||
):
|
||||
combined[unit_type] += count
|
||||
return dict(combined)
|
||||
|
||||
@cached_property
|
||||
def total(self) -> int:
|
||||
return (
|
||||
sum(self.present.values())
|
||||
+ sum(self.ordered.values())
|
||||
+ sum(self.transferring.values())
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunwayStatus:
|
||||
damaged: bool = False
|
||||
@ -732,47 +758,24 @@ class ControlPoint(MissionTarget, ABC):
|
||||
u.position.x = u.position.x + delta.x
|
||||
u.position.y = u.position.y + delta.y
|
||||
|
||||
@property
|
||||
def pending_frontline_aa_deliveries_count(self):
|
||||
"""
|
||||
Get number of pending frontline aa units
|
||||
"""
|
||||
if self.pending_unit_deliveries:
|
||||
return sum(
|
||||
[
|
||||
v
|
||||
for k, v in self.pending_unit_deliveries.units.items()
|
||||
if k in TYPE_SHORAD
|
||||
]
|
||||
)
|
||||
else:
|
||||
return 0
|
||||
def allocated_ground_units(
|
||||
self, transfers: PendingTransfers
|
||||
) -> GroundUnitAllocations:
|
||||
on_order = {}
|
||||
for unit_bought, count in self.pending_unit_deliveries.units.items():
|
||||
if issubclass(unit_bought, VehicleType):
|
||||
on_order[unit_bought] = count
|
||||
|
||||
@property
|
||||
def pending_deliveries_count(self):
|
||||
"""
|
||||
Get number of pending units
|
||||
"""
|
||||
if self.pending_unit_deliveries:
|
||||
return sum([v for k, v in self.pending_unit_deliveries.units.items()])
|
||||
else:
|
||||
return 0
|
||||
transferring: dict[Type[VehicleType], int] = defaultdict(int)
|
||||
for transfer in transfers:
|
||||
if transfer.destination == self:
|
||||
for unit_type, count in transfer.units.items():
|
||||
transferring[unit_type] += count
|
||||
|
||||
@property
|
||||
def expected_ground_units_next_turn(self) -> PendingOccupancy:
|
||||
on_order = 0
|
||||
for unit_bought in self.pending_unit_deliveries.units:
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
continue
|
||||
if unit_bought in TYPE_SHORAD:
|
||||
continue
|
||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||
|
||||
return PendingOccupancy(
|
||||
self.base.total_armor,
|
||||
return GroundUnitAllocations(
|
||||
self.base.armor,
|
||||
on_order,
|
||||
# Ground unit transfers not yet implemented.
|
||||
transferring=0,
|
||||
transferring,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import random
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
@ -5,7 +6,8 @@ from typing import Dict, List
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from gen.ground_forces.ai_ground_planner_db import *
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
MAX_COMBAT_GROUP_PER_CP = 10
|
||||
@ -91,37 +93,35 @@ class GroundPlanner:
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
|
||||
|
||||
# Create combat groups and assign them randomly to each enemy CP
|
||||
for key in self.cp.base.armor.keys():
|
||||
|
||||
role = None
|
||||
collection = None
|
||||
if key in TYPE_TANKS:
|
||||
for unit_type in self.cp.base.armor:
|
||||
if unit_type in GroundUnitClass.Tank:
|
||||
collection = self.tank_groups
|
||||
role = CombatGroupRole.TANK
|
||||
elif key in TYPE_APC:
|
||||
elif unit_type in GroundUnitClass.Apc:
|
||||
collection = self.apc_group
|
||||
role = CombatGroupRole.APC
|
||||
elif key in TYPE_ARTILLERY:
|
||||
elif unit_type in GroundUnitClass.Artillery:
|
||||
collection = self.art_group
|
||||
role = CombatGroupRole.ARTILLERY
|
||||
elif key in TYPE_IFV:
|
||||
elif unit_type in GroundUnitClass.Ifv:
|
||||
collection = self.ifv_group
|
||||
role = CombatGroupRole.IFV
|
||||
elif key in TYPE_LOGI:
|
||||
elif unit_type in GroundUnitClass.Logistics:
|
||||
collection = self.logi_groups
|
||||
role = CombatGroupRole.LOGI
|
||||
elif key in TYPE_ATGM:
|
||||
elif unit_type in GroundUnitClass.Atgm:
|
||||
collection = self.atgm_group
|
||||
role = CombatGroupRole.ATGM
|
||||
elif key in TYPE_SHORAD:
|
||||
elif unit_type in GroundUnitClass.Shorads:
|
||||
collection = self.shorad_groups
|
||||
role = CombatGroupRole.SHORAD
|
||||
else:
|
||||
print("Warning unit type not handled by ground generator")
|
||||
print(key)
|
||||
logging.warning(
|
||||
f"Unused front line vehicle at base {unit_type}: unknown unit class"
|
||||
)
|
||||
continue
|
||||
|
||||
available = self.cp.base.armor[key]
|
||||
available = self.cp.base.armor[unit_type]
|
||||
|
||||
if available > remaining_available_frontline_units:
|
||||
available = remaining_available_frontline_units
|
||||
@ -151,7 +151,7 @@ class GroundPlanner:
|
||||
group.assigned_enemy_cp = "__reserve__"
|
||||
|
||||
for i in range(n):
|
||||
group.units.append(key)
|
||||
group.units.append(unit_type)
|
||||
collection.append(group)
|
||||
|
||||
if remaining_available_frontline_units == 0:
|
||||
@ -161,7 +161,7 @@ class GroundPlanner:
|
||||
print("Ground Planner : ")
|
||||
print(self.cp.name)
|
||||
print("------------------")
|
||||
for key in self.units_per_cp.keys():
|
||||
print("For : #" + str(key))
|
||||
for group in self.units_per_cp[key]:
|
||||
for unit_type in self.units_per_cp.keys():
|
||||
print("For : #" + str(unit_type))
|
||||
for group in self.units_per_cp[unit_type]:
|
||||
print(str(group))
|
||||
|
||||
@ -1,189 +0,0 @@
|
||||
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
|
||||
|
||||
from pydcs_extensions.frenchpack import frenchpack
|
||||
|
||||
TYPE_TANKS = [
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_72B3,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_Leopard_2A4,
|
||||
Armor.MBT_Leopard_2A4_Trs,
|
||||
Armor.MBT_Leopard_2A5,
|
||||
Armor.MBT_Leopard_2A6M,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_Chieftain_Mk_3,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_IV,
|
||||
Armor.ZTZ_96B,
|
||||
Armor.LT_PT_76,
|
||||
# WW2
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.Tk_PzIV_H,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
Armor.Tk_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.SPG_StuG_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
Armor.SPG_Sturmpanzer_IV_Brummbar,
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
]
|
||||
|
||||
TYPE_ATGM = [
|
||||
Armor.ATGM_HMMWV,
|
||||
Armor.ATGM_VAB_Mephisto,
|
||||
Armor.ATGM_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
# WW2 (Tank Destroyers)
|
||||
Unarmed.Carrier_M30_Cargo,
|
||||
Armor.SPG_Jagdpanzer_IV,
|
||||
Armor.SPG_Jagdpanther_G1,
|
||||
Armor.SPG_M10_GMC,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
]
|
||||
|
||||
TYPE_IFV = [
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_Warrior,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.SPG_Stryker_MGS,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
# WW2
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.Car_M8_Greyhound_Armored,
|
||||
Armor.Car_Daimler_Armored,
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13,
|
||||
]
|
||||
|
||||
TYPE_APC = [
|
||||
Armor.Scout_HMMWV,
|
||||
Armor.IFV_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.IFV_BTR_82A,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.Scout_Cobra,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
Armor.APC_AAV_7_Amphibious,
|
||||
Armor.APC_TPz_Fuchs,
|
||||
Armor.Scout_BRDM_2,
|
||||
Armor.APC_BTR_RD,
|
||||
Artillery.Grad_MRL_FDDM__FC,
|
||||
# WW2
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
]
|
||||
|
||||
TYPE_ARTILLERY = [
|
||||
Artillery.MLRS_9A52_Smerch_HE_300mm,
|
||||
Artillery.SPH_2S1_Gvozdika_122mm,
|
||||
Artillery.SPH_2S3_Akatsia_152mm,
|
||||
Artillery.MLRS_BM_21_Grad_122mm,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
|
||||
Artillery.SPH_M109_Paladin_155mm,
|
||||
Artillery.MLRS_M270_227mm,
|
||||
Artillery.SPM_2S9_Nona_120mm_M,
|
||||
Artillery.SPH_Dana_vz77_152mm,
|
||||
Artillery.SPH_T155_Firtina_155mm,
|
||||
Artillery.PLZ_05,
|
||||
Artillery.SPH_2S19_Msta_152mm,
|
||||
Artillery.MLRS_9A52_Smerch_CM_300mm,
|
||||
# WW2
|
||||
Artillery.SPG_M12_GMC_155mm,
|
||||
]
|
||||
|
||||
TYPE_LOGI = [
|
||||
Unarmed.Truck_M818_6x6,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
Unarmed.Truck_Ural_375,
|
||||
Unarmed.Truck_GAZ_66,
|
||||
Unarmed.Truck_GAZ_3307,
|
||||
Unarmed.Truck_GAZ_3308,
|
||||
Unarmed.Truck_Ural_4320_31_Arm_d,
|
||||
Unarmed.Truck_Ural_4320T,
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||
Unarmed.LUV_Kettenrad,
|
||||
Unarmed.Car_Willys_Jeep,
|
||||
Unarmed.LUV_Land_Rover_109,
|
||||
Unarmed.Truck_Land_Rover_101_FC,
|
||||
# Mods
|
||||
frenchpack.VBL,
|
||||
frenchpack.VAB,
|
||||
]
|
||||
|
||||
TYPE_INFANTRY = [
|
||||
Infantry.Insurgent_AK_74,
|
||||
Infantry.Infantry_AK_74,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Infantry_M4_Georgia,
|
||||
Infantry.Infantry_AK_74_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Infantry_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Infantry_RPG,
|
||||
]
|
||||
|
||||
TYPE_SHORAD = [
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker___Bradley_M6,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Avenger__Stinger,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
AirDefence.AAA_QF_3_7,
|
||||
]
|
||||
Loading…
x
Reference in New Issue
Block a user