Files
snake-python/templates/side/dashboard.htm
T
2026-04-07 03:25:10 +02:00

206 lines
8.0 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Snake Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/root.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body>
<div class="page">
<header class="topbar">
<div>
<h1 class="title">Battlesnake Replay Dashboard</h1>
<p class="subtitle">Full-screen replay with turn-by-turn move reasoning and snake state.</p>
</div>
<div class="stats" id="stats"></div>
</header>
<main class="main">
<section class="panel">
<div class="panel-header">
<h2>Games</h2>
<p>Pick a game to inspect the match, move timeline, and why your snake chose each turn.</p>
</div>
<div class="games">
<table>
<thead>
<tr>
<th>Game</th>
<th>Status</th>
<th>W/L</th>
<th>Turns</th>
</tr>
</thead>
<tbody id="games-body"></tbody>
</table>
</div>
</section>
<section class="panel right">
<div class="controls">
<button id="prev-btn">Prev</button>
<button id="play-btn" class="primary" title="Play" aria-label="Play"></button>
<button id="next-btn">Next</button>
<label>
<span>Speed</span>
<select id="speed">
<option value="900">0.75x</option>
<option value="650" selected>1x</option>
<option value="400">1.5x</option>
<option value="250">2x</option>
<option value="160">2.5x</option>
</select>
</label>
<input type="range" min="0" max="0" step="1" id="turn-slider" value="0">
<strong class="turn-badge" id="turn-label">Turn -</strong>
</div>
<div class="content">
<div class="board-wrap">
<div class="board" id="board"></div>
</div>
<div class="thinking" id="thinking"></div>
</div>
</section>
</main>
</div>
<script src="{{ url_for('static', filename='js/Utils.js') }}"></script>
<script src="{{ url_for('static', filename='js/Snake.js') }}"></script>
<script src="{{ url_for('static', filename='js/MoveTable.js') }}"></script>
<script src="{{ url_for('static', filename='js/SnakeTable.js') }}"></script>
<script src="{{ url_for('static', filename='js/GameBoard.js') }}"></script>
<script src="{{ url_for('static', filename='js/OverallStats.js') }}"></script>
<script src="{{ url_for('static', filename='js/GamesTable.js') }}"></script>
<script src="{{ url_for('static', filename='js/Thinking.js') }}"></script>
<script src="{{ url_for('static', filename='js/DashboardWebSocket.js') }}"></script>
<script src="{{ url_for('static', filename='js/GameState.js') }}"></script>
<script>
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: [] };
const overallStats = new OverallStats(document.getElementById("stats"));
const gameBoard = new GameBoard(document.getElementById("board"));
const thinkingPanel = new ThinkingPanel(document.getElementById("thinking"));
const sliderEl = document.getElementById("turn-slider");
const turnLabelEl = document.getElementById("turn-label");
const gameState = new GameState({ gameBoard, thinkingPanel, sliderEl, turnLabelEl });
gameState.activeGameId = String(initialGameId || "");
const gamesTable = new GamesTable(
document.getElementById("games-body"),
"{{ battlesnake_url }}",
(gameId) => gameState.loadReplay(gameId),
);
gameState._gamesTable = gamesTable;
const dashboardWS = new DashboardWebSocket({
onGamesUpdate: (payload) => {
if (payload.summary) {
dashboardSummary = payload.summary;
overallStats.render(dashboardSummary);
}
if (payload.games) {
dashboardGamesPayload = payload.games;
const games = Array.isArray(dashboardGamesPayload.games) ? dashboardGamesPayload.games : [];
gamesTable.render(games, gameState.activeGameId);
}
},
});
gameState.setWebSocket(dashboardWS);
document.getElementById("play-btn").addEventListener("click", () => {
if (gameState.isPlaying) gameState.stopPlayback();
else gameState.startPlayback();
});
document.getElementById("prev-btn").addEventListener("click", () => {
gameState.stepBackward();
});
document.getElementById("next-btn").addEventListener("click", () => {
gameState.stepForward();
});
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(); gameState.stepBackward(); return; }
if (event.key === "ArrowRight") { event.preventDefault(); gameState.stepForward(); return; }
if (event.key === "ArrowUp") { event.preventDefault(); gameState.adjustSpeed(1); return; }
if (event.key === "ArrowDown") { event.preventDefault(); gameState.adjustSpeed(-1); return; }
if (event.key === " " || event.key === "Spacebar") {
event.preventDefault();
if (gameState.isPlaying) gameState.stopPlayback();
else gameState.startPlayback();
}
});
sliderEl.addEventListener("input", () => {
gameState.stopPlayback();
gameState.turnIndex = Number(sliderEl.value || 0);
gameState.renderTurn();
});
document.getElementById("speed").addEventListener("change", () => {
if (gameState.isPlaying) gameState.startPlayback();
});
document.getElementById("thinking").addEventListener("click", (event) => {
const row = event.target.closest(".snake-row");
if (!row) return;
const section = row.closest(".snakes-section");
if (!section) return;
const clickedId = row.dataset.snakeId || null;
if (row.classList.contains("highlighted")) {
row.classList.remove("highlighted");
section.classList.remove("has-highlight");
gameState.setSelectedSnakeId(null);
} else {
section.querySelectorAll(".snake-row.highlighted").forEach((r) => r.classList.remove("highlighted"));
row.classList.add("highlighted");
section.classList.add("has-highlight");
gameState.setSelectedSnakeId(clickedId);
}
const game = gameState.replay && gameState.replay.game ? gameState.replay.game : {};
const turns = gameState.replay && Array.isArray(gameState.replay.turns) ? gameState.replay.turns : [];
if (turns[gameState.turnIndex]) {
gameBoard.paintBoard(turns[gameState.turnIndex], game.width, game.height, gameState.selectedSnakeId, gameState.replay);
}
});
window.addEventListener("resize", () => {
thinkingPanel.syncMonoOffset();
});
window.addEventListener("beforeunload", () => {
dashboardWS.shutdown();
});
async function boot() {
thinkingPanel.render(null, null);
overallStats.render(dashboardSummary);
dashboardWS.connect();
await dashboardWS.waitForOpen(2500);
const games = Array.isArray(dashboardGamesPayload.games) ? dashboardGamesPayload.games : [];
gamesTable.render(games, gameState.activeGameId);
if (gameState.activeGameId) {
await gameState.loadReplay(gameState.activeGameId);
} else if (games.length > 0) {
await gameState.loadReplay(games[0].game_id);
}
thinkingPanel.syncMonoOffset();
}
boot();
</script>
</body>
</html>