Move transfers one CP per turn.

https://github.com/Khopa/dcs_liberation/issues/824
This commit is contained in:
Dan Albert 2021-04-17 19:03:33 -07:00
parent 65f6a4eddd
commit bd9cbf5e3b
5 changed files with 154 additions and 21 deletions

View File

@ -275,7 +275,7 @@ class Game:
for control_point in self.theater.controlpoints:
control_point.process_turn(self)
self.transfers.complete_transfers()
self.transfers.perform_transfers()
self.process_enemy_income()

View File

@ -2,4 +2,5 @@ from .base import *
from .conflicttheater import *
from .controlpoint import *
from .missiontarget import MissionTarget
from .supplyroutes import SupplyRoute
from .theatergroundobject import SamGroundObject

View File

@ -1,10 +1,37 @@
from __future__ import annotations
from typing import Iterator, List, Optional
import heapq
import math
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Dict, Iterator, List, Optional
from game.theater.controlpoint import ControlPoint
@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 SupplyRoute:
def __init__(self, control_points: List[ControlPoint]) -> None:
self.control_points = control_points
@ -21,3 +48,49 @@ class SupplyRoute:
if not connected_friendly_points:
return None
return SupplyRoute([control_point] + connected_friendly_points)
def shortest_path_between(
self, origin: ControlPoint, destination: ControlPoint
) -> List[ControlPoint]:
if origin not in self:
raise ValueError(f"{origin.name} is not in this supply route")
if destination not in self:
raise ValueError(f"{destination.name} is not in this supply route")
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 current.connected_points:
if current.captured != neighbor.captured:
continue
new_cost = cost + 1
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 RuntimeError(
f"Could not reconstruct path to {destination} from {origin}"
)
current = previous
path.reverse()
return path

View File

@ -1,9 +1,10 @@
import logging
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Dict, List, Type
from dcs.unittype import VehicleType
from game.theater import ControlPoint
from game.theater.supplyroutes import SupplyRoute
@dataclass
@ -30,6 +31,19 @@ class RoadTransferOrder(TransferOrder):
#: The units being transferred.
units: Dict[Type[VehicleType], int]
#: The current position of the group being transferred. Groups move one control
#: point a turn through the supply line.
position: ControlPoint = field(init=False)
def __post_init__(self) -> None:
self.position = self.origin
def path(self) -> List[ControlPoint]:
supply_route = SupplyRoute.for_control_point(self.position)
if supply_route is None:
raise RuntimeError(f"Supply route from {self.position.name} interrupted")
return supply_route.shortest_path_between(self.position, self.destination)
class PendingTransfers:
def __init__(self) -> None:
@ -50,27 +64,66 @@ class PendingTransfers:
self.pending_transfers.remove(transfer)
transfer.origin.base.commision_units(transfer.units)
def complete_transfers(self) -> None:
def perform_transfers(self) -> None:
incomplete = []
for transfer in self.pending_transfers:
self.complete_transfer(transfer)
self.pending_transfers.clear()
if not self.perform_transfer(transfer):
incomplete.append(transfer)
self.pending_transfers = incomplete
@staticmethod
def complete_transfer(transfer: RoadTransferOrder) -> None:
if transfer.player == transfer.destination.captured:
def perform_transfer(self, transfer: RoadTransferOrder) -> bool:
if transfer.player != transfer.destination.captured:
logging.info(
f"Transfer destination {transfer.destination.name} was captured."
)
self.handle_route_interrupted(transfer)
return True
supply_route = SupplyRoute.for_control_point(transfer.destination)
if supply_route is None or transfer.position not in supply_route:
logging.info(
f"Route from {transfer.position.name} to {transfer.destination.name} "
"was cut off."
)
self.handle_route_interrupted(transfer)
return True
path = transfer.path()
next_hop = path[0]
if next_hop == transfer.destination:
logging.info(
f"Units transferred from {transfer.origin.name} to "
f"{transfer.destination.name}"
)
transfer.destination.base.commision_units(transfer.units)
elif transfer.player == transfer.origin.captured:
logging.info(
f"{transfer.destination.name} was captured. Transferring units are "
f"returning to {transfer.origin.name}"
)
transfer.origin.base.commision_units(transfer.units)
else:
logging.info(
f"Both {transfer.origin.name} and {transfer.destination.name} were "
"captured. Units were surrounded and captured during transfer."
)
return True
logging.info(
f"Units transferring from {transfer.origin.name} to "
f"{transfer.destination.name} arrived at {next_hop.name}. {len(path) - 1} "
"turns remaining."
)
transfer.position = next_hop
return False
@staticmethod
def handle_route_interrupted(transfer: RoadTransferOrder):
# Halt the transfer in place if safe.
if transfer.player == transfer.position.captured:
logging.info(f"Transferring units are halting at {transfer.position.name}.")
transfer.position.base.commision_units(transfer.units)
return
# If the current position was captured attempt to divert to a neighboring
# friendly CP.
for connected in transfer.position.connected_points:
if connected.captured == transfer.player:
logging.info(f"Transferring units are re-routing to {connected.name}.")
connected.base.commision_units(transfer.units)
return
# If the units are cutoff they are destroyed.
logging.info(
f"Both {transfer.position.name} and {transfer.destination.name} were "
"captured. Units were surrounded and destroyed during transfer."
)

View File

@ -22,6 +22,7 @@ from PySide2.QtWidgets import (
QVBoxLayout,
)
from game.theater.supplyroutes import SupplyRoute
from game.transfers import RoadTransferOrder
from qt_ui.delegate_helpers import painter_context
from qt_ui.models import GameModel, TransferModel
@ -50,7 +51,12 @@ class TransferDelegate(QStyledItemDelegate):
def second_row_text(self, index: QModelIndex) -> str:
transfer = self.transfer(index)
return f"Currently at {transfer.origin}. Arrives at destination in 1 turn."
path = transfer.path()
if len(path) == 1:
turns = "1 turn"
else:
turns = f"{len(path)} turns"
return f"Currently at {transfer.position}. Arrives at destination in {turns}."
def paint(
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex