Spaces:
Sleeping
Sleeping
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 +8 -0
- backend/app/simulation.py +74 -5
- frontend/app/page.tsx +15 -0
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 |
-
|
|
|
|
| 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 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}
|