Clean up mission result handling.

This commit is contained in:
Dan Albert 2021-10-22 13:32:58 -07:00
parent 74291271e3
commit b728fcc2d6
11 changed files with 109 additions and 191 deletions

View File

@ -8,6 +8,7 @@ import threading
import time import time
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path
from typing import ( from typing import (
Any, Any,
Callable, Callable,
@ -35,6 +36,7 @@ from game.ato.flight import Flight
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.sim import MissionSimulation
DEBRIEFING_LOG_EXTENSION = "log" DEBRIEFING_LOG_EXTENSION = "log"
@ -361,13 +363,14 @@ class PollDebriefingFileThread(threading.Thread):
regularly for the stopped() condition.""" regularly for the stopped() condition."""
def __init__( def __init__(
self, callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap self,
callback: Callable[[Debriefing], None],
mission_simulation: MissionSimulation,
) -> None: ) -> None:
super().__init__() super().__init__()
self._stop_event = threading.Event() self._stop_event = threading.Event()
self.callback = callback self.callback = callback
self.game = game self.mission_sim = mission_simulation
self.unit_map = unit_map
def stop(self) -> None: def stop(self) -> None:
self._stop_event.set() self._stop_event.set()
@ -386,10 +389,9 @@ class PollDebriefingFileThread(threading.Thread):
os.path.isfile("state.json") os.path.isfile("state.json")
and os.path.getmtime("state.json") > last_modified and os.path.getmtime("state.json") > last_modified
): ):
with open("state.json", "r", encoding="utf-8") as json_file: self.callback(
json_data = json.load(json_file) self.mission_sim.debrief_current_state(Path("state.json"))
debriefing = Debriefing(json_data, self.game, self.unit_map) )
self.callback(debriefing)
break break
except json.JSONDecodeError: except json.JSONDecodeError:
logging.exception( logging.exception(
@ -400,8 +402,8 @@ class PollDebriefingFileThread(threading.Thread):
def wait_for_debriefing( def wait_for_debriefing(
callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap callback: Callable[[Debriefing], None], mission_simulation: MissionSimulation
) -> PollDebriefingFileThread: ) -> PollDebriefingFileThread:
thread = PollDebriefingFileThread(callback, game, unit_map) thread = PollDebriefingFileThread(callback, mission_simulation)
thread.start() thread.start()
return thread return thread

View File

@ -1,2 +0,0 @@
from .event import *
from .frontlineattack import *

View File

@ -1,10 +0,0 @@
from __future__ import annotations
from .event import Event
class AirWarEvent(Event):
"""Event handler for the air battle"""
def __str__(self) -> str:
return "AirWar"

View File

@ -1,12 +0,0 @@
from .event import Event
class FrontlineAttackEvent(Event):
"""
An event centered on a FrontLine Conflict.
Currently the same as its parent, but here for legacy compatibility as well as to allow for
future unique Event handling
"""
def __str__(self) -> str:
return "Frontline attack"

View File

@ -6,9 +6,9 @@ import math
from collections import Iterator from collections import Iterator
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from enum import Enum from enum import Enum
from typing import Any, List, Type, Union, cast, TYPE_CHECKING from typing import Any, List, TYPE_CHECKING, Type, Union, cast
from dcs.countries import Switzerland, UnitedNationsPeacekeepers, USAFAggressors from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers
from dcs.country import Country from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike from dcs.task import CAP, CAS, PinpointStrike
@ -19,19 +19,16 @@ from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from gen import naming from gen import naming
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from .ato.flighttype import FlightType
from gen.ground_forces.ai_ground_planner import GroundPlanner from gen.ground_forces.ai_ground_planner import GroundPlanner
from . import persistency from . import persistency
from .ato.flighttype import FlightType
from .campaignloader import CampaignAirWingConfig from .campaignloader import CampaignAirWingConfig
from .coalition import Coalition from .coalition import Coalition
from .debriefing import Debriefing
from .event.event import Event
from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction from .factions.faction import Faction
from .infos.information import Information from .infos.information import Information
from .profiling import logged_duration from .profiling import logged_duration
from .settings import Settings from .settings import Settings
from .theater import ConflictTheater, ControlPoint from .theater import ConflictTheater
from .theater.bullseye import Bullseye from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .weather import Conditions, TimeOfDay from .weather import Conditions, TimeOfDay
@ -44,7 +41,6 @@ if TYPE_CHECKING:
from .navmesh import NavMesh from .navmesh import NavMesh
from .squadrons import AirWing from .squadrons import AirWing
from .threatzones import ThreatZones from .threatzones import ThreatZones
from .unitmap import UnitMap
COMMISION_UNIT_VARIETY = 4 COMMISION_UNIT_VARIETY = 4
COMMISION_LIMITS_SCALE = 1.5 COMMISION_LIMITS_SCALE = 1.5
@ -99,7 +95,6 @@ class Game:
enemy_budget: float, enemy_budget: float,
) -> None: ) -> None:
self.settings = settings self.settings = settings
self.events: List[Event] = []
self.theater = theater self.theater = theater
self.turn = 0 self.turn = 0
# NB: This is the *start* date. It is never updated. # NB: This is the *start* date. It is never updated.
@ -183,20 +178,6 @@ class Game:
def bullseye_for(self, player: bool) -> Bullseye: def bullseye_for(self, player: bool) -> Bullseye:
return self.coalition_for(player).bullseye return self.coalition_for(player).bullseye
def _generate_player_event(
self, event_class: Type[Event], player_cp: ControlPoint, enemy_cp: ControlPoint
) -> None:
self.events.append(
event_class(
self,
player_cp,
enemy_cp,
enemy_cp.position,
self.blue.faction.name,
self.red.faction.name,
)
)
@property @property
def neutral_country(self) -> Type[Country]: def neutral_country(self) -> Type[Country]:
"""Return the best fitting country that can be used as neutral faction in the generated mission""" """Return the best fitting country that can be used as neutral faction in the generated mission"""
@ -208,14 +189,6 @@ class Game:
else: else:
return USAFAggressors return USAFAggressors
def _generate_events(self) -> None:
for front_line in self.theater.conflicts():
self._generate_player_event(
FrontlineAttackEvent,
front_line.blue_cp,
front_line.red_cp,
)
def coalition_for(self, player: bool) -> Coalition: def coalition_for(self, player: bool) -> Coalition:
if player: if player:
return self.blue return self.blue
@ -224,21 +197,6 @@ class Game:
def adjust_budget(self, amount: float, player: bool) -> None: def adjust_budget(self, amount: float, player: bool) -> None:
self.coalition_for(player).adjust_budget(amount) self.coalition_for(player).adjust_budget(amount)
@staticmethod
def initiate_event(event: Event) -> UnitMap:
# assert event in self.events
logging.info("Generating {} (regular)".format(event))
return event.generate()
def finish_event(self, event: Event, debriefing: Debriefing) -> None:
logging.info("Finishing event {}".format(event))
event.commit(debriefing)
if event in self.events:
self.events.remove(event)
else:
logging.info("finish_event: event not in the events!")
def on_load(self, game_still_initializing: bool = False) -> None: def on_load(self, game_still_initializing: bool = False) -> None:
if not hasattr(self, "name_generator"): if not hasattr(self, "name_generator"):
self.name_generator = naming.namegen self.name_generator = naming.namegen
@ -378,8 +336,6 @@ class Game:
for_red: True if opfor should be re-initialized. for_red: True if opfor should be re-initialized.
for_blue: True if the player coalition should be re-initialized. for_blue: True if the player coalition should be re-initialized.
""" """
self.events = []
self._generate_events()
self.set_bullseye() self.set_bullseye()
# Update statistics # Update statistics

1
game/sim/__init__.py Normal file
View File

@ -0,0 +1 @@
from .missionsimulation import MissionSimulation

View File

@ -1,18 +1,12 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import List, TYPE_CHECKING, Type from typing import TYPE_CHECKING
from dcs.mapping import Point
from dcs.task import Task
from game import persistency
from game.debriefing import Debriefing from game.debriefing import Debriefing
from game.theater import ControlPoint from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from ..ato.airtaaskingorder import AirTaskingOrder from ..ato.airtaaskingorder import AirTaskingOrder
from ..missiongenerator import MissionGenerator
from ..unitmap import UnitMap
if TYPE_CHECKING: if TYPE_CHECKING:
from ..game import Game from ..game import Game
@ -23,44 +17,24 @@ DEFEAT_INFLUENCE = 0.3
STRONG_DEFEAT_INFLUENCE = 0.5 STRONG_DEFEAT_INFLUENCE = 0.5
class Event: class MissionResultsProcessor:
silent = False def __init__(self, game: Game) -> None:
informational = False
game = None # type: Game
location = None # type: Point
from_cp = None # type: ControlPoint
to_cp = None # type: ControlPoint
difficulty = 1 # type: int
def __init__(
self,
game: Game,
from_cp: ControlPoint,
target_cp: ControlPoint,
location: Point,
attacker_name: str,
defender_name: str,
) -> None:
self.game = game self.game = game
self.from_cp = from_cp
self.to_cp = target_cp
self.location = location
self.attacker_name = attacker_name
self.defender_name = defender_name
@property def commit(self, debriefing: Debriefing) -> None:
def is_player_attacking(self) -> bool: logging.info("Committing mission results")
return self.attacker_name == self.game.blue.faction.name self.commit_air_losses(debriefing)
self.commit_pilot_experience()
@property self.commit_front_line_losses(debriefing)
def tasks(self) -> List[Type[Task]]: self.commit_convoy_losses(debriefing)
return [] self.commit_cargo_ship_losses(debriefing)
self.commit_airlift_losses(debriefing)
def generate(self) -> UnitMap: self.commit_ground_object_losses(debriefing)
return MissionGenerator(self.game).generate_miz( self.commit_building_losses(debriefing)
persistency.mission_path_for("liberation_nextturn.miz") self.commit_damaged_runways(debriefing)
) self.commit_captures(debriefing)
self.commit_front_line_battle_impact(debriefing)
self.record_carcasses(debriefing)
def commit_air_losses(self, debriefing: Debriefing) -> None: def commit_air_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.air_losses.losses: for loss in debriefing.air_losses.losses:
@ -200,27 +174,11 @@ class Event:
except Exception: except Exception:
logging.exception(f"Could not process base capture {captured}") logging.exception(f"Could not process base capture {captured}")
def commit(self, debriefing: Debriefing) -> None: def record_carcasses(self, debriefing: Debriefing) -> 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_object_losses(debriefing)
self.commit_building_losses(debriefing)
self.commit_damaged_runways(debriefing)
self.commit_captures(debriefing)
# Destroyed units carcass
# -------------------------
for destroyed_unit in debriefing.state_data.destroyed_statics: for destroyed_unit in debriefing.state_data.destroyed_statics:
self.game.add_destroyed_units(destroyed_unit) self.game.add_destroyed_units(destroyed_unit)
# ----------------------------------- def commit_front_line_battle_impact(self, debriefing: Debriefing) -> None:
# Compute damage to bases
for cp in self.game.theater.player_points(): for cp in self.game.theater.player_points():
enemy_cps = [e for e in cp.connected_points if not e.captured] enemy_cps = [e for e in cp.connected_points if not e.captured]
for enemy_cp in enemy_cps: for enemy_cp in enemy_cps:

View File

@ -0,0 +1,49 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Optional, TYPE_CHECKING
from game.debriefing import Debriefing
from game.missiongenerator import MissionGenerator
from game.sim.missionresultsprocessor import MissionResultsProcessor
from game.unitmap import UnitMap
if TYPE_CHECKING:
from game import Game
class MissionSimulation:
def __init__(self, game: Game) -> None:
self.game = game
self.unit_map: Optional[UnitMap] = None
def generate_miz(self, output: Path) -> None:
self.unit_map = MissionGenerator(self.game).generate_miz(output)
def debrief_current_state(
self, state_path: Path, force_end: bool = False
) -> Debriefing:
if self.unit_map is None:
raise RuntimeError(
"Simulation has no unit map. Results processing began before a mission "
"was generated."
)
with state_path.open("r", encoding="utf-8") as state_file:
data = json.load(state_file)
if force_end:
data["mission_ended"] = True
return Debriefing(data, self.game, self.unit_map)
def process_results(self, debriefing: Debriefing) -> None:
if self.unit_map is None:
raise RuntimeError(
"Simulation has no unit map. Results processing began before a mission "
"was generated."
)
MissionResultsProcessor(self.game).commit(debriefing)
def finish(self) -> None:
self.unit_map = None

View File

@ -10,11 +10,11 @@ from PySide2.QtWidgets import (
) )
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
from game import Game from game import Game, persistency
from game.event.airwar import AirWarEvent
from game.profiling import logged_duration
from game.utils import meters
from game.ato.package import Package from game.ato.package import Package
from game.profiling import logged_duration
from game.sim import MissionSimulation
from game.utils import meters
from gen.flights.traveltime import TotEstimator from gen.flights.traveltime import TotEstimator
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.widgets.QBudgetBox import QBudgetBox
@ -276,18 +276,11 @@ class QTopPanel(QFrame):
if negative_starts: if negative_starts:
if not self.confirm_negative_start_time(negative_starts): if not self.confirm_negative_start_time(negative_starts):
return return
closest_cps = self.game.theater.closest_opposing_control_points()
game_event = AirWarEvent(
self.game,
closest_cps[0],
closest_cps[1],
self.game.theater.controlpoints[0].position,
self.game.blue.faction.name,
self.game.red.faction.name,
)
unit_map = self.game.initiate_event(game_event) sim = MissionSimulation(self.game)
waiting = QWaitingForMissionResultWindow(game_event, self.game, unit_map, self) sim.generate_miz(persistency.mission_path_for("liberation_nextturn.miz"))
waiting = QWaitingForMissionResultWindow(self.game, sim, self)
waiting.exec_() waiting.exec_()
def budget_update(self, game: Game): def budget_update(self, game: Game):

View File

@ -5,7 +5,7 @@ from typing import Optional
from PySide2.QtCore import QObject, Signal from PySide2.QtCore import QObject, Signal
from game import Game from game import Game
from game.event import Debriefing from game.debriefing import Debriefing
class GameUpdateSignal(QObject): class GameUpdateSignal(QObject):

View File

@ -1,12 +1,13 @@
from __future__ import annotations from __future__ import annotations
import json import logging
import os import os
from typing import Sized, Optional from pathlib import Path
from typing import Optional
from PySide2 import QtCore from PySide2 import QtCore
from PySide2.QtCore import QObject, Qt, Signal from PySide2.QtCore import QObject, Signal
from PySide2.QtGui import QIcon, QMovie, QPixmap, QWindow from PySide2.QtGui import QIcon, QMovie, QPixmap
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QDialog, QDialog,
QFileDialog, QFileDialog,
@ -14,18 +15,17 @@ from PySide2.QtWidgets import (
QGroupBox, QGroupBox,
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QMessageBox,
QPushButton, QPushButton,
QTextBrowser, QTextBrowser,
QWidget, QWidget,
) )
from jinja2 import Environment, FileSystemLoader, select_autoescape from jinja2 import Environment, FileSystemLoader, select_autoescape
from game import Game
from game.debriefing import Debriefing, wait_for_debriefing from game.debriefing import Debriefing, wait_for_debriefing
from game.game import Event, Game, logging
from game.persistency import base_path from game.persistency import base_path
from game.profiling import logged_duration from game.profiling import logged_duration
from game.unitmap import UnitMap from game.sim import MissionSimulation
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@ -52,16 +52,14 @@ DebriefingFileWrittenSignal()
class QWaitingForMissionResultWindow(QDialog): class QWaitingForMissionResultWindow(QDialog):
def __init__( def __init__(
self, self,
gameEvent: Event,
game: Game, game: Game,
unit_map: UnitMap, mission_simulation: MissionSimulation,
parent: Optional[QWidget] = None, parent: Optional[QWidget] = None,
) -> None: ) -> None:
super(QWaitingForMissionResultWindow, self).__init__(parent=parent) super(QWaitingForMissionResultWindow, self).__init__(parent=parent)
self.setWindowModality(QtCore.Qt.WindowModal) self.setWindowModality(QtCore.Qt.WindowModal)
self.gameEvent = gameEvent
self.game = game self.game = game
self.unit_map = unit_map self.mission_sim = mission_simulation
self.setWindowTitle("Waiting for mission completion.") self.setWindowTitle("Waiting for mission completion.")
self.setWindowIcon(QIcon("./resources/icon.png")) self.setWindowIcon(QIcon("./resources/icon.png"))
self.setMinimumHeight(570) self.setMinimumHeight(570)
@ -71,9 +69,7 @@ class QWaitingForMissionResultWindow(QDialog):
self.updateLayout self.updateLayout
) )
self.wait_thread = wait_for_debriefing( self.wait_thread = wait_for_debriefing(
lambda debriefing: self.on_debriefing_update(debriefing), lambda debriefing: self.on_debriefing_update(debriefing), self.mission_sim
self.game,
self.unit_map,
) )
def initUi(self): def initUi(self):
@ -209,12 +205,12 @@ class QWaitingForMissionResultWindow(QDialog):
except Exception: except Exception:
logging.exception("Got an error while sending debriefing") logging.exception("Got an error while sending debriefing")
self.wait_thread = wait_for_debriefing( self.wait_thread = wait_for_debriefing(
lambda d: self.on_debriefing_update(d), self.game, self.unit_map lambda d: self.on_debriefing_update(d), self.mission_sim
) )
def process_debriefing(self): def process_debriefing(self):
with logged_duration("Turn processing"): with logged_duration("Turn processing"):
self.game.finish_event(event=self.gameEvent, debriefing=self.debriefing) self.mission_sim.process_results(self.debriefing)
self.game.pass_turn() self.game.pass_turn()
GameUpdateSignal.get_instance().sendDebriefing(self.debriefing) GameUpdateSignal.get_instance().sendDebriefing(self.debriefing)
@ -233,20 +229,7 @@ class QWaitingForMissionResultWindow(QDialog):
file = QFileDialog.getOpenFileName( file = QFileDialog.getOpenFileName(
self, "Select game file to open", filter="json(*.json)", dir="." self, "Select game file to open", filter="json(*.json)", dir="."
) )
print(file) logging.debug("Processing manually submitted %s", file[0])
try: self.on_debriefing_update(
with open(file[0], "r", encoding="utf-8") as json_file: self.mission_sim.debrief_current_state(Path(file[0], force_end=True))
json_data = json.load(json_file) )
json_data["mission_ended"] = True
debriefing = Debriefing(json_data, self.game, self.unit_map)
self.on_debriefing_update(debriefing)
except Exception as e:
logging.error(e)
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText("Invalid file : " + file[0])
msg.setWindowTitle("Invalid file.")
msg.setStandardButtons(QMessageBox.Ok)
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
msg.exec_()
return