feat(viewer): render shrinking play-zone in replay viewer

Draws the active zone boundary and out-of-zone danger area using
per-turn zone_bounds data from the replay. The zone renders as:
- Red semi-transparent overlay outside the safe zone
- Solid red boundary circle with dashed inner ring
- Center cross marker
- Inactive zones show as subtle dashed outline

Changes:
- Add ZoneBounds type to types.ts
- Add zone_bounds field to ReplayTurn
- Implement drawZone() method in replay-viewer.ts
- Call drawZone() in renderStandardView()
- Update replay-schema-v1.json with ZoneBounds definition

Accepts: bf-k1oy

Closes: bf-k1oy
This commit is contained in:
jedarden 2026-05-24 10:24:44 -04:00
parent af46a1da97
commit 16fce127ff
3 changed files with 99 additions and 0 deletions

View file

@ -609,6 +609,29 @@
"additionalProperties": {
"$ref": "#/$defs/DebugInfo"
}
},
"zone_bounds": {
"description": "Active shrinking zone bounds at this turn. Only present when zone is enabled in config.",
"$ref": "#/$defs/ZoneBounds"
}
}
},
"ZoneBounds": {
"type": "object",
"description": "Bounds of the shrinking play-zone (storm) at a turn.",
"required": ["center", "radius", "active"],
"properties": {
"center": {
"$ref": "#/$defs/Position"
},
"radius": {
"type": "integer",
"description": "Radius of the safe zone in tiles. Bots outside this radius are killed.",
"minimum": 0
},
"active": {
"type": "boolean",
"description": "Whether the zone is currently shrinking. False before zone_start_turn."
}
}
},

View file

@ -1842,6 +1842,9 @@ export class ReplayViewer {
this.drawCell(wall.row, wall.col, wallColor);
}
// Draw shrinking zone (if active)
this.drawZone();
// Draw cores
for (const core of turnData.cores) {
if (visible && !visible.has(this.posKey(core.position))) continue;
@ -2572,6 +2575,72 @@ export class ReplayViewer {
}
}
private drawZone(): void {
const turnData = this.replay?.turns[this.currentTurn];
if (!turnData?.zone_bounds) return;
const { ctx, cellSize } = this;
const { center, radius, active } = turnData.zone_bounds;
// Zone center in world coordinates
const cx = center.col * cellSize + cellSize / 2;
const cy = center.row * cellSize + cellSize / 2;
const cr = radius * cellSize;
if (!active) {
// Zone not active yet — draw subtle outline
ctx.strokeStyle = 'rgba(239, 68, 68, 0.3)';
ctx.lineWidth = 2;
ctx.setLineDash([8, 8]);
ctx.beginPath();
ctx.arc(cx, cy, cr, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
return;
}
// Draw danger area (outside zone) — semi-transparent red overlay
const mapW = this.replay!.map.cols * cellSize;
const mapH = this.replay!.map.rows * cellSize;
ctx.fillStyle = 'rgba(239, 68, 68, 0.15)';
ctx.fillRect(0, 0, mapW, mapH);
// Use destination-out to "cut out" the safe zone circle
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
ctx.arc(cx, cy, cr, 0, Math.PI * 2);
ctx.fill();
ctx.globalCompositeOperation = 'source-over';
// Draw zone boundary circle
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cx, cy, cr, 0, Math.PI * 2);
ctx.stroke();
// Draw inner dashed ring for visibility
ctx.strokeStyle = 'rgba(239, 68, 68, 0.5)';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.arc(cx, cy, cr - 3, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
// Draw zone center marker (small cross)
const crossSize = 4;
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx - crossSize, cy);
ctx.lineTo(cx + crossSize, cy);
ctx.moveTo(cx, cy - crossSize);
ctx.lineTo(cx, cy + crossSize);
ctx.stroke();
}
private drawBot(bot: ReplayBot, color: string): void {
const { cellSize } = this;
const targetX = bot.position.col * cellSize + cellSize / 2;

View file

@ -64,6 +64,12 @@ export interface GameEvent {
details: unknown;
}
export interface ZoneBounds {
center: Position;
radius: number;
active: boolean;
}
export interface ReplayTurn {
turn: number;
bots: ReplayBot[];
@ -73,6 +79,7 @@ export interface ReplayTurn {
energy_held: number[];
events?: GameEvent[];
debug?: Record<number, DebugInfo>;
zone_bounds?: ZoneBounds;
}
export interface ReplayCriticalMoment {