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 committed by Raffson
parent e04f23bf15
commit a2e98f485c
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
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 collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import cached_property
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
from game.typeguard import self_type_guard from game.typeguard import self_type_guard
@ -23,7 +22,6 @@ from ..starttype import StartType
from ..traveltime import GroundSpeed from ..traveltime import GroundSpeed
if TYPE_CHECKING: if TYPE_CHECKING:
from game.dcs.aircrafttype import FuelConsumption
from game.theater import ControlPoint from game.theater import ControlPoint
from ..flight import Flight from ..flight import Flight
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@ -162,39 +160,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
def tot(self) -> datetime: def tot(self) -> datetime:
return self.package.time_over_target + self.tot_offset 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: def max_distance_from(self, cp: ControlPoint) -> Distance:
"""Returns the farthest waypoint of the flight plan from a ControlPoint. """Returns the farthest waypoint of the flight plan from a ControlPoint.
:arg cp The ControlPoint to measure distance from. :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 datetime import datetime
from typing import Any, Optional, TYPE_CHECKING from typing import Any, Optional, TYPE_CHECKING
from dcs import Mission from dcs import Mission, Point
from dcs.action import DoScript from dcs.action import DoScript
from dcs.flyingunit import FlyingUnit from dcs.flyingunit import FlyingUnit
from dcs.task import OptReactOnThreat from dcs.task import OptReactOnThreat
@ -24,6 +24,7 @@ from game.runways import RunwayData
from game.squadrons import Pilot from game.squadrons import Pilot
from .aircraftbehavior import AircraftBehavior from .aircraftbehavior import AircraftBehavior
from .aircraftpainter import AircraftPainter from .aircraftpainter import AircraftPainter
from .bingoestimator import BingoEstimator
from .flightdata import FlightData from .flightdata import FlightData
from .waypoints import WaypointGenerator from .waypoints import WaypointGenerator
from ...ato.flightmember import FlightMember from ...ato.flightmember import FlightMember
@ -115,6 +116,16 @@ class FlightGroupConfigurator:
# Need to set uncontrolled to false, otherwise the AI will skip the mission and just land # Need to set uncontrolled to false, otherwise the AI will skip the mission and just land
self.group.uncontrolled = False self.group.uncontrolled = False
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( return FlightData(
package=self.flight.package, package=self.flight.package,
aircraft_type=self.flight.unit_type, aircraft_type=self.flight.unit_type,
@ -133,8 +144,8 @@ class FlightGroupConfigurator:
divert=divert, divert=divert,
waypoints=waypoints, waypoints=waypoints,
intra_flight_channel=flight_channel, intra_flight_channel=flight_channel,
bingo_fuel=self.flight.flight_plan.bingo_fuel, bingo_fuel=bingo_estimator.estimate_bingo(),
joker_fuel=self.flight.flight_plan.joker_fuel, joker_fuel=bingo_estimator.estimate_joker(),
custom_name=self.flight.custom_name, custom_name=self.flight.custom_name,
laser_codes=laser_codes, 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