mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Move transfers one CP per turn.
https://github.com/Khopa/dcs_liberation/issues/824
This commit is contained in:
parent
65f6a4eddd
commit
bd9cbf5e3b
@ -275,7 +275,7 @@ class Game:
|
|||||||
for control_point in self.theater.controlpoints:
|
for control_point in self.theater.controlpoints:
|
||||||
control_point.process_turn(self)
|
control_point.process_turn(self)
|
||||||
|
|
||||||
self.transfers.complete_transfers()
|
self.transfers.perform_transfers()
|
||||||
|
|
||||||
self.process_enemy_income()
|
self.process_enemy_income()
|
||||||
|
|
||||||
|
|||||||
@ -2,4 +2,5 @@ from .base import *
|
|||||||
from .conflicttheater import *
|
from .conflicttheater import *
|
||||||
from .controlpoint import *
|
from .controlpoint import *
|
||||||
from .missiontarget import MissionTarget
|
from .missiontarget import MissionTarget
|
||||||
|
from .supplyroutes import SupplyRoute
|
||||||
from .theatergroundobject import SamGroundObject
|
from .theatergroundobject import SamGroundObject
|
||||||
|
|||||||
@ -1,10 +1,37 @@
|
|||||||
from __future__ import annotations
|
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
|
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:
|
class SupplyRoute:
|
||||||
def __init__(self, control_points: List[ControlPoint]) -> None:
|
def __init__(self, control_points: List[ControlPoint]) -> None:
|
||||||
self.control_points = control_points
|
self.control_points = control_points
|
||||||
@ -21,3 +48,49 @@ class SupplyRoute:
|
|||||||
if not connected_friendly_points:
|
if not connected_friendly_points:
|
||||||
return None
|
return None
|
||||||
return SupplyRoute([control_point] + connected_friendly_points)
|
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
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from typing import Dict, List, Type
|
from typing import Dict, List, Type
|
||||||
|
|
||||||
from dcs.unittype import VehicleType
|
from dcs.unittype import VehicleType
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
|
from game.theater.supplyroutes import SupplyRoute
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -30,6 +31,19 @@ class RoadTransferOrder(TransferOrder):
|
|||||||
#: The units being transferred.
|
#: The units being transferred.
|
||||||
units: Dict[Type[VehicleType], int]
|
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:
|
class PendingTransfers:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@ -50,27 +64,66 @@ class PendingTransfers:
|
|||||||
self.pending_transfers.remove(transfer)
|
self.pending_transfers.remove(transfer)
|
||||||
transfer.origin.base.commision_units(transfer.units)
|
transfer.origin.base.commision_units(transfer.units)
|
||||||
|
|
||||||
def complete_transfers(self) -> None:
|
def perform_transfers(self) -> None:
|
||||||
|
incomplete = []
|
||||||
for transfer in self.pending_transfers:
|
for transfer in self.pending_transfers:
|
||||||
self.complete_transfer(transfer)
|
if not self.perform_transfer(transfer):
|
||||||
self.pending_transfers.clear()
|
incomplete.append(transfer)
|
||||||
|
self.pending_transfers = incomplete
|
||||||
|
|
||||||
@staticmethod
|
def perform_transfer(self, transfer: RoadTransferOrder) -> bool:
|
||||||
def complete_transfer(transfer: RoadTransferOrder) -> None:
|
if transfer.player != transfer.destination.captured:
|
||||||
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(
|
logging.info(
|
||||||
f"Units transferred from {transfer.origin.name} to "
|
f"Units transferred from {transfer.origin.name} to "
|
||||||
f"{transfer.destination.name}"
|
f"{transfer.destination.name}"
|
||||||
)
|
)
|
||||||
transfer.destination.base.commision_units(transfer.units)
|
transfer.destination.base.commision_units(transfer.units)
|
||||||
elif transfer.player == transfer.origin.captured:
|
return True
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
f"{transfer.destination.name} was captured. Transferring units are "
|
f"Units transferring from {transfer.origin.name} to "
|
||||||
f"returning to {transfer.origin.name}"
|
f"{transfer.destination.name} arrived at {next_hop.name}. {len(path) - 1} "
|
||||||
|
"turns remaining."
|
||||||
)
|
)
|
||||||
transfer.origin.base.commision_units(transfer.units)
|
transfer.position = next_hop
|
||||||
else:
|
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(
|
logging.info(
|
||||||
f"Both {transfer.origin.name} and {transfer.destination.name} were "
|
f"Both {transfer.position.name} and {transfer.destination.name} were "
|
||||||
"captured. Units were surrounded and captured during transfer."
|
"captured. Units were surrounded and destroyed during transfer."
|
||||||
)
|
)
|
||||||
|
|||||||
@ -22,6 +22,7 @@ from PySide2.QtWidgets import (
|
|||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from game.theater.supplyroutes import SupplyRoute
|
||||||
from game.transfers import RoadTransferOrder
|
from game.transfers import RoadTransferOrder
|
||||||
from qt_ui.delegate_helpers import painter_context
|
from qt_ui.delegate_helpers import painter_context
|
||||||
from qt_ui.models import GameModel, TransferModel
|
from qt_ui.models import GameModel, TransferModel
|
||||||
@ -50,7 +51,12 @@ class TransferDelegate(QStyledItemDelegate):
|
|||||||
|
|
||||||
def second_row_text(self, index: QModelIndex) -> str:
|
def second_row_text(self, index: QModelIndex) -> str:
|
||||||
transfer = self.transfer(index)
|
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(
|
def paint(
|
||||||
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
|
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user