Remove bingo estimates from FlightPlan.

This doesn't need to be a part of FlightPlan, and it's easier to test if
it isn't. Move it out and add the tests.

It's pretty misleading to allow this in the core of the flight plan code
anything. This is an extremely unreliable estimate for most aircraft so
it should be more clearly just for briefing purposes.
This commit is contained in:
Dan Albert 2023-08-23 20:05:17 -07:00
parent 99eed33241
commit cb9063b5be
4 changed files with 131 additions and 38 deletions

View File

@ -12,7 +12,6 @@ from abc import ABC, abstractmethod
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime, timedelta
from functools import cached_property
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
from game.typeguard import self_type_guard
@ -23,7 +22,6 @@ from ..starttype import StartType
from ..traveltime import GroundSpeed
if TYPE_CHECKING:
from game.dcs.aircrafttype import FuelConsumption
from game.theater import ControlPoint
from ..flight import Flight
from ..flightwaypoint import FlightWaypoint
@ -145,39 +143,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
def tot(self) -> datetime:
return self.package.time_over_target + self.tot_offset
@cached_property
def bingo_fuel(self) -> int:
"""Bingo fuel value for the FlightPlan"""
if (fuel := self.flight.unit_type.fuel_consumption) is not None:
return self._bingo_estimate(fuel)
return self._legacy_bingo_estimate()
def _bingo_estimate(self, fuel: FuelConsumption) -> int:
distance_to_arrival = self.max_distance_from(self.flight.arrival)
fuel_consumed = fuel.cruise * distance_to_arrival.nautical_miles
bingo = fuel_consumed + fuel.min_safe
return math.ceil(bingo / 100) * 100
def _legacy_bingo_estimate(self) -> int:
distance_to_arrival = self.max_distance_from(self.flight.arrival)
bingo = 1000.0 # Minimum Emergency Fuel
bingo += 500 # Visual Traffic
bingo += 15 * distance_to_arrival.nautical_miles
# TODO: Per aircraft tweaks.
if self.flight.divert is not None:
max_divert_distance = self.max_distance_from(self.flight.divert)
bingo += 10 * max_divert_distance.nautical_miles
return round(bingo / 100) * 100
@cached_property
def joker_fuel(self) -> int:
"""Joker fuel value for the FlightPlan"""
return self.bingo_fuel + 1000
def max_distance_from(self, cp: ControlPoint) -> Distance:
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
:arg cp The ControlPoint to measure distance from.

View File

@ -0,0 +1,66 @@
from __future__ import annotations
import math
from typing import TYPE_CHECKING
from dcs import Point
from game.utils import Distance, meters
if TYPE_CHECKING:
from game.ato.flightwaypoint import FlightWaypoint
from game.dcs.aircrafttype import FuelConsumption
class BingoEstimator:
"""Estimates bingo/joker fuel values for a flight plan.
The results returned by this class are bogus for most airframes. Only the few
airframes which have fuel consumption data available can provide even moderately
reliable estimates. **Do not use this for flight planning.** This should only be
used in briefing context where it's okay to be wrong.
"""
def __init__(
self,
fuel_consumption: FuelConsumption | None,
arrival: Point,
divert: Point | None,
waypoints: list[FlightWaypoint],
) -> None:
self.fuel_consumption = fuel_consumption
self.arrival = arrival
self.divert = divert
self.waypoints = waypoints
def estimate_bingo(self) -> int:
"""Bingo fuel value for the FlightPlan"""
if (fuel := self.fuel_consumption) is not None:
return self._fuel_consumption_based_estimate(fuel)
return self._legacy_bingo_estimate()
def estimate_joker(self) -> int:
"""Joker fuel value for the FlightPlan"""
return self.estimate_bingo() + 1000
def _fuel_consumption_based_estimate(self, fuel: FuelConsumption) -> int:
distance_to_arrival = self._max_distance_from(self.arrival)
fuel_consumed = fuel.cruise * distance_to_arrival.nautical_miles
bingo = fuel_consumed + fuel.min_safe
return math.ceil(bingo / 100) * 100
def _legacy_bingo_estimate(self) -> int:
distance_to_arrival = self._max_distance_from(self.arrival)
bingo = 1000.0 # Minimum Emergency Fuel
bingo += 500 # Visual Traffic
bingo += 15 * distance_to_arrival.nautical_miles
if self.divert is not None:
max_divert_distance = self._max_distance_from(self.divert)
bingo += 10 * max_divert_distance.nautical_miles
return round(bingo / 100) * 100
def _max_distance_from(self, point: Point) -> Distance:
return max(meters(point.distance_to_point(w.position)) for w in self.waypoints)

