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.infos.information import Information
from game.operation.operation import Operation
from gen.environmentgen import EnvironmentSettings
from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
from theater.start_generator import generate_airbase_defense_group
@ -42,7 +41,6 @@ class Event:
operation = None # type: Operation
difficulty = 1 # type: int
environment_settings = None # type: EnvironmentSettings
BONUS_BASE = 5
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 random
import sys
from datetime import datetime, timedelta
from datetime import date, datetime, timedelta
from typing import Any, Dict, List
from dcs.action import Coalition
@ -28,6 +28,7 @@ from .event.event import Event, UnitsDeliveryEvent
from .event.frontlineattack import FrontlineAttackEvent
from .infos.information import Information
from .settings import Settings
from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4
COMMISION_LIMITS_SCALE = 1.5
@ -78,7 +79,7 @@ class Game:
self.enemy_name = enemy_name
self.enemy_country = db.FACTIONS[enemy_name]["country"]
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.update(self)
self.ground_planners: Dict[int, GroundPlanner] = {}
@ -91,6 +92,8 @@ class Game:
self.current_unit_id = 0
self.current_group_id = 0
self.conditions = self.generate_conditions()
self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder()
@ -101,6 +104,9 @@ class Game:
self.sanitize_sides()
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):
"""
@ -218,6 +224,12 @@ class Game:
def on_load(self) -> None:
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):
logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
@ -252,6 +264,8 @@ class Game:
for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp)
self.conditions = self.generate_conditions()
# Plan flights & combat for next turn
self.__culling_points = self.compute_conflicts_position()
self.ground_planners = {}
@ -340,11 +354,11 @@ class Game:
self.informations.append(info)
@property
def current_turn_daytime(self):
return ["dawn", "day", "dusk", "night"][self.turn % 4]
def current_turn_time_of_day(self) -> TimeOfDay:
return list(TimeOfDay)[self.turn % 4]
@property
def current_day(self):
def current_day(self) -> date:
return self.date + timedelta(days=self.turn // 4)
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.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator
from gen.environmentgen import EnviromentGenerator
from gen.environmentgen import EnvironmentGenerator
from gen.forcedoptionsgen import ForcedOptionsGenerator
from gen.groundobjectsgen import GroundObjectsGenerator
from gen.kneeboard import KneeboardGenerator
@ -45,7 +45,6 @@ class Operation:
triggersgen = None # type: TriggersGenerator
airsupportgen = None # type: AirSupportConflictGenerator
visualgen = None # type: VisualGenerator
envgen = None # type: EnviromentGenerator
groundobjectgen = None # type: GroundObjectsGenerator
briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator
@ -162,13 +161,9 @@ class Operation:
for frequency in unique_map_frequencies:
radio_registry.reserve(frequency)
# Generate meteo
envgen = EnviromentGenerator(self.current_mission, self.conflict,
self.game)
if self.environment_settings is None:
self.environment_settings = envgen.generate()
else:
envgen.load(self.environment_settings)
# Set mission time and weather conditions.
EnvironmentGenerator(self.current_mission,
self.game.conditions).generate()
# 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
import random
from datetime import timedelta
from typing import Optional
from dcs.mission import Mission
from dcs.weather import Weather, Wind
from .conflictgen import Conflict
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
}
from game.weather import Clouds, Fog, Conditions, WindConditions
class EnvironmentSettings:
weather_dict = None
start_time = None
class EnviromentGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game):
class EnvironmentGenerator:
def __init__(self, mission: Mission, conditions: Conditions) -> None:
self.mission = mission
self.conflict = conflict
self.game = game
self.conditions = conditions
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
logging.info("Mission time will be {}".format(daytime))
if self.game.settings.night_disabled:
logging.info("Skip Night mission due to user settings")
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 set_wind(self, wind: WindConditions) -> None:
self.mission.weather.wind_at_ground = wind.at_0m
self.mission.weather.wind_at_2000 = wind.at_2000m
self.mission.weather.wind_at_8000 = wind.at_8000m
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:
return
self.turnCounter.setCurrentTurn(game.turn, game.current_day)
self.turnCounter.setCurrentTurn(game.turn, game.conditions)
self.budgetBox.setGame(game)
self.factionsInfos.setGame(game)

View File

@ -1,7 +1,8 @@
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
@ -13,23 +14,37 @@ class QTurnCounter(QGroupBox):
def __init__(self):
super(QTurnCounter, self).__init__("Turn")
self.icons = [CONST.ICONS["Dawn"], CONST.ICONS["Day"], CONST.ICONS["Dusk"], CONST.ICONS["Night"]]
self.daytime_icon = QLabel()
self.daytime_icon.setPixmap(self.icons[0])
self.turn_info = QLabel()
self.icons = {
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
TimeOfDay.Day: CONST.ICONS["Day"],
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
TimeOfDay.Night: CONST.ICONS["Night"],
}
self.layout = QHBoxLayout()
self.layout.addWidget(self.daytime_icon)
self.layout.addWidget(self.turn_info)
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
:arg turn Current turn number
:arg current_day Current day
"""
self.daytime_icon.setPixmap(self.icons[turn % 4])
self.turn_info.setText(current_day.strftime("%d %b %Y"))
self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
self.date_display.setText(conditions.start_time.strftime("%d %b %Y"))
self.time_display.setText(
conditions.start_time.strftime("%H:%M:%S Local"))
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.radar_db import UNITS_WITH_RADAR
from game.utils import meter_to_feet
from game.weather import TimeOfDay
from gen import Conflict, PackageWaypointTiming
from gen.ato import Package
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
@ -509,9 +510,9 @@ class QLiberationMap(QGraphicsView):
scene.addPixmap(bg)
# 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
elif self.game.current_turn_daytime == "night":
elif self.game.current_turn_time_of_day == TimeOfDay.Night:
ov = QPixmap(bg.width(), bg.height())
ov.fill(CONST.COLORS["night_overlay"])
overlay = scene.addPixmap(ov)