adityaverma977 commited on
Commit
30e238f
·
1 Parent(s): 25f6a95

Add fire status ETA event, probabilistic lethality by intensity, display ETA in UI

Browse files
backend/app/models.py CHANGED
@@ -104,6 +104,14 @@ class FireSpreadEvent(BaseModel):
104
  new_intensity: float
105
 
106
 
 
 
 
 
 
 
 
 
107
  class ChatEntry(BaseModel):
108
  agent_id: str
109
  message: str
 
104
  new_intensity: float
105
 
106
 
107
+ class FireStatusEvent(BaseModel):
108
+ type: Literal["fire_status"] = "fire_status"
109
+ radius: float
110
+ intensity: float
111
+ ticks_to_extinguish: Optional[int] = None
112
+ secs_to_extinguish: Optional[float] = None
113
+
114
+
115
  class ChatEntry(BaseModel):
116
  agent_id: str
117
  message: str
backend/app/simulation.py CHANGED
@@ -1,6 +1,7 @@
1
  import asyncio
2
  import math
3
- from typing import Union
 
4
 
5
  from .models import (
6
  AgentModel,
@@ -11,6 +12,7 @@ from .models import (
11
  WaterCollectedEvent,
12
  FireExtinguishedEvent,
13
  FireSpreadEvent,
 
14
  SimulationState,
15
  TickResponse,
16
  ChatEntry,
@@ -121,6 +123,19 @@ class SimulationEngine:
121
  death_events = self._kill_agents_in_fire(living_agents, fire)
122
  events.extend(death_events)
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  # 7. Check win condition
125
  self.state.round += 1
126
  living_count = len([a for a in self.state.agents if a.alive])
@@ -301,10 +316,64 @@ class SimulationEngine:
301
 
302
  dist_to_fire = math.dist((agent.x, agent.y), (fire.x, fire.y))
303
 
304
- # Agent dies if inside fire radius
 
305
  if dist_to_fire < fire.radius:
306
- agent.alive = False
307
- events.append(DeathEvent(model=agent.model_name))
308
- events.append(MessageEvent(model=agent.model_name, content="No!!! The fire got me..."))
 
 
 
 
 
 
 
309
 
310
  return events
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import asyncio
2
  import math
3
+ import random
4
+ from typing import Union, Optional
5
 
6
  from .models import (
7
  AgentModel,
 
12
  WaterCollectedEvent,
13
  FireExtinguishedEvent,
14
  FireSpreadEvent,
15
+ FireStatusEvent,
16
  SimulationState,
17
  TickResponse,
18
  ChatEntry,
 
123
  death_events = self._kill_agents_in_fire(living_agents, fire)
124
  events.extend(death_events)
125
 
126
+ # Add fire status event (ETA/progress) for frontend visualization
127
+ try:
128
+ status = self._compute_fire_status(living_agents, fire)
129
+ if status is not None:
130
+ events.append(FireStatusEvent(
131
+ radius=status['radius'],
132
+ intensity=status['intensity'],
133
+ ticks_to_extinguish=status['ticks_to_extinguish'],
134
+ secs_to_extinguish=status['secs_to_extinguish'],
135
+ ))
136
+ except Exception:
137
+ pass
138
+
139
  # 7. Check win condition
140
  self.state.round += 1
141
  living_count = len([a for a in self.state.agents if a.alive])
 
316
 
317
  dist_to_fire = math.dist((agent.x, agent.y), (fire.x, fire.y))
318
 
319
+ # Agent dies if inside fire radius.
320
+ # Use intensity-based lethality: high intensity => near-certain death; lower intensity => probabilistic.
321
  if dist_to_fire < fire.radius:
322
+ # Immediate fatality at very high intensity
323
+ if fire.intensity >= 90.0:
324
+ lethal = True
325
+ else:
326
+ lethal = (random.random() < (fire.intensity / 100.0))
327
+
328
+ if lethal:
329
+ agent.alive = False
330
+ events.append(DeathEvent(model=agent.model_name))
331
+ events.append(MessageEvent(model=agent.model_name, content="No!!! The fire got me..."))
332
 
333
  return events
334
+
335
+ def _compute_fire_status(self, agents, fire) -> Optional[dict]:
336
+ """Estimate ticks and seconds to extinguish based on current agents with water.
337
+ Returns dict with radius, intensity, ticks_to_extinguish, secs_to_extinguish or None if not applicable."""
338
+ if not fire:
339
+ return None
340
+
341
+ # Count agents actively extinguishing (carrying water and near enough)
342
+ extinguishers = 0
343
+ for a in agents:
344
+ if a.water_collected and a.status == 'extinguishing_fire':
345
+ dist = math.dist((a.x, a.y), (fire.x, fire.y))
346
+ if dist <= fire.radius + EXTINGUISH_RANGE:
347
+ extinguishers += 1
348
+
349
+ if extinguishers == 0:
350
+ return {
351
+ 'radius': fire.radius,
352
+ 'intensity': fire.intensity,
353
+ 'ticks_to_extinguish': None,
354
+ 'secs_to_extinguish': None,
355
+ }
356
+
357
+ living_count = len([a for a in agents if a.alive]) or 1
358
+ scale = (7.0) / (living_count + 2.0)
359
+ scale = max(0.5, min(2.5, scale))
360
+ per_agent_rate = BASE_EXTINGUISH_RATE * scale
361
+ per_agent_rate = max(MIN_EXTINGUISH_RATE, min(MAX_EXTINGUISH_RATE, per_agent_rate))
362
+
363
+ per_tick_reduction = extinguishers * per_agent_rate
364
+ if per_tick_reduction <= 0:
365
+ return {
366
+ 'radius': fire.radius,
367
+ 'intensity': fire.intensity,
368
+ 'ticks_to_extinguish': None,
369
+ 'secs_to_extinguish': None,
370
+ }
371
+
372
+ ticks = math.ceil(fire.intensity / per_tick_reduction)
373
+ secs = ticks * TICK_INTERVAL_SECONDS
374
+ return {
375
+ 'radius': fire.radius,
376
+ 'intensity': fire.intensity,
377
+ 'ticks_to_extinguish': int(ticks),
378
+ 'secs_to_extinguish': float(secs),
379
+ }
frontend/app/page.tsx CHANGED
@@ -111,6 +111,7 @@ export default function Page() {
111
  const [tickHistory, setTickHistory] = useState<any[]>([])
112
  const [reportOpen, setReportOpen] = useState(false)
113
  const [reportData, setReportData] = useState<any | null>(null)
 
114
  const [mapSize, setMapSize] = useState({ width: 1200, height: 800 })
115
  const wsRef = useRef<WebSocket | null>(null)
116
  const mapDivRef = useRef<HTMLDivElement>(null)
@@ -168,6 +169,10 @@ export default function Page() {
168
  if (e.type === 'alliance_proposal') return { agent_id: e.from_model, text: '', type: 'alliance_proposal', to_model: e.to_model }
169
  if (e.type === 'alliance_accept') return { agent_id: e.model_a, text: '', type: 'alliance_accept', to_model: e.model_b }
170
  if (e.type === 'alliance_reject') return { agent_id: e.from_model, text: '', type: 'alliance_reject', to_model: e.to_model }
 
 
 
 
171
  return null
172
  }).filter(Boolean)
173
  setChatMessages(prevMsgs => [...prevMsgs, ...newMsgs])
@@ -249,6 +254,16 @@ export default function Page() {
249
  </div>
250
 
251
  <div className="p-6 border-t border-white/5">
 
 
 
 
 
 
 
 
 
 
252
  {appState === "selecting" && (
253
  <button
254
  onClick={handleStart}
 
111
  const [tickHistory, setTickHistory] = useState<any[]>([])
112
  const [reportOpen, setReportOpen] = useState(false)
113
  const [reportData, setReportData] = useState<any | null>(null)
114
+ const [fireStatus, setFireStatus] = useState<any | null>(null)
115
  const [mapSize, setMapSize] = useState({ width: 1200, height: 800 })
116
  const wsRef = useRef<WebSocket | null>(null)
117
  const mapDivRef = useRef<HTMLDivElement>(null)
 
169
  if (e.type === 'alliance_proposal') return { agent_id: e.from_model, text: '', type: 'alliance_proposal', to_model: e.to_model }
170
  if (e.type === 'alliance_accept') return { agent_id: e.model_a, text: '', type: 'alliance_accept', to_model: e.model_b }
171
  if (e.type === 'alliance_reject') return { agent_id: e.from_model, text: '', type: 'alliance_reject', to_model: e.to_model }
172
+ if (e.type === 'fire_status') {
173
+ setFireStatus({ intensity: e.intensity, radius: e.radius, ticks: e.ticks_to_extinguish, secs: e.secs_to_extinguish })
174
+ return null
175
+ }
176
  return null
177
  }).filter(Boolean)
178
  setChatMessages(prevMsgs => [...prevMsgs, ...newMsgs])
 
254
  </div>
255
 
256
  <div className="p-6 border-t border-white/5">
257
+ {simState?.fire && (
258
+ <div className="mb-4 text-xs font-mono text-white/60">
259
+ <div>Fire Intensity: <span className="font-bold text-white">{Math.round(simState.fire.intensity)}%</span></div>
260
+ {fireStatus && fireStatus.secs != null ? (
261
+ <div>ETA to extinguish: <span className="font-bold text-white">{Math.round(fireStatus.secs)}s</span></div>
262
+ ) : (
263
+ <div>ETA to extinguish: <span className="text-white/30">—</span></div>
264
+ )}
265
+ </div>
266
+ )}
267
  {appState === "selecting" && (
268
  <button
269
  onClick={handleStart}