Generate weather conditions at turn start.

Weather and exact time of day information is helpful during mission
planning, so generate it at the start of the turn rather than at
takeoff time.

Another advantage aside from planning is that we can now use the wind
information to set carrier headings and takeoff runways appropriately.
This commit is contained in:
Dan Albert 2020-10-16 18:25:25 -07:00
parent 7aa17e5ad6
commit 49b6951ac3
8 changed files with 266 additions and 171 deletions

View File

@ -12,7 +12,6 @@ from game import db, persistency
from game.debriefing import Debriefing from game.debriefing import Debriefing
from game.infos.information import Information from game.infos.information import Information
from game.operation.operation import Operation from game.operation.operation import Operation
from gen.environmentgen import EnvironmentSettings
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint from theater import ControlPoint
from theater.start_generator import generate_airbase_defense_group from theater.start_generator import generate_airbase_defense_group
@ -42,7 +41,6 @@ class Event:
operation = None # type: Operation operation = None # type: Operation
difficulty = 1 # type: int difficulty = 1 # type: int
environment_settings = None # type: EnvironmentSettings
BONUS_BASE = 5 BONUS_BASE = 5
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):

View File

@ -2,7 +2,7 @@ import logging
import math import math
import random import random
import sys import sys
from datetime import datetime, timedelta from datetime import date, datetime, timedelta
from typing import Any, Dict, List from typing import Any, Dict, List
from dcs.action import Coalition from dcs.action import Coalition
@ -28,6 +28,7 @@ from .event.event import Event, UnitsDeliveryEvent
from .event.frontlineattack import FrontlineAttackEvent from .event.frontlineattack import FrontlineAttackEvent
from .infos.information import Information from .infos.information import Information
from .settings import Settings from .settings import Settings
from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4 COMMISION_UNIT_VARIETY = 4
COMMISION_LIMITS_SCALE = 1.5 COMMISION_LIMITS_SCALE = 1.5
@ -78,7 +79,7 @@ class Game:
self.enemy_name = enemy_name self.enemy_name = enemy_name
self.enemy_country = db.FACTIONS[enemy_name]["country"] self.enemy_country = db.FACTIONS[enemy_name]["country"]
self.turn = 0 self.turn = 0
self.date = datetime(start_date.year, start_date.month, start_date.day) self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats() self.game_stats = GameStats()
self.game_stats.update(self) self.game_stats.update(self)
self.ground_planners: Dict[int, GroundPlanner] = {} self.ground_planners: Dict[int, GroundPlanner] = {}
@ -91,6 +92,8 @@ class Game:
self.current_unit_id = 0 self.current_unit_id = 0
self.current_group_id = 0 self.current_group_id = 0
self.conditions = self.generate_conditions()
self.blue_ato = AirTaskingOrder() self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder() self.red_ato = AirTaskingOrder()
@ -101,6 +104,9 @@ class Game:
self.sanitize_sides() self.sanitize_sides()
self.on_load() self.on_load()
def generate_conditions(self) -> Conditions:
return Conditions.generate(self.theater, self.date,
self.current_turn_time_of_day, self.settings)
def sanitize_sides(self): def sanitize_sides(self):
""" """
@ -218,6 +224,12 @@ class Game:
def on_load(self) -> None: def on_load(self) -> None:
ObjectiveDistanceCache.set_theater(self.theater) ObjectiveDistanceCache.set_theater(self.theater)
# Save game compatibility.
# TODO: Remove in 2.3.
if not hasattr(self, "conditions"):
self.conditions = self.generate_conditions()
def pass_turn(self, no_action=False): def pass_turn(self, no_action=False):
logging.info("Pass turn") logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
@ -252,6 +264,8 @@ class Game:
for cp in self.theater.controlpoints: for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp) self.aircraft_inventory.set_from_control_point(cp)
self.conditions = self.generate_conditions()
# Plan flights & combat for next turn # Plan flights & combat for next turn
self.__culling_points = self.compute_conflicts_position() self.__culling_points = self.compute_conflicts_position()
self.ground_planners = {} self.ground_planners = {}
@ -340,11 +354,11 @@ class Game:
self.informations.append(info) self.informations.append(info)
@property @property
def current_turn_daytime(self): def current_turn_time_of_day(self) -> TimeOfDay:
return ["dawn", "day", "dusk", "night"][self.turn % 4] return list(TimeOfDay)[self.turn % 4]
@property @property
def current_day(self): def current_day(self) -> date:
return self.date + timedelta(days=self.turn // 4) return self.date + timedelta(days=self.turn // 4)
def next_unit_id(self): def next_unit_id(self):

View File

@ -21,7 +21,7 @@ from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo from gen.armor import GroundConflictGenerator, JtacInfo
from gen.beacons import load_beacons_for_terrain from gen.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator from gen.briefinggen import BriefingGenerator
from gen.environmentgen import EnviromentGenerator from gen.environmentgen import EnvironmentGenerator
from gen.forcedoptionsgen import ForcedOptionsGenerator from gen.forcedoptionsgen import ForcedOptionsGenerator
from gen.groundobjectsgen import GroundObjectsGenerator from gen.groundobjectsgen import GroundObjectsGenerator
from gen.kneeboard import KneeboardGenerator from gen.kneeboard import KneeboardGenerator
@ -45,7 +45,6 @@ class Operation:
triggersgen = None # type: TriggersGenerator triggersgen = None # type: TriggersGenerator
airsupportgen = None # type: AirSupportConflictGenerator airsupportgen = None # type: AirSupportConflictGenerator
visualgen = None # type: VisualGenerator visualgen = None # type: VisualGenerator
envgen = None # type: EnviromentGenerator
groundobjectgen = None # type: GroundObjectsGenerator groundobjectgen = None # type: GroundObjectsGenerator
briefinggen = None # type: BriefingGenerator briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator forcedoptionsgen = None # type: ForcedOptionsGenerator
@ -162,13 +161,9 @@ class Operation:
for frequency in unique_map_frequencies: for frequency in unique_map_frequencies:
radio_registry.reserve(frequency) radio_registry.reserve(frequency)
# Generate meteo # Set mission time and weather conditions.
envgen = EnviromentGenerator(self.current_mission, self.conflict, EnvironmentGenerator(self.current_mission,
self.game) self.game.conditions).generate()
if self.environment_settings is None:
self.environment_settings = envgen.generate()
else:
envgen.load(self.environment_settings)
# Generate ground object first # Generate ground object first

183
game/weather.py Normal file
View File

@ -0,0 +1,183 @@
from __future__ import annotations
import datetime
import logging
import random
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from dcs.weather import Weather as PydcsWeather, Wind
from game.settings import Settings
from theater import ConflictTheater
class TimeOfDay(Enum):
Dawn = "dawn"
Day = "day"
Dusk = "dusk"
Night = "night"
@dataclass(frozen=True)
class WindConditions:
at_0m: Wind
at_2000m: Wind
at_8000m: Wind
@dataclass(frozen=True)
class Clouds:
base: int
density: int
thickness: int
precipitation: PydcsWeather.Preceptions
@dataclass(frozen=True)
class Fog:
visibility: int
thickness: int
class Weather:
def __init__(self) -> None:
self.clouds = self.generate_clouds()
self.fog = self.generate_fog()
self.wind = self.generate_wind()
def generate_clouds(self) -> Optional[Clouds]:
raise NotImplementedError
def generate_fog(self) -> Optional[Fog]:
if random.randrange(5) != 0:
return None
return Fog(
visibility=random.randint(2500, 5000),
thickness=random.randint(100, 500)
)
def generate_wind(self) -> WindConditions:
raise NotImplementedError
@staticmethod
def random_wind(minimum: int, maximum) -> WindConditions:
wind_direction = random.randint(0, 360)
at_0m_factor = 1
at_2000m_factor = 2
at_8000m_factor = 3
base_wind = random.randint(minimum, maximum)
return WindConditions(
# Always some wind to make the smoke move a bit.
at_0m=Wind(wind_direction, min(1, base_wind * at_0m_factor)),
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor)
)
@staticmethod
def random_cloud_base() -> int:
return random.randint(2000, 3000)
@staticmethod
def random_cloud_thickness() -> int:
return random.randint(100, 400)
class ClearSkies(Weather):
def generate_clouds(self) -> Optional[Clouds]:
return None
def generate_fog(self) -> Optional[Fog]:
return None
def generate_wind(self) -> WindConditions:
return self.random_wind(0, 0)
class Cloudy(Weather):
def generate_clouds(self) -> Optional[Clouds]:
return Clouds(
base=self.random_cloud_base(),
density=random.randint(1, 8),
thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.None_
)
def generate_wind(self) -> WindConditions:
return self.random_wind(0, 4)
class Raining(Weather):
def generate_clouds(self) -> Optional[Clouds]:
return Clouds(
base=self.random_cloud_base(),
density=random.randint(5, 8),
thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.Rain
)
def generate_wind(self) -> WindConditions:
return self.random_wind(0, 6)
class Thunderstorm(Weather):
def generate_clouds(self) -> Optional[Clouds]:
return Clouds(
base=self.random_cloud_base(),
density=random.randint(9, 10),
thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.Thunderstorm
)
def generate_wind(self) -> WindConditions:
return self.random_wind(0, 8)
@dataclass
class Conditions:
time_of_day: TimeOfDay
start_time: datetime.datetime
weather: Weather
@classmethod
def generate(cls, theater: ConflictTheater, day: datetime.date,
time_of_day: TimeOfDay, settings: Settings) -> Conditions:
return cls(
time_of_day=time_of_day,
start_time=cls.generate_start_time(
theater, day, time_of_day, settings.night_disabled
),
weather=cls.generate_weather()
)
@classmethod
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date,
time_of_day: TimeOfDay,
night_disabled: bool) -> datetime.datetime:
if night_disabled:
logging.info("Skip Night mission due to user settings")
time_range = {
TimeOfDay.Dawn: (8, 9),
TimeOfDay.Day: (10, 12),
TimeOfDay.Dusk: (12, 14),
TimeOfDay.Night: (14, 17),
}[time_of_day]
else:
time_range = theater.daytime_map[time_of_day.value]
time = datetime.time(hour=random.randint(*time_range))
return datetime.datetime.combine(day, time)
@classmethod
def generate_weather(cls) -> Weather:
chances = {
Thunderstorm: 1,
Raining: 20,
Cloudy: 60,
ClearSkies: 20,
}
weather_type = random.choices(list(chances.keys()),
weights=list(chances.values()))[0]
return weather_type()

