arkai2025's picture
refactor(config): centralize scenario configuration defaults
85cbecd
"""
Fire-Rescue - Data Models
Defines core data structures for the fire rescue simulation.
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import random
from config import SCENARIO_DEFAULTS
FIRE_COUNT_DEFAULTS = SCENARIO_DEFAULTS["fire_count"]
BUILDING_COUNT_DEFAULTS = SCENARIO_DEFAULTS["building_count"]
class UnitType(str, Enum):
"""Types of firefighting units available."""
FIRE_TRUCK = "fire_truck"
HELICOPTER = "helicopter"
class CellType(str, Enum):
"""Types of terrain cells in the grid."""
EMPTY = "empty"
BUILDING = "building"
FOREST = "forest"
class SimulationStatus(str, Enum):
"""Status of the simulation."""
IDLE = "idle"
RUNNING = "running"
SUCCESS = "success"
FAIL = "fail"
class FireLevel(str, Enum):
"""Initial fire intensity levels."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
@dataclass
class Fire:
"""Represents a fire cell in the grid."""
x: int
y: int
intensity: float # 0.0 to 1.0
def to_dict(self) -> dict:
return {
"x": self.x,
"y": self.y,
"intensity": round(self.intensity, 2)
}
@dataclass
class Unit:
"""Represents a firefighting unit."""
id: str
unit_type: UnitType
owner: str # "player" or "ai"
x: int
y: int
cooldown: int = 0 # Ticks until next action
def to_dict(self) -> dict:
return {
"id": self.id,
"type": self.unit_type.value,
"owner": self.owner,
"x": self.x,
"y": self.y
}
@dataclass
class Event:
"""Represents a simulation event."""
tick: int
event_type: str
details: dict = field(default_factory=dict)
def to_dict(self) -> dict:
return {
"tick": self.tick,
"type": self.event_type,
**self.details
}
@dataclass
class Cell:
"""Represents a cell in the grid."""
x: int
y: int
cell_type: CellType
fire_intensity: float = 0.0 # 0.0 = no fire, 1.0 = max fire
damage: float = 0.0 # Accumulated damage (0.0 to 1.0)
def is_on_fire(self) -> bool:
return self.fire_intensity > 0.0
def is_destroyed(self) -> bool:
return self.damage >= 1.0
@dataclass
class WorldState:
"""
Represents the complete state of the simulation world.
Uses a 2D grid system.
"""
width: int
height: int
tick: int = 0
status: SimulationStatus = SimulationStatus.IDLE
# Grid cells
grid: list[list[Cell]] = field(default_factory=list)
# Units on the field
units: list[Unit] = field(default_factory=list)
# Recent events for logging
recent_events: list[Event] = field(default_factory=list)
# Global metrics
building_integrity: float = 1.0 # Average building health (0.0 to 1.0)
forest_burn_ratio: float = 0.0 # Percentage of forest burned (0.0 to 1.0)
# Configuration
max_ticks: int = 200
max_units: int = 10
seed: Optional[int] = None
# Building positions (for dynamic placement)
building_positions: list[tuple[int, int]] = field(default_factory=list)
# Unit ID counter
_unit_counter: int = 0
def initialize_grid(
self,
seed: Optional[int] = None,
fire_count: int = 4,
fire_intensity: float = 0.6,
building_count: int = 16
):
f"""
Initialize the grid with terrain and initial fires.
Args:
seed: Random seed for reproducibility
fire_count: Number of initial fire points ({int(FIRE_COUNT_DEFAULTS.minimum)}-{int(FIRE_COUNT_DEFAULTS.maximum)})
fire_intensity: Initial fire intensity (0.0-1.0)
building_count: Number of buildings to place ({int(BUILDING_COUNT_DEFAULTS.minimum)}-{int(BUILDING_COUNT_DEFAULTS.maximum)})
"""
if seed is not None:
random.seed(seed)
self.seed = seed
# Clamp values using shared configuration
fire_count = int(FIRE_COUNT_DEFAULTS.clamp(fire_count))
fire_intensity = max(0.0, min(1.0, fire_intensity))
building_count = int(BUILDING_COUNT_DEFAULTS.clamp(building_count))
# Generate random building positions (connected cluster)
self.building_positions = self._generate_building_positions(building_count)
building_set = set(self.building_positions)
self.grid = []
for y in range(self.height):
row = []
for x in range(self.width):
# Default to forest
cell_type = CellType.FOREST
# Place buildings at generated positions
if (x, y) in building_set:
cell_type = CellType.BUILDING
row.append(Cell(x=x, y=y, cell_type=cell_type))
self.grid.append(row)
# Place initial fires
fires_placed = 0
attempts = 0
max_attempts = 100
while fires_placed < fire_count and attempts < max_attempts:
x = random.randint(0, self.width - 1)
y = random.randint(0, self.height - 1)
# Only place fire on forest initially (not buildings)
cell = self.grid[y][x]
if cell.cell_type == CellType.FOREST and cell.fire_intensity == 0:
cell.fire_intensity = fire_intensity
fires_placed += 1
attempts += 1
def _generate_building_positions(self, count: int) -> list[tuple[int, int]]:
"""
Generate random building positions ensuring connectivity.
Buildings grow as a connected cluster from a random starting point.
At least 2 buildings will be adjacent (if count >= 2).
"""
if count <= 0:
return []
positions = []
# Start with a random position (avoid edges for better growth)
start_x = random.randint(2, self.width - 3)
start_y = random.randint(2, self.height - 3)
positions.append((start_x, start_y))
if count == 1:
return positions
# Grow the cluster by adding adjacent cells
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] # 4-directional adjacency
while len(positions) < count:
# Get all possible adjacent positions to existing buildings
candidates = set()
for (px, py) in positions:
for dx, dy in directions:
nx, ny = px + dx, py + dy
# Check bounds and not already a building
if 0 <= nx < self.width and 0 <= ny < self.height:
if (nx, ny) not in positions:
candidates.add((nx, ny))
if not candidates:
# No more valid positions (unlikely but handle it)
break
# Randomly select one candidate
new_pos = random.choice(list(candidates))
positions.append(new_pos)
return positions
def get_cell(self, x: int, y: int) -> Optional[Cell]:
"""Get cell at position, returns None if out of bounds."""
if 0 <= x < self.width and 0 <= y < self.height:
return self.grid[y][x]
return None
def get_fires(self) -> list[Fire]:
"""Get list of all active fires."""
fires = []
for row in self.grid:
for cell in row:
if cell.is_on_fire():
fires.append(Fire(x=cell.x, y=cell.y, intensity=cell.fire_intensity))
return fires
def generate_unit_id(self, unit_type: UnitType) -> str:
"""Generate a unique unit ID."""
self._unit_counter += 1
prefix = "truck" if unit_type == UnitType.FIRE_TRUCK else "heli"
return f"{prefix}_{self._unit_counter}"
def add_unit(self, unit_type: UnitType, x: int, y: int, source: str) -> Optional[Unit]:
"""Add a new unit to the world. Returns None if limit reached or position invalid."""
if len(self.units) >= self.max_units:
return None
if not (0 <= x < self.width and 0 <= y < self.height):
return None
# Check cell conditions - cannot deploy on fire or buildings
cell = self.get_cell(x, y)
if cell is None:
return None
# Cannot deploy on burning cells
if cell.fire_intensity > 0:
return None
# Cannot deploy on buildings
if cell.cell_type == CellType.BUILDING:
return None
unit = Unit(
id=self.generate_unit_id(unit_type),
unit_type=unit_type,
owner="player",
x=x,
y=y
)
self.units.append(unit)
# Record event
self.recent_events.append(Event(
tick=self.tick,
event_type="deploy_unit",
details={
"by": source,
"unit_type": unit_type.value,
"x": x,
"y": y
}
))
# Keep only recent events (last 20)
if len(self.recent_events) > 20:
self.recent_events = self.recent_events[-20:]
return unit
def calculate_metrics(self):
"""Recalculate global metrics (building damage ratio)."""
total_buildings = 0
burning_buildings = 0
for row in self.grid:
for cell in row:
if cell.cell_type == CellType.BUILDING:
total_buildings += 1
# Building is burning if it has fire on it
if cell.fire_intensity > 0:
burning_buildings += 1
# Building integrity: ratio of non-burning buildings
if total_buildings > 0:
self.building_integrity = (total_buildings - burning_buildings) / total_buildings
else:
self.building_integrity = 1.0
# Store total buildings for reference
self._total_buildings = total_buildings
self._burning_buildings = burning_buildings
# Forest burn ratio is no longer used (replaced by active fires count)
self.forest_burn_ratio = 0.0
def to_dict(self) -> dict:
"""Serialize world state to dictionary."""
return {
"tick": self.tick,
"status": self.status.value,
"width": self.width,
"height": self.height,
"fires": [f.to_dict() for f in self.get_fires()],
"units": [u.to_dict() for u in self.units],
"building_integrity": round(self.building_integrity, 2),
"forest_burn_ratio": round(self.forest_burn_ratio, 2),
"recent_events": [e.to_dict() for e in self.recent_events[-5:]],
"buildings": [{"x": x, "y": y} for x, y in self.building_positions],
"max_units": self.max_units,
}