Files
snake-python/templates/files/js/SnakeTable.js
T

123 lines
5.9 KiB
JavaScript

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 "<tr><td colspan=\"7\">No snake data available</td></tr>";
}
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 = `<span class="health-wrap"><span class="health-fill" style="width:${healthClamped}%;background:${healthColor};"></span></span><span class="health-text">${healthText}</span>`;
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 `
<tr class="snake-row${deadClass}" data-snake-id="${snakeId}" ${rowStyle}>
<td class="name-cell">${Utils.safeString(snake.snake_name)}${snake.is_you ? " (you)" : ""}${deadLabel}</td>
<td class="num-cell">${Utils.safeString(snake.latency)}</td>
<td class="num-cell">${moveText}</td>
<td class="num-cell">${healthCell}</td>
<td class="num-cell">${Utils.safeString(snake.length)}</td>
</tr>`;
}).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 "";
}
}