| """ |
| Operational briefing system — generates a structured incident briefing on reset(). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import random |
| from typing import TYPE_CHECKING, List, Optional, Tuple |
|
|
| from pydantic import BaseModel |
|
|
| if TYPE_CHECKING: |
| import numpy as np |
| from .grid import Grid |
| from .models import TierConfig |
|
|
| _IGNITION_CAUSES = [ |
| "Lightning strike", |
| "Downed power line", |
| "Unattended campfire", |
| "Equipment spark", |
| "Arson (under investigation)", |
| "Vehicle exhaust", |
| ] |
|
|
| _INFRA_LABELS = ["North Road", "East Road", "Supply Route", "Evacuation Corridor"] |
|
|
| _WIND_SHIFT_FORECASTS = [ |
| "Wind shift southwest expected by step 60.", |
| "Forecast: wind backing to northwest by step 70, speed increasing.", |
| "Weather service warns of sudden direction change near step 65.", |
| "Dry front approaching — wind shift likely between steps 55 and 80.", |
| ] |
|
|
| _GENERIC_FORECASTS = [ |
| "Humidity expected to drop below 20% by mid-episode.", |
| "Elevated fire weather conditions through entire operational period.", |
| "No precipitation expected. Fire behavior will remain extreme.", |
| "Overnight humidity recovery may assist suppression after step 100.", |
| ] |
|
|
|
|
| class OperationalBriefing(BaseModel): |
| incident_id: str |
| ignition_cause: str |
| priority_populated_zones: List[Tuple[int, int]] |
| priority_infrastructure: List[Tuple[int, int]] |
| forecast_events: List[str] |
| declared_time: str |
|
|
|
|
| def generate_briefing( |
| tier_config: "TierConfig", |
| rng: "np.random.Generator", |
| grid: "Grid", |
| ) -> OperationalBriefing: |
| py_rng = random.Random(int(rng.integers(0, 2**31))) |
|
|
| |
| pop_cells: List[Tuple[int, int, int]] = [] |
| for r in range(grid.rows): |
| for c in range(grid.cols): |
| static = grid.static_grid[r][c] |
| if static.is_populated and static.population > 0: |
| pop_cells.append((r, c, static.population)) |
|
|
| pop_cells.sort(key=lambda x: x[2], reverse=True) |
| priority_zones = [(r, c) for r, c, _ in pop_cells[:2]] |
|
|
| |
| from .models import FuelType |
| road_cells = [ |
| (r, c) |
| for r in range(grid.rows) |
| for c in range(grid.cols) |
| if grid.static_grid[r][c].fuel_type == FuelType.ROAD |
| ] |
| |
| step = max(1, len(road_cells) // 2) |
| infra = road_cells[::step][:2] |
|
|
| |
| forecasts = [] |
| if tier_config.enable_wind_shifts: |
| forecasts.append(py_rng.choice(_WIND_SHIFT_FORECASTS)) |
| forecasts.append(py_rng.choice(_GENERIC_FORECASTS)) |
|
|
| |
| hour = py_rng.randint(0, 23) |
| minute = py_rng.choice([0, 15, 30, 45]) |
| incident_id = f"WF-{tier_config.tier_name.upper()[:3]}-{py_rng.randint(1000, 9999)}" |
| declared_time = f"{hour:02d}:{minute:02d}" |
|
|
| return OperationalBriefing( |
| incident_id=incident_id, |
| ignition_cause=py_rng.choice(_IGNITION_CAUSES), |
| priority_populated_zones=priority_zones, |
| priority_infrastructure=infra, |
| forecast_events=forecasts, |
| declared_time=declared_time, |
| ) |
|
|
|
|
| def briefing_to_text(briefing: OperationalBriefing) -> str: |
| zone_list = ", ".join(f"({r},{c})" for r, c in briefing.priority_populated_zones) or "none identified" |
| infra_list = ", ".join(f"({r},{c})" for r, c in briefing.priority_infrastructure) or "none identified" |
| forecast_lines = "\n".join(f"- {f}" for f in briefing.forecast_events) |
|
|
| return ( |
| f"=== OPERATIONAL BRIEFING ===\n" |
| f"Incident {briefing.incident_id} declared at {briefing.declared_time}.\n" |
| f"Cause: {briefing.ignition_cause}.\n" |
| f"\n" |
| f"PRIORITY 1: Protect populated zones at {zone_list}.\n" |
| f"PRIORITY 2: Maintain routes at {infra_list} open where possible.\n" |
| f"\n" |
| f"FORECAST:\n" |
| f"{forecast_lines}\n" |
| f"\n" |
| f"Commander's intent: Contain fire with zero civilian casualties. " |
| f"Preserve crew safety." |
| ) |
|
|