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 dataclasses import dataclass
from game.theater import ConflictTheater, ControlPoint
from game.theater import ControlPoint
from game.theater.theatergroundobject import VehicleGroupGroundObject
from game.utils import meters, nautical_miles
from game.utils import meters
@dataclass
class Garrisons:
blocking_capture: list[VehicleGroupGroundObject]
defending_front_line: list[VehicleGroupGroundObject]
reserves: list[VehicleGroupGroundObject]
@property
def in_priority_order(self) -> Iterator[VehicleGroupGroundObject]:
yield from self.blocking_capture
yield from self.defending_front_line
yield from self.reserves
def eliminate(self, garrison: VehicleGroupGroundObject) -> None:
if garrison in self.blocking_capture:
self.blocking_capture.remove(garrison)
if garrison in self.defending_front_line:
self.defending_front_line.remove(garrison)
if garrison in self.reserves:
self.reserves.remove(garrison)
def __contains__(self, item: VehicleGroupGroundObject) -> bool:
return item in self.in_priority_order
@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.
Any garrisons blocking base capture are the highest priority, followed by other
garrisons at front-line bases, and finally any garrisons in reserve at other
bases.
Any garrisons blocking base capture are the highest priority.
"""
blocking = []
defending = []
reserves = []
for cp in theater.control_points_for(player_owned):
garrisons = [
tgo
for tgo in cp.ground_objects
if isinstance(tgo, VehicleGroupGroundObject) and not tgo.is_dead
]
if not cp.has_active_frontline:
reserves.extend(garrisons)
continue
garrisons = [
tgo
for tgo in control_point.ground_objects
if isinstance(tgo, VehicleGroupGroundObject) and not tgo.is_dead
]
for garrison in garrisons:
if (
meters(garrison.distance_to(control_point))
< ControlPoint.CAPTURE_DISTANCE
):
blocking.append(garrison)
else:
defending.append(garrison)
for garrison in garrisons:
if meters(garrison.distance_to(cp)) < ControlPoint.CAPTURE_DISTANCE:
blocking.append(garrison)
else:
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)
)
def all_possible_targets(self) -> Iterator[MissionTarget]:
"""Iterates over all possible mission targets in the theater.
Valid mission targets are control points (airfields and carriers), front
lines, and ground objects (SAM sites, factories, resource extraction
sites, etc).
"""
for cp in self.game.theater.controlpoints:
yield cp
yield from cp.ground_objects
yield from self.front_lines()
def prioritized_unisolated_points(self) -> list[ControlPoint]:
prioritized = []
capturable_later = []
for cp in self.game.theater.control_points_for(not self.is_player):
if cp.is_isolated:
continue
if cp.has_active_frontline:
prioritized.append(cp)
else:
capturable_later.append(cp)
prioritized.extend(self._targets_by_range(capturable_later))
return prioritized
@staticmethod
def closest_airfields_to(location: MissionTarget) -> ClosestAirfields:

View File

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

View File

@ -14,12 +14,12 @@ class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
if self.target not in state.enemy_garrisons:
if not state.has_garrison(self.target):
return False
return self.target_area_preconditions_met(state)
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:
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.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
@ -17,20 +14,14 @@ class BreakthroughAttack(FrontLineStanceTask):
def have_sufficient_front_line_advantage(self) -> bool:
return self.ground_force_balance >= 2.0
@property
def opposing_garrisons_eliminated(self) -> bool:
# TODO: Should operate on TheaterState to account for BAIs planned this turn.
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 opposing_garrisons_eliminated(self, state: TheaterState) -> bool:
garrisons = state.enemy_garrisons[self.enemy_cp]
return not bool(garrisons.blocking_capture)
def preconditions_met(self, state: TheaterState) -> bool:
if not super().preconditions_met(state):
return False
return self.opposing_garrisons_eliminated
return self.opposing_garrisons_eliminated(state)
def apply_effects(self, state: TheaterState) -> None:
super().apply_effects(state)

View File

@ -15,6 +15,7 @@ from game.theater.theatergroundobject import (
TheaterGroundObject,
NavalGroundObject,
IadsGroundObject,
VehicleGroupGroundObject,
)
from game.threatzones import ThreatZones
from game.transfers import Convoy, CargoShip
@ -41,7 +42,7 @@ class TheaterState(WorldState["TheaterState"]):
enemy_convoys: list[Convoy]
enemy_shipping: list[CargoShip]
enemy_ships: list[NavalGroundObject]
enemy_garrisons: Garrisons
enemy_garrisons: dict[ControlPoint, Garrisons]
oca_targets: list[ControlPoint]
strike_targets: list[TheaterGroundObject[Any]]
enemy_barcaps: list[ControlPoint]
@ -72,6 +73,12 @@ class TheaterState(WorldState["TheaterState"]):
self.enemy_ships.remove(target)
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:
# Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly
# expensive.
@ -89,7 +96,9 @@ class TheaterState(WorldState["TheaterState"]):
enemy_convoys=list(self.enemy_convoys),
enemy_shipping=list(self.enemy_shipping),
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),
strike_targets=list(self.strike_targets),
enemy_barcaps=list(self.enemy_barcaps),
@ -110,6 +119,7 @@ class TheaterState(WorldState["TheaterState"]):
finder = ObjectiveFinder(game, player)
auto_stance = game.settings.automate_front_line_stance
auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
ordered_capturable_points = finder.prioritized_unisolated_points()
return TheaterState(
player=player,
stance_automation_enabled=auto_stance,
@ -126,7 +136,9 @@ class TheaterState(WorldState["TheaterState"]):
enemy_convoys=list(finder.convoys()),
enemy_shipping=list(finder.cargo_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)),
strike_targets=list(finder.strike_targets()),
enemy_barcaps=list(game.theater.control_points_for(not player)),

View File

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