update Dashboard with new keyboard bindings fix errors with hazards, set win or loss on stale games (older) and push new games real time over the websocket connection when they are finished
Build and Push Docker Container / build-and-push (push) Failing after 12m39s
Build and Push Docker Container / build-and-push (push) Failing after 12m39s
This commit is contained in:
+225
-38
@@ -390,20 +390,19 @@
|
||||
}
|
||||
.hazard {
|
||||
background-color: var(--hazard);
|
||||
filter: grayscale(0.35) brightness(0.62);
|
||||
}
|
||||
.hazard::after {
|
||||
.hazard::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: repeating-linear-gradient(
|
||||
background: rgba(20, 10, 50, 0.38) repeating-linear-gradient(
|
||||
135deg,
|
||||
rgba(106, 90, 155, 0.55) 0,
|
||||
rgba(106, 90, 155, 0.55) 2px,
|
||||
rgba(80, 60, 140, 0.6) 0,
|
||||
rgba(80, 60, 140, 0.6) 2px,
|
||||
transparent 2px,
|
||||
transparent 6px
|
||||
);
|
||||
z-index: 2;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
}
|
||||
.snake-you { background: var(--you); }
|
||||
@@ -419,6 +418,7 @@
|
||||
border-radius: 50%;
|
||||
background: var(--head-ring);
|
||||
opacity: 0.9;
|
||||
z-index: 2;
|
||||
}
|
||||
.snake-head.head-style-1::after { border-radius: 50%; }
|
||||
.snake-head.head-style-2::after { border-radius: 2px; transform: rotate(45deg); }
|
||||
@@ -437,6 +437,7 @@
|
||||
height: 36%;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
z-index: 2;
|
||||
}
|
||||
.snake-tail-you.tail-style-1::after,
|
||||
.snake-tail-enemy.tail-style-1::after { width: 38%; height: 36%; }
|
||||
@@ -472,7 +473,7 @@
|
||||
}
|
||||
|
||||
.icon-layer--tail {
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
@@ -741,9 +742,14 @@
|
||||
const initialGameId = {{ initial_game_id|tojson }};
|
||||
const initialSummary = {{ initial_summary|tojson }};
|
||||
const initialGamesPayload = {{ initial_games|tojson }};
|
||||
let dashboardSummary = (initialSummary && typeof initialSummary === "object") ? initialSummary : {};
|
||||
let dashboardGamesPayload = (initialGamesPayload && typeof initialGamesPayload === "object") ? initialGamesPayload : { games: [] };
|
||||
let replay = null;
|
||||
let turnIndex = 0;
|
||||
let timer = null;
|
||||
let activeGameId = String(initialGameId || "");
|
||||
let gamesWebSocket = null;
|
||||
let gamesWebSocketReconnectTimer = null;
|
||||
let selectedSnakeId = null;
|
||||
const svgCache = new Map();
|
||||
|
||||
@@ -756,6 +762,7 @@
|
||||
const sliderEl = document.getElementById("turn-slider");
|
||||
|
||||
function toTitle(value) {
|
||||
if (String(value || "").toLowerCase() === "finished") return "Done";
|
||||
return String(value || "").replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase());
|
||||
}
|
||||
|
||||
@@ -782,13 +789,51 @@
|
||||
}
|
||||
|
||||
function loadSummary() {
|
||||
renderStats(initialSummary || {});
|
||||
renderStats(dashboardSummary);
|
||||
}
|
||||
|
||||
function renderGamesTable(games) {
|
||||
gamesBodyEl.innerHTML = games.map((g) => `
|
||||
<tr data-game-id="${g.game_id}">
|
||||
<td><code>${shortId(g.game_id)}</code><br><small>${safeString(displayGameTypeOrMap(g))}</small></td>
|
||||
<td>${toTitle(g.status)}</td>
|
||||
<td>${g.status === "running" ? "-" : g.winner_you ? "Win" : "Loss"}</td>
|
||||
<td>${safeString(g.final_turn)}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
for (const row of gamesBodyEl.querySelectorAll("tr")) {
|
||||
row.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
const gameId = row.getAttribute("data-game-id");
|
||||
if (gameId) {
|
||||
activeGameId = gameId;
|
||||
loadReplay(gameId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (activeGameId) {
|
||||
setActiveGame(activeGameId);
|
||||
}
|
||||
}
|
||||
|
||||
function shortId(gameId) {
|
||||
return String(gameId || "-").slice(0, 8);
|
||||
}
|
||||
|
||||
function displayGameTypeOrMap(game) {
|
||||
const mapName = String((game && game.map) || "").trim();
|
||||
const gameType = String((game && game.game_type) || "").trim();
|
||||
if (gameType.toLowerCase() === "duel" && mapName.toLowerCase() === "standard") {
|
||||
return "duel";
|
||||
}
|
||||
if (mapName && mapName.toLowerCase() !== "empty") {
|
||||
return mapName;
|
||||
}
|
||||
return gameType || "-";
|
||||
}
|
||||
|
||||
function extractReasoningList(reasoning) {
|
||||
const parts = [];
|
||||
if (!reasoning || typeof reasoning !== "object") {
|
||||
@@ -1308,34 +1353,105 @@
|
||||
}
|
||||
|
||||
async function loadGames() {
|
||||
const games = (initialGamesPayload && Array.isArray(initialGamesPayload.games))
|
||||
? initialGamesPayload.games
|
||||
const games = (dashboardGamesPayload && Array.isArray(dashboardGamesPayload.games))
|
||||
? dashboardGamesPayload.games
|
||||
: [];
|
||||
|
||||
gamesBodyEl.innerHTML = games.map((g) => `
|
||||
<tr data-game-id="${g.game_id}">
|
||||
<td><code>${shortId(g.game_id)}</code><br><small>${safeString((g.map && g.map.trim() && g.map.trim() !== "empty") ? g.map.trim() : g.game_type)}</small></td>
|
||||
<td>${toTitle(g.status)}</td>
|
||||
<td>${g.status === "running" ? "-" : g.winner_you ? "Win" : "Loss"}</td>
|
||||
<td>${safeString(g.final_turn)}</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
renderGamesTable(games);
|
||||
|
||||
for (const row of gamesBodyEl.querySelectorAll("tr")) {
|
||||
row.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
const gameId = row.getAttribute("data-game-id");
|
||||
if (gameId) loadReplay(gameId);
|
||||
});
|
||||
}
|
||||
|
||||
if (initialGameId) {
|
||||
await loadReplay(initialGameId);
|
||||
if (activeGameId) {
|
||||
await loadReplay(activeGameId);
|
||||
} else if (games.length > 0) {
|
||||
await loadReplay(games[0].game_id);
|
||||
activeGameId = games[0].game_id;
|
||||
await loadReplay(activeGameId);
|
||||
}
|
||||
}
|
||||
|
||||
function dashboardGamesWebSocketUrl() {
|
||||
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
return `${protocol}://${window.location.host}/dashboard/ws/games`;
|
||||
}
|
||||
|
||||
function scheduleDashboardGamesWebSocketReconnect() {
|
||||
if (gamesWebSocketReconnectTimer) return;
|
||||
gamesWebSocketReconnectTimer = window.setTimeout(() => {
|
||||
gamesWebSocketReconnectTimer = null;
|
||||
connectDashboardGamesWebSocket();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function applyDashboardGamesUpdate(payload) {
|
||||
const nextSummary = payload && payload.summary && typeof payload.summary === "object"
|
||||
? payload.summary
|
||||
: null;
|
||||
const nextGames = payload && payload.games && typeof payload.games === "object"
|
||||
? payload.games
|
||||
: null;
|
||||
|
||||
if (nextSummary) {
|
||||
dashboardSummary = nextSummary;
|
||||
renderStats(dashboardSummary);
|
||||
}
|
||||
|
||||
if (nextGames) {
|
||||
dashboardGamesPayload = nextGames;
|
||||
const games = Array.isArray(dashboardGamesPayload.games) ? dashboardGamesPayload.games : [];
|
||||
renderGamesTable(games);
|
||||
|
||||
const gameIds = new Set(games.map((g) => g.game_id));
|
||||
if (activeGameId && !gameIds.has(activeGameId)) {
|
||||
activeGameId = "";
|
||||
replay = null;
|
||||
turnIndex = 0;
|
||||
renderTurn();
|
||||
clearActiveGame();
|
||||
}
|
||||
|
||||
const payloadGameId = payload && payload.game_id ? String(payload.game_id) : "";
|
||||
if (payloadGameId && payloadGameId === activeGameId) {
|
||||
loadReplay(payloadGameId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeGameId && games.length > 0) {
|
||||
activeGameId = games[0].game_id;
|
||||
loadReplay(activeGameId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function connectDashboardGamesWebSocket() {
|
||||
const wsUrl = dashboardGamesWebSocketUrl();
|
||||
try {
|
||||
gamesWebSocket = new WebSocket(wsUrl);
|
||||
} catch {
|
||||
scheduleDashboardGamesWebSocketReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
gamesWebSocket.addEventListener("message", (event) => {
|
||||
let payload = null;
|
||||
try {
|
||||
payload = JSON.parse(event.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!payload || payload.type !== "dashboard_games_update") return;
|
||||
applyDashboardGamesUpdate(payload);
|
||||
});
|
||||
|
||||
gamesWebSocket.addEventListener("close", () => {
|
||||
gamesWebSocket = null;
|
||||
scheduleDashboardGamesWebSocketReconnect();
|
||||
});
|
||||
|
||||
gamesWebSocket.addEventListener("error", () => {
|
||||
if (gamesWebSocket) {
|
||||
gamesWebSocket.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearActiveGame() {
|
||||
for (const row of gamesBodyEl.querySelectorAll("tr")) {
|
||||
row.classList.remove("active");
|
||||
@@ -1427,7 +1543,7 @@
|
||||
const hasHeadIcon = headIconByCell.has(key);
|
||||
const hasTailIcon = tailIconByCell.has(key);
|
||||
if (hasHeadIcon || hasTailIcon) {
|
||||
cell.style.background = "var(--cell)";
|
||||
//cell.style.background = "var(--cell)";
|
||||
} else {
|
||||
cell.style.background = snakeBody.get(key);
|
||||
}
|
||||
@@ -1499,6 +1615,7 @@
|
||||
return;
|
||||
}
|
||||
replay = await response.json();
|
||||
activeGameId = String(gameId || "");
|
||||
await preloadReplaySvgs();
|
||||
turnIndex = 0;
|
||||
const count = Array.isArray(replay.turns) ? replay.turns.length : 0;
|
||||
@@ -1520,6 +1637,33 @@
|
||||
playBtn.setAttribute("aria-label", "Play");
|
||||
}
|
||||
|
||||
function stepTurnBackward() {
|
||||
stopPlayback();
|
||||
if (!replay || turnIndex <= 0) return;
|
||||
turnIndex -= 1;
|
||||
renderTurn();
|
||||
}
|
||||
|
||||
function stepTurnForward() {
|
||||
stopPlayback();
|
||||
if (!replay || !Array.isArray(replay.turns) || turnIndex >= replay.turns.length - 1) return;
|
||||
turnIndex += 1;
|
||||
renderTurn();
|
||||
}
|
||||
|
||||
function adjustPlaybackSpeed(direction) {
|
||||
const speedEl = document.getElementById("speed");
|
||||
const optionCount = speedEl.options.length;
|
||||
if (optionCount <= 1) return;
|
||||
|
||||
const currentIndex = speedEl.selectedIndex;
|
||||
const nextIndex = Math.max(0, Math.min(optionCount - 1, currentIndex + direction));
|
||||
if (nextIndex === currentIndex) return;
|
||||
|
||||
speedEl.selectedIndex = nextIndex;
|
||||
speedEl.dispatchEvent(new Event("change"));
|
||||
}
|
||||
|
||||
function startPlayback() {
|
||||
if (!replay || !Array.isArray(replay.turns) || replay.turns.length < 2) return;
|
||||
if (turnIndex >= replay.turns.length - 1) {
|
||||
@@ -1548,17 +1692,48 @@
|
||||
});
|
||||
|
||||
document.getElementById("prev-btn").addEventListener("click", () => {
|
||||
stopPlayback();
|
||||
if (!replay || turnIndex <= 0) return;
|
||||
turnIndex -= 1;
|
||||
renderTurn();
|
||||
stepTurnBackward();
|
||||
});
|
||||
|
||||
document.getElementById("next-btn").addEventListener("click", () => {
|
||||
stopPlayback();
|
||||
if (!replay || !Array.isArray(replay.turns) || turnIndex >= replay.turns.length - 1) return;
|
||||
turnIndex += 1;
|
||||
renderTurn();
|
||||
stepTurnForward();
|
||||
});
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
const target = event.target;
|
||||
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
stepTurnBackward();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
stepTurnForward();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
adjustPlaybackSpeed(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
adjustPlaybackSpeed(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === " " || event.key === "Spacebar") {
|
||||
event.preventDefault();
|
||||
if (timer) stopPlayback();
|
||||
else startPlayback();
|
||||
}
|
||||
});
|
||||
|
||||
sliderEl.addEventListener("input", () => {
|
||||
@@ -1575,6 +1750,7 @@
|
||||
renderThinking(null);
|
||||
loadSummary();
|
||||
await loadGames();
|
||||
connectDashboardGamesWebSocket();
|
||||
syncMonoOffset();
|
||||
}
|
||||
|
||||
@@ -1603,6 +1779,17 @@
|
||||
syncMonoOffset();
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
if (gamesWebSocketReconnectTimer) {
|
||||
clearTimeout(gamesWebSocketReconnectTimer);
|
||||
gamesWebSocketReconnectTimer = null;
|
||||
}
|
||||
if (gamesWebSocket) {
|
||||
gamesWebSocket.close();
|
||||
gamesWebSocket = null;
|
||||
}
|
||||
});
|
||||
|
||||
boot();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user