mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Removing the per-transit type supply routes allows us to find the best route from A to B even if the unit needs to switch transit modes along the way. The "best" route is the one that will generate better gameplay. That is, convoys are preferred to ships (use cases for GMT are rare in DCS), and ships are preferred to airlift (reasons to attack cargo ships are also rare). Avoiding airlift is also a good strategic choice generally since it consumes aircraft that could be performing other missions. The extreme weight against airlift in the pathfinding algorithm could probably be scaled way down so that airlift would be given preference over a very long trip, possibly only for urgent transfers. Later when we add rail that will probably be given the most preference, but possibly between road and shipping. https://github.com/Khopa/dcs_liberation/issues/823
179 lines
6.3 KiB
Python
179 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import heapq
|
|
import math
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum, auto
|
|
from typing import Dict, Iterator, List, Optional, Set, Tuple
|
|
|
|
from game.theater import ConflictTheater
|
|
from game.theater.controlpoint import ControlPoint
|
|
|
|
|
|
class NoPathError(RuntimeError):
|
|
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
|
super().__init__(f"Could not reconstruct path to {destination} from {origin}")
|
|
|
|
|
|
@dataclass(frozen=True, order=True)
|
|
class FrontierNode:
|
|
cost: float
|
|
point: ControlPoint = field(compare=False)
|
|
|
|
|
|
class Frontier:
|
|
def __init__(self) -> None:
|
|
self.nodes: List[FrontierNode] = []
|
|
|
|
def push(self, poly: ControlPoint, cost: float) -> None:
|
|
heapq.heappush(self.nodes, FrontierNode(cost, poly))
|
|
|
|
def pop(self) -> Optional[FrontierNode]:
|
|
try:
|
|
return heapq.heappop(self.nodes)
|
|
except IndexError:
|
|
return None
|
|
|
|
def __bool__(self) -> bool:
|
|
return bool(self.nodes)
|
|
|
|
|
|
class TransitConnection(Enum):
|
|
Road = auto()
|
|
Shipping = auto()
|
|
Airlift = auto()
|
|
|
|
|
|
class TransitNetwork:
|
|
def __init__(self) -> None:
|
|
self.nodes: Dict[
|
|
ControlPoint, Dict[ControlPoint, TransitConnection]
|
|
] = defaultdict(dict)
|
|
|
|
def has_destinations(self, control_point: ControlPoint) -> bool:
|
|
return bool(self.nodes[control_point])
|
|
|
|
def has_link(self, a: ControlPoint, b: ControlPoint) -> bool:
|
|
return b in self.nodes[a]
|
|
|
|
def link_type(self, a: ControlPoint, b: ControlPoint) -> TransitConnection:
|
|
return self.nodes[a][b]
|
|
|
|
def link_with(
|
|
self, a: ControlPoint, b: ControlPoint, link_type: TransitConnection
|
|
) -> None:
|
|
self.nodes[a][b] = link_type
|
|
self.nodes[b][a] = link_type
|
|
|
|
def link_road(self, a: ControlPoint, b: ControlPoint) -> None:
|
|
self.link_with(a, b, TransitConnection.Road)
|
|
|
|
def link_shipping(self, a: ControlPoint, b: ControlPoint) -> None:
|
|
self.link_with(a, b, TransitConnection.Shipping)
|
|
|
|
def link_airport(self, a: ControlPoint, b: ControlPoint) -> None:
|
|
self.link_with(a, b, TransitConnection.Airlift)
|
|
|
|
def connections_from(self, control_point: ControlPoint) -> Iterator[ControlPoint]:
|
|
yield from self.nodes[control_point]
|
|
|
|
def cost(self, a: ControlPoint, b: ControlPoint) -> float:
|
|
return {
|
|
TransitConnection.Road: 1,
|
|
TransitConnection.Shipping: 3,
|
|
# Set arbitrarily high so that other methods are preferred, but still scaled
|
|
# by distance so that when we do need it we still pick the closest airfield.
|
|
# The units of distance are meters so there's no risk of these
|
|
TransitConnection.Airlift: a.position.distance_to_point(b.position),
|
|
}[self.link_type(a, b)]
|
|
|
|
def shortest_path_between(
|
|
self, origin: ControlPoint, destination: ControlPoint
|
|
) -> List[ControlPoint]:
|
|
return self.shortest_path_with_cost(origin, destination)[0]
|
|
|
|
def shortest_path_with_cost(
|
|
self, origin: ControlPoint, destination: ControlPoint
|
|
) -> Tuple[List[ControlPoint], float]:
|
|
if origin not in self.nodes:
|
|
raise ValueError(f"{origin} is not in the transit network.")
|
|
if destination not in self.nodes:
|
|
raise ValueError(f"{destination} is not in the transit network.")
|
|
|
|
frontier = Frontier()
|
|
frontier.push(origin, 0)
|
|
|
|
came_from: Dict[ControlPoint, Optional[ControlPoint]] = {origin: None}
|
|
|
|
best_known: Dict[ControlPoint, float] = defaultdict(lambda: math.inf)
|
|
best_known[origin] = 0.0
|
|
|
|
while (node := frontier.pop()) is not None:
|
|
cost = node.cost
|
|
current = node.point
|
|
if cost > best_known[current]:
|
|
continue
|
|
|
|
for neighbor in self.connections_from(current):
|
|
new_cost = cost + self.cost(node.point, neighbor)
|
|
if new_cost < best_known[neighbor]:
|
|
best_known[neighbor] = new_cost
|
|
frontier.push(neighbor, new_cost)
|
|
came_from[neighbor] = current
|
|
|
|
# Reconstruct and reverse the path.
|
|
current = destination
|
|
path: List[ControlPoint] = []
|
|
while current != origin:
|
|
path.append(current)
|
|
previous = came_from[current]
|
|
if previous is None:
|
|
raise NoPathError(origin, destination)
|
|
current = previous
|
|
path.reverse()
|
|
return path, best_known[destination]
|
|
|
|
|
|
class TransitNetworkBuilder:
|
|
def __init__(self, theater: ConflictTheater, for_player: bool) -> None:
|
|
self.control_points = list(theater.control_points_for(for_player))
|
|
self.network = TransitNetwork()
|
|
self.airports: Set[ControlPoint] = {
|
|
cp
|
|
for cp in self.control_points
|
|
if cp.is_friendly(for_player) and cp.runway_is_operational()
|
|
}
|
|
|
|
def build(self) -> TransitNetwork:
|
|
seen = set()
|
|
for control_point in self.control_points:
|
|
if control_point not in seen:
|
|
seen.add(control_point)
|
|
self.add_transit_links(control_point)
|
|
return self.network
|
|
|
|
def add_transit_links(self, control_point: ControlPoint) -> None:
|
|
# Prefer road connections.
|
|
for road_connection in control_point.connected_points:
|
|
if road_connection.is_friendly_to(control_point):
|
|
self.network.link_road(control_point, road_connection)
|
|
|
|
# Use sea connections if there's no road or rail connection.
|
|
for sea_connection in control_point.shipping_lanes:
|
|
if self.network.has_link(control_point, sea_connection):
|
|
continue
|
|
if sea_connection.is_friendly_to(control_point):
|
|
self.network.link_shipping(control_point, sea_connection)
|
|
|
|
# And use airports as a last resort.
|
|
if control_point in self.airports:
|
|
for airport in self.airports:
|
|
if control_point == airport:
|
|
continue
|
|
if self.network.has_link(control_point, airport):
|
|
continue
|
|
if not airport.is_friendly_to(control_point):
|
|
continue
|
|
self.network.link_airport(control_point, airport)
|