mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
refactor to enum typing and many other fixes fix tests attempt to fix some typescript more typescript fixes more typescript test fixes revert all API changes update to pydcs mypy fixes Use properties to check if player is blue/red/neutral update requirements.txt black -_- bump pydcs and fix mypy add opponent property bump pydcs
382 lines
17 KiB
Python
382 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from game.debriefing import Debriefing
|
|
from game.ground_forces.combat_stance import CombatStance
|
|
from game.theater import ControlPoint
|
|
from .gameupdateevents import GameUpdateEvents
|
|
from ..ato.airtaaskingorder import AirTaskingOrder
|
|
|
|
if TYPE_CHECKING:
|
|
from ..game import Game
|
|
|
|
|
|
MINOR_DEFEAT_INFLUENCE = 0.1
|
|
DEFEAT_INFLUENCE = 0.3
|
|
STRONG_DEFEAT_INFLUENCE = 0.5
|
|
|
|
|
|
class MissionResultsProcessor:
|
|
def __init__(self, game: Game) -> None:
|
|
self.game = game
|
|
|
|
def commit(self, debriefing: Debriefing, events: GameUpdateEvents) -> None:
|
|
logging.info("Committing mission results")
|
|
self.commit_air_losses(debriefing)
|
|
self.commit_pilot_experience()
|
|
self.commit_front_line_losses(debriefing)
|
|
self.commit_convoy_losses(debriefing)
|
|
self.commit_cargo_ship_losses(debriefing)
|
|
self.commit_airlift_losses(debriefing)
|
|
self.commit_ground_losses(debriefing, events)
|
|
self.commit_damaged_runways(debriefing)
|
|
self.commit_captures(debriefing, events)
|
|
self.commit_front_line_battle_impact(debriefing, events)
|
|
self.record_carcasses(debriefing)
|
|
|
|
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
|
for loss in debriefing.air_losses.losses:
|
|
if loss.pilot is not None and (
|
|
not loss.pilot.player
|
|
or not self.game.settings.invulnerable_player_pilots
|
|
):
|
|
loss.pilot.kill()
|
|
squadron = loss.flight.squadron
|
|
aircraft = loss.flight.unit_type
|
|
available = squadron.owned_aircraft
|
|
if available <= 0:
|
|
logging.error(
|
|
f"Found killed {aircraft} from {squadron} but that airbase has "
|
|
"none available."
|
|
)
|
|
continue
|
|
|
|
logging.info(f"{aircraft} destroyed from {squadron}")
|
|
squadron.owned_aircraft -= 1
|
|
|
|
@staticmethod
|
|
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
|
|
for package in ato.packages:
|
|
for flight in package.flights:
|
|
for idx, pilot in enumerate(flight.roster.iter_pilots()):
|
|
if pilot is None:
|
|
logging.error(
|
|
f"Cannot award experience to pilot #{idx} of {flight} "
|
|
"because no pilot is assigned"
|
|
)
|
|
continue
|
|
pilot.record.missions_flown += 1
|
|
|
|
def commit_pilot_experience(self) -> None:
|
|
self._commit_pilot_experience(self.game.blue.ato)
|
|
self._commit_pilot_experience(self.game.red.ato)
|
|
|
|
@staticmethod
|
|
def commit_front_line_losses(debriefing: Debriefing) -> None:
|
|
for loss in debriefing.front_line_losses:
|
|
unit_type = loss.unit_type
|
|
control_point = loss.origin
|
|
available = control_point.base.total_units_of_type(unit_type)
|
|
if available <= 0:
|
|
logging.error(
|
|
f"Found killed {unit_type} from {control_point} but that "
|
|
"airbase has none available."
|
|
)
|
|
continue
|
|
|
|
logging.info(f"{unit_type} destroyed from {control_point}")
|
|
control_point.base.armor[unit_type] -= 1
|
|
|
|
@staticmethod
|
|
def commit_convoy_losses(debriefing: Debriefing) -> None:
|
|
for loss in debriefing.convoy_losses:
|
|
unit_type = loss.unit_type
|
|
convoy = loss.convoy
|
|
available = loss.convoy.units.get(unit_type, 0)
|
|
convoy_name = f"convoy from {convoy.origin} to {convoy.destination}"
|
|
if available <= 0:
|
|
logging.error(
|
|
f"Found killed {unit_type} in {convoy_name} but that convoy has "
|
|
"none available."
|
|
)
|
|
continue
|
|
|
|
logging.info(f"{unit_type} destroyed in {convoy_name}")
|
|
convoy.kill_unit(unit_type)
|
|
|
|
@staticmethod
|
|
def commit_cargo_ship_losses(debriefing: Debriefing) -> None:
|
|
for ship in debriefing.cargo_ship_losses:
|
|
logging.info(
|
|
f"All units destroyed in cargo ship from {ship.origin} to "
|
|
f"{ship.destination}."
|
|
)
|
|
ship.kill_all()
|
|
|
|
@staticmethod
|
|
def commit_airlift_losses(debriefing: Debriefing) -> None:
|
|
for loss in debriefing.airlift_losses:
|
|
transfer = loss.transfer
|
|
airlift_name = f"airlift from {transfer.origin} to {transfer.destination}"
|
|
for unit_type in loss.cargo:
|
|
try:
|
|
transfer.kill_unit(unit_type)
|
|
logging.info(f"{unit_type} destroyed in {airlift_name}")
|
|
except KeyError:
|
|
logging.exception(
|
|
f"Found killed {unit_type} in {airlift_name} but that airlift "
|
|
"has none available."
|
|
)
|
|
|
|
@staticmethod
|
|
def commit_ground_losses(debriefing: Debriefing, events: GameUpdateEvents) -> None:
|
|
for ground_object_loss in debriefing.ground_object_losses:
|
|
ground_object_loss.theater_unit.kill(events)
|
|
for scenery_object_loss in debriefing.scenery_object_losses:
|
|
scenery_object_loss.ground_unit.kill(events)
|
|
|
|
@staticmethod
|
|
def commit_damaged_runways(debriefing: Debriefing) -> None:
|
|
for damaged_runway in debriefing.damaged_runways:
|
|
damaged_runway.damage_runway()
|
|
|
|
def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None:
|
|
for captured in debriefing.base_captures:
|
|
try:
|
|
if captured.captured_by_player.is_blue:
|
|
self.game.message(
|
|
f"{captured.control_point} captured!",
|
|
f"We took control of {captured.control_point}.",
|
|
)
|
|
else:
|
|
self.game.message(
|
|
f"{captured.control_point} lost!",
|
|
f"The enemy took control of {captured.control_point}.",
|
|
)
|
|
|
|
captured.control_point.capture(
|
|
self.game, events, captured.captured_by_player
|
|
)
|
|
except Exception:
|
|
logging.exception(f"Could not process base capture {captured}")
|
|
|
|
for captured in debriefing.base_captures:
|
|
logging.info(f"Will run redeploy for {captured.control_point}")
|
|
self.redeploy_units(captured.control_point)
|
|
|
|
def record_carcasses(self, debriefing: Debriefing) -> None:
|
|
for destroyed_unit in debriefing.state_data.destroyed_statics:
|
|
self.game.add_destroyed_units(destroyed_unit)
|
|
|
|
def commit_front_line_battle_impact(
|
|
self, debriefing: Debriefing, events: GameUpdateEvents
|
|
) -> None:
|
|
for cp in self.game.theater.player_points():
|
|
enemy_cps = [e for e in cp.connected_points if not e.captured]
|
|
for enemy_cp in enemy_cps:
|
|
front_line = cp.front_line_with(enemy_cp)
|
|
front_line.update_position()
|
|
events.update_front_line(front_line)
|
|
|
|
print(
|
|
"Compute frontline progression for : "
|
|
+ cp.name
|
|
+ " to "
|
|
+ enemy_cp.name
|
|
)
|
|
|
|
delta = 0.0
|
|
player_won = True
|
|
status_msg: str = ""
|
|
ally_casualties = debriefing.casualty_count(cp)
|
|
enemy_casualties = debriefing.casualty_count(enemy_cp)
|
|
ally_units_alive = cp.base.total_armor
|
|
enemy_units_alive = enemy_cp.base.total_armor
|
|
|
|
print(f"Remaining allied units: {ally_units_alive}")
|
|
print(f"Remaining enemy units: {enemy_units_alive}")
|
|
print(f"Allied casualties {ally_casualties}")
|
|
print(f"Enemy casualties {enemy_casualties}")
|
|
|
|
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
|
|
|
|
player_aggresive = cp.stances[enemy_cp.id] in [
|
|
CombatStance.AGGRESSIVE,
|
|
CombatStance.ELIMINATION,
|
|
CombatStance.BREAKTHROUGH,
|
|
]
|
|
|
|
if ally_units_alive == 0:
|
|
player_won = False
|
|
delta = STRONG_DEFEAT_INFLUENCE
|
|
status_msg = f"No allied units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces suffer a strong defeat."
|
|
elif enemy_units_alive == 0:
|
|
player_won = True
|
|
delta = STRONG_DEFEAT_INFLUENCE
|
|
status_msg = f"No enemy units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces win a strong victory."
|
|
elif cp.stances[enemy_cp.id] == CombatStance.RETREAT:
|
|
player_won = False
|
|
delta = STRONG_DEFEAT_INFLUENCE
|
|
status_msg = f"Allied forces are retreating along the {cp.name}-{enemy_cp.name} frontline, suffering a strong defeat."
|
|
else:
|
|
if enemy_casualties > ally_casualties:
|
|
player_won = True
|
|
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
|
delta = STRONG_DEFEAT_INFLUENCE
|
|
status_msg = f"Allied forces break through the {cp.name}-{enemy_cp.name} frontline, winning a strong victory"
|
|
else:
|
|
if ratio > 3:
|
|
delta = STRONG_DEFEAT_INFLUENCE
|
|
status_msg = f"Enemy casualties massively outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a strong victory."
|
|
elif ratio < 1.5:
|
|
delta = MINOR_DEFEAT_INFLUENCE
|
|
status_msg = f"Enemy casualties minorly outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a minor victory."
|
|
else:
|
|
delta = DEFEAT_INFLUENCE
|
|
status_msg = f"Enemy casualties outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces claim a victory."
|
|
elif ally_casualties > enemy_casualties:
|
|
if (
|
|
ally_units_alive > 2 * enemy_units_alive
|
|
and player_aggresive
|
|
):
|
|
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
|
|
player_won = True
|
|
delta = MINOR_DEFEAT_INFLUENCE
|
|
status_msg = f"Despite suffering losses, allied forces still outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a minor victory."
|
|
elif (
|
|
ally_units_alive > 3 * enemy_units_alive
|
|
and player_aggresive
|
|
):
|
|
player_won = True
|
|
delta = STRONG_DEFEAT_INFLUENCE
|
|
status_msg = f"Despite suffering losses, allied forces still heavily outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a major victory."
|
|
else:
|
|
# But if the enemy is not outnumbered, we lose
|
|
player_won = False
|
|
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
|
delta = STRONG_DEFEAT_INFLUENCE
|
|
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces have overextended themselves, suffering a major defeat."
|
|
else:
|
|
delta = DEFEAT_INFLUENCE
|
|
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces suffer a defeat."
|
|
|
|
# No progress with defensive strategies
|
|
if player_won and cp.stances[enemy_cp.id] in [
|
|
CombatStance.DEFENSIVE,
|
|
CombatStance.AMBUSH,
|
|
]:
|
|
print(
|
|
f"Allied forces have adopted a defensive stance along the {cp.name}-{enemy_cp.name} "
|
|
f"frontline, making only limited progress."
|
|
)
|
|
delta = MINOR_DEFEAT_INFLUENCE
|
|
|
|
# Handle the case where there are no casualties at all on either side but both sides still have units
|
|
if delta == 0.0:
|
|
print(status_msg)
|
|
self.game.message(
|
|
"Frontline Report",
|
|
f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.",
|
|
)
|
|
else:
|
|
if player_won:
|
|
print(status_msg)
|
|
cp.base.affect_strength(delta)
|
|
enemy_cp.base.affect_strength(-delta)
|
|
self.game.message(
|
|
"Frontline Report",
|
|
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
|
|
)
|
|
else:
|
|
print(status_msg)
|
|
enemy_cp.base.affect_strength(delta)
|
|
cp.base.affect_strength(-delta)
|
|
self.game.message(
|
|
"Frontline Report",
|
|
f"Our ground forces from {cp.name} are losing ground against the enemy forces from "
|
|
f"{enemy_cp.name}. {status_msg}",
|
|
)
|
|
|
|
def redeploy_units(self, cp: ControlPoint) -> None:
|
|
""" "
|
|
Auto redeploy units to newly captured base
|
|
"""
|
|
enemy_connected_cps = [
|
|
ocp for ocp in cp.connected_points if cp.captured != ocp.captured
|
|
]
|
|
|
|
# If the newly captured cp does not have enemy connected cp,
|
|
# then it is not necessary to redeploy frontline units there.
|
|
if len(enemy_connected_cps) == 0:
|
|
return
|
|
|
|
ally_connected_cps = [
|
|
ocp
|
|
for ocp in cp.transitive_connected_friendly_destinations()
|
|
if cp.captured == ocp.captured and ocp.base.total_armor
|
|
]
|
|
|
|
settings = cp.coalition.game.settings
|
|
factor = (
|
|
settings.frontline_reserves_factor
|
|
if cp.captured
|
|
else settings.frontline_reserves_factor_red
|
|
)
|
|
|
|
# From each ally cp, send reinforcements
|
|
for ally_cp in sorted(
|
|
ally_connected_cps,
|
|
key=lambda x: len(
|
|
[cp for cp in x.connected_points if x.captured != cp.captured]
|
|
),
|
|
):
|
|
self.redeploy_between(cp, ally_cp)
|
|
if cp.base.total_armor > factor * cp.deployable_front_line_units:
|
|
break
|
|
|
|
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
|
|
total_units_redeployed = 0
|
|
moved_units = {}
|
|
|
|
settings = source.coalition.game.settings
|
|
reserves = max(
|
|
1,
|
|
(
|
|
settings.reserves_procurement_target
|
|
if source.captured
|
|
else settings.reserves_procurement_target_red
|
|
),
|
|
)
|
|
total_units = source.base.total_armor
|
|
reserves_factor = (reserves - 1) / total_units # slight underestimation
|
|
|
|
source_frontline_count = len(
|
|
[cp for cp in source.connected_points if not source.is_friendly_to(cp)]
|
|
)
|
|
|
|
move_factor = max(0.0, 1 / (source_frontline_count + 1) - reserves_factor)
|
|
|
|
for frontline_unit, count in source.base.armor.items():
|
|
moved_count = int(count * move_factor)
|
|
moved_units[frontline_unit] = moved_count
|
|
total_units_redeployed += moved_count
|
|
|
|
destination.base.commission_units(moved_units)
|
|
source.base.commit_losses(moved_units)
|
|
|
|
# Also transfer pending deliveries.
|
|
for unit_type, count in source.ground_unit_orders.units.items():
|
|
move_count = int(count * move_factor)
|
|
source.ground_unit_orders.sell({unit_type: move_count})
|
|
destination.ground_unit_orders.order({unit_type: move_count})
|
|
total_units_redeployed += move_count
|
|
|
|
if total_units_redeployed > 0:
|
|
self.game.message(
|
|
"Units redeployed",
|
|
f"{total_units_redeployed} units have been redeployed from "
|
|
f"{source.name} to {destination.name}",
|
|
)
|