Skip to content

Commit

Permalink
Doctrine load from YAML (#3291)
Browse files Browse the repository at this point in the history
This PR refactors the Doctrine class to load from YAML files in the
resources folder instead of being hardcoded as a step towards making
doctrines moddable (Issue #829).

I haven't added anything to the changelog as a couple of things should
get cleaned up first:
- As far as I can tell, the flags in the Doctrine class (cap, cas, sead
etc.) aren't used anywhere. Need to test further, and if they're truly
not used, will remove them.
- Probably need to update the Wiki
  • Loading branch information
zhexu14 committed Dec 18, 2023
1 parent a213215 commit 4631ee0
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 147 deletions.
210 changes: 91 additions & 119 deletions game/data/doctrine.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
from __future__ import annotations

from pathlib import Path
import yaml
from typing import ClassVar

from dataclasses import dataclass
from datetime import timedelta

Expand All @@ -15,6 +21,16 @@ def for_unit_class(self, unit_class: UnitClass) -> float:
except KeyError:
return 0.0

@staticmethod
def from_dict(data: dict[str, float]) -> GroundUnitProcurementRatios:
unit_class_enum_from_name = {unit.value: unit for unit in UnitClass}
r = {}
for unit_class in data:
if unit_class not in unit_class_enum_from_name:
raise ValueError(f"Could not find unit type {unit_class}")
r[unit_class_enum_from_name[unit_class]] = float(data[unit_class])
return GroundUnitProcurementRatios(r)


@dataclass(frozen=True)
class Doctrine:
Expand Down Expand Up @@ -79,122 +95,78 @@ class Doctrine:

ground_unit_procurement_ratios: GroundUnitProcurementRatios


MODERN_DOCTRINE = Doctrine(
"modern",
cap=True,
cas=True,
sead=True,
strike=True,
antiship=True,
rendezvous_altitude=feet(25000),
hold_distance=nautical_miles(25),
push_distance=nautical_miles(20),
join_distance=nautical_miles(20),
max_ingress_distance=nautical_miles(45),
min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(20000),
min_patrol_altitude=feet(15000),
max_patrol_altitude=feet(33000),
pattern_altitude=feet(5000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(15),
cap_max_track_length=nautical_miles(40),
cap_min_distance_from_cp=nautical_miles(10),
cap_max_distance_from_cp=nautical_miles(40),
cap_engagement_range=nautical_miles(50),
cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(60),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
UnitClass.TANK: 3,
UnitClass.ATGM: 2,
UnitClass.APC: 2,
UnitClass.IFV: 3,
UnitClass.ARTILLERY: 1,
UnitClass.SHORAD: 2,
UnitClass.RECON: 1,
}
),
)

COLDWAR_DOCTRINE = Doctrine(
name="coldwar",
cap=True,
cas=True,
sead=True,
strike=True,
antiship=True,
rendezvous_altitude=feet(22000),
hold_distance=nautical_miles(15),
push_distance=nautical_miles(10),
join_distance=nautical_miles(10),
max_ingress_distance=nautical_miles(30),
min_ingress_distance=nautical_miles(10),
ingress_altitude=feet(18000),
min_patrol_altitude=feet(10000),
max_patrol_altitude=feet(24000),
pattern_altitude=feet(5000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(12),
cap_max_track_length=nautical_miles(24),
cap_min_distance_from_cp=nautical_miles(8),
cap_max_distance_from_cp=nautical_miles(25),
cap_engagement_range=nautical_miles(35),
cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(40),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
UnitClass.TANK: 4,
UnitClass.ATGM: 2,
UnitClass.APC: 3,
UnitClass.IFV: 2,
UnitClass.ARTILLERY: 1,
UnitClass.SHORAD: 2,
UnitClass.RECON: 1,
}
),
)

WWII_DOCTRINE = Doctrine(
name="ww2",
cap=True,
cas=True,
sead=False,
strike=True,
antiship=True,
hold_distance=nautical_miles(10),
push_distance=nautical_miles(5),
join_distance=nautical_miles(5),
rendezvous_altitude=feet(10000),
max_ingress_distance=nautical_miles(7),
min_ingress_distance=nautical_miles(5),
ingress_altitude=feet(8000),
min_patrol_altitude=feet(4000),
max_patrol_altitude=feet(15000),
pattern_altitude=feet(5000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nautical_miles(8),
cap_max_track_length=nautical_miles(18),
cap_min_distance_from_cp=nautical_miles(0),
cap_max_distance_from_cp=nautical_miles(5),
cap_engagement_range=nautical_miles(20),
cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(10),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
UnitClass.TANK: 3,
UnitClass.ATGM: 3,
UnitClass.APC: 3,
UnitClass.ARTILLERY: 1,
UnitClass.SHORAD: 3,
UnitClass.RECON: 1,
}
),
)

