123 lines
5.9 KiB
JavaScript
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 "";
|
|
}
|
|
}
|