View File

@ -1,147 +1,36 @@
import logging from typing import Optional
import random
from datetime import timedelta
from dcs.mission import Mission from dcs.mission import Mission
from dcs.weather import Weather, Wind
from .conflictgen import Conflict from game.weather import Clouds, Fog, Conditions, WindConditions
WEATHER_CLOUD_BASE = 2000, 3000
WEATHER_CLOUD_DENSITY = 1, 8
WEATHER_CLOUD_THICKNESS = 100, 400
WEATHER_CLOUD_BASE_MIN = 1600
WEATHER_FOG_CHANCE = 20
WEATHER_FOG_VISIBILITY = 2500, 5000
WEATHER_FOG_THICKNESS = 100, 500
RANDOM_TIME = {
"night": 7,
"dusk": 40,
"dawn": 40,
"day": 100,
}
RANDOM_WEATHER = {
1: 0, # thunderstorm
2: 20, # rain
3: 80, # clouds
4: 100, # clear
}
class EnvironmentSettings: class EnvironmentGenerator:
weather_dict = None def __init__(self, mission: Mission, conditions: Conditions) -> None:
start_time = None
class EnviromentGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game):
self.mission = mission self.mission = mission
self.conflict = conflict self.conditions = conditions
self.game = game
def _gen_time(self): def set_clouds(self, clouds: Optional[Clouds]) -> None:
if clouds is None:
return
self.mission.weather.clouds_base = clouds.base
self.mission.weather.clouds_thickness = clouds.thickness
self.mission.weather.clouds_density = clouds.density
self.mission.weather.clouds_iprecptns = clouds.precipitation
start_time = self.game.current_day def set_fog(self, fog: Optional[Fog]) -> None:
if fog is None:
return
self.mission.weather.fog_visibility = fog.visibility
self.mission.weather.fog_thickness = fog.thickness
daytime = self.game.current_turn_daytime def set_wind(self, wind: WindConditions) -> None:
logging.info("Mission time will be {}".format(daytime)) self.mission.weather.wind_at_ground = wind.at_0m
if self.game.settings.night_disabled: self.mission.weather.wind_at_2000 = wind.at_2000m
logging.info("Skip Night mission due to user settings") self.mission.weather.wind_at_8000 = wind.at_8000m
if daytime == "dawn":
time_range = (8, 9)
elif daytime == "day":
time_range = (10, 12)
elif daytime == "dusk":
time_range = (12, 14)
elif daytime == "night":
time_range = (14, 17)
else:
time_range = (10, 12)
else:
time_range = self.game.theater.daytime_map[daytime]
start_time += timedelta(hours=random.randint(*time_range))
logging.info("time - {}, slot - {}, night skipped - {}".format(
str(start_time),
str(time_range),
self.game.settings.night_disabled))
self.mission.start_time = start_time
def _generate_wind(self, wind_speed, wind_direction=None):
# wind
if not wind_direction:
wind_direction = random.randint(0, 360)
self.mission.weather.wind_at_ground = Wind(wind_direction, wind_speed)
self.mission.weather.wind_at_2000 = Wind(wind_direction, wind_speed * 2)
self.mission.weather.wind_at_8000 = Wind(wind_direction, wind_speed * 3)
def _generate_base_weather(self):
# clouds
self.mission.weather.clouds_base = random.randint(*WEATHER_CLOUD_BASE)
self.mission.weather.clouds_density = random.randint(*WEATHER_CLOUD_DENSITY)
self.mission.weather.clouds_thickness = random.randint(*WEATHER_CLOUD_THICKNESS)
# wind
self._generate_wind(random.randint(0, 4))
# fog
if random.randint(0, 100) < WEATHER_FOG_CHANCE:
self.mission.weather.fog_visibility = random.randint(*WEATHER_FOG_VISIBILITY)
self.mission.weather.fog_thickness = random.randint(*WEATHER_FOG_THICKNESS)
def _gen_random_weather(self):
weather_type = None
for k, v in RANDOM_WEATHER.items():
if random.randint(0, 100) <= v:
weather_type = k
break
logging.info("generated weather {}".format(weather_type))
if weather_type == 1:
# thunderstorm
self._generate_base_weather()
self._generate_wind(random.randint(0, 8))
self.mission.weather.clouds_density = random.randint(9, 10)
self.mission.weather.clouds_iprecptns = Weather.Preceptions.Thunderstorm
elif weather_type == 2:
# rain
self._generate_base_weather()
self.mission.weather.clouds_density = random.randint(5, 8)
self.mission.weather.clouds_iprecptns = Weather.Preceptions.Rain
self._generate_wind(random.randint(0, 6))
elif weather_type == 3:
# clouds
self._generate_base_weather()
elif weather_type == 4:
# clear
pass
if self.mission.weather.clouds_density > 0:
# sometimes clouds are randomized way too low and need to be fixed
self.mission.weather.clouds_base = max(self.mission.weather.clouds_base, WEATHER_CLOUD_BASE_MIN)
if self.mission.weather.wind_at_ground.speed == 0:
# frontline smokes look silly w/o any wind
self._generate_wind(1)
def generate(self) -> EnvironmentSettings:
self._gen_time()
self._gen_random_weather()
settings = EnvironmentSettings()
settings.start_time = self.mission.start_time
settings.weather_dict = self.mission.weather.dict()
return settings
def load(self, settings: EnvironmentSettings):
self.mission.start_time = settings.start_time
self.mission.weather.load_from_dict(settings.weather_dict)
def generate(self):
self.mission.start_time = self.conditions.start_time
self.set_clouds(self.conditions.weather.clouds)
self.set_fog(self.conditions.weather.fog)
self.set_wind(self.conditions.weather.wind)

