"use client" import { useEffect, useRef, useState } from "react" type Agent = { model_name: string x: number y: number alive: boolean water_collected: boolean is_leader: boolean mode: "solo" | "coalition" status: "searching" | "collecting_water" | "extinguishing_fire" | "escaping" | "idle" } type Fire = { x: number y: number radius: number intensity: number } type WaterSource = { id: string x: number y: number water_amount: number } const BACKEND_W = 1200 const BACKEND_H = 800 function getAgentColor(name: string) { let hash = 0 for (let i = 0; i < name.length; i++) { hash = name.charCodeAt(i) + ((hash << 5) - hash) } return `hsl(${hash % 360}, 70%, 60%)` } function getStatusColor(status: string) { switch (status) { case "searching": return "#3b82f6" // blue case "collecting_water": return "#06b6d4" // cyan case "extinguishing_fire": return "#ef4444" // red case "escaping": return "#f59e0b" // amber default: return "#6b7280" // gray } } export default function MapCanvas({ agents, fire, waterSources, waitingForScenario, gameOver, winnerLabel, mapSize, onMapClick, tracks, }: { agents: Agent[] fire: Fire | null waterSources: WaterSource[] waitingForScenario: boolean gameOver: boolean winnerLabel?: string | null mapSize: { width: number; height: number } onMapClick?: (x: number, y: number) => void tracks?: Record }) { const gridSize = 40 const sx = (bx: number) => (bx / BACKEND_W) * mapSize.width const sy = (by: number) => (by / BACKEND_H) * mapSize.height const leader = agents.find(a => a.alive && a.is_leader) const coalitionAgents = agents.filter(a => a.alive && a.mode === "coalition" && !a.is_leader) function handleClick(e: React.MouseEvent) { if (!waitingForScenario || !onMapClick) return const rect = e.currentTarget.getBoundingClientRect() const backendX = Math.round(((e.clientX - rect.left) / mapSize.width) * BACKEND_W) const backendY = Math.round(((e.clientY - rect.top) / mapSize.height) * BACKEND_H) onMapClick(backendX, backendY) } return (
{/* Grid */}
{/* Fire */} {fire && (
)} {/* Water Sources */} {waterSources.map((water) => (
💧
))} {/* Coalition Links */} {leader && coalitionAgents.length > 0 && ( {coalitionAgents.map((agent) => ( ))} )} {/* Tracks */} {tracks && ( {Object.entries(tracks).map(([model, points]) => ( points.length > 1 && ( `${sx(p.x)},${sy(p.y)}`).join(" ")} fill="none" stroke={getAgentColor(model)} strokeWidth={2} strokeOpacity={0.6} /> ) ))} )} {/* Agents */} {agents.map((agent) => { const color = getAgentColor(agent.model_name) const isDead = !agent.alive const statusColor = getStatusColor(agent.status) const nodeSize = agent.water_collected ? 10 : 6 return (
{!isDead && (
{agent.model_name} {agent.is_leader && 👑} {agent.water_collected && 💧}
{agent.status}
)}
{isDead && (
💀
)}
) })} {/* Fire Intensity Meter */} {fire && (
Fire Intensity
{fire.intensity.toFixed(0)}%
)} {/* Coalition Panel */}
🎯 Coalition Status
{agents.filter(a => a.alive && a.mode === "coalition").map(agent => (
{agent.is_leader ? "👑 " : " "}{agent.model_name.split("/")[0]} {agent.water_collected && " 💧"}
))} {agents.filter(a => a.alive && a.mode === "solo").length > 0 && (
Lone Wolves:
)} {agents.filter(a => a.alive && a.mode === "solo").map(agent => (
🐺 {agent.model_name.split("/")[0]} {agent.water_collected && " 💧"}
))}
{/* Instructions */} {waitingForScenario && (
Click to place the fire and begin
)} {/* Game Over */} {gameOver && (

🏁 GAME OVER

{fire && fire.intensity <= 0 ? "🔥 FIRE EXTINGUISHED!" : "❌ ALL BURNED"}

{winnerLabel || "The flames consumed the arena"}

)}
) }