File size: 11,290 Bytes
ec380bc 85cbecd ec380bc 85cbecd ec380bc 85cbecd ec380bc 85cbecd ec380bc 85cbecd ec380bc 85cbecd ec380bc |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 |
"""
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,
}
|