ALL_DOCTRINES = [
COLDWAR_DOCTRINE,
MODERN_DOCTRINE,
WWII_DOCTRINE,
]
_by_name: ClassVar[dict[str, Doctrine]] = {}
_loaded: ClassVar[bool] = False

@classmethod
def register(cls, doctrine: Doctrine) -> None:
if doctrine.name in cls._by_name:
duplicate = cls._by_name[doctrine.name]
raise ValueError(f"Doctrine {doctrine.name} is already loaded")
cls._by_name[doctrine.name] = doctrine

@classmethod
def named(cls, name: str) -> Doctrine:
if not cls._loaded:
cls.load_all()
return cls._by_name[name]

@classmethod
def all_doctrines(cls) -> list[Doctrine]:
if not cls._loaded:
cls.load_all()
return list(cls._by_name.values())

@classmethod
def load_all(cls) -> None:
if cls._loaded:
return
for doctrine_file_path in Path("resources/doctrines").glob("**/*.yaml"):
with doctrine_file_path.open(encoding="utf8") as doctrine_file:
data = yaml.safe_load(doctrine_file)
cls.register(
Doctrine(
name=data["name"],
cap=data["cap"],
cas=data["cas"],
sead=data["sead"],
strike=data["strike"],
antiship=data["antiship"],
rendezvous_altitude=feet(data["rendezvous_altitude_ft_msl"]),
hold_distance=nautical_miles(data["hold_distance_nm"]),
push_distance=nautical_miles(data["push_distance_nm"]),
join_distance=nautical_miles(data["join_distance_nm"]),
max_ingress_distance=nautical_miles(
data["max_ingress_distance_nm"]
),
min_ingress_distance=nautical_miles(
data["min_ingress_distance_nm"]
),
ingress_altitude=feet(data["ingress_altitude_ft_msl"]),
min_patrol_altitude=feet(data["min_patrol_altitude_ft_msl"]),
max_patrol_altitude=feet(data["max_patrol_altitude_ft_msl"]),
pattern_altitude=feet(data["pattern_altitude_ft_msl"]),
cap_duration=timedelta(minutes=data["cap_duration_minutes"]),
cap_min_track_length=nautical_miles(
data["cap_min_track_length_nm"]
),
cap_max_track_length=nautical_miles(
data["cap_max_track_length_nm"]
),
cap_min_distance_from_cp=nautical_miles(
data["cap_min_distance_from_cp_nm"]
),
cap_max_distance_from_cp=nautical_miles(
data["cap_max_distance_from_cp_nm"]
),
cap_engagement_range=nautical_miles(
data["cap_engagement_range_nm"]
),
cas_duration=timedelta(minutes=data["cas_duration_minutes"]),
sweep_distance=nautical_miles(data["sweep_distance_nm"]),
ground_unit_procurement_ratios=GroundUnitProcurementRatios.from_dict(
data["ground_unit_procurement_ratios"]
),
)
)
cls._loaded = True
18 changes: 3 additions & 15 deletions game/factions/faction.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@
WW2_FREE,
WW2_GERMANY_BUILDINGS,
)
from game.data.doctrine import (
COLDWAR_DOCTRINE,
Doctrine,
MODERN_DOCTRINE,
WWII_DOCTRINE,
)
from game.data.doctrine import Doctrine
from game.data.groups import GroupRole
from game.data.units import UnitClass
from game.dcs.aircrafttype import AircraftType
Expand Down Expand Up @@ -106,7 +101,7 @@ class Faction:
jtac_unit: Optional[AircraftType] = field(default=None)

# doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
doctrine: Doctrine = field(default=Doctrine.named("modern"))

# List of available building layouts for this faction
building_set: List[str] = field(default_factory=list)
Expand Down Expand Up @@ -238,14 +233,7 @@ def from_dict(cls: Type[Faction], json: Dict[str, Any]) -> Faction:

