Files
2026-04-07 03:25:10 +02:00

222 lines
7.3 KiB
JavaScript

class SnakeUtils {
static snakeColor(index) {
return `var(--snake-${((index % 10) + 1)})`;
}
static stableColorIndexFromId(snakeId) {
const raw = String(snakeId || "");
let hash = 0;
for (let i = 0; i < raw.length; i += 1) {
hash = ((hash * 31) + raw.charCodeAt(i)) >>> 0;
}
return hash % 10;
}
static resolveSnakeColor(snakeId, isYou, colorById) {
if (snakeId && colorById && colorById.has(snakeId)) {
return colorById.get(snakeId);
}
if (isYou) return "var(--you)";
return SnakeUtils.snakeColor(SnakeUtils.stableColorIndexFromId(snakeId));
}
static extractSnakeColor(rawSnake) {
if (!rawSnake || typeof rawSnake !== "object") return null;
const direct = rawSnake.color;
const custom = rawSnake.customizations && rawSnake.customizations.color;
const appearance = rawSnake.appearance && rawSnake.appearance.color;
const color = direct || custom || appearance;
if (!color) return null;
return String(color).trim();
}
static buildSnakeColorById(turnData, replay) {
const colorById = new Map();
const replayTurns = replay && Array.isArray(replay.turns) ? replay.turns : [];
const boardSnakes = turnData && turnData.board && Array.isArray(turnData.board.snakes)
? turnData.board.snakes
: [];
const replaySnakes = Array.isArray(turnData && turnData.snakes) ? turnData.snakes : [];
const historicalSnakes = [];
for (const replayTurn of replayTurns) {
if (replayTurn && Array.isArray(replayTurn.snakes)) {
historicalSnakes.push(...replayTurn.snakes);
}
if (replayTurn && replayTurn.board && Array.isArray(replayTurn.board.snakes)) {
historicalSnakes.push(...replayTurn.board.snakes);
}
}
for (const snake of [...historicalSnakes, ...boardSnakes, ...replaySnakes]) {
if (!snake) continue;
const snakeId = snake.id || snake.snake_id;
if (!snakeId) continue;
const color = SnakeUtils.extractSnakeColor(snake);
if (color) colorById.set(snakeId, color);
}
return colorById;
}
static buildSnakeCustomizationById(turnData, replay) {
const customById = new Map();
const replayTurns = replay && Array.isArray(replay.turns) ? replay.turns : [];
const sources = [];
for (const replayTurn of replayTurns) {
if (replayTurn && replayTurn.board && Array.isArray(replayTurn.board.snakes)) {
sources.push(...replayTurn.board.snakes);
}
}
if (turnData && turnData.board && Array.isArray(turnData.board.snakes)) {
sources.push(...turnData.board.snakes);
}
for (const snake of sources) {
if (!snake) continue;
const snakeId = snake.id || snake.snake_id;
if (!snakeId) continue;
const custom = snake.customizations || {};
const head = custom.head || null;
const tail = custom.tail || null;
if (head || tail) {
customById.set(snakeId, { head, tail });
}
}
return customById;
}
static buildCustomizationIconUrl(kind, value) {
const raw = String(value || "").trim().toLowerCase();
if (!raw) return null;
if (!/^[a-z0-9-]+$/.test(raw)) return null;
return `/dashboard/customizations/${kind}/${raw}.svg`;
}
static parseSnakeColor(color) {
if (!color) return null;
const value = String(color).trim();
if (value.startsWith("#")) {
const hex = value.slice(1);
if (hex.length === 3) {
const r = parseInt(hex[0] + hex[0], 16);
const g = parseInt(hex[1] + hex[1], 16);
const b = parseInt(hex[2] + hex[2], 16);
return Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) ? null : { r, g, b };
}
if (hex.length === 6) {
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b) ? null : { r, g, b };
}
}
const rgb = value.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (rgb) {
return {
r: Math.max(0, Math.min(255, Number(rgb[1]))),
g: Math.max(0, Math.min(255, Number(rgb[2]))),
b: Math.max(0, Math.min(255, Number(rgb[3]))),
};
}
return null;
}
static snakeRowBackground(color) {
const parsed = SnakeUtils.parseSnakeColor(color);
if (!parsed) return "transparent";
const isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
const alpha = isDark ? 0.26 : 0.16;
return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${alpha})`;
}
static inferHeadDirection(snake) {
const body = Array.isArray(snake && snake.body) ? snake.body : [];
if (body.length >= 2) {
const head = body[0];
const neck = body[1];
if (head && neck) {
const dx = Number(head.x) - Number(neck.x);
const dy = Number(head.y) - Number(neck.y);
if (dx > 0) return "right";
if (dx < 0) return "left";
if (dy > 0) return "up";
if (dy < 0) return "down";
}
}
const inferred = String(snake && snake.inferred_move ? snake.inferred_move : "").toLowerCase();
if (["up", "down", "left", "right"].includes(inferred)) return inferred;
if (body.length < 2) return "right";
const head = body[0];
const neck = body[1];
if (!head || !neck) return "right";
const dx = Number(head.x) - Number(neck.x);
const dy = Number(head.y) - Number(neck.y);
if (dx > 0) return "right";
if (dx < 0) return "left";
if (dy > 0) return "up";
if (dy < 0) return "down";
return "right";
}
static inferTailDirection(snake) {
const body = Array.isArray(snake && snake.body) ? snake.body : [];
if (body.length < 2) return "right";
const tail = body[body.length - 1];
if (!tail) return "right";
let beforeTail = null;
for (let idx = body.length - 2; idx >= 0; idx -= 1) {
const candidate = body[idx];
if (!candidate) continue;
if (Number(candidate.x) !== Number(tail.x) || Number(candidate.y) !== Number(tail.y)) {
beforeTail = candidate;
break;
}
}
if (!beforeTail) {
const inferred = String(snake && snake.inferred_move ? snake.inferred_move : "").toLowerCase();
if (["up", "down", "left", "right"].includes(inferred)) return inferred;
return "right";
}
const dx = Number(beforeTail.x) - Number(tail.x);
const dy = Number(beforeTail.y) - Number(tail.y);
if (dx > 0) return "right";
if (dx < 0) return "left";
if (dy > 0) return "up";
if (dy < 0) return "down";
return "right";
}
static directionToHeadTransform(direction) {
if (direction === "left") return "scaleX(-1)";
if (direction === "up") return "rotate(270deg)";
if (direction === "down") return "rotate(90deg)";
return "rotate(0deg)";
}
static directionToTailTransform(direction) {
if (direction === "right") return "scaleX(-1)";
if (direction === "left") return "rotate(0deg)";
if (direction === "up") return "rotate(270deg) scaleX(-1)";
if (direction === "down") return "rotate(90deg) scaleX(-1)";
return "scaleX(-1)";
}
static stableVariantFromString(value) {
const raw = String(value || "");
if (!raw) return 1;
let hash = 0;
for (let i = 0; i < raw.length; i += 1) {
hash = ((hash * 33) + raw.charCodeAt(i)) >>> 0;
}
return (hash % 5) + 1;
}
}