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:
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
|
||||
|
||||
Reference in New Issue
Block a user