# Load doctrine
doctrine = json.get("doctrine", "modern")
if doctrine == "modern":
faction.doctrine = MODERN_DOCTRINE
elif doctrine == "coldwar":
faction.doctrine = COLDWAR_DOCTRINE
elif doctrine == "ww2":
faction.doctrine = WWII_DOCTRINE
else:
faction.doctrine = MODERN_DOCTRINE
faction.doctrine = Doctrine.named(doctrine)

# Load the building set
faction.building_set = []
Expand Down
7 changes: 2 additions & 5 deletions game/flightplan/waypointsolverloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@
from shapely.geometry import shape
from shapely.geometry.base import BaseGeometry

from game.data.doctrine import Doctrine, ALL_DOCTRINES
from game.data.doctrine import Doctrine
from .ipsolver import IpSolver
from .waypointsolver import WaypointSolver
from ..theater.theaterloader import TERRAINS_BY_NAME


def doctrine_from_name(name: str) -> Doctrine:
for doctrine in ALL_DOCTRINES:
if doctrine.name == name:
return doctrine
raise KeyError
return Doctrine.named(name)


def geometry_ll_to_xy(geometry: BaseGeometry, terrain: Terrain) -> BaseGeometry:
Expand Down
32 changes: 32 additions & 0 deletions resources/doctrines/coldwar.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: coldwar
cap: true
cas: true
sead: true
strike: true
antiship: true
rendezvous_altitude_ft_msl: 22000
hold_distance_nm: 15
push_distance_nm: 10
join_distance_nm: 10
max_ingress_distance_nm: 30
min_ingress_distance_nm: 10
ingress_altitude_ft_msl: 18000
min_patrol_altitude_ft_msl: 10000
max_patrol_altitude_ft_msl: 24000
pattern_altitude_ft_msl: 5000
cap_duration_minutes: 30
cap_min_track_length_nm: 12
cap_max_track_length_nm: 24
cap_min_distance_from_cp_nm: 8
cap_max_distance_from_cp_nm: 25
cap_engagement_range_nm: 35
cas_duration_minutes: 30
sweep_distance_nm: 40
ground_unit_procurement_ratios:
Tank: 4
ATGM: 2
APC: 3
IFV: 2
Artillery: 1
SHORAD: 2
Recon: 1
32 changes: 32 additions & 0 deletions resources/doctrines/modern.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: modern
cap: true
cas: true
sead: true
strike: true
antiship: true
rendezvous_altitude_ft_msl: 25000
hold_distance_nm: 25
push_distance_nm: 20
join_distance_nm: 20
max_ingress_distance_nm: 45
min_ingress_distance_nm: 10
ingress_altitude_ft_msl: 20000
min_patrol_altitude_ft_msl: 15000
max_patrol_altitude_ft_msl: 33000
pattern_altitude_ft_msl: 5000
cap_duration_minutes: 30
cap_min_track_length_nm: 15
cap_max_track_length_nm: 40
cap_min_distance_from_cp_nm: 10
cap_max_distance_from_cp_nm: 40
cap_engagement_range_nm: 50
cas_duration_minutes: 30
sweep_distance_nm: 60
ground_unit_procurement_ratios:
Tank: 3
ATGM: 2
APC: 2
IFV: 3
Artillery: 1
SHORAD: 2
Recon: 1
31 changes: 31 additions & 0 deletions resources/doctrines/ww2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: ww2
cap: true
cas: true
sead: false
strike: true
antiship: true
hold_distance_nm: 10
push_distance_nm: 5
join_distance_nm: 5
rendezvous_altitude_ft_msl: 10000
max_ingress_distance_nm: 7
min_ingress_distance_nm: 5
ingress_altitude_ft_msl: 8000
min_patrol_altitude_ft_msl: 4000
max_patrol_altitude_ft_msl: 15000
pattern_altitude_ft_msl: 5000
cap_duration_minutes: 30
cap_min_track_length_nm: 8
cap_max_track_length_nm: 18
cap_min_distance_from_cp_nm: 0
cap_max_distance_from_cp_nm: 5
cap_engagement_range_nm: 20
cas_duration_minutes: 30
sweep_distance_nm: 10
ground_unit_procurement_ratios:
Tank: 3
ATGM: 3
APC: 3
Artillery: 1
SHORAD: 3
Recon: 1
Loading

0 comments on commit 4631ee0

Please sign in to comment.