Prioritize ammo depots when appropriate.

The AI will now prioritize targeting ammo depots if the current
deployable enemy forces outnumber the friendly cap by 50% or more.
This commit is contained in:
Dan Albert 2021-07-13 17:06:25 -07:00
parent 9568bc7ea6
commit 587034ad03
5 changed files with 89 additions and 24 deletions

View File

@ -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

View File

@ -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)]

View File

@ -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.

View File

@ -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]]:

View File

@ -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"