Account for planned missions for breakthrough.

Consider BAI missions planned this turn when determining if a control
point is still garrisioned for preventing breakthrough.

This isn't very accurate yet since the HTN isn't checking for aircraft
fulfillment yet, so it might *plan* a mission to kill the garrison, but
there's no way to know if it will be fulfilled.
This commit is contained in:
Dan Albert 2021-07-13 13:50:50 -07:00
parent c180eb466d
commit 4534758c21
7 changed files with 59 additions and 59 deletions

View File

@ -3,59 +3,50 @@ from __future__ import annotations
from collections import Iterator from collections import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from game.theater import ConflictTheater, ControlPoint from game.theater import ControlPoint
from game.theater.theatergroundobject import VehicleGroupGroundObject from game.theater.theatergroundobject import VehicleGroupGroundObject
from game.utils import meters, nautical_miles from game.utils import meters
@dataclass @dataclass
class Garrisons: class Garrisons:
blocking_capture: list[VehicleGroupGroundObject] blocking_capture: list[VehicleGroupGroundObject]
defending_front_line: list[VehicleGroupGroundObject] defending_front_line: list[VehicleGroupGroundObject]
reserves: list[VehicleGroupGroundObject]
@property @property
def in_priority_order(self) -> Iterator[VehicleGroupGroundObject]: def in_priority_order(self) -> Iterator[VehicleGroupGroundObject]:
yield from self.blocking_capture yield from self.blocking_capture
yield from self.defending_front_line yield from self.defending_front_line
yield from self.reserves
def eliminate(self, garrison: VehicleGroupGroundObject) -> None: def eliminate(self, garrison: VehicleGroupGroundObject) -> None:
if garrison in self.blocking_capture: if garrison in self.blocking_capture:
self.blocking_capture.remove(garrison) self.blocking_capture.remove(garrison)
if garrison in self.defending_front_line: if garrison in self.defending_front_line:
self.defending_front_line.remove(garrison) self.defending_front_line.remove(garrison)
if garrison in self.reserves:
self.reserves.remove(garrison)
def __contains__(self, item: VehicleGroupGroundObject) -> bool: def __contains__(self, item: VehicleGroupGroundObject) -> bool:
return item in self.in_priority_order return item in self.in_priority_order
@classmethod @classmethod
def from_theater(cls, theater: ConflictTheater, player_owned: bool) -> Garrisons: def for_control_point(cls, control_point: ControlPoint) -> Garrisons:
"""Categorize garrison groups based on target priority. """Categorize garrison groups based on target priority.
Any garrisons blocking base capture are the highest priority, followed by other Any garrisons blocking base capture are the highest priority.
garrisons at front-line bases, and finally any garrisons in reserve at other
bases.
""" """
blocking = [] blocking = []
defending = [] defending = []
reserves = []
for cp in theater.control_points_for(player_owned):
garrisons = [ garrisons = [
tgo tgo
for tgo in cp.ground_objects for tgo in control_point.ground_objects
if isinstance(tgo, VehicleGroupGroundObject) and not tgo.is_dead if isinstance(tgo, VehicleGroupGroundObject) and not tgo.is_dead
] ]
if not cp.has_active_frontline:
reserves.extend(garrisons)
continue
for garrison in garrisons: for garrison in garrisons:
if meters(garrison.distance_to(cp)) < ControlPoint.CAPTURE_DISTANCE: if (
meters(garrison.distance_to(control_point))
< ControlPoint.CAPTURE_DISTANCE
):
blocking.append(garrison) blocking.append(garrison)
else: else:
defending.append(garrison) defending.append(garrison)
return Garrisons(blocking, defending, reserves) return Garrisons(blocking, defending)

View File

