Skip to content

Commit 2232248

Browse files
committed
Merge branch 'main' of github.com:CodeClash-ai/CodeClash
2 parents 1f2cb79 + 8250fa8 commit 2232248

29 files changed

Lines changed: 2432 additions & 962 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""BattleSnake replay renderer.
2+
3+
Parses a recorded ``sim_*.jsonl`` (v1 API state frames) into normalized playback data
4+
and draws the board — snakes with heads/eyes, food, hazards, per-snake health.
5+
Ported from the former ``scripts/replay_battlesnake.py``.
6+
7+
The jsonl format: per-turn v1 state frames ({game, turn, board, you}) and a final result
8+
line ({winnerName, isDraw}).
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import json
14+
15+
from codeclash.replay.base import ReplayData, ReplayRenderer
16+
17+
PALETTE = ["#3B78FF", "#E5484D", "#30A46C", "#F5A623", "#8E4EC6", "#12A594", "#E93D82", "#F76B15"]
18+
19+
DRAW_JS = """
20+
const ARENA = (function(){
21+
let W, H, CELL, PAD, px, py;
22+
function setup(cv, G){
23+
W = G.w; H = G.h;
24+
CELL = Math.max(18, Math.min(44, Math.floor(560 / Math.max(W, H)))); PAD = CELL * 0.12;
25+
cv.width = W * CELL; cv.height = H * CELL;
26+
px = (x) => x * CELL; py = (y) => (H - 1 - y) * CELL; // v1 y-up -> canvas y-down
27+
}
28+
function draw(ctx, cv, G, i){
29+
const f = G.frames[i], COL = G.colors;
30+
ctx.clearRect(0, 0, cv.width, cv.height);
31+
ctx.strokeStyle = '#21262d';
32+
for(let x=0;x<=W;x++){ctx.beginPath();ctx.moveTo(x*CELL,0);ctx.lineTo(x*CELL,H*CELL);ctx.stroke();}
33+
for(let y=0;y<=H;y++){ctx.beginPath();ctx.moveTo(0,y*CELL);ctx.lineTo(W*CELL,y*CELL);ctx.stroke();}
34+
f.hazards.forEach(([x,y])=>{ctx.fillStyle='rgba(245,166,35,0.15)';ctx.fillRect(px(x),py(y),CELL,CELL);});
35+
f.food.forEach(([x,y])=>{ctx.fillStyle='#ff5252';ctx.beginPath();ctx.arc(px(x)+CELL/2,py(y)+CELL/2,CELL*0.22,0,7);ctx.fill();});
36+
f.snakes.forEach(s=>{
37+
const c = COL[s.name] || '#888';
38+
s.body.forEach(([x,y],j)=>{
39+
ctx.fillStyle=c; ctx.globalAlpha=j===0?1:0.85;
40+
const r=j===0?CELL*0.5:CELL*0.32;
41+
ctx.beginPath(); ctx.roundRect(px(x)+PAD,py(y)+PAD,CELL-2*PAD,CELL-2*PAD, r); ctx.fill();
42+
});
43+
ctx.globalAlpha=1;
44+
const [hx,hy]=s.body[0]; ctx.fillStyle='#0d1117';
45+
ctx.beginPath();ctx.arc(px(hx)+CELL*0.62,py(hy)+CELL*0.38,CELL*0.08,0,7);ctx.fill();
46+
});
47+
}
48+
function side(G, i){
49+
const f = G.frames[i], COL = G.colors;
50+
const alive = new Set(f.snakes.map(s=>s.name));
51+
return Object.keys(COL).map(nm=>{
52+
const s = f.snakes.find(x=>x.name===nm); const hp = s?s.health:0; const dead = !alive.has(nm);
53+
return `<div class="sn ${dead?'dead':''}"><span class="sw" style="background:${COL[nm]}"></span>
54+
<span style="min-width:80px">${nm}</span>
55+
<span class="hb"><span class="hf" style="width:${hp}%;background:${COL[nm]}"></span></span>
56+
<span>${dead?'\\u2620':hp}</span></div>`;
57+
}).join('');
58+
}
59+
return {setup, draw, side};
60+
})();
61+
"""
62+
63+
64+
class BattleSnakeReplayer(ReplayRenderer):
65+
arena = "BattleSnake"
66+
sim_glob = "sim_*.jsonl"
67+
DRAW_JS = DRAW_JS
68+
69+
def parse(self, raw: bytes, players=None) -> ReplayData:
70+
rows = [json.loads(line) for line in raw.decode().splitlines() if line.strip()]
71+
# per-turn board states (dedupe: keep the last frame seen for each turn)
72+
by_turn = {}
73+
for r in rows:
74+
if isinstance(r, dict) and "board" in r and "turn" in r:
75+
by_turn[r["turn"]] = r["board"]
76+
turns = sorted(by_turn)
77+
result = next((r for r in reversed(rows) if isinstance(r, dict) and "winnerName" in r), {})
78+
if not turns:
79+
return ReplayData(w=0, h=0, frames=[], winner=result.get("winnerName"), draw=result.get("isDraw", False))
80+
81+
# stable color per snake name (prefer the snake's own color from the log if present)
82+
names, colors = [], {}
83+
for t in turns:
84+
for s in by_turn[t]["snakes"]:
85+
if s["name"] not in names:
86+
names.append(s["name"])
87+
for i, nm in enumerate(names):
88+
colors[nm] = PALETTE[i % len(PALETTE)]
89+
for t in turns:
90+
for s in by_turn[t]["snakes"]:
91+
if s.get("color"):
92+
colors[s["name"]] = s["color"]
93+
94+
b0 = by_turn[turns[0]]
95+
frames = []
96+
for t in turns:
97+
b = by_turn[t]
98+
frames.append(
99+
{
100+
"turn": t,
101+
"food": [[c["x"], c["y"]] for c in b.get("food", [])],
102+
"hazards": [[c["x"], c["y"]] for c in b.get("hazards", [])],
103+
"snakes": [
104+
{
105+
"name": s["name"],
106+
"health": s.get("health", 0),
107+
"body": [[c["x"], c["y"]] for c in s["body"]],
108+
}
109+
for s in b["snakes"]
110+
],
111+
}
112+
)
113+
return ReplayData(
114+
w=b0["width"],
115+
h=b0["height"],
116+
frames=frames,
117+
winner=result.get("winnerName"),
118+
draw=result.get("isDraw", False),
119+
extra={"colors": colors},
120+
)
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""Bomberland replay renderer.
2+
3+
Parses a per-game Bomberland trace (written by ``run_bomberland.py`` as
4+
``sim_<idx>.json``) into normalized playback data and draws the grid: metal /
5+
wood walls, bombs, blasts, and units colored per agent with health-as-brightness.
6+
A ``side(G, i)`` panel shows per-agent unit counts / total hp / current tick.
7+
8+
The trace JSON is a single object ``{width, height, winner, sim, player_order,
9+
frames:[...]}``. Each frame has ``tick``, ``units`` (each with ``unit_id``,
10+
``agent_id``, ``coordinates`` [x,y], ``hp``) and ``entities`` (each with ``type``
11+
in {"m" metal, "w" wood, "b" bomb, "x" blast} and ``coordinates`` [x,y]; bombs
12+
additionally carry ``timer`` / ``owner`` / ``blast_diameter``, blasts carry
13+
``ttl``). ``player_order`` is the two agent ids for this sim; the first is Blue,
14+
the second Red.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import json
20+
21+
from codeclash.replay.base import ReplayData, ReplayRenderer
22+
23+
START_HP = 3
24+
TEAM_COLORS = {"Blue": "#3B78FF", "Red": "#E5484D"}
25+
26+
DRAW_JS = """
27+
const ARENA = (function(){
28+
let W, H, CELL, PAD, COL, NAMES, AGENTS, MAXHP;
29+
function teamOf(agentId){ return (AGENTS[0] === agentId) ? 'Blue' : 'Red'; }
30+
function setup(cv, G){
31+
W = G.w; H = G.h; COL = G.colors; NAMES = G.names; AGENTS = G.agents; MAXHP = G.maxhp;
32+
CELL = Math.max(16, Math.min(40, Math.floor(640 / Math.max(W, H)))); PAD = CELL * 0.14;
33+
cv.width = W * CELL; cv.height = H * CELL;
34+
}
35+
function px(x){ return x * CELL; }
36+
function py(y){ return y * CELL; }
37+
function draw(ctx, cv, G, i){
38+
const f = G.frames[i];
39+
ctx.clearRect(0, 0, cv.width, cv.height);
40+
ctx.fillStyle = '#0d1117'; ctx.fillRect(0, 0, cv.width, cv.height);
41+
ctx.strokeStyle = '#21262d'; ctx.lineWidth = 1;
42+
for(let x=0;x<=W;x++){ctx.beginPath();ctx.moveTo(x*CELL,0);ctx.lineTo(x*CELL,H*CELL);ctx.stroke();}
43+
for(let y=0;y<=H;y++){ctx.beginPath();ctx.moveTo(0,y*CELL);ctx.lineTo(W*CELL,y*CELL);ctx.stroke();}
44+
// World entities: draw walls first, then blasts, then bombs.
45+
const bombs = [], blasts = [];
46+
f.entities.forEach(e=>{
47+
const x = e.coordinates[0], y = e.coordinates[1];
48+
if(e.type === 'm'){
49+
ctx.fillStyle = '#6e7681';
50+
ctx.fillRect(px(x)+1, py(y)+1, CELL-2, CELL-2);
51+
} else if(e.type === 'w'){
52+
ctx.fillStyle = '#8a5a2b';
53+
ctx.fillRect(px(x)+2, py(y)+2, CELL-4, CELL-4);
54+
ctx.strokeStyle = '#5c3a1c'; ctx.lineWidth = 1;
55+
ctx.strokeRect(px(x)+2, py(y)+2, CELL-4, CELL-4);
56+
} else if(e.type === 'b'){
57+
bombs.push(e);
58+
} else if(e.type === 'x'){
59+
blasts.push(e);
60+
}
61+
});
62+
blasts.forEach(e=>{
63+
const x = e.coordinates[0], y = e.coordinates[1];
64+
ctx.fillStyle = 'rgba(255,140,0,0.55)';
65+
ctx.fillRect(px(x)+1, py(y)+1, CELL-2, CELL-2);
66+
ctx.fillStyle = 'rgba(255,220,60,0.7)';
67+
ctx.beginPath(); ctx.arc(px(x)+CELL/2, py(y)+CELL/2, CELL*0.22, 0, 7); ctx.fill();
68+
});
69+
bombs.forEach(e=>{
70+
const x = e.coordinates[0], y = e.coordinates[1];
71+
const cx = px(x)+CELL/2, cy = py(y)+CELL/2;
72+
ctx.fillStyle = '#0b0b0b';
73+
ctx.beginPath(); ctx.arc(cx, cy, CELL*0.3, 0, 7); ctx.fill();
74+
ctx.strokeStyle = '#ff5722'; ctx.lineWidth = 2;
75+
ctx.beginPath(); ctx.arc(cx, cy, CELL*0.3, 0, 7); ctx.stroke();
76+
if(e.timer != null){
77+
ctx.fillStyle = '#ffd21f'; ctx.font = `${Math.floor(CELL*0.34)}px system-ui`;
78+
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
79+
ctx.fillText(String(e.timer), cx, cy);
80+
}
81+
});
82+
// Units on top.
83+
f.units.forEach(u=>{
84+
if(u.hp <= 0) return;
85+
const tm = teamOf(u.agent_id);
86+
const base = COL[tm] || '#888';
87+
const frac = Math.max(0.3, u.hp / MAXHP);
88+
ctx.globalAlpha = frac;
89+
ctx.fillStyle = base;
90+
const r = CELL * 0.28;
91+
ctx.beginPath(); ctx.roundRect(px(u.x)+PAD, py(u.y)+PAD, CELL-2*PAD, CELL-2*PAD, r); ctx.fill();
92+
ctx.globalAlpha = 1;
93+
const bw = CELL-2*PAD, bx = px(u.x)+PAD, by = py(u.y)+CELL-PAD*0.5;
94+
ctx.fillStyle = 'rgba(0,0,0,0.45)'; ctx.fillRect(bx, by-3, bw, 3);
95+
ctx.fillStyle = '#eafff0'; ctx.fillRect(bx, by-3, bw*Math.min(1, u.hp/MAXHP), 3);
96+
});
97+
}
98+
function side(G, i){
99+
const f = G.frames[i], COL = G.colors, NAMES = G.names, AGENTS = G.agents, MAXHP = G.maxhp;
100+
const agg = {Blue:{n:0,hp:0}, Red:{n:0,hp:0}};
101+
let totalUnits = {Blue:0, Red:0};
102+
f.units.forEach(u=>{
103+
const tm = (AGENTS[0] === u.agent_id) ? 'Blue' : 'Red';
104+
totalUnits[tm]++;
105+
if(u.hp > 0){ agg[tm].n++; agg[tm].hp += u.hp; }
106+
});
107+
return ['Blue','Red'].map(tm=>{
108+
const a = agg[tm], dead = a.n===0, maxhp = Math.max(1, totalUnits[tm]) * MAXHP;
109+
return `<div class="team ${dead?'tdead':''}">
110+
<div class="tname"><span class="sw" style="background:${COL[tm]}"></span>${NAMES[tm]} <span class="muted">(${tm})</span></div>
111+
<div class="stat"><span>units alive</span><b>${a.n}</b></div>
112+
<div class="stat"><span>total hp</span><b>${a.hp}</b></div>
113+
<div class="hb"><span class="hf" style="width:${Math.min(100,100*a.hp/maxhp)}%;background:${COL[tm]}"></span></div>
114+
</div>`;
115+
}).join('') + `<div class="row muted" style="font-size:12px">tick ${f.tick} \\u00b7 hp = brightness \\u00b7 \\ud83d\\udca3 bomb (timer) \\u00b7 \\ud83d\\udd25 blast</div>`;
116+
}
117+
return {setup, draw, side};
118+
})();
119+
"""
120+
121+
122+
class BomberlandReplayer(ReplayRenderer):
123+
arena = "Bomberland"
124+
sim_glob = "sim_*.json"
125+
DRAW_JS = DRAW_JS
126+
127+
def parse(self, raw: bytes, players=None) -> ReplayData:
128+
data = json.loads(raw.decode())
129+
width = int(data.get("width", 0))
130+
height = int(data.get("height", 0))
131+
frames_raw = data.get("frames", [])
132+
player_order = data.get("player_order", [])
133+
134+
frames = []
135+
for fr in frames_raw:
136+
units = []
137+
for u in fr.get("units", []):
138+
coords = u.get("coordinates", [0, 0])
139+
units.append(
140+
{
141+
"unit_id": u.get("unit_id"),
142+
"agent_id": u.get("agent_id"),
143+
"x": coords[0],
144+
"y": coords[1],
145+
"hp": u.get("hp", 0),
146+
}
147+
)
148+
entities = []
149+
for e in fr.get("entities", []):
150+
coords = e.get("coordinates", [0, 0])
151+
ent = {"type": e.get("type"), "coordinates": [coords[0], coords[1]]}
152+
if "timer" in e:
153+
ent["timer"] = e["timer"]
154+
if "owner" in e:
155+
ent["owner"] = e["owner"]
156+
if "ttl" in e:
157+
ent["ttl"] = e["ttl"]
158+
entities.append(ent)
159+
frames.append({"turn": fr.get("tick"), "tick": fr.get("tick"), "units": units, "entities": entities})
160+
161+
# First player_order id -> Blue, second -> Red. Fall back to display names.
162+
blue_id = player_order[0] if len(player_order) > 0 else "Blue"
163+
red_id = player_order[1] if len(player_order) > 1 else "Red"
164+
names = {"Blue": blue_id, "Red": red_id}
165+
166+
winner_raw = data.get("winner")
167+
if winner_raw == blue_id:
168+
winner, draw = names["Blue"], False
169+
elif winner_raw == red_id:
170+
winner, draw = names["Red"], False
171+
else: # "TIE" or unknown
172+
winner, draw = None, True
173+
174+
return ReplayData(
175+
w=width,
176+
h=height,
177+
frames=frames,
178+
winner=winner,
179+
draw=draw,
180+
extra={
181+
"names": names,
182+
"colors": TEAM_COLORS,
183+
"agents": [blue_id, red_id],
184+
"maxhp": START_HP,
185+
},
186+
)

0 commit comments

Comments
 (0)