adityaverma977 commited on
Commit
bc3902d
·
1 Parent(s): 263efa8

Collect tick history, render tracks and show per-model report

Browse files
frontend/app/page.tsx CHANGED
@@ -7,16 +7,95 @@ import ChatFeed from "../components/ChatFeed"
7
  import ModelSelector from "../components/ModelSelector"
8
  import { startSimulation, placeVolcano } from "../lib/api"
9
  import { createSimulationSocket } from "../lib/websocket"
 
10
 
11
  type AppState = "loading" | "selecting" | "placing" | "running" | "gameover"
12
 
13
  export default function Page() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  const [appState, setAppState] = useState<AppState>("loading")
15
  const [models, setModels] = useState<string[]>([])
16
  const [simState, setSimState] = useState<any>(null)
17
  const [chatMessages, setChatMessages] = useState<any[]>([])
18
  const [winnerLabel, setWinnerLabel] = useState<string | null>(null)
19
  const [loading, setLoading] = useState(false)
 
 
 
20
  const [mapSize, setMapSize] = useState({ width: 1200, height: 800 })
21
  const wsRef = useRef<WebSocket | null>(null)
22
  const mapDivRef = useRef<HTMLDivElement>(null)
@@ -40,6 +119,7 @@ export default function Page() {
40
  setSimState(data.state)
41
  setWinnerLabel(null)
42
  setChatMessages([])
 
43
  setAppState("placing")
44
  } catch (err) {
45
  console.error(err)
@@ -58,6 +138,8 @@ export default function Page() {
58
  const ws = createSimulationSocket(
59
  simState.simulation_id,
60
  (msg) => {
 
 
61
  if (msg.type === "finished") {
62
  if (msg.state) {
63
  setSimState(msg.state)
@@ -89,6 +171,9 @@ export default function Page() {
89
  if (msg.state?.status === "finished") {
90
  setWinnerLabel(msg.state.winner_model || null)
91
  setAppState("gameover")
 
 
 
92
  }
93
  },
94
  () => setAppState("gameover")
@@ -114,6 +199,7 @@ export default function Page() {
114
  winnerLabel={winnerLabel}
115
  mapSize={mapSize}
116
  onMapClick={handleMapClick}
 
117
  />
118
  </div>
119
 
@@ -154,15 +240,23 @@ export default function Page() {
154
  </div>
155
  )}
156
  {(appState === "running" || appState === "gameover") && (
157
- <button
158
- onClick={() => window.location.reload()}
159
- className="w-full bg-white/5 text-white/50 font-mono text-[10px] py-3 rounded-lg hover:bg-white/10 transition-all uppercase tracking-widest"
160
- >
161
- Reset Arena
162
- </button>
 
 
 
 
 
163
  )}
164
  </div>
165
  </aside>
 
 
 
166
  </main>
167
  )
168
  }
 
7
  import ModelSelector from "../components/ModelSelector"
8
  import { startSimulation, placeVolcano } from "../lib/api"
9
  import { createSimulationSocket } from "../lib/websocket"
10
+ import ReportPanel from "../components/ReportPanel"
11
 
12
  type AppState = "loading" | "selecting" | "placing" | "running" | "gameover"
13
 
14
  export default function Page() {
15
+ function buildReport(history: any[]) {
16
+ if (!history || history.length === 0) return null
17
+ const rounds = history.length
18
+ const last = history[history.length - 1]
19
+ const models = (last.agents || []).map((a: any) => ({ id: a.model_name, display_name: a.display_name || a.model_name }))
20
+
21
+ const per = models.map((m: any) => {
22
+ const positions = history.map(h => {
23
+ const a = (h.agents || []).find((x: any) => x.model_name === m.id)
24
+ return a ? { x: a.x, y: a.y, water: a.water_collected, status: a.status, extinguish_score: a.extinguish_score || 0, last_message: a.last_message } : null
25
+ }).filter(Boolean)
26
+
27
+ let distance = 0
28
+ for (let i = 1; i < positions.length; i++) {
29
+ const p0 = positions[i-1]
30
+ const p1 = positions[i]
31
+ const dx = p1.x - p0.x
32
+ const dy = p1.y - p0.y
33
+ distance += Math.sqrt(dx*dx + dy*dy)
34
+ }
35
+
36
+ let water_picks = 0
37
+ let logical_moves = 0
38
+ let logical_checks = 0
39
+ const messages: Record<string, number> = {}
40
+
41
+ for (let i = 0; i < positions.length; i++) {
42
+ const cur = positions[i]
43
+ if (cur.last_message) messages[cur.last_message] = (messages[cur.last_message]||0)+1
44
+ if (i>0) {
45
+ const prev = positions[i-1]
46
+ if (!prev.water && cur.water) water_picks++
47
+ // logical check: when searching, distance to nearest water should decrease
48
+ if (cur.status === 'searching') {
49
+ // compute nearest water in this tick
50
+ const h = history[i]
51
+ const ws = h.water_sources || []
52
+ if (ws.length>0) {
53
+ const distPrev = Math.min(...ws.map((w: any)=> Math.hypot(prev.x - w.x, prev.y - w.y)))
54
+ const distCur = Math.min(...ws.map((w: any)=> Math.hypot(cur.x - w.x, cur.y - w.y)))
55
+ logical_checks++
56
+ if (distCur <= distPrev) logical_moves++
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ const sortedMessages = Object.entries(messages).sort((a:any,b:any)=>b[1]-a[1]).slice(0,6).map((x:any)=>x[0])
63
+
64
+ return {
65
+ id: m.id,
66
+ display_name: m.display_name,
67
+ decisions: positions.length,
68
+ distance,
69
+ water_picks,
70
+ extinguish_score: positions.length>0 ? positions[positions.length-1].extinguish_score||0 : 0,
71
+ logical_pct: logical_checks>0 ? (logical_moves / logical_checks)*100 : 0,
72
+ top_messages: sortedMessages,
73
+ }
74
+ })
75
+
76
+ return { rounds, models: per }
77
+ }
78
+
79
+ function tickHistoryToTracks(history: any[]) {
80
+ const tracks: Record<string, { x: number; y: number }[]> = {}
81
+ if (!history || history.length === 0) return tracks
82
+ for (const h of history) {
83
+ for (const a of h.agents || []) {
84
+ if (!tracks[a.model_name]) tracks[a.model_name] = []
85
+ tracks[a.model_name].push({ x: a.x, y: a.y })
86
+ }
87
+ }
88
+ return tracks
89
+ }
90
  const [appState, setAppState] = useState<AppState>("loading")
91
  const [models, setModels] = useState<string[]>([])
92
  const [simState, setSimState] = useState<any>(null)
93
  const [chatMessages, setChatMessages] = useState<any[]>([])
94
  const [winnerLabel, setWinnerLabel] = useState<string | null>(null)
95
  const [loading, setLoading] = useState(false)
96
+ const [tickHistory, setTickHistory] = useState<any[]>([])
97
+ const [reportOpen, setReportOpen] = useState(false)
98
+ const [reportData, setReportData] = useState<any | null>(null)
99
  const [mapSize, setMapSize] = useState({ width: 1200, height: 800 })
100
  const wsRef = useRef<WebSocket | null>(null)
101
  const mapDivRef = useRef<HTMLDivElement>(null)
 
119
  setSimState(data.state)
120
  setWinnerLabel(null)
121
  setChatMessages([])
122
+ setTickHistory([])
123
  setAppState("placing")
124
  } catch (err) {
125
  console.error(err)
 
138
  const ws = createSimulationSocket(
139
  simState.simulation_id,
140
  (msg) => {
141
+ // store each tick state snapshot for post-game analysis
142
+ if (msg.state) setTickHistory(prev => [...prev, msg.state])
143
  if (msg.type === "finished") {
144
  if (msg.state) {
145
  setSimState(msg.state)
 
171
  if (msg.state?.status === "finished") {
172
  setWinnerLabel(msg.state.winner_model || null)
173
  setAppState("gameover")
174
+ // prepare report
175
+ const report = buildReport([...tickHistory, msg.state])
176
+ setReportData(report)
177
  }
178
  },
179
  () => setAppState("gameover")
 
199
  winnerLabel={winnerLabel}
200
  mapSize={mapSize}
201
  onMapClick={handleMapClick}
202
+ tracks={tickHistoryToTracks(tickHistory)}
203
  />
204
  </div>
205
 
 
240
  </div>
241
  )}
242
  {(appState === "running" || appState === "gameover") && (
243
+ <div className="space-y-2">
244
+ {appState === 'gameover' && reportData && (
245
+ <button onClick={() => setReportOpen(true)} className="w-full bg-white text-black font-mono text-xs font-bold py-3 rounded-xl">View Report</button>
246
+ )}
247
+ <button
248
+ onClick={() => window.location.reload()}
249
+ className="w-full bg-white/5 text-white/50 font-mono text-[10px] py-3 rounded-lg hover:bg-white/10 transition-all uppercase tracking-widest"
250
+ >
251
+ Reset Arena
252
+ </button>
253
+ </div>
254
  )}
255
  </div>
256
  </aside>
257
+ {reportOpen && reportData && (
258
+ <ReportPanel report={reportData} onClose={() => setReportOpen(false)} />
259
+ )}
260
  </main>
261
  )
262
  }
frontend/components/MapCanvas.tsx CHANGED
@@ -57,6 +57,7 @@ export default function MapCanvas({
57
  winnerLabel,
58
  mapSize,
59
  onMapClick,
 
60
  }: {
61
  agents: Agent[]
62
  fire: Fire | null
@@ -66,6 +67,7 @@ export default function MapCanvas({
66
  winnerLabel?: string | null
67
  mapSize: { width: number; height: number }
68
  onMapClick?: (x: number, y: number) => void
 
69
  }) {
70
  const gridSize = 40
71
  const sx = (bx: number) => (bx / BACKEND_W) * mapSize.width
@@ -146,6 +148,24 @@ export default function MapCanvas({
146
  </svg>
147
  )}
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  {/* Agents */}
150
  {agents.map((agent) => {
151
  const color = getAgentColor(agent.model_name)
 
57
  winnerLabel,
58
  mapSize,
59
  onMapClick,
60
+ tracks,
61
  }: {
62
  agents: Agent[]
63
  fire: Fire | null
 
67
  winnerLabel?: string | null
68
  mapSize: { width: number; height: number }
69
  onMapClick?: (x: number, y: number) => void
70
+ tracks?: Record<string, { x: number; y: number }[]>
71
  }) {
72
  const gridSize = 40
73
  const sx = (bx: number) => (bx / BACKEND_W) * mapSize.width
 
148
  </svg>
149
  )}
150
 
151
+ {/* Tracks */}
152
+ {tracks && (
153
+ <svg className="absolute inset-0 w-full h-full pointer-events-none">
154
+ {Object.entries(tracks).map(([model, points]) => (
155
+ points.length > 1 && (
156
+ <polyline
157
+ key={model}
158
+ points={points.map(p => `${sx(p.x)},${sy(p.y)}`).join(" ")}
159
+ fill="none"
160
+ stroke={getAgentColor(model)}
161
+ strokeWidth={2}
162
+ strokeOpacity={0.6}
163
+ />
164
+ )
165
+ ))}
166
+ </svg>
167
+ )}
168
+
169
  {/* Agents */}
170
  {agents.map((agent) => {
171
  const color = getAgentColor(agent.model_name)
frontend/components/ReportPanel.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import React from "react"
4
+
5
+ export default function ReportPanel({ report, onClose }: { report: any, onClose: () => void }) {
6
+ if (!report) return null
7
+
8
+ const models = report.models || []
9
+
10
+ return (
11
+ <div className="absolute inset-0 z-50 flex items-start justify-center p-6 pointer-events-auto">
12
+ <div className="w-full max-w-4xl bg-black/95 border border-white/10 rounded-xl p-6 text-white shadow-2xl overflow-auto max-h-[90vh]">
13
+ <div className="flex items-center justify-between mb-4">
14
+ <h2 className="text-lg font-bold">Simulation Report</h2>
15
+ <button onClick={onClose} className="px-3 py-1 bg-white/5 rounded">Close</button>
16
+ </div>
17
+
18
+ <div className="mb-4 text-sm text-white/70">Summary metrics for this match.</div>
19
+
20
+ <div className="grid grid-cols-2 gap-4 mb-6">
21
+ <div className="bg-gray-900 p-4 rounded">
22
+ <div className="text-xs text-white/60">Rounds</div>
23
+ <div className="text-xl font-mono">{report.rounds}</div>
24
+ </div>
25
+ <div className="bg-gray-900 p-4 rounded">
26
+ <div className="text-xs text-white/60">Models</div>
27
+ <div className="text-xl font-mono">{models.length}</div>
28
+ </div>
29
+ </div>
30
+
31
+ <div className="space-y-4">
32
+ {models.map((m: any) => (
33
+ <div key={m.id} className="bg-gray-900 p-4 rounded">
34
+ <div className="flex items-center justify-between">
35
+ <div>
36
+ <div className="text-sm font-bold">{m.display_name || m.id}</div>
37
+ <div className="text-xs text-white/60">{m.id}</div>
38
+ </div>
39
+ <div className="text-right">
40
+ <div className="text-xs text-white/60">Decisions</div>
41
+ <div className="font-mono text-lg">{m.decisions}</div>
42
+ </div>
43
+ </div>
44
+
45
+ <div className="mt-3 grid grid-cols-4 gap-3 text-xs text-white/60">
46
+ <div>Distance travelled: <div className="text-white font-mono">{m.distance.toFixed(0)} px</div></div>
47
+ <div>Water pickups: <div className="text-white font-mono">{m.water_picks}</div></div>
48
+ <div>Extinguish score: <div className="text-white font-mono">{m.extinguish_score.toFixed(1)}</div></div>
49
+ <div>Logical moves: <div className="text-white font-mono">{Math.round(m.logical_pct)}%</div></div>
50
+ </div>
51
+
52
+ <div className="mt-3 text-xs">
53
+ <div className="text-white/70 mb-1">Top messages</div>
54
+ <div className="grid grid-cols-2 gap-2">
55
+ {(m.top_messages || []).map((msg: string, idx: number) => (
56
+ <div key={idx} className="text-[12px] font-mono text-white/80">- {msg}</div>
57
+ ))}
58
+ </div>
59
+ </div>
60
+ </div>
61
+ ))}
62
+ </div>
63
+
64
+ </div>
65
+ </div>
66
+ )
67
+ }