- Add autoSave() function that saves replay silently on game over - Add beforeunload listener to save incomplete games when navigating away - Add prominent 'Save Now' button in status bar (visible during play) - Show 'Replay auto-saved!' notification in status bar - Add gameSaved flag to prevent duplicate saves
435 lines
15 KiB
JavaScript
435 lines
15 KiB
JavaScript
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 =
|
|
'<span class="player-name ' + player.color + '">' + player.name + '</span>' +
|
|
'<span class="player-meta">' +
|
|
'<span class="score">Score: ' + player.score + '</span>' +
|
|
'<span class="pieces">' + player.pieces_remaining + '/' + player.total_pieces + '</span>' +
|
|
'</span>';
|
|
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 =
|
|
'<button id="save-btn">Save Replay</button>' +
|
|
'<a href="/">Back to Menu</a>';
|
|
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 =
|
|
'<span class="move-num">' + (index + 1) + '</span>' +
|
|
'<span class="move-player ' + playerColor + '">P' + (move.player_idx + 1) + isHuman + '</span>' +
|
|
'<span class="move-piece">' + move.piece_name + '</span>' +
|
|
'<span class="move-pos">(' + move.x + ', ' + move.y + ')</span>';
|
|
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 = '<span>' + row[0] + '</span><span>' + row[1] + '</span>';
|
|
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);
|
|
}
|