"""Weather system with procedural transitions.""" from __future__ import annotations import enum import random from dataclasses import dataclass class WeatherCondition(enum.Enum): clear = "clear" cloudy = "cloudy" rain = "rain" storm = "storm" snow = "snow" fog = "fog" @dataclass class WeatherState: condition: WeatherCondition intensity: float # 0.0 to 1.0 def get_weather_description(state: WeatherState) -> str: """Return atmospheric text for the current weather.""" condition = state.condition intensity = state.intensity if condition == WeatherCondition.clear: return "the sky is clear" elif condition == WeatherCondition.cloudy: if intensity < 0.4: return "thin clouds drift overhead" elif intensity < 0.7: return "clouds fill the sky" else: return "heavy clouds loom darkly" elif condition == WeatherCondition.rain: if intensity < 0.3: return "a light drizzle falls" elif intensity < 0.6: return "rain patters steadily" else: return "rain hammers down relentlessly" elif condition == WeatherCondition.storm: if intensity < 0.5: return "thunder rumbles in the distance" else: return "lightning splits the sky as the storm rages" elif condition == WeatherCondition.snow: if intensity < 0.3: return "light snow drifts down" elif intensity < 0.6: return "snow falls steadily" else: return "heavy snow blankets everything" elif condition == WeatherCondition.fog: if intensity < 0.4: return "thin mist hangs in the air" elif intensity < 0.7: return "fog obscures the distance" else: return "thick fog shrouds everything" return "" # Climate profiles: transition weights for each condition # Format: {from_condition: {to_condition: weight, ...}, ...} CLIMATE_PROFILES = { "temperate": { WeatherCondition.clear: { "clear": 50, "cloudy": 40, "fog": 10, }, WeatherCondition.cloudy: { "clear": 30, "cloudy": 35, "rain": 20, "snow": 10, "fog": 5, }, WeatherCondition.rain: { "cloudy": 35, "rain": 30, "storm": 15, "snow": 10, "fog": 10, }, WeatherCondition.storm: { "rain": 60, "cloudy": 40, }, WeatherCondition.snow: { "snow": 40, "cloudy": 35, "fog": 25, }, WeatherCondition.fog: { "fog": 30, "cloudy": 40, "clear": 30, }, }, "arid": { WeatherCondition.clear: { "clear": 90, "cloudy": 10, }, WeatherCondition.cloudy: { "clear": 70, "cloudy": 20, "rain": 5, "fog": 5, }, WeatherCondition.rain: { "cloudy": 60, "rain": 20, "clear": 20, }, WeatherCondition.storm: { "rain": 50, "cloudy": 50, }, WeatherCondition.snow: { "cloudy": 100, }, WeatherCondition.fog: { "clear": 60, "fog": 20, "cloudy": 20, }, }, "arctic": { WeatherCondition.clear: { "cloudy": 50, "fog": 30, "clear": 20, }, WeatherCondition.cloudy: { "snow": 40, "cloudy": 35, "fog": 25, }, WeatherCondition.rain: { "snow": 40, "cloudy": 40, "fog": 20, }, WeatherCondition.storm: { "snow": 60, "cloudy": 40, }, WeatherCondition.snow: { "snow": 50, "cloudy": 30, "fog": 20, }, WeatherCondition.fog: { "fog": 40, "cloudy": 40, "snow": 20, }, }, } def advance_weather( current: WeatherState, season: str = "summer", rng: random.Random | None = None, climate: str = "temperate", ) -> WeatherState: """Advance weather by one step (one game hour).""" if rng is None: rng = random.Random() # Get climate profile profile = CLIMATE_PROFILES.get(climate, CLIMATE_PROFILES["temperate"]) # Get transition weights for current condition transitions = profile.get(current.condition, {}) # Filter out snow if not winter/autumn if season not in ("winter", "autumn"): transitions = {k: v for k, v in transitions.items() if k != "snow"} # If no valid transitions, stay in current condition if not transitions: return WeatherState( condition=current.condition, intensity=rng.uniform(0.0, 1.0) ) # Weighted random choice conditions = list(transitions.keys()) weights = list(transitions.values()) # Convert string keys to WeatherCondition enums resolved_conditions = [] for cond in conditions: if isinstance(cond, str): resolved_conditions.append(WeatherCondition[cond]) else: resolved_conditions.append(cond) new_condition = rng.choices(resolved_conditions, weights=weights, k=1)[0] # Generate appropriate intensity for the new condition if new_condition == WeatherCondition.clear: new_intensity = 0.5 # Clear has no meaningful intensity variation elif new_condition == WeatherCondition.storm: new_intensity = rng.uniform(0.5, 1.0) # Storms are always intense else: new_intensity = rng.uniform(0.0, 1.0) return WeatherState(condition=new_condition, intensity=new_intensity) # Global weather state _current_weather: WeatherState | None = None def init_weather( condition: WeatherCondition = WeatherCondition.clear, intensity: float = 0.5 ) -> None: """Initialize the global weather state. Args: condition: Initial weather condition intensity: Initial intensity (0.0 to 1.0) """ global _current_weather _current_weather = WeatherState(condition=condition, intensity=intensity) def get_current_weather() -> WeatherState: """Get the current global weather state. Returns: Current weather state (defaults to clear if not initialized) """ if _current_weather is None: return WeatherState(condition=WeatherCondition.clear, intensity=0.5) return _current_weather def tick_weather(season: str = "summer", climate: str = "temperate") -> None: """Advance weather by one step. Called once per game hour. Args: season: Current season climate: Climate profile to use """ global _current_weather current = get_current_weather() _current_weather = advance_weather(current, season=season, climate=climate) def get_weather_ambience(condition: WeatherCondition) -> list[str]: """Return ambient messages appropriate for the weather.""" if condition == WeatherCondition.rain: return [ "rain patters on the ground around you.", "water drips from above.", "you hear the steady rhythm of rainfall.", ] elif condition == WeatherCondition.storm: return [ "thunder cracks overhead.", "lightning flashes in the distance.", "the wind howls fiercely.", ] elif condition == WeatherCondition.snow: return [ "snowflakes drift silently down.", "the world is muffled under falling snow.", ] elif condition == WeatherCondition.fog: return [ "mist swirls around your feet.", "shapes loom and fade in the fog.", ] else: # clear or cloudy: no extra ambience return []