diff --git a/game/commander/tasks/compound/capturebase.py b/game/commander/tasks/compound/capturebase.py index 86d2b86e..11936033 100644 --- a/game/commander/tasks/compound/capturebase.py +++ b/game/commander/tasks/compound/capturebase.py @@ -4,10 +4,13 @@ from dataclasses import dataclass from game.commander.tasks.compound.destroyenemygroundunits import ( DestroyEnemyGroundUnits, ) +from game.commander.tasks.compound.reduceenemyfrontlinecapacity import ( + ReduceEnemyFrontLineCapacity, +) from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack from game.commander.theaterstate import TheaterState from game.htn import CompoundTask, Method -from game.theater import FrontLine +from game.theater import FrontLine, ControlPoint @dataclass(frozen=True) @@ -17,3 +20,32 @@ class CaptureBase(CompoundTask[TheaterState]): def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: yield [BreakthroughAttack(self.front_line, state.context.coalition.player)] yield [DestroyEnemyGroundUnits(self.front_line)] + if self.worth_destroying_ammo_depots(state): + yield [ReduceEnemyFrontLineCapacity(self.enemy_cp(state))] + + def enemy_cp(self, state: TheaterState) -> ControlPoint: + return self.front_line.control_point_hostile_to(state.context.coalition.player) + + def units_deployable(self, state: TheaterState, player: bool) -> int: + cp = self.front_line.control_point_friendly_to(player) + ammo_depots = list(state.ammo_dumps_at(cp)) + return cp.deployable_front_line_units_with(len(ammo_depots)) + + def unit_cap(self, state: TheaterState, player: bool) -> int: + cp = self.front_line.control_point_friendly_to(player) + ammo_depots = list(state.ammo_dumps_at(cp)) + return cp.front_line_capacity_with(len(ammo_depots)) + + def enemy_has_ammo_dumps(self, state: TheaterState) -> bool: + return bool(state.ammo_dumps_at(self.enemy_cp(state))) + + def worth_destroying_ammo_depots(self, state: TheaterState) -> bool: + if not self.enemy_has_ammo_dumps(state): + return False + + friendly_cap = self.unit_cap(state, state.context.coalition.player) + enemy_deployable = self.units_deployable(state, state.context.coalition.player) + + # If the enemy can currently deploy 50% more units than we possibly could, it's + # worth killing an ammo depot. + return enemy_deployable / friendly_cap > 1.5 diff --git a/game/commander/tasks/compound/reduceenemyfrontlinecapacity.py b/game/commander/tasks/compound/reduceenemyfrontlinecapacity.py index 327acecd..1b8b0e7c 100644 --- a/game/commander/tasks/compound/reduceenemyfrontlinecapacity.py +++ b/game/commander/tasks/compound/reduceenemyfrontlinecapacity.py @@ -1,19 +1,16 @@ from collections import Iterator from dataclasses import dataclass -from game.commander.tasks.primitive.aggressiveattack import AggressiveAttack -from game.commander.tasks.primitive.cas import PlanCas -from game.commander.tasks.primitive.eliminationattack import EliminationAttack +from game.commander.tasks.primitive.strike import PlanStrike from game.commander.theaterstate import TheaterState from game.htn import CompoundTask, Method -from game.theater import FrontLine +from game.theater import ControlPoint @dataclass(frozen=True) -class DestroyEnemyGroundUnits(CompoundTask[TheaterState]): - front_line: FrontLine +class ReduceEnemyFrontLineCapacity(CompoundTask[TheaterState]): + control_point: ControlPoint def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: - yield [EliminationAttack(self.front_line, state.context.coalition.player)] - yield [AggressiveAttack(self.front_line, state.context.coalition.player)] - yield [PlanCas(self.front_line)] + for ammo_dump in state.ammo_dumps_at(self.control_point): + yield [PlanStrike(ammo_dump)] diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py index cf41f9a5..e04a13a3 100644 --- a/game/commander/theaterstate.py +++ b/game/commander/theaterstate.py @@ -3,6 +3,7 @@ from __future__ import annotations import dataclasses import itertools import math +from collections import Iterator from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Union, Optional @@ -18,6 +19,7 @@ from game.theater.theatergroundobject import ( NavalGroundObject, IadsGroundObject, VehicleGroupGroundObject, + BuildingGroundObject, ) from game.threatzones import ThreatZones from gen.ground_forces.combat_stance import CombatStance @@ -88,6 +90,15 @@ class TheaterState(WorldState["TheaterState"]): def eliminate_garrison(self, target: VehicleGroupGroundObject) -> None: self.enemy_garrisons[target.control_point].eliminate(target) + def ammo_dumps_at( + self, control_point: ControlPoint + ) -> Iterator[BuildingGroundObject]: + for target in self.strike_targets: + if target.control_point != control_point: + continue + if target.is_ammo_depot: + yield target + def clone(self) -> TheaterState: # Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly # expensive. diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 7fedc7fe..075f4f5e 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -40,7 +40,11 @@ from gen.ground_forces.combat_stance import CombatStance from gen.runways import RunwayAssigner, RunwayData from .base import Base from .missiontarget import MissionTarget -from .theatergroundobject import GenericCarrierGroundObject, TheaterGroundObject +from .theatergroundobject import ( + GenericCarrierGroundObject, + TheaterGroundObject, + BuildingGroundObject, +) from ..dcs.aircrafttype import AircraftType from ..dcs.groundunittype import GroundUnitType from ..utils import nautical_miles @@ -728,30 +732,47 @@ class ControlPoint(MissionTarget, ABC): @property def deployable_front_line_units(self) -> int: - return min(self.frontline_unit_count_limit, self.base.total_armor) + return self.deployable_front_line_units_with(self.active_ammo_depots_count) + + def deployable_front_line_units_with(self, ammo_depot_count: int) -> int: + return min( + self.front_line_capacity_with(ammo_depot_count), self.base.total_armor + ) + + @classmethod + def front_line_capacity_with(cls, ammo_depot_count: int) -> int: + return ( + FREE_FRONTLINE_UNIT_SUPPLY + + ammo_depot_count * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION + ) @property def frontline_unit_count_limit(self) -> int: - return ( - FREE_FRONTLINE_UNIT_SUPPLY - + self.active_ammo_depots_count * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION - ) + return self.front_line_capacity_with(self.active_ammo_depots_count) + + @property + def all_ammo_depots(self) -> Iterator[BuildingGroundObject]: + for tgo in self.connected_objectives: + if not tgo.is_ammo_depot: + continue + assert isinstance(tgo, BuildingGroundObject) + yield tgo + + @property + def active_ammo_depots(self) -> Iterator[BuildingGroundObject]: + for tgo in self.all_ammo_depots: + if not tgo.is_dead: + yield tgo @property def active_ammo_depots_count(self) -> int: """Return the number of available ammo depots""" - return len( - [ - obj - for obj in self.connected_objectives - if obj.category == "ammo" and not obj.is_dead - ] - ) + return len(list(self.active_ammo_depots)) @property def total_ammo_depots_count(self) -> int: """Return the number of ammo depots, including dead ones""" - return len([obj for obj in self.connected_objectives if obj.category == "ammo"]) + return len(list(self.all_ammo_depots)) @property def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]: diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 180dd352..f063a1ea 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -181,6 +181,10 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]): def threat_range(self, group: GroupT, radar_only: bool = False) -> Distance: return self._max_range_of_type(group, "threat_range") + @property + def is_ammo_depot(self) -> bool: + return self.category == "ammo" + @property def is_factory(self) -> bool: return self.category == "factory"