View File

@ -4,7 +4,7 @@ import logging
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING
from dcs import Mission
from dcs import Mission, Point
from dcs.flyingunit import FlyingUnit
from dcs.unit import Skill
from dcs.unitgroup import FlyingGroup
@ -21,6 +21,7 @@ from game.squadrons import Pilot
from game.unitmap import UnitMap
from .aircraftbehavior import AircraftBehavior
from .aircraftpainter import AircraftPainter
from .bingoestimator import BingoEstimator
from .flightdata import FlightData
from .waypoints import WaypointGenerator
from ...ato.flightmember import FlightMember
@ -102,6 +103,16 @@ class FlightGroupConfigurator:
self.unit_map,
).create_waypoints()
divert_position: Point | None = None
if self.flight.divert is not None:
divert_position = self.flight.divert.position
bingo_estimator = BingoEstimator(
self.flight.unit_type.fuel_consumption,
self.flight.arrival.position,
divert_position,
self.flight.flight_plan.waypoints,
)
return FlightData(
package=self.flight.package,
aircraft_type=self.flight.unit_type,
@ -119,8 +130,8 @@ class FlightGroupConfigurator:
divert=divert,
waypoints=waypoints,
intra_flight_channel=flight_channel,
bingo_fuel=self.flight.flight_plan.bingo_fuel,
joker_fuel=self.flight.flight_plan.joker_fuel,
bingo_fuel=bingo_estimator.estimate_bingo(),
joker_fuel=bingo_estimator.estimate_joker(),
custom_name=self.flight.custom_name,
laser_codes=laser_codes,
)

View File

@ -0,0 +1,51 @@
import pytest
from dcs import Point
from dcs.terrain import Terrain, Caucasus
from game.ato import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.dcs.aircrafttype import FuelConsumption
from game.missiongenerator.aircraft.bingoestimator import BingoEstimator
from game.utils import nautical_miles
@pytest.fixture(name="terrain")
def terrain_fixture() -> Terrain:
return Caucasus()
@pytest.fixture(name="waypoints")
def waypoints_fixture(terrain: Terrain) -> list[FlightWaypoint]:
return [
FlightWaypoint(
"", FlightWaypointType.NAV, Point(0, nautical_miles(d).meters, terrain)
)
for d in range(101)
]
def test_legacy_bingo_estimator(
waypoints: list[FlightWaypoint], terrain: Terrain
) -> None:
estimator = BingoEstimator(None, Point(0, 0, terrain), None, waypoints)
assert estimator.estimate_bingo() == 3000
assert estimator.estimate_joker() == estimator.estimate_bingo() + 1000
estimator = BingoEstimator(
None, Point(0, 0, terrain), Point(0, 5, terrain), waypoints
)
assert estimator.estimate_bingo() == 4000
assert estimator.estimate_joker() == estimator.estimate_bingo() + 1000
def test_fuel_consumption_based_bingo_estimator(
waypoints: list[FlightWaypoint], terrain: Terrain
) -> None:
consumption = FuelConsumption(100, 50, 10, 25, 1000)
estimator = BingoEstimator(consumption, Point(0, 0, terrain), None, waypoints)
assert estimator.estimate_bingo() == 2000
assert estimator.estimate_joker() == estimator.estimate_bingo() + 1000
estimator = BingoEstimator(
consumption, Point(0, 0, terrain), Point(0, 5, terrain), waypoints
)
assert estimator.estimate_bingo() == 2000
assert estimator.estimate_joker() == estimator.estimate_bingo() + 1000