View File

@ -97,7 +97,7 @@ class QTopPanel(QFrame):
if game is None: if game is None:
return return
self.turnCounter.setCurrentTurn(game.turn, game.current_day) self.turnCounter.setCurrentTurn(game.turn, game.conditions)
self.budgetBox.setGame(game) self.budgetBox.setGame(game)
self.factionsInfos.setGame(game) self.factionsInfos.setGame(game)

View File

@ -1,7 +1,8 @@
import datetime import datetime
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
from game.weather import Conditions, TimeOfDay
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
@ -13,23 +14,37 @@ class QTurnCounter(QGroupBox):
def __init__(self): def __init__(self):
super(QTurnCounter, self).__init__("Turn") super(QTurnCounter, self).__init__("Turn")
self.icons = [CONST.ICONS["Dawn"], CONST.ICONS["Day"], CONST.ICONS["Dusk"], CONST.ICONS["Night"]] self.icons = {
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
self.daytime_icon = QLabel() TimeOfDay.Day: CONST.ICONS["Day"],
self.daytime_icon.setPixmap(self.icons[0]) TimeOfDay.Dusk: CONST.ICONS["Dusk"],
self.turn_info = QLabel() TimeOfDay.Night: CONST.ICONS["Night"],
}
self.layout = QHBoxLayout() self.layout = QHBoxLayout()
self.layout.addWidget(self.daytime_icon)
self.layout.addWidget(self.turn_info)
self.setLayout(self.layout) self.setLayout(self.layout)
def setCurrentTurn(self, turn: int, current_day: datetime): self.daytime_icon = QLabel()
self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
self.layout.addWidget(self.daytime_icon)
self.time_column = QVBoxLayout()
self.layout.addLayout(self.time_column)
self.date_display = QLabel()
self.time_column.addWidget(self.date_display)
self.time_display = QLabel()
self.time_column.addWidget(self.time_display)
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
""" """
Set the money amount to display self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
:arg turn Current turn number self.date_display.setText(conditions.start_time.strftime("%d %b %Y"))
:arg current_day Current day self.time_display.setText(
""" conditions.start_time.strftime("%H:%M:%S Local"))
self.daytime_icon.setPixmap(self.icons[turn % 4])
self.turn_info.setText(current_day.strftime("%d %b %Y"))
self.setTitle("Turn " + str(turn + 1)) self.setTitle("Turn " + str(turn + 1))

View File

@ -21,6 +21,7 @@ from game import Game, db
from game.data.aaa_db import AAA_UNITS from game.data.aaa_db import AAA_UNITS
from game.data.radar_db import UNITS_WITH_RADAR from game.data.radar_db import UNITS_WITH_RADAR
from game.utils import meter_to_feet from game.utils import meter_to_feet
from game.weather import TimeOfDay
from gen import Conflict, PackageWaypointTiming from gen import Conflict, PackageWaypointTiming
from gen.ato import Package from gen.ato import Package
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
@ -509,9 +510,9 @@ class QLiberationMap(QGraphicsView):
scene.addPixmap(bg) scene.addPixmap(bg)
# Apply graphical effects to simulate current daytime # Apply graphical effects to simulate current daytime
if self.game.current_turn_daytime == "day": if self.game.current_turn_time_of_day == TimeOfDay.Day:
pass pass
elif self.game.current_turn_daytime == "night": elif self.game.current_turn_time_of_day == TimeOfDay.Night:
ov = QPixmap(bg.width(), bg.height()) ov = QPixmap(bg.width(), bg.height())
ov.fill(CONST.COLORS["night_overlay"]) ov.fill(CONST.COLORS["night_overlay"])
overlay = scene.addPixmap(ov) overlay = scene.addPixmap(ov)