@ -223,17 +223,18 @@ class ObjectiveFinder:
if not c.is_friendly(self.is_player) if not c.is_friendly(self.is_player)
) )
def all_possible_targets(self) -> Iterator[MissionTarget]: def prioritized_unisolated_points(self) -> list[ControlPoint]:
"""Iterates over all possible mission targets in the theater. prioritized = []
capturable_later = []
Valid mission targets are control points (airfields and carriers), front for cp in self.game.theater.control_points_for(not self.is_player):
lines, and ground objects (SAM sites, factories, resource extraction if cp.is_isolated:
sites, etc). continue
""" if cp.has_active_frontline:
for cp in self.game.theater.controlpoints: prioritized.append(cp)
yield cp else:
yield from cp.ground_objects capturable_later.append(cp)
yield from self.front_lines() prioritized.extend(self._targets_by_range(capturable_later))
return prioritized
@staticmethod @staticmethod
def closest_airfields_to(location: MissionTarget) -> ClosestAirfields: def closest_airfields_to(location: MissionTarget) -> ClosestAirfields:

View File

@ -7,5 +7,6 @@ from game.htn import CompoundTask, Method
class AttackGarrisons(CompoundTask[TheaterState]): class AttackGarrisons(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
for garrison in state.enemy_garrisons.in_priority_order: for garrisons in state.enemy_garrisons.values():
for garrison in garrisons.in_priority_order:
yield [PlanBai(garrison)] yield [PlanBai(garrison)]

View File

@ -14,12 +14,12 @@ class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state): if not super().preconditions_met(state):
return False return False
if self.target not in state.enemy_garrisons: if not state.has_garrison(self.target):
return False return False
return self.target_area_preconditions_met(state) return self.target_area_preconditions_met(state)
def apply_effects(self, state: TheaterState) -> None: def apply_effects(self, state: TheaterState) -> None:
state.enemy_garrisons.eliminate(self.target) state.eliminate_garrison(self.target)
def propose_flights(self, doctrine: Doctrine) -> None: def propose_flights(self, doctrine: Doctrine) -> None:
self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive) self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive)

View File

@ -2,9 +2,6 @@ from __future__ import annotations
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
from game.commander.theaterstate import TheaterState from game.commander.theaterstate import TheaterState
from game.theater import ControlPoint
from game.theater.theatergroundobject import VehicleGroupGroundObject
from game.utils import meters
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
@ -17,20 +14,14 @@ class BreakthroughAttack(FrontLineStanceTask):
def have_sufficient_front_line_advantage(self) -> bool: def have_sufficient_front_line_advantage(self) -> bool:
return self.ground_force_balance >= 2.0 return self.ground_force_balance >= 2.0
@property def opposing_garrisons_eliminated(self, state: TheaterState) -> bool:
def opposing_garrisons_eliminated(self) -> bool: garrisons = state.enemy_garrisons[self.enemy_cp]
# TODO: Should operate on TheaterState to account for BAIs planned this turn. return not bool(garrisons.blocking_capture)
for tgo in self.enemy_cp.ground_objects:
if not isinstance(tgo, VehicleGroupGroundObject):
continue
if meters(tgo.distance_to(self.enemy_cp)) < ControlPoint.CAPTURE_DISTANCE:
return False
return True
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state): if not super().preconditions_met(state):
return False return False
return self.opposing_garrisons_eliminated return self.opposing_garrisons_eliminated(state)
def apply_effects(self, state: TheaterState) -> None: def apply_effects(self, state: TheaterState) -> None:
super().apply_effects(state) super().apply_effects(state)

View File

