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 ( from game.commander.tasks.compound.destroyenemygroundunits import (
DestroyEnemyGroundUnits, DestroyEnemyGroundUnits,
) )
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
ReduceEnemyFrontLineCapacity,
)
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method from game.htn import CompoundTask, Method
from game.theater import FrontLine from game.theater import FrontLine, ControlPoint
@dataclass(frozen=True) @dataclass(frozen=True)
@ -17,3 +20,32 @@ class CaptureBase(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [BreakthroughAttack(self.front_line, state.context.coalition.player)] yield [BreakthroughAttack(self.front_line, state.context.coalition.player)]
yield [DestroyEnemyGroundUnits(self.front_line)] 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 collections import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from game.commander.tasks.primitive.aggressiveattack import AggressiveAttack from game.commander.tasks.primitive.strike import PlanStrike
from game.commander.tasks.primitive.cas import PlanCas
from game.commander.tasks.primitive.eliminationattack import EliminationAttack
from game.commander.theaterstate import TheaterState from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method from game.htn import CompoundTask, Method
from game.theater import FrontLine from game.theater import ControlPoint
@dataclass(frozen=True) @dataclass(frozen=True)
class DestroyEnemyGroundUnits(CompoundTask[TheaterState]): class ReduceEnemyFrontLineCapacity(CompoundTask[TheaterState]):
front_line: FrontLine control_point: ControlPoint
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [EliminationAttack(self.front_line, state.context.coalition.player)] for ammo_dump in state.ammo_dumps_at(self.control_point):
yield [AggressiveAttack(self.front_line, state.context.coalition.player)] yield [PlanStrike(ammo_dump)]
yield [PlanCas(self.front_line)]

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import dataclasses import dataclasses
import itertools import itertools
import math import math
from collections import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Union, Optional from typing import TYPE_CHECKING, Any, Union, Optional
@ -18,6 +19,7 @@ from game.theater.theatergroundobject import (
NavalGroundObject, NavalGroundObject,
IadsGroundObject, IadsGroundObject,
VehicleGroupGroundObject, VehicleGroupGroundObject,
BuildingGroundObject,
) )
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
@ -88,6 +90,15 @@ class TheaterState(WorldState["TheaterState"]):
def eliminate_garrison(self, target: VehicleGroupGroundObject) -> None: def eliminate_garrison(self, target: VehicleGroupGroundObject) -> None:
self.enemy_garrisons[target.control_point].eliminate(target) 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: def clone(self) -> TheaterState:
# Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly # Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly
# expensive. # expensive.

View File

@ -40,7 +40,11 @@ from gen.ground_forces.combat_stance import CombatStance
from gen.runways import RunwayAssigner, RunwayData from gen.runways import RunwayAssigner, RunwayData
from .base import Base from .base import Base
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import GenericCarrierGroundObject, TheaterGroundObject from .theatergroundobject import (
GenericCarrierGroundObject,
TheaterGroundObject,
BuildingGroundObject,
)
from ..dcs.aircrafttype import AircraftType from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType from ..dcs.groundunittype import GroundUnitType
from ..utils import nautical_miles from ..utils import nautical_miles
@ -728,30 +732,47 @@ class ControlPoint(MissionTarget, ABC):
@property @property
def deployable_front_line_units(self) -> int: 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 @property
def frontline_unit_count_limit(self) -> int: def frontline_unit_count_limit(self) -> int:
return ( return self.front_line_capacity_with(self.active_ammo_depots_count)
FREE_FRONTLINE_UNIT_SUPPLY
+ self.active_ammo_depots_count * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION @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 @property
def active_ammo_depots_count(self) -> int: def active_ammo_depots_count(self) -> int:
"""Return the number of available ammo depots""" """Return the number of available ammo depots"""
return len( return len(list(self.active_ammo_depots))
[
obj
for obj in self.connected_objectives
if obj.category == "ammo" and not obj.is_dead
]
)
@property @property
def total_ammo_depots_count(self) -> int: def total_ammo_depots_count(self) -> int:
"""Return the number of ammo depots, including dead ones""" """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 @property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]: 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: def threat_range(self, group: GroupT, radar_only: bool = False) -> Distance:
return self._max_range_of_type(group, "threat_range") return self._max_range_of_type(group, "threat_range")
@property
def is_ammo_depot(self) -> bool:
return self.category == "ammo"
@property @property
def is_factory(self) -> bool: def is_factory(self) -> bool:
return self.category == "factory" return self.category == "factory"