diff --git a/changelog.md b/changelog.md index 34de24eb..dcb02a6f 100644 --- a/changelog.md +++ b/changelog.md @@ -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. diff --git a/game/data/__init__.py b/game/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/game/data/doctrine.py b/game/data/doctrine.py index 31b0a03b..4e017c1b 100644 --- a/game/data/doctrine.py +++ b/game/data/doctrine.py @@ -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, + } + ), ) diff --git a/game/data/groundunitclass.py b/game/data/groundunitclass.py new file mode 100644 index 00000000..411b1c93 --- /dev/null +++ b/game/data/groundunitclass.py @@ -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 diff --git a/game/factions/faction.py b/game/factions/faction.py index 147b7c08..ef06ef0d 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -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")) diff --git a/game/procurement.py b/game/procurement.py index b7222dc4..b89d05dd 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -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 diff --git a/game/settings.py b/game/settings.py index 247ae10b..07f21c7d 100644 --- a/game/settings.py +++ b/game/settings.py @@ -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 diff --git a/game/theater/base.py b/game/theater/base.py index fa329531..27390b2d 100644 --- a/game/theater/base.py +++ b/game/theater/base.py @@ -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()) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 270f5986..3fe0c90e 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -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 diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py index 761bf76b..075b5a05 100644 --- a/gen/ground_forces/ai_ground_planner.py +++ b/gen/ground_forces/ai_ground_planner.py @@ -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)) diff --git a/gen/ground_forces/ai_ground_planner_db.py b/gen/ground_forces/ai_ground_planner_db.py deleted file mode 100644 index fe200fbd..00000000 --- a/gen/ground_forces/ai_ground_planner_db.py +++ /dev/null @@ -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, -]