@ -15,6 +15,7 @@ from game.theater.theatergroundobject import (
TheaterGroundObject, TheaterGroundObject,
NavalGroundObject, NavalGroundObject,
IadsGroundObject, IadsGroundObject,
VehicleGroupGroundObject,
) )
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from game.transfers import Convoy, CargoShip from game.transfers import Convoy, CargoShip
@ -41,7 +42,7 @@ class TheaterState(WorldState["TheaterState"]):
enemy_convoys: list[Convoy] enemy_convoys: list[Convoy]
enemy_shipping: list[CargoShip] enemy_shipping: list[CargoShip]
enemy_ships: list[NavalGroundObject] enemy_ships: list[NavalGroundObject]
enemy_garrisons: Garrisons enemy_garrisons: dict[ControlPoint, Garrisons]
oca_targets: list[ControlPoint] oca_targets: list[ControlPoint]
strike_targets: list[TheaterGroundObject[Any]] strike_targets: list[TheaterGroundObject[Any]]
enemy_barcaps: list[ControlPoint] enemy_barcaps: list[ControlPoint]
@ -72,6 +73,12 @@ class TheaterState(WorldState["TheaterState"]):
self.enemy_ships.remove(target) self.enemy_ships.remove(target)
self._rebuild_threat_zones() self._rebuild_threat_zones()
def has_garrison(self, target: VehicleGroupGroundObject) -> bool:
return target in self.enemy_garrisons[target.control_point]
def eliminate_garrison(self, target: VehicleGroupGroundObject) -> None:
self.enemy_garrisons[target.control_point].eliminate(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.
@ -89,7 +96,9 @@ class TheaterState(WorldState["TheaterState"]):
enemy_convoys=list(self.enemy_convoys), enemy_convoys=list(self.enemy_convoys),
enemy_shipping=list(self.enemy_shipping), enemy_shipping=list(self.enemy_shipping),
enemy_ships=list(self.enemy_ships), enemy_ships=list(self.enemy_ships),
enemy_garrisons=dataclasses.replace(self.enemy_garrisons), enemy_garrisons={
cp: dataclasses.replace(g) for cp, g in self.enemy_garrisons.items()
},
oca_targets=list(self.oca_targets), oca_targets=list(self.oca_targets),
strike_targets=list(self.strike_targets), strike_targets=list(self.strike_targets),
enemy_barcaps=list(self.enemy_barcaps), enemy_barcaps=list(self.enemy_barcaps),
@ -110,6 +119,7 @@ class TheaterState(WorldState["TheaterState"]):
finder = ObjectiveFinder(game, player) finder = ObjectiveFinder(game, player)
auto_stance = game.settings.automate_front_line_stance auto_stance = game.settings.automate_front_line_stance
auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
ordered_capturable_points = finder.prioritized_unisolated_points()
return TheaterState( return TheaterState(
player=player, player=player,
stance_automation_enabled=auto_stance, stance_automation_enabled=auto_stance,
@ -126,7 +136,9 @@ class TheaterState(WorldState["TheaterState"]):
enemy_convoys=list(finder.convoys()), enemy_convoys=list(finder.convoys()),
enemy_shipping=list(finder.cargo_ships()), enemy_shipping=list(finder.cargo_ships()),
enemy_ships=list(finder.enemy_ships()), enemy_ships=list(finder.enemy_ships()),
enemy_garrisons=Garrisons.from_theater(game.theater, not player), enemy_garrisons={
cp: Garrisons.for_control_point(cp) for cp in ordered_capturable_points
},
oca_targets=list(finder.oca_targets(min_aircraft=20)), oca_targets=list(finder.oca_targets(min_aircraft=20)),
strike_targets=list(finder.strike_targets()), strike_targets=list(finder.strike_targets()),
enemy_barcaps=list(game.theater.control_points_for(not player)), enemy_barcaps=list(game.theater.control_points_for(not player)),

View File

@ -342,9 +342,13 @@ class ControlPoint(MissionTarget, ABC):
return self.name return self.name
@property @property
def is_global(self) -> bool: def is_isolated(self) -> bool:
return not self.connected_points return not self.connected_points
@property
def is_global(self) -> bool:
return self.is_isolated
def transitive_connected_friendly_points( def transitive_connected_friendly_points(
self, seen: Optional[Set[ControlPoint]] = None self, seen: Optional[Set[ControlPoint]] = None
) -> List[ControlPoint]: ) -> List[ControlPoint]: