Factor out Coalition from Game.

This commit is contained in:
Dan Albert
2021-07-12 16:10:12 -07:00
parent 4534758c21
commit 17c19d453b
34 changed files with 471 additions and 437 deletions

215
game/coalition.py Normal file
View File

@@ -0,0 +1,215 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional
from dcs import Point
from faker import Faker
from game.commander import TheaterCommander
from game.income import Income
from game.navmesh import NavMesh
from game.profiling import logged_duration, MultiEventTracer
from game.threatzones import ThreatZones
from game.transfers import PendingTransfers
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
if TYPE_CHECKING:
from game import Game
from game.data.doctrine import Doctrine
from game.factions.faction import Faction
from game.procurement import AircraftProcurementRequest, ProcurementAi
from game.squadrons import AirWing
from game.theater.bullseye import Bullseye
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from gen import AirTaskingOrder
class Coalition:
def __init__(
self, game: Game, faction: Faction, budget: float, player: bool
) -> None:
self.game = game
self.player = player
self.faction = faction
self.budget = budget
self.ato = AirTaskingOrder()
self.transit_network = TransitNetwork()
self.procurement_requests: list[AircraftProcurementRequest] = []
self.bullseye = Bullseye(Point(0, 0))
self.faker = Faker(self.faction.locales)
self.air_wing = AirWing(game, self)
self.transfers = PendingTransfers(game, player)
# Late initialized because the two coalitions in the game are mutually
# dependent, so must be both constructed before this property can be set.
self._opponent: Optional[Coalition] = None
# Volatile properties that are not persisted to the save file since they can be
# recomputed on load. Keeping this data out of the save file makes save compat
# breaks less frequent. Each of these properties has a non-underscore-prefixed
# @property that should be used for non-Optional access.
#
# All of these are late-initialized (whether via on_load or called later), but
# will be non-None after the game has finished loading.
self._threat_zone: Optional[ThreatZones] = None
self._navmesh: Optional[NavMesh] = None
self.on_load()
@property
def doctrine(self) -> Doctrine:
return self.faction.doctrine
@property
def coalition_id(self) -> int:
if self.player:
return 2
return 1
@property
def country_name(self) -> str:
return self.faction.country
@property
def opponent(self) -> Coalition:
assert self._opponent is not None
return self._opponent
@property
def threat_zone(self) -> ThreatZones:
assert self._threat_zone is not None
return self._threat_zone
@property
def nav_mesh(self) -> NavMesh:
assert self._navmesh is not None
return self._navmesh
def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically
# recomputed on load for the sake of save compatibility.
del state["_threat_zone"]
del state["_navmesh"]
del state["faker"]
return state
def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state)
# Regenerate any state that was not persisted.
self.on_load()
def on_load(self) -> None:
self.faker = Faker(self.faction.locales)
def set_opponent(self, opponent: Coalition) -> None:
if self._opponent is not None:
raise RuntimeError("Double-initialization of Coalition.opponent")
self._opponent = opponent
def adjust_budget(self, amount: float) -> None:
self.budget += amount
def compute_threat_zones(self) -> None:
self._threat_zone = ThreatZones.for_faction(self.game, self.player)
def compute_nav_meshes(self) -> None:
self._navmesh = NavMesh.from_threat_zones(
self.opponent.threat_zone, self.game.theater
)
def update_transit_network(self) -> None:
self.transit_network = TransitNetworkBuilder(
self.game.theater, self.player
).build()
def set_bullseye(self, bullseye: Bullseye) -> None:
self.bullseye = bullseye
def end_turn(self) -> None:
"""Processes coalition-specific turn finalization.
For more information on turn finalization in general, see the documentation for
`Game.finish_turn`.
"""
self.air_wing.replenish()
self.budget += Income(self.game, self.player).total
# Need to recompute before transfers and deliveries to account for captures.
# This happens in in initialize_turn as well, because cheating doesn't advance a
# turn but can capture bases so we need to recompute there as well.
self.update_transit_network()
# Must happen *before* unit deliveries are handled, or else new units will spawn
# one hop ahead. ControlPoint.process_turn handles unit deliveries. The
# coalition-specific turn-end happens before the theater-wide turn-end, so this
# is handled correctly.
self.transfers.perform_transfers()
def initialize_turn(self) -> None:
"""Processes coalition-specific turn initialization.
For more information on turn initialization in general, see the documentation
for `Game.initialize_turn`.
"""
# Needs to happen *before* planning transfers so we don't cancel them.
self.ato.clear()
self.air_wing.reset()
self.refund_outstanding_orders()
self.procurement_requests.clear()
with logged_duration("Transit network identification"):
self.update_transit_network()
with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets()
with logged_duration("Transport planning"):
self.transfers.plan_transports()
self.plan_missions()
self.plan_procurement()
def refund_outstanding_orders(self) -> None:
# TODO: Split orders between air and ground units.
# This isn't quite right. If the player has ground purchases automated we should
# be refunding the ground units, and if they have air automated but not ground
# we should be refunding air units.
if self.player and not self.game.settings.automate_aircraft_reinforcements:
return
for cp in self.game.theater.control_points_for(self.player):
cp.pending_unit_deliveries.refund_all(self)
def plan_missions(self) -> None:
color = "Blue" if self.player else "Red"
with MultiEventTracer() as tracer:
mission_planner = CoalitionMissionPlanner(self.game, self.player)
with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"):
commander = TheaterCommander(self.game, self.player)
commander.plan_missions(mission_planner, tracer)
with tracer.trace(f"{color} mission fulfillment"):
mission_planner.fulfill_missions()
def plan_procurement(self) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much
# more of the budget that turn. Otherwise budget (after repairs) is split evenly
# between air and ground. For the default starting budget of 2000 this gives 600
# to ground forces and 1400 to aircraft. After that the budget will be spent
# proportionally based on how much is already invested.
if self.player:
manage_runways = self.game.settings.automate_runway_repair
manage_front_line = self.game.settings.automate_front_line_reinforcements
manage_aircraft = self.game.settings.automate_aircraft_reinforcements
else:
manage_runways = False
manage_front_line = False
manage_aircraft = False
self.budget = ProcurementAi(
self.game,
self.player,
self.faction,
manage_runways,
manage_front_line,
manage_aircraft,
).spend_budget(self.budget)

View File

@@ -14,9 +14,9 @@ from game.theater import (
Airfield,
)
from game.theater.theatergroundobject import (
NavalGroundObject,
BuildingGroundObject,
IadsGroundObject,
NavalGroundObject,
)
from game.transfers import CargoShip, Convoy
from game.utils import meters, nautical_miles
@@ -163,13 +163,17 @@ class ObjectiveFinder:
def convoys(self) -> Iterator[Convoy]:
for front_line in self.front_lines():
yield from self.game.transfers.convoys.travelling_to(
yield from self.game.coalition_for(
self.is_player
).transfers.convoys.travelling_to(
front_line.control_point_hostile_to(self.is_player)
)
def cargo_ships(self) -> Iterator[CargoShip]:
for front_line in self.front_lines():
yield from self.game.transfers.cargo_ships.travelling_to(
yield from self.game.coalition_for(
self.is_player
).transfers.cargo_ships.travelling_to(
front_line.control_point_hostile_to(self.is_player)
)

View File

@@ -6,7 +6,7 @@ from game.commander.missionproposals import EscortType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.data.doctrine import Doctrine
from game.theater import NavalGroundObject
from game.theater.theatergroundobject import NavalGroundObject
from gen.flights.flight import FlightType

View File

@@ -18,7 +18,6 @@ from typing import (
Union,
)
from game import db
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater import Airfield, ControlPoint
@@ -136,10 +135,8 @@ class Debriefing:
self.game = game
self.unit_map = unit_map
self.player_country = game.player_country
self.enemy_country = game.enemy_country
self.player_country_id = db.country_id_from_name(game.player_country)
self.enemy_country_id = db.country_id_from_name(game.enemy_country)
self.player_country = game.blue.country_name
self.enemy_country = game.red.country_name
self.air_losses = self.dead_aircraft()
self.ground_losses = self.dead_ground_units()

View File

@@ -53,7 +53,7 @@ class Event:
@property
def is_player_attacking(self) -> bool:
return self.attacker_name == self.game.player_faction.name
return self.attacker_name == self.game.blue.faction.name
@property
def tasks(self) -> List[Type[Task]]:
@@ -114,10 +114,10 @@ class Event:
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
self._transfer_aircraft(
self.game.blue_ato, debriefing.air_losses, for_player=True
self.game.blue.ato, debriefing.air_losses, for_player=True
)
self._transfer_aircraft(
self.game.red_ato, debriefing.air_losses, for_player=False
self.game.red.ato, debriefing.air_losses, for_player=False
)
def commit_air_losses(self, debriefing: Debriefing) -> None:
@@ -154,8 +154,8 @@ class Event:
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)
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:

View File

@@ -1,13 +1,11 @@
import itertools
import logging
import math
import random
import sys
from collections import Iterator
from datetime import date, datetime, timedelta
from enum import Enum
from typing import Any, List, Type, Union, cast
from dcs.action import Coalition
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
@@ -19,28 +17,25 @@ from game.plugins import LuaPluginManager
from gen import naming
from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner import GroundPlanner
from . import persistency
from .commander import TheaterCommander
from .coalition import Coalition
from .debriefing import Debriefing
from .event.event import Event
from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .income import Income
from .infos.information import Information
from .navmesh import NavMesh
from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration, MultiEventTracer
from .settings import Settings, AutoAtoBehavior
from .procurement import AircraftProcurementRequest
from .profiling import logged_duration
from .settings import Settings
from .squadrons import AirWing
from .theater import ConflictTheater, ControlPoint
from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .threatzones import ThreatZones
from .transfers import PendingTransfers
from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay
@@ -98,10 +93,6 @@ class Game:
self.settings = settings
self.events: List[Event] = []
self.theater = theater
self.player_faction = player_faction
self.player_country = player_faction.country
self.enemy_faction = enemy_faction
self.enemy_country = enemy_faction.country
# pass_turn() will be called when initialization is complete which will
# increment this to turn 0 before it reaches the player.
self.turn = -1
@@ -124,108 +115,70 @@ class Game:
self.conditions = self.generate_conditions()
self.blue_transit_network = TransitNetwork()
self.red_transit_network = TransitNetwork()
self.blue_procurement_requests: List[AircraftProcurementRequest] = []
self.red_procurement_requests: List[AircraftProcurementRequest] = []
self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder()
self.blue_bullseye = Bullseye(Point(0, 0))
self.red_bullseye = Bullseye(Point(0, 0))
self.sanitize_sides(player_faction, enemy_faction)
self.blue = Coalition(self, player_faction, player_budget, player=True)
self.red = Coalition(self, enemy_faction, enemy_budget, player=False)
self.blue.set_opponent(self.red)
self.red.set_opponent(self.blue)
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.transfers = PendingTransfers(self)
self.sanitize_sides()
self.blue_faker = Faker(self.player_faction.locales)
self.red_faker = Faker(self.enemy_faction.locales)
self.blue_air_wing = AirWing(self, player=True)
self.red_air_wing = AirWing(self, player=False)
self.on_load(game_still_initializing=True)
def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically
# recomputed on load for the sake of save compatibility.
del state["blue_threat_zone"]
del state["red_threat_zone"]
del state["blue_navmesh"]
del state["red_navmesh"]
del state["blue_faker"]
del state["red_faker"]
return state
def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state)
# Regenerate any state that was not persisted.
self.on_load()
@property
def coalitions(self) -> Iterator[Coalition]:
yield self.blue
yield self.red
def ato_for(self, player: bool) -> AirTaskingOrder:
if player:
return self.blue_ato
return self.red_ato
return self.coalition_for(player).ato
def procurement_requests_for(
self, player: bool
) -> List[AircraftProcurementRequest]:
if player:
return self.blue_procurement_requests
return self.red_procurement_requests
) -> list[AircraftProcurementRequest]:
return self.coalition_for(player).procurement_requests
def transit_network_for(self, player: bool) -> TransitNetwork:
if player:
return self.blue_transit_network
return self.red_transit_network
return self.coalition_for(player).transit_network
def generate_conditions(self) -> Conditions:
return Conditions.generate(
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
)
def sanitize_sides(self) -> None:
@staticmethod
def sanitize_sides(player_faction: Faction, enemy_faction: Faction) -> None:
"""
Make sure the opposing factions are using different countries
:return:
"""
if self.player_country == self.enemy_country:
if self.player_country == "USA":
self.enemy_country = "USAF Aggressors"
elif self.player_country == "Russia":
self.enemy_country = "USSR"
if player_faction.country == enemy_faction.country:
if player_faction.country == "USA":
enemy_faction.country = "USAF Aggressors"
elif player_faction.country == "Russia":
enemy_faction.country = "USSR"
else:
self.enemy_country = "Russia"
enemy_faction.country = "Russia"
def faction_for(self, player: bool) -> Faction:
if player:
return self.player_faction
return self.enemy_faction
return self.coalition_for(player).faction
def faker_for(self, player: bool) -> Faker:
if player:
return self.blue_faker
return self.red_faker
return self.coalition_for(player).faker
def air_wing_for(self, player: bool) -> AirWing:
if player:
return self.blue_air_wing
return self.red_air_wing
return self.coalition_for(player).air_wing
def country_for(self, player: bool) -> str:
if player:
return self.player_country
return self.enemy_country
return self.coalition_for(player).country_name
def bullseye_for(self, player: bool) -> Bullseye:
if player:
return self.blue_bullseye
return self.red_bullseye
return self.coalition_for(player).bullseye
def _generate_player_event(
self, event_class: Type[Event], player_cp: ControlPoint, enemy_cp: ControlPoint
@@ -236,8 +189,8 @@ class Game:
player_cp,
enemy_cp,
enemy_cp.position,
self.player_faction.name,
self.enemy_faction.name,
self.blue.faction.name,
self.red.faction.name,
)
)
@@ -249,20 +202,13 @@ class Game:
front_line.red_cp,
)
def adjust_budget(self, amount: float, player: bool) -> None:
def coalition_for(self, player: bool) -> Coalition:
if player:
self.budget += amount
else:
self.enemy_budget += amount
return self.blue
return self.red
def process_player_income(self) -> None:
self.budget += Income(self, player=True).total
def process_enemy_income(self) -> None:
# TODO: Clean up save compat.
if not hasattr(self, "enemy_budget"):
self.enemy_budget = 0
self.enemy_budget += Income(self, player=False).total
def adjust_budget(self, amount: float, player: bool) -> None:
self.coalition_for(player).adjust_budget(amount)
@staticmethod
def initiate_event(event: Event) -> UnitMap:
@@ -293,12 +239,6 @@ class Game:
self.compute_conflicts_position()
if not game_still_initializing:
self.compute_threat_zones()
self.blue_faker = Faker(self.faction_for(player=True).locales)
self.red_faker = Faker(self.faction_for(player=False).locales)
def reset_ato(self) -> None:
self.blue_ato.clear()
self.red_ato.clear()
def finish_turn(self, skipped: bool = False) -> None:
"""Finalizes the current turn and advances to the next turn.
@@ -333,23 +273,16 @@ class Game:
)
self.turn += 1
# Need to recompute before transfers and deliveries to account for captures.
# This happens in in initialize_turn as well, because cheating doesn't advance a
# turn but can capture bases so we need to recompute there as well.
self.compute_transit_networks()
# The coalition-specific turn finalization *must* happen before unit deliveries,
# since the coalition-specific finalization handles transit network updates and
# transfer processing. If in the other order, units may be delivered to captured
# bases, and freshly delivered units will spawn one leg through their journey.
self.blue.end_turn()
self.red.end_turn()
# Must happen *before* unit deliveries are handled, or else new units will spawn
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
self.transfers.perform_transfers()
# Needs to happen *before* planning transfers so we don't cancel them.
self.reset_ato()
for control_point in self.theater.controlpoints:
control_point.process_turn(self)
self.blue_air_wing.replenish()
self.red_air_wing.replenish()
if not skipped:
for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
@@ -360,9 +293,6 @@ class Game:
self.conditions = self.generate_conditions()
self.process_enemy_income()
self.process_player_income()
def begin_turn_0(self) -> None:
"""Initialization for the first turn of the game."""
self.turn = 0
@@ -402,8 +332,8 @@ class Game:
def set_bullseye(self) -> None:
player_cp, enemy_cp = self.theater.closest_opposing_control_points()
self.blue_bullseye = Bullseye(enemy_cp.position)
self.red_bullseye = Bullseye(player_cp.position)
self.blue.bullseye = Bullseye(enemy_cp.position)
self.red.bullseye = Bullseye(player_cp.position)
def initialize_turn(self, for_red: bool = True, for_blue: bool = True) -> None:
"""Performs turn initialization for the specified players.
@@ -451,13 +381,20 @@ class Game:
if turn_state in (TurnState.LOSS, TurnState.WIN):
return self.process_win_loss(turn_state)
# Plan flights & combat for next turn
with logged_duration("Computing conflict positions"):
self.compute_conflicts_position()
with logged_duration("Threat zone computation"):
self.compute_threat_zones()
# Plan Coalition specific turn
if for_red:
self.initialize_turn_for(player=False)
if for_blue:
self.initialize_turn_for(player=True)
if for_red:
self.initialize_turn_for(player=False)
# Plan GroundWar
self.ground_planners = {}
for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
@@ -465,83 +402,10 @@ class Game:
self.ground_planners[cp.id] = gplanner
def initialize_turn_for(self, player: bool) -> None:
"""Processes coalition-specific turn initialization.
For more information on turn initialization in general, see the documentation
for `Game.initialize_turn`.
Args:
player: True if the player coalition is being initialized. False for opfor
initialization.
"""
self.ato_for(player).clear()
self.air_wing_for(player).reset()
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
self.aircraft_inventory.reset(player)
for cp in self.theater.control_points_for(player):
self.aircraft_inventory.set_from_control_point(cp)
# Refund all pending deliveries for opfor and if player
# has automate_aircraft_reinforcements
if (not player and not cp.captured) or (
player
and cp.captured
and self.settings.automate_aircraft_reinforcements
):
cp.pending_unit_deliveries.refund_all(self)
# Plan flights & combat for next turn
with logged_duration("Computing conflict positions"):
self.compute_conflicts_position()
with logged_duration("Threat zone computation"):
self.compute_threat_zones()
with logged_duration("Transit network identification"):
self.compute_transit_networks()
self.ground_planners = {}
self.procurement_requests_for(player).clear()
with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets()
with logged_duration("Transport planning"):
self.transfers.plan_transports()
color = "Blue" if player else "Red"
with MultiEventTracer() as tracer:
mission_planner = CoalitionMissionPlanner(self, player)
with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"):
commander = TheaterCommander(self, player)
commander.plan_missions(mission_planner, tracer)
with tracer.trace(f"{color} mission fulfillment"):
mission_planner.fulfill_missions()
self.plan_procurement_for(player)
def plan_procurement_for(self, for_player: bool) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
# gets much more of the budget that turn. Otherwise budget (after
# repairs) is split evenly between air and ground. For the default
# starting budget of 2000 this gives 600 to ground forces and 1400 to
# aircraft. After that the budget will be spend proportionally based on how much is already invested
if for_player:
self.budget = ProcurementAi(
self,
for_player=True,
faction=self.player_faction,
manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements,
).spend_budget(self.budget)
else:
self.enemy_budget = ProcurementAi(
self,
for_player=False,
faction=self.enemy_faction,
manage_runways=True,
manage_front_line=True,
manage_aircraft=True,
).spend_budget(self.enemy_budget)
self.coalition_for(player).initialize_turn()
def message(self, text: str) -> None:
self.informations.append(Information(text, turn=self.turn))
@@ -568,32 +432,20 @@ class Game:
self.current_group_id += 1
return self.current_group_id
def compute_transit_networks(self) -> None:
self.blue_transit_network = self.compute_transit_network_for(player=True)
self.red_transit_network = self.compute_transit_network_for(player=False)
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
return TransitNetworkBuilder(self.theater, player).build()
def compute_threat_zones(self) -> None:
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
self.blue_navmesh = NavMesh.from_threat_zones(
self.red_threat_zone, self.theater
)
self.red_navmesh = NavMesh.from_threat_zones(
self.blue_threat_zone, self.theater
)
self.blue.compute_threat_zones()
self.red.compute_threat_zones()
self.blue.compute_nav_meshes()
self.red.compute_nav_meshes()
def threat_zone_for(self, player: bool) -> ThreatZones:
if player:
return self.blue_threat_zone
return self.red_threat_zone
return self.coalition_for(player).threat_zone
def navmesh_for(self, player: bool) -> NavMesh:
if player:
return self.blue_navmesh
return self.red_navmesh
return self.coalition_for(player).nav_mesh
def compute_conflicts_position(self) -> None:
"""
@@ -636,7 +488,7 @@ class Game:
if cpoint is not None:
zones.append(cpoint)
packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages)
packages = itertools.chain(self.blue.ato.packages, self.red.ato.packages)
for package in packages:
if package.primary_task is FlightType.BARCAP:
# BARCAPs will be planned at most locations on smaller theaters,
@@ -682,25 +534,6 @@ class Game:
"""
return self.__culling_zones
# 1 = red, 2 = blue
def get_player_coalition_id(self) -> int:
return 2
def get_enemy_coalition_id(self) -> int:
return 1
def get_player_coalition(self) -> Coalition:
return Coalition.Blue
def get_enemy_coalition(self) -> Coalition:
return Coalition.Red
def get_player_color(self) -> str:
return "blue"
def get_enemy_color(self) -> str:
return "red"
def process_win_loss(self, turn_state: TurnState) -> None:
if turn_state is TurnState.WIN:
self.message(

View File

@@ -2,9 +2,7 @@
from __future__ import annotations
from collections import defaultdict
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING
from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import Flight
@@ -86,10 +84,11 @@ class GlobalAircraftInventory:
cp: ControlPointAircraftInventory(cp) for cp in control_points
}
def reset(self) -> None:
"""Clears all control points and their inventories."""
def reset(self, for_player: bool) -> None:
"""Clears the inventory of every control point owned by the given coalition."""
for inventory in self.inventories.values():
inventory.clear()
if inventory.control_point.captured == for_player:
inventory.clear()
def set_from_control_point(self, control_point: ControlPoint) -> None:
"""Set the control point's aircraft inventory.

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import Iterable, List, Set, TYPE_CHECKING, cast
from typing import List, Set, TYPE_CHECKING, cast
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
@@ -81,10 +81,10 @@ class Operation:
return Conflict(
cls.game.theater,
FrontLine(player_cp, enemy_cp),
cls.game.player_faction.name,
cls.game.enemy_faction.name,
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
cls.game.blue.faction.name,
cls.game.red.faction.name,
cls.current_mission.country(cls.game.blue.country_name),
cls.current_mission.country(cls.game.red.country_name),
mid_point,
)
@@ -95,14 +95,14 @@ class Operation:
@classmethod
def _setup_mission_coalitions(cls) -> None:
cls.current_mission.coalition["blue"] = Coalition(
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
"blue", bullseye=cls.game.blue.bullseye.to_pydcs()
)
cls.current_mission.coalition["red"] = Coalition(
"red", bullseye=cls.game.red_bullseye.to_pydcs()
"red", bullseye=cls.game.red.bullseye.to_pydcs()
)
p_country = cls.game.player_country
e_country = cls.game.enemy_country
p_country = cls.game.blue.country_name
e_country = cls.game.red.country_name
cls.current_mission.coalition["blue"].add_country(
country_dict[db.country_id_from_name(p_country)]()
)
@@ -268,7 +268,7 @@ class Operation:
and cls.game.settings.perf_destroyed_units
):
cls.current_mission.static_group(
country=cls.current_mission.country(cls.game.player_country),
country=cls.current_mission.country(cls.game.blue.country_name),
name="",
_type=utype,
hidden=True,
@@ -358,18 +358,18 @@ class Operation:
cls.airgen.clear_parking_slots()
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.player_country),
cls.game.blue_ato,
cls.current_mission.country(cls.game.blue.country_name),
cls.game.blue.ato,
cls.groundobjectgen.runways,
)
cls.airgen.generate_flights(
cls.current_mission.country(cls.game.enemy_country),
cls.game.red_ato,
cls.current_mission.country(cls.game.red.country_name),
cls.game.red.ato,
cls.groundobjectgen.runways,
)
cls.airgen.spawn_unused_aircraft(
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
cls.current_mission.country(cls.game.blue.country_name),
cls.current_mission.country(cls.game.red.country_name),
)
@classmethod
@@ -380,10 +380,10 @@ class Operation:
player_cp = front_line.blue_cp
enemy_cp = front_line.red_cp
conflict = Conflict.frontline_cas_conflict(
cls.game.player_faction.name,
cls.game.enemy_faction.name,
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
cls.game.blue.faction.name,
cls.game.red.faction.name,
cls.current_mission.country(cls.game.blue.country_name),
cls.current_mission.country(cls.game.red.country_name),
front_line,
cls.game.theater,
)

View File

@@ -72,7 +72,9 @@ class ProcurementAi:
return 1
for cp in self.owned_points:
cp_ground_units = cp.allocated_ground_units(self.game.transfers)
cp_ground_units = cp.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers
)
armor_investment += cp_ground_units.total_value
cp_aircraft = cp.allocated_aircraft(self.game)
aircraft_investment += cp_aircraft.total_value
@@ -316,7 +318,9 @@ class ProcurementAi:
continue
purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
allocated = cp.allocated_ground_units(self.game.transfers)
allocated = cp.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers
)
if allocated.total >= purchase_target:
# Control point is already sufficiently defended.
continue
@@ -343,7 +347,9 @@ class ProcurementAi:
if not cp.can_recruit_ground_units(self.game):
continue
allocated = cp.allocated_ground_units(self.game.transfers)
allocated = cp.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers
)
if allocated.total >= self.game.settings.reserves_procurement_target:
continue
@@ -356,7 +362,9 @@ class ProcurementAi:
def cost_ratio_of_ground_unit(
self, control_point: ControlPoint, unit_class: GroundUnitClass
) -> float:
allocations = control_point.allocated_ground_units(self.game.transfers)
allocations = control_point.allocated_ground_units(
self.game.coalition_for(self.is_player).transfers
)
class_cost = 0
total_cost = 0
for unit_type, count in allocations.all.items():

View File

@@ -20,10 +20,11 @@ import yaml
from faker import Faker
from game.dcs.aircrafttype import AircraftType
from game.settings import AutoAtoBehavior
from game.settings import AutoAtoBehavior, Settings
if TYPE_CHECKING:
from game import Game
from game.coalition import Coalition
from gen.flights.flight import FlightType
@@ -96,16 +97,13 @@ class Squadron:
init=False, hash=False, compare=False
)
# We need a reference to the Game so that we can access the Faker without needing to
# persist it to the save game, or having to reconstruct it (it's not cheap) each
# time we create or load a squadron.
game: Game = field(hash=False, compare=False)
player: bool
coalition: Coalition = field(hash=False, compare=False)
settings: Settings = field(hash=False, compare=False)
def __post_init__(self) -> None:
if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
raise ValueError("Squadrons can only be created with active pilots.")
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
self._recruit_pilots(self.settings.squadron_pilot_limit)
self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str:
@@ -113,9 +111,13 @@ class Squadron:
return self.name
return f'{self.name} "{self.nickname}"'
@property
def player(self) -> bool:
return self.coalition.player
@property
def pilot_limits_enabled(self) -> bool:
return self.game.settings.enable_squadron_pilot_limits
return self.settings.enable_squadron_pilot_limits
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
if self.pilot_limits_enabled:
@@ -131,7 +133,7 @@ class Squadron:
if not self.player:
return self.available_pilots.pop()
preference = self.game.settings.auto_ato_behavior
preference = self.settings.auto_ato_behavior
# No preference, so the first pilot is fine.
if preference is AutoAtoBehavior.Default:
@@ -184,7 +186,7 @@ class Squadron:
return
replenish_count = min(
self.game.settings.squadron_replenishment_rate,
self.settings.squadron_replenishment_rate,
self._number_of_unfilled_pilot_slots,
)
if replenish_count > 0:
@@ -206,7 +208,7 @@ class Squadron:
@property
def faker(self) -> Faker:
return self.game.faker_for(self.player)
return self.coalition.faker
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.current_roster if p.status == status]
@@ -228,7 +230,7 @@ class Squadron:
@property
def _number_of_unfilled_pilot_slots(self) -> int:
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
return self.settings.squadron_pilot_limit - len(self.active_pilots)
@property
def number_of_available_pilots(self) -> int:
@@ -252,7 +254,7 @@ class Squadron:
return self.current_roster[index]
@classmethod
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
def from_yaml(cls, path: Path, game: Game, coalition: Coalition) -> Squadron:
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
from gen.flights.flight import FlightType
@@ -287,8 +289,8 @@ class Squadron:
livery=data.get("livery"),
mission_types=tuple(mission_types),
pilot_pool=pilots,
game=game,
player=player,
coalition=coalition,
settings=game.settings,
)
def __setstate__(self, state: dict[str, Any]) -> None:
@@ -299,9 +301,9 @@ class Squadron:
class SquadronLoader:
def __init__(self, game: Game, player: bool) -> None:
def __init__(self, game: Game, coalition: Coalition) -> None:
self.game = game
self.player = player
self.coalition = coalition
@staticmethod
def squadron_directories() -> Iterator[Path]:
@@ -312,8 +314,8 @@ class SquadronLoader:
def load(self) -> dict[AircraftType, list[Squadron]]:
squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
country = self.game.country_for(self.player)
faction = self.game.faction_for(self.player)
country = self.coalition.country_name
faction = self.coalition.faction
any_country = country.startswith("Combined Joint Task Forces ")
for directory in self.squadron_directories():
for path, squadron in self.load_squadrons_from(directory):
@@ -347,7 +349,7 @@ class SquadronLoader:
for squadron_path in directory.glob("*/*.yaml"):
try:
yield squadron_path, Squadron.from_yaml(
squadron_path, self.game, self.player
squadron_path, self.game, self.coalition
)
except Exception as ex:
raise RuntimeError(
@@ -356,29 +358,28 @@ class SquadronLoader:
class AirWing:
def __init__(self, game: Game, player: bool) -> None:
def __init__(self, game: Game, coalition: Coalition) -> None:
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
self.game = game
self.player = player
self.squadrons = SquadronLoader(game, player).load()
self.squadrons = SquadronLoader(game, coalition).load()
count = itertools.count(1)
for aircraft in game.faction_for(player).aircrafts:
for aircraft in coalition.faction.aircrafts:
if aircraft in self.squadrons:
continue
self.squadrons[aircraft] = [
Squadron(
name=f"Squadron {next(count):03}",
nickname=self.random_nickname(),
country=game.country_for(player),
country=coalition.country_name,
role="Flying Squadron",
aircraft=aircraft,
livery=None,
mission_types=tuple(tasks_for_aircraft(aircraft)),
pilot_pool=[],
game=game,
player=player,
coalition=coalition,
settings=game.settings,
)
]

View File

@@ -40,11 +40,7 @@ from gen.ground_forces.combat_stance import CombatStance
from gen.runways import RunwayAssigner, RunwayData
from .base import Base
from .missiontarget import MissionTarget
from .theatergroundobject import (
GenericCarrierGroundObject,
TheaterGroundObject,
NavalGroundObject,
)
from .theatergroundobject import GenericCarrierGroundObject, TheaterGroundObject
from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType
from ..utils import nautical_miles
@@ -606,7 +602,7 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Should be Airbase specific.
def capture(self, game: Game, for_player: bool) -> None:
self.pending_unit_deliveries.refund_all(game)
self.pending_unit_deliveries.refund_all(game.coalition_for(for_player))
self.retreat_ground_units(game)
self.retreat_air_units(game)
self.depopulate_uncapturable_tgos()
@@ -623,11 +619,7 @@ class ControlPoint(MissionTarget, ABC):
...
def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]:
if self.captured:
ato = game.blue_ato
else:
ato = game.red_ato
ato = game.coalition_for(self.captured).ato
transferring: defaultdict[AircraftType, int] = defaultdict(int)
for package in ato.packages:
for flight in package.flights:

View File

@@ -11,7 +11,7 @@ from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from game import Game, db
from game import Game
from game.factions.faction import Faction
from game.scenery_group import SceneryGroup
from game.theater import Carrier, Lha, PointWithHeading
@@ -171,14 +171,11 @@ class ControlPointGroundObjectGenerator:
@property
def faction_name(self) -> str:
if self.control_point.captured:
return self.game.player_faction.name
else:
return self.game.enemy_faction.name
return self.faction.name
@property
def faction(self) -> Faction:
return db.FACTIONS[self.faction_name]
return self.game.coalition_for(self.control_point.captured).faction
def generate(self) -> bool:
self.control_point.connected_objectives = []

View File

@@ -316,7 +316,9 @@ class AirliftPlanner:
capacity = flight_size * capacity_each
if capacity < self.transfer.size:
transfer = self.game.transfers.split_transfer(self.transfer, capacity)
transfer = self.game.coalition_for(
self.for_player
).transfers.split_transfer(self.transfer, capacity)
else:
transfer = self.transfer
@@ -534,8 +536,9 @@ class CargoShipMap(TransportMap[CargoShip]):
class PendingTransfers:
def __init__(self, game: Game) -> None:
def __init__(self, game: Game, player: bool) -> None:
self.game = game
self.player = player
self.convoys = ConvoyMap()
self.cargo_ships = CargoShipMap()
self.pending_transfers: List[TransferOrder] = []
@@ -609,7 +612,7 @@ class PendingTransfers:
flight = transport.flight
flight.package.remove_flight(flight)
if not flight.package.flights:
self.game.ato_for(transport.player_owned).remove_package(flight.package)
self.game.ato_for(self.player).remove_package(flight.package)
self.game.aircraft_inventory.return_from_flight(flight)
flight.clear_roster()
@@ -647,7 +650,7 @@ class PendingTransfers:
self.arrange_transport(transfer)
def order_airlift_assets(self) -> None:
for control_point in self.game.theater.controlpoints:
for control_point in self.game.theater.control_points_for(self.player):
if self.game.air_wing_for(control_point.captured).can_auto_plan(
FlightType.TRANSPORT
):
@@ -682,7 +685,7 @@ class PendingTransfers:
# aesthetic.
gap += 1
self.game.procurement_requests_for(player=control_point.captured).append(
self.game.procurement_requests_for(self.player).append(
AircraftProcurementRequest(
control_point, nautical_miles(200), FlightType.TRANSPORT, gap
)

View File

@@ -6,6 +6,7 @@ from dataclasses import dataclass
from typing import Optional, TYPE_CHECKING, Any
from game.theater import ControlPoint
from .coalition import Coalition
from .dcs.groundunittype import GroundUnitType
from .dcs.unittype import UnitType
from .theater.transitnetwork import (
@@ -41,24 +42,22 @@ class PendingUnitDeliveries:
for k, v in units.items():
self.units[k] -= v
def refund_all(self, game: Game) -> None:
self.refund(game, self.units)
def refund_all(self, coalition: Coalition) -> None:
self.refund(coalition, self.units)
self.units = defaultdict(int)
def refund_ground_units(self, game: Game) -> None:
def refund_ground_units(self, coalition: Coalition) -> None:
ground_units: dict[UnitType[Any], int] = {
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
}
self.refund(game, ground_units)
self.refund(coalition, ground_units)
for gu in ground_units.keys():
del self.units[gu]
def refund(self, game: Game, units: dict[UnitType[Any], int]) -> None:
def refund(self, coalition: Coalition, units: dict[UnitType[Any], int]) -> None:
for unit_type, count in units.items():
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
game.adjust_budget(
unit_type.price * count, player=self.destination.captured
)
coalition.adjust_budget(unit_type.price * count)
def pending_orders(self, unit_type: UnitType[Any]) -> int:
pending_units = self.units.get(unit_type)
@@ -71,19 +70,20 @@ class PendingUnitDeliveries:
return self.pending_orders(unit_type) + current_units
def process(self, game: Game) -> None:
coalition = game.coalition_for(self.destination.captured)
ground_unit_source = self.find_ground_unit_source(game)
if ground_unit_source is None:
game.message(
f"{self.destination.name} lost its source for ground unit "
"reinforcements. Refunding purchase price."
)
self.refund_ground_units(game)
self.refund_ground_units(coalition)
bought_units: dict[UnitType[Any], int] = {}
units_needing_transfer: dict[GroundUnitType, int] = {}
sold_units: dict[UnitType[Any], int] = {}
for unit_type, count in self.units.items():
coalition = "Ally" if self.destination.captured else "Enemy"
allegiance = "Ally" if self.destination.captured else "Enemy"
d: dict[Any, int]
if (
isinstance(unit_type, GroundUnitType)
@@ -98,11 +98,11 @@ class PendingUnitDeliveries:
if count >= 0:
d[unit_type] = count
game.message(
f"{coalition} reinforcements: {unit_type} x {count} at {source}"
f"{allegiance} reinforcements: {unit_type} x {count} at {source}"
)
else:
sold_units[unit_type] = -count
game.message(f"{coalition} sold: {unit_type} x {-count} at {source}")
game.message(f"{allegiance} sold: {unit_type} x {-count} at {source}")
self.units = defaultdict(int)
self.destination.base.commission_units(bought_units)
@@ -111,16 +111,19 @@ class PendingUnitDeliveries:
if units_needing_transfer:
if ground_unit_source is None:
raise RuntimeError(
f"ground unit source could not be found for {self.destination} but still tried to "
f"transfer units to there"
f"Ground unit source could not be found for {self.destination} but "
"still tried to transfer units to there"
)
ground_unit_source.base.commission_units(units_needing_transfer)
self.create_transfer(game, ground_unit_source, units_needing_transfer)
self.create_transfer(coalition, ground_unit_source, units_needing_transfer)
def create_transfer(
self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int]
self,
coalition: Coalition,
source: ControlPoint,
units: dict[GroundUnitType, int],
) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
coalition.transfers.new_transfer(TransferOrder(source, self.destination, units))
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
# This is running *after* the turn counter has been incremented, so this is the