class GameState { constructor({ gameBoard, thinkingPanel, gamesTable, sliderEl, turnLabelEl }) { this._gameBoard = gameBoard; this._thinkingPanel = thinkingPanel; this._gamesTable = gamesTable; this._sliderEl = sliderEl; this._turnLabelEl = turnLabelEl; this._webSocket = null; this.replay = null; this.turnIndex = 0; this.activeGameId = ""; this.selectedSnakeId = null; this._timer = null; this._hasLoadedReplayOnce = false; } setWebSocket(webSocket) { this._webSocket = webSocket; } get isPlaying() { return Boolean(this._timer); } async loadReplay(gameId) { let nextReplay = null; try { nextReplay = await this._webSocket.requestReplay(gameId); } catch { if (!this._hasLoadedReplayOnce) { this._thinkingPanel.render( { my_move: "-", my_thinking: { error: `Replay websocket unavailable for ${gameId}` } }, null, ); return; } const response = await fetch(`/dashboard/game/${gameId}`); if (!response.ok) { this._thinkingPanel.render( { my_move: "-", my_thinking: { error: `Replay load failed for ${gameId}` } }, null, ); return; } nextReplay = await response.json(); } this.replay = nextReplay; this._hasLoadedReplayOnce = true; this.activeGameId = String(gameId || ""); await this._gameBoard.preloadSvgs(this.replay); this.turnIndex = 0; const count = Array.isArray(this.replay.turns) ? this.replay.turns.length : 0; this._sliderEl.max = String(Math.max(0, count - 1)); this._sliderEl.value = "0"; this._gamesTable.setActive(gameId); this.renderTurn(); } renderTurn() { if (!this.replay || !Array.isArray(this.replay.turns) || this.replay.turns.length === 0) { this._turnLabelEl.textContent = "Turn -"; this._gameBoard.clearBoard(); this._thinkingPanel.render(null, null); return; } const game = this.replay.game || {}; const turns = this.replay.turns; const turn = turns[this.turnIndex]; this._turnLabelEl.textContent = `Turn ${turn.turn} / ${turns[turns.length - 1].turn}`; this._sliderEl.value = String(this.turnIndex); this._gameBoard.paintBoard(turn, game.width, game.height, this.selectedSnakeId, this.replay); this._thinkingPanel.render(turn, this.replay); if (this.selectedSnakeId) { this._thinkingPanel.highlightSnake(this.selectedSnakeId); } } stopPlayback() { if (this._timer) { clearInterval(this._timer); this._timer = null; } const playBtn = document.getElementById("play-btn"); playBtn.textContent = "▶"; playBtn.setAttribute("title", "Play"); playBtn.setAttribute("aria-label", "Play"); } startPlayback() { if (!this.replay || !Array.isArray(this.replay.turns) || this.replay.turns.length < 2) return; if (this.turnIndex >= this.replay.turns.length - 1) { this.turnIndex = 0; this.renderTurn(); } this.stopPlayback(); const interval = Number(document.getElementById("speed").value || 650); this._timer = setInterval(() => { if (!this.replay || this.turnIndex >= this.replay.turns.length - 1) { this.stopPlayback(); return; } this.turnIndex += 1; this.renderTurn(); }, interval); const playBtn = document.getElementById("play-btn"); playBtn.textContent = "❚❚"; playBtn.setAttribute("title", "Pause"); playBtn.setAttribute("aria-label", "Pause"); } stepBackward() { this.stopPlayback(); if (!this.replay || this.turnIndex <= 0) return; this.turnIndex -= 1; this.renderTurn(); } stepForward() { this.stopPlayback(); if (!this.replay || !Array.isArray(this.replay.turns) || this.turnIndex >= this.replay.turns.length - 1) return; this.turnIndex += 1; this.renderTurn(); } adjustSpeed(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")); } setSelectedSnakeId(id) { this.selectedSnakeId = id; } }