document.addEventListener('DOMContentLoaded', function() { if (document.getElementById('game-container')) { initGame(); } else if (document.getElementById('replay-container')) { initReplay(); } }); function initGame() { const sessionId = window.GAME_SESSION_ID; const boardSize = window.BOARD_SIZE; const cellSize = window.CELL_SIZE || 30; const cellGap = 1; let selectedPieceId = null; let currentPlacements = []; let gameSaved = false; async function refresh() { const response = await fetch(`/api/game/${sessionId}/state`); const state = await response.json(); renderBoard(state.board, boardSize, cellSize); renderSidebar(state); renderStatusBar(state); if (state.game_over) { handleGameOver(state); } } function renderBoard(board, size, cs) { const boardEl = document.getElementById('board'); boardEl.innerHTML = ''; boardEl.style.gridTemplateColumns = `repeat(${size}, ${cs}px)`; boardEl.style.gridTemplateRows = `repeat(${size}, ${cs}px)`; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const cell = document.createElement('div'); cell.className = `cell player-${board[y][x]}`; boardEl.appendChild(cell); } } } function renderSidebar(state) { const sidebar = document.getElementById('sidebar'); sidebar.innerHTML = ''; const playerInfo = document.createElement('div'); playerInfo.id = 'player-info'; state.players.forEach(function(player) { const el = document.createElement('div'); el.className = 'player-info'; el.innerHTML = '' + player.name + '' + '' + 'Score: ' + player.score + '' + '' + player.pieces_remaining + '/' + player.total_pieces + '' + ''; playerInfo.appendChild(el); }); sidebar.appendChild(playerInfo); const piecesEl = document.createElement('div'); piecesEl.id = 'pieces'; const piecesTitle = document.createElement('h3'); piecesTitle.textContent = 'Your Pieces'; piecesEl.appendChild(piecesTitle); if (state.available_pieces.length === 0) { const emptyMsg = document.createElement('p'); emptyMsg.className = 'empty'; emptyMsg.textContent = 'No pieces remaining'; piecesEl.appendChild(emptyMsg); } else { state.available_pieces.forEach(function(piece) { piecesEl.appendChild(renderPiece(piece, state.players[state.human_player].color)); }); } sidebar.appendChild(piecesEl); const controls = document.createElement('div'); controls.id = 'controls'; controls.innerHTML = '' + 'Back to Menu'; sidebar.appendChild(controls); document.getElementById('save-btn').addEventListener('click', saveReplay); document.getElementById('status-save-btn').addEventListener('click', saveReplay); } function renderPiece(piece, playerColor) { const container = document.createElement('div'); container.className = 'piece'; container.dataset.pieceId = piece.id; const width = Math.max.apply(null, piece.squares.map(function(s) { return s[0]; })) + 1; const height = Math.max.apply(null, piece.squares.map(function(s) { return s[1]; })) + 1; container.style.gridTemplateColumns = 'repeat(' + width + ', 1fr)'; container.style.gridTemplateRows = 'repeat(' + height + ', 1fr)'; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const cell = document.createElement('div'); cell.className = 'piece-cell'; const isFilled = piece.squares.some(function(s) { return s[0] === x && s[1] === y; }); if (isFilled) { cell.classList.add('filled'); cell.style.background = getPlayerColor(playerColor); } container.appendChild(cell); } } container.addEventListener('click', function() { selectPiece(piece.id); }); return container; } function getPlayerColor(name) { var colors = { red: '#ff6b6b', blue: '#4dabf7', yellow: '#ffd43b', green: '#51cf66' }; return colors[name] || '#ff6b6b'; } async function selectPiece(pieceId) { if (selectedPieceId === pieceId) { selectedPieceId = null; clearPlacements(); document.querySelectorAll('.piece').forEach(function(p) { p.classList.remove('selected'); }); return; } selectedPieceId = pieceId; document.querySelectorAll('.piece').forEach(function(p) { p.classList.remove('selected'); }); var selectedEl = document.querySelector('.piece[data-piece-id="' + pieceId + '"]'); if (selectedEl) { selectedEl.classList.add('selected'); } const response = await fetch(`/api/game/${sessionId}/placements?piece_id=${pieceId}`); const data = await response.json(); currentPlacements = data.placements; renderPlacements(currentPlacements); } function renderPlacements(placements) { const overlay = document.getElementById('placement-overlay'); overlay.innerHTML = ''; if (placements.length === 0) { return; } placements.forEach(function(placement) { placement.cells.forEach(function(cell) { const x = cell[0], y = cell[1]; const el = document.createElement('div'); el.className = 'placement-cell'; el.style.left = (x * (cellSize + cellGap)) + 'px'; el.style.top = (y * (cellSize + cellGap)) + 'px'; el.style.width = cellSize + 'px'; el.style.height = cellSize + 'px'; el.dataset.action = placement.action; el.addEventListener('click', function() { makeMove(placement.action); }); overlay.appendChild(el); }); }); } function clearPlacements() { currentPlacements = []; document.getElementById('placement-overlay').innerHTML = ''; } async function makeMove(action) { const response = await fetch(`/api/game/${sessionId}/move`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: action }), }); const result = await response.json(); if (result.success) { selectedPieceId = null; clearPlacements(); document.querySelectorAll('.piece').forEach(function(p) { p.classList.remove('selected'); }); await refresh(); } else { console.error('Move failed:', result.error); } } async function saveReplay() { const response = await fetch(`/api/game/${sessionId}/save`, { method: 'POST' }); const result = await response.json(); if (result.success) { gameSaved = true; alert('Replay saved!'); } else { alert('Failed to save: ' + result.error); } } async function autoSave() { if (gameSaved) return; try { const response = await fetch(`/api/game/${sessionId}/save`, { method: 'POST' }); const result = await response.json(); if (result.success) { gameSaved = true; const statusBar = document.getElementById('status-bar'); statusBar.textContent += ' | Replay auto-saved!'; } } catch (e) { console.error('Auto-save failed:', e); } } function renderStatusBar(state) { const statusText = document.getElementById('status-text'); const saveBtn = document.getElementById('status-save-btn'); if (state.game_over) { if (state.winner !== null) { statusText.textContent = 'Game Over! Winner: ' + state.players[state.winner].name; } else { statusText.textContent = 'Game Over! Tie!'; } saveBtn.style.display = 'none'; } else { statusText.textContent = 'Current player: ' + state.players[state.current_player].name; saveBtn.style.display = 'inline-block'; } } function handleGameOver(state) { const statusText = document.getElementById('status-text'); const saveBtn = document.getElementById('status-save-btn'); let text = 'Game Over! '; if (state.winner !== null) { text += 'Winner: ' + state.players[state.winner].name; } else { text += 'Tie!'; } text += ' | Scores: ' + state.players.map(function(p) { return p.name + ': ' + p.score; }).join(', '); statusText.textContent = text; saveBtn.style.display = 'none'; document.querySelectorAll('.piece').forEach(function(p) { p.style.pointerEvents = 'none'; p.style.opacity = '0.5'; }); autoSave(); } refresh(); window.addEventListener('beforeunload', function() { if (!gameSaved) { navigator.sendBeacon(`/api/game/${sessionId}/save`); } }); } function initReplay() { const boardSize = window.BOARD_SIZE; const totalMoves = window.TOTAL_MOVES; const replayData = window.REPLAY_DATA; const cellSize = window.CELL_SIZE || 30; const states = replayData.states; const moves = replayData.moves; const config = replayData.config; const statistics = replayData.statistics; let currentStep = 0; let isPlaying = false; let playInterval = null; function renderStep(step) { currentStep = step; const state = states[step]; renderBoard(state.board, boardSize, cellSize); renderMoveList(moves, step, config.human_player); renderStatistics(statistics); const slider = document.getElementById('step-slider'); slider.value = step; slider.max = totalMoves; document.getElementById('step-indicator').textContent = step + ' / ' + totalMoves; document.getElementById('prev-btn').disabled = step === 0; document.getElementById('next-btn').disabled = step >= totalMoves; if (step >= totalMoves && isPlaying) { stopPlay(); } } function renderBoard(board, size, cs) { const boardEl = document.getElementById('board'); boardEl.innerHTML = ''; boardEl.style.gridTemplateColumns = 'repeat(' + size + ', ' + cs + 'px)'; boardEl.style.gridTemplateRows = 'repeat(' + size + ', ' + cs + 'px)'; for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const cell = document.createElement('div'); cell.className = 'cell player-' + board[y][x]; boardEl.appendChild(cell); } } } function renderMoveList(moves, currentStep, humanPlayer) { const listEl = document.getElementById('move-list'); listEl.innerHTML = ''; moves.forEach(function(move, index) { const el = document.createElement('div'); el.className = 'move-entry'; if (index < currentStep) { el.classList.add('played'); } if (index === currentStep - 1) { el.classList.add('current'); } var playerColor = ['red', 'blue', 'yellow', 'green'][move.player_idx]; var isHuman = move.player_idx === humanPlayer ? ' (you)' : ''; el.innerHTML = '' + (index + 1) + '' + 'P' + (move.player_idx + 1) + isHuman + '' + '' + move.piece_name + '' + '(' + move.x + ', ' + move.y + ')'; listEl.appendChild(el); }); } function renderStatistics(stats) { const statsEl = document.getElementById('statistics'); statsEl.innerHTML = ''; const title = document.createElement('h3'); title.textContent = 'Statistics'; statsEl.appendChild(title); var rows = [ ['Coverage', stats.coverage + '%'], ['Total Moves', stats.total_moves] ]; stats.squares_placed.forEach(function(score, i) { rows.push(['P' + (i + 1) + ' Score', score]); }); stats.pieces_placed.forEach(function(placed, i) { rows.push(['P' + (i + 1) + ' Pieces', placed]); }); rows.push(['Winner Margin', stats.winner_margin]); rows.forEach(function(row) { var r = document.createElement('div'); r.className = 'stat-row'; r.innerHTML = '' + row[0] + '' + row[1] + ''; statsEl.appendChild(r); }); } function togglePlay() { if (isPlaying) { stopPlay(); } else { startPlay(); } } function startPlay() { if (currentStep >= totalMoves) { currentStep = 0; } isPlaying = true; document.getElementById('play-btn').textContent = '\u23F8 Pause'; var speed = parseInt(document.getElementById('speed-select').value); playInterval = setInterval(function() { if (currentStep < totalMoves) { renderStep(currentStep + 1); } else { stopPlay(); } }, speed); } function stopPlay() { isPlaying = false; document.getElementById('play-btn').textContent = '\u25B6 Play'; if (playInterval) { clearInterval(playInterval); playInterval = null; } } document.getElementById('prev-btn').addEventListener('click', function() { if (currentStep > 0) { renderStep(currentStep - 1); } }); document.getElementById('next-btn').addEventListener('click', function() { if (currentStep < totalMoves) { renderStep(currentStep + 1); } }); document.getElementById('play-btn').addEventListener('click', togglePlay); document.getElementById('step-slider').addEventListener('input', function(e) { var step = parseInt(e.target.value); if (isPlaying) { stopPlay(); } renderStep(step); }); renderStep(0); }