Skip to content

Commit

Permalink
WIP: Build common interface for waypoint geometry constraints.
Browse files Browse the repository at this point in the history
  • Loading branch information
DanAlbert committed Jul 28, 2023
1 parent 159120b commit 2482c0d
Show file tree
Hide file tree
Showing 9 changed files with 528 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IpZonesLayer } from "./IpZones";
import { JoinZonesLayer } from "./JoinZones";
import { LayersControl } from "react-leaflet";

const ENABLE_EXPENSIVE_DEBUG_TOOLS = false;
const ENABLE_EXPENSIVE_DEBUG_TOOLS = true;

export function WaypointDebugZonesControls() {
const selectedFlightId = useAppSelector(selectSelectedFlightId);
Expand Down
15 changes: 9 additions & 6 deletions game/ato/packagewaypoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from dcs import Point

from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
from game.flightplan import JoinZoneGeometry
from game.flightplan.ipsolver import IpSolver
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
from game.utils import dcs_to_shapely_point

if TYPE_CHECKING:
from game.ato import Package
Expand All @@ -26,11 +28,12 @@ def create(package: Package, coalition: Coalition) -> PackageWaypoints:
origin = package.departure_closest_to_target()

# Start by picking the best IP for the attack.
ingress_point = IpZoneGeometry(
package.target.position,
origin.position,
coalition,
).find_best_ip()
ingress_point = IpSolver(
dcs_to_shapely_point(package.target.position),
dcs_to_shapely_point(origin.position),
coalition.doctrine,
coalition.threat_zone.all,
).solve()

join_point = JoinZoneGeometry(
package.target.position,
Expand Down
78 changes: 78 additions & 0 deletions game/flightplan/ipsolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from shapely.geometry import MultiPolygon, Point

from game.data.doctrine import Doctrine
from game.flightplan.waypointsolver import WaypointSolver
from game.flightplan.waypointstrategy import WaypointStrategy
from game.utils import meters, nautical_miles


class ThreatTolerantIpStrategy(WaypointStrategy):
def __init__(
self,
departure: Point,
target: Point,
threat_zones: MultiPolygon,
) -> None:
super().__init__(threat_zones)
self.prerequisite(target).min_distance_from(departure, nautical_miles(10))
self.require().at_least(nautical_miles(5)).away_from(departure)
self.require().at_most(meters(departure.distance(target))).away_from(departure)
self.require().at_least(nautical_miles(10)).away_from(target)
max_ip_range = min(nautical_miles(45), departure.distance(target))
self.require().at_most(max_ip_range).away_from(target)
self.threat_tolerance(target, max_ip_range, nautical_miles(5))
self.nearest(departure)


class UnsafeIpStrategy(WaypointStrategy):
def __init__(
self,
departure: Point,
target: Point,
threat_zones: MultiPolygon,
) -> None:
super().__init__(threat_zones)
self.prerequisite(target).min_distance_from(departure, nautical_miles(10))
self.require().at_least(nautical_miles(5)).away_from(departure)
self.require().at_most(meters(departure.distance(target))).away_from(departure)
self.require().at_least(nautical_miles(10)).away_from(target)
max_ip_range = min(nautical_miles(45), departure.distance(target))
self.require().at_most(max_ip_range).away_from(target)
self.nearest(departure)


class SafeIpStrategy(WaypointStrategy):
def __init__(
self,
departure: Point,
target: Point,
doctrine: Doctrine,
threat_zones: MultiPolygon,
) -> None:
super().__init__(threat_zones)
self.prerequisite(departure).safe()
self.prerequisite(target).min_distance_from(
departure, doctrine.min_ingress_distance
)
self.require().at_least(nautical_miles(5)).away_from(departure)
self.require().at_most(meters(departure.distance(target))).away_from(departure)
self.require().at_least(doctrine.min_ingress_distance).away_from(target)
self.require().at_most(
min(doctrine.max_ingress_distance, meters(departure.distance(target)))
).away_from(target)
self.require().safe()
self.nearest(departure)


class IpSolver(WaypointSolver):
def __init__(
self,
departure: Point,
target: Point,
doctrine: Doctrine,
threat_zones: MultiPolygon,
) -> None:
super().__init__()
self.add_strategy(SafeIpStrategy(departure, target, doctrine, threat_zones))
self.add_strategy(ThreatTolerantIpStrategy(departure, target, threat_zones))
self.add_strategy(UnsafeIpStrategy(departure, target, threat_zones))
165 changes: 0 additions & 165 deletions game/flightplan/ipzonegeometry.py

This file was deleted.

2 changes: 2 additions & 0 deletions game/flightplan/joinzonegeometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def __init__(
self.preferred_lines = preferred_lines

def find_best_join_point(self) -> Point:
# TODO: afaict the permissible_lines case is entirely unnecessary. The two
# definitions appear equivalent.
if self.preferred_lines.is_empty:
join, _ = shapely.ops.nearest_points(self.permissible_zones, self.ip)
else:
Expand Down
42 changes: 42 additions & 0 deletions game/flightplan/waypointsolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from dcs import Point

if TYPE_CHECKING:
from .waypointstrategy import WaypointStrategy


class WaypointSolver:
def __init__(self) -> None:
self.strategies: list[WaypointStrategy] = []
self.debug_output_directory: Path | None = None

def add_strategy(self, strategy: WaypointStrategy) -> None:
self.strategies.append(strategy)

def set_debug_output_directory(self, path: Path) -> None:
self.debug_output_directory = path

def dump_debug_info(self) -> None:
path = self.debug_output_directory
if path is None:
return

def solve(self) -> Point:
if not self.strategies:
raise ValueError(
"WaypointSolver.solve() called before any strategies were added"
)

for strategy in self.strategies:
if (point := strategy.find()) is not None:
return point

self.dump_debug_info()
debug_details = "No debug output directory set"
if (debug_path := self.debug_output_directory) is not None:
debug_details = f"Debug details written to {debug_path}"
raise RuntimeError(f"No solutions found for waypoint. {debug_details}")
Loading

0 comments on commit 2482c0d

Please sign in to comment.