feat: auto-save replays on game over and page unload

- 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
This commit is contained in:
mattlamb227@gmail.com
2026-08-09 22:11:50 -04:00
parent ba6e26ab59
commit 10d8b0a376
3 changed files with 60 additions and 7 deletions
+38 -6
View File
@@ -14,6 +14,7 @@ function initGame() {
let selectedPieceId = null; let selectedPieceId = null;
let currentPlacements = []; let currentPlacements = [];
let gameSaved = false;
async function refresh() { async function refresh() {
const response = await fetch(`/api/game/${sessionId}/state`); const response = await fetch(`/api/game/${sessionId}/state`);
@@ -87,6 +88,7 @@ function initGame() {
sidebar.appendChild(controls); sidebar.appendChild(controls);
document.getElementById('save-btn').addEventListener('click', saveReplay); document.getElementById('save-btn').addEventListener('click', saveReplay);
document.getElementById('status-save-btn').addEventListener('click', saveReplay);
} }
function renderPiece(piece, playerColor) { function renderPiece(piece, playerColor) {
@@ -194,27 +196,48 @@ function initGame() {
const response = await fetch(`/api/game/${sessionId}/save`, { method: 'POST' }); const response = await fetch(`/api/game/${sessionId}/save`, { method: 'POST' });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
gameSaved = true;
alert('Replay saved!'); alert('Replay saved!');
} else { } else {
alert('Failed to save: ' + result.error); 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) { function renderStatusBar(state) {
const statusBar = document.getElementById('status-bar'); const statusText = document.getElementById('status-text');
const saveBtn = document.getElementById('status-save-btn');
if (state.game_over) { if (state.game_over) {
if (state.winner !== null) { if (state.winner !== null) {
statusBar.textContent = 'Game Over! Winner: ' + state.players[state.winner].name; statusText.textContent = 'Game Over! Winner: ' + state.players[state.winner].name;
} else { } else {
statusBar.textContent = 'Game Over! Tie!'; statusText.textContent = 'Game Over! Tie!';
} }
saveBtn.style.display = 'none';
} else { } else {
statusBar.textContent = 'Current player: ' + state.players[state.current_player].name; statusText.textContent = 'Current player: ' + state.players[state.current_player].name;
saveBtn.style.display = 'inline-block';
} }
} }
function handleGameOver(state) { function handleGameOver(state) {
const statusBar = document.getElementById('status-bar'); const statusText = document.getElementById('status-text');
const saveBtn = document.getElementById('status-save-btn');
let text = 'Game Over! '; let text = 'Game Over! ';
if (state.winner !== null) { if (state.winner !== null) {
text += 'Winner: ' + state.players[state.winner].name; text += 'Winner: ' + state.players[state.winner].name;
@@ -224,15 +247,24 @@ function initGame() {
text += ' | Scores: ' + state.players.map(function(p) { text += ' | Scores: ' + state.players.map(function(p) {
return p.name + ': ' + p.score; return p.name + ': ' + p.score;
}).join(', '); }).join(', ');
statusBar.textContent = text; statusText.textContent = text;
saveBtn.style.display = 'none';
document.querySelectorAll('.piece').forEach(function(p) { document.querySelectorAll('.piece').forEach(function(p) {
p.style.pointerEvents = 'none'; p.style.pointerEvents = 'none';
p.style.opacity = '0.5'; p.style.opacity = '0.5';
}); });
autoSave();
} }
refresh(); refresh();
window.addEventListener('beforeunload', function() {
if (!gameSaved) {
navigator.sendBeacon(`/api/game/${sessionId}/save`);
}
});
} }
function initReplay() { function initReplay() {
+18
View File
@@ -342,6 +342,24 @@ h3 {
font-weight: bold; font-weight: bold;
font-size: 1.05em; font-size: 1.05em;
min-height: 24px; min-height: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
#status-save-btn {
padding: 4px 12px;
background: var(--blue);
color: var(--bg);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
font-weight: bold;
}
#status-save-btn:hover {
background: #5bb3ff;
} }
/* ===== Replay Page ===== */ /* ===== Replay Page ===== */
+4 -1
View File
@@ -15,7 +15,10 @@
</div> </div>
</div> </div>
</div> </div>
<div id="status-bar">Loading...</div> <div id="status-bar">
<span id="status-text">Loading...</span>
<button id="status-save-btn" style="display:none;">Save Now</button>
</div>
<script> <script>
window.GAME_SESSION_ID = "{{ session_id }}"; window.GAME_SESSION_ID = "{{ session_id }}";