class SnakeTable { static buildSnakesRows(turn, replay) { console.log(turn); const currentTurn = Number(turn && turn.turn !== undefined ? turn.turn : 0); const turns = replay && Array.isArray(replay.turns) ? replay.turns : []; const lastSeenById = new Map(); const lastSeenTurnById = new Map(); const aliveById = new Map(); for (const historyTurn of turns) { const historyTurnNumber = Number(historyTurn && historyTurn.turn !== undefined ? historyTurn.turn : 0); if (historyTurnNumber > currentTurn) continue; for (const snake of (historyTurn.snakes || [])) { if (!snake) continue; const snakeId = snake.snake_id || snake.id || `${Utils.safeString(snake.snake_name)}-${historyTurnNumber}`; lastSeenById.set(snakeId, snake); lastSeenTurnById.set(snakeId, historyTurnNumber); if (historyTurnNumber === currentTurn) { aliveById.set(snakeId, snake); } } } const snakes = []; for (const [snakeId, snake] of lastSeenById.entries()) { const aliveSnake = aliveById.get(snakeId); snakes.push({ ...(aliveSnake || snake), _snake_id: snakeId, _is_dead: !aliveSnake, _last_seen_turn: Number(lastSeenTurnById.get(snakeId) ?? currentTurn), }); } snakes.sort((a, b) => { const youDelta = Number(Boolean(b.is_you)) - Number(Boolean(a.is_you)); if (youDelta !== 0) return youDelta; const deadDelta = Number(Boolean(a._is_dead)) - Number(Boolean(b._is_dead)); if (deadDelta !== 0) return deadDelta; return Utils.safeString(a.snake_name).localeCompare(Utils.safeString(b.snake_name)); }); const colorById = SnakeUtils.buildSnakeColorById(turn, replay); if (snakes.length === 0) { return "No snake data available"; } return snakes.map((snake, idx) => { const healthValue = Number(snake.health ?? 0); const healthClamped = snake._is_dead ? 0 : Math.max(0, Math.min(100, healthValue)); const healthColor = healthClamped > 60 ? "#28a264" : (healthClamped > 30 ? "#d39a1c" : "#c34939"); const healthText = snake._is_dead ? "dead" : Utils.safeString(snake.health); const healthCell = `${healthText}`; const snakeId = snake._snake_id || snake.snake_id || snake.id || `${Utils.safeString(snake.snake_name)}-${idx}`; const rowColor = SnakeUtils.resolveSnakeColor(snakeId, snake.is_you, colorById); const rowBg = SnakeUtils.snakeRowBackground(rowColor); const rowStyle = `style="--snake-row-color:${rowColor};--snake-row-bg:${rowBg};"`; const diedTurn = Number(snake._last_seen_turn ?? currentTurn) + 1; const moveText = snake._is_dead ? `dead @ ${diedTurn}` : Utils.safeString(snake.inferred_move); const deadClass = snake._is_dead ? " dead-row" : ""; const causeLabel = SnakeTable._getCauseLabel(snake, turns, replay); const deadLabel = snake._is_dead ? ` (dead${causeLabel})` : ""; return ` ${Utils.safeString(snake.snake_name)}${snake.is_you ? " (you)" : ""}${deadLabel} ${Utils.safeString(snake.latency)} ${moveText} ${healthCell} ${Utils.safeString(snake.length)} ${snake.head ? `${snake.head.x},${snake.head.y}` : "-"} ${Array.isArray(snake.body) && snake.body.length > 0 ? `${snake.body[snake.body.length - 1].x},${snake.body[snake.body.length - 1].y}` : "-"} `; }).join(""); } static _getCauseLabel(snake, turns, replay) { if (!snake._is_dead) return ""; const lastTurn = turns.find((t) => t && t.turn === snake._last_seen_turn); if (!lastTurn) return ""; const lastSelf = (lastTurn.snakes || []).find((s) => (s.snake_id || s.id) === snake._snake_id); if (!lastSelf) return ""; // Starvation: health ≤ 1 at last seen turn if (Number(lastSelf.health) <= 1) return " · starved"; // Project head to next position based on body direction const body = Array.isArray(lastSelf.body) ? lastSelf.body : []; const h = lastSelf.head || (body[0] || null); const neck = body[1] || null; const projX = h && neck ? h.x + (h.x - neck.x) : null; const projY = h && neck ? h.y + (h.y - neck.y) : null; const nextTurn = turns.find((t) => t && t.turn === snake._last_seen_turn + 1); if (projX !== null) { const game = replay && replay.game ? replay.game : {}; const bw = Number(game.width || 0); const bh = Number(game.height || 0); // Wall collision: projected head out of bounds if (bw > 0 && bh > 0 && (projX < 0 || projX >= bw || projY < 0 || projY >= bh)) { return " · wall"; } if (nextTurn) { // Head-to-head: another alive snake's head at projected position for (const other of (nextTurn.snakes || [])) { const otherId = other.snake_id || other.id; if (otherId === snake._snake_id) continue; if (other.head && other.head.x === projX && other.head.y === projY) { return ` · head-to-head ${Utils.safeString(other.snake_name)}`; } } // Body collision: projected head inside another snake's body for (const other of (nextTurn.snakes || [])) { const otherId = other.snake_id || other.id; if (otherId === snake._snake_id) continue; const otherBody = Array.isArray(other.body) ? other.body : []; const hitBody = otherBody.some((seg, i) => i > 0 && seg.x === projX && seg.y === projY); if (hitBody) return ` · hit ${Utils.safeString(other.snake_name)}`; } // Hazard: projected position is in hazard list const hazards = nextTurn.hazards || []; if (hazards.some((hz) => hz.x === projX && hz.y === projY)) return " · Hazard"; } } return ""; } }