Add surrender and rematch UI - Part 1

- Add surrender button to game controls
- Add game-over modal with stats and rematch option
- Add surrender confirmation modal
- Add all CSS styling for new modals and buttons
- Add surrender-rematch.js with global helper functions
- Update multiplayer.js constructor to track opponent for rematch
This commit is contained in:
2025-12-22 17:39:43 +11:00
parent 6c4aedec1d
commit 622a7e4094
5 changed files with 554 additions and 70 deletions

View File

@@ -45,10 +45,19 @@
<main class="game-container">
<!-- Game Mode Toggle -->
<div class="mode-selector">
<button class="mode-btn active" id="localModeBtn" onclick="toggleGameMode('local')">
<button
class="mode-btn active"
id="localModeBtn"
onclick="toggleGameMode('local')"
>
🎮 Local Play
</button>
<button class="mode-btn" id="multiplayerModeBtn" onclick="toggleGameMode('multiplayer')" disabled>
<button
class="mode-btn"
id="multiplayerModeBtn"
onclick="toggleGameMode('multiplayer')"
disabled
>
🌐 Multiplayer
</button>
</div>
@@ -74,11 +83,17 @@
<span class="player-marker" id="currentPlayer">X</span>
</div>
<!-- Status text integrated here -->
<span class="status-text-small" id="statusMessage">Player X starts</span>
<span class="status-text-small" id="statusMessage"
>Player X starts</span
>
</div>
<!-- Right Side: Player Identity (Multiplayer only) -->
<div class="status-right" id="playerIdentitySection" style="display: none;">
<div
class="status-right"
id="playerIdentitySection"
style="display: none"
>
<span class="label">You are:</span>
<div class="player-display">
<span class="player-marker" id="playerIdentity">?</span>
@@ -113,6 +128,27 @@
</svg>
New Game
</button>
<button
class="surrender-btn"
id="surrenderBtn"
style="display: none"
onclick="confirmSurrender()"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
Surrender
</button>
</div>
<div class="board-wrapper">
@@ -120,7 +156,11 @@
</div>
<!-- Multiplayer Panel -->
<div id="multiplayerPanel" class="multiplayer-panel" style="display: none;">
<div
id="multiplayerPanel"
class="multiplayer-panel"
style="display: none"
>
<div class="player-stats-card">
<h3>Your Stats</h3>
<div class="stats-grid">
@@ -191,20 +231,39 @@
<span class="db-status-value" id="dbTimestamp">--</span>
</div>
<div class="db-status-item">
<button class="db-retry-btn" id="dbRetryBtn" onclick="window.dbStatusMonitor && window.dbStatusMonitor.retryConnection()" title="Retry SQL Connection">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
<button
class="db-retry-btn"
id="dbRetryBtn"
onclick="window.dbStatusMonitor && window.dbStatusMonitor.retryConnection()"
title="Retry SQL Connection"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"
/>
</svg>
Retry
</button>
</div>
</div>
<!-- Database Status Details (for testing phase) -->
<div class="db-status-details" id="dbStatusDetails" style="display: none;">
<div class="db-status-details" id="dbStatusDetails" style="display: none">
<div class="db-details-header">
<span class="db-details-title">🔍 SQL Connection Details</span>
<button class="db-details-close" onclick="document.getElementById('dbStatusDetails').style.display='none'">×</button>
<button
class="db-details-close"
onclick="document.getElementById('dbStatusDetails').style.display='none'"
>
×
</button>
</div>
<div class="db-details-content" id="dbDetailsContent">
<div class="db-detail-item">
@@ -221,14 +280,16 @@
</div>
<div class="db-detail-item">
<span class="db-detail-label">Error:</span>
<span class="db-detail-value db-detail-error" id="detailError">None</span>
<span class="db-detail-value db-detail-error" id="detailError"
>None</span
>
</div>
<div class="db-detail-logs" id="detailLogs"></div>
</div>
</div>
<!-- SQL Error Display (Testing Phase) -->
<div class="sql-error-banner" id="sqlErrorBanner" style="display: none;">
<div class="sql-error-banner" id="sqlErrorBanner" style="display: none">
<div class="sql-error-header">
<span class="sql-error-icon">⚠️</span>
<span class="sql-error-title">SQL Connection Error</span>
@@ -256,15 +317,97 @@
<div class="modal-overlay" id="usernameModal">
<div class="modal-content username-modal">
<h2>Enter Your Username</h2>
<p class="modal-subtitle">Choose a family-friendly name (3-20 characters)</p>
<input type="text" id="usernameInput" placeholder="Your username" maxlength="20" />
<div id="usernameError" class="error-message" style="display: none;"></div>
<p class="modal-subtitle">
Choose a family-friendly name (3-20 characters)
</p>
<input
type="text"
id="usernameInput"
placeholder="Your username"
maxlength="20"
/>
<div
id="usernameError"
class="error-message"
style="display: none"
></div>
<button class="submit-btn" onclick="submitUsername()">
Join Multiplayer
</button>
</div>
</div>
<!-- Game Over Modal -->
<div class="modal-overlay" id="gameOverModal">
<div class="modal-content game-over-modal">
<div class="game-over-icon" id="gameOverIcon">🎮</div>
<h2 id="gameOverTitle">Game Over</h2>
<p class="game-over-subtitle" id="gameOverMessage">
Thanks for playing!
</p>
<div class="game-over-stats">
<div class="stat-item">
<span class="stat-label">Wins:</span>
<span class="stat-value" id="gameOverWins">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Losses:</span>
<span class="stat-value" id="gameOverLosses">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Draws:</span>
<span class="stat-value" id="gameOverDraws">0</span>
</div>
</div>
<div class="game-over-actions">
<button
class="rematch-btn"
id="rematchBtn"
onclick="requestRematch()"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"
/>
</svg>
Challenge Again
</button>
<button class="lobby-btn" onclick="returnToLobby()">
← Return to Lobby
</button>
</div>
</div>
</div>
<!-- Surrender Confirmation Modal -->
<div class="modal-overlay" id="surrenderModal">
<div class="modal-content surrender-modal">
<div class="surrender-icon">⚠️</div>
<h2>Surrender Game?</h2>
<p class="modal-subtitle">
Are you sure you want to surrender? This will count as a loss.
</p>
<div class="surrender-actions">
<button class="confirm-surrender-btn" onclick="executeSurrender()">
Yes, Surrender
</button>
<button class="cancel-surrender-btn" onclick="cancelSurrender()">
Cancel
</button>
</div>
</div>
</div>
<script src="surrender-rematch.js"></script>
<script src="storage.js"></script>
<script src="db-status.js"></script>
<script src="multiplayer.js"></script>
@@ -272,51 +415,51 @@
<script>
// Game mode toggling
function toggleGameMode(mode) {
const localBtn = document.getElementById('localModeBtn');
const multiplayerBtn = document.getElementById('multiplayerModeBtn');
const gameControls = document.getElementById('gameControls');
const multiplayerPanel = document.getElementById('multiplayerPanel');
const boardWrapper = document.querySelector('.board-wrapper');
const localBtn = document.getElementById("localModeBtn");
const multiplayerBtn = document.getElementById("multiplayerModeBtn");
const gameControls = document.getElementById("gameControls");
const multiplayerPanel = document.getElementById("multiplayerPanel");
const boardWrapper = document.querySelector(".board-wrapper");
// statusMessage is now inside gameControls, so we don't need to toggle it separately
if (mode === 'local') {
localBtn.classList.add('active');
multiplayerBtn.classList.remove('active');
gameControls.style.display = 'grid';
multiplayerPanel.style.display = 'none';
boardWrapper.style.display = 'flex';
if (mode === "local") {
localBtn.classList.add("active");
multiplayerBtn.classList.remove("active");
gameControls.style.display = "grid";
multiplayerPanel.style.display = "none";
boardWrapper.style.display = "flex";
// Reset to local mode
if (window.multiplayerClient) {
window.multiplayerClient.isMultiplayer = false;
}
// Reset turn label to default
const turnLabel = document.getElementById('turnLabel');
const turnLabel = document.getElementById("turnLabel");
if (turnLabel) {
turnLabel.textContent = 'Current Turn:';
turnLabel.textContent = "Current Turn:";
}
} else {
localBtn.classList.remove('active');
multiplayerBtn.classList.add('active');
gameControls.style.display = 'none';
multiplayerPanel.style.display = 'block';
boardWrapper.style.display = 'none';
localBtn.classList.remove("active");
multiplayerBtn.classList.add("active");
gameControls.style.display = "none";
multiplayerPanel.style.display = "block";
boardWrapper.style.display = "none";
// Initialize multiplayer if not already
if (!window.multiplayerClient) {
if (!window.game) {
console.log('⏳ Waiting for game to initialize...');
// Poll for game to be ready
const checkGame = setInterval(() => {
if (window.game) {
clearInterval(checkGame);
console.log('✅ Game ready, initializing multiplayer...');
window.multiplayerClient = new MultiplayerClient(window.game);
window.multiplayerClient.connect();
}
}, 100); // Check every 100ms
return;
console.log("⏳ Waiting for game to initialize...");
// Poll for game to be ready
const checkGame = setInterval(() => {
if (window.game) {
clearInterval(checkGame);
console.log("✅ Game ready, initializing multiplayer...");
window.multiplayerClient = new MultiplayerClient(window.game);
window.multiplayerClient.connect();
}
}, 100); // Check every 100ms
return;
}
window.multiplayerClient = new MultiplayerClient(window.game);
window.multiplayerClient.connect();
@@ -326,59 +469,61 @@
// Submit username
function submitUsername() {
const input = document.getElementById('usernameInput');
const error = document.getElementById('usernameError');
const input = document.getElementById("usernameInput");
const error = document.getElementById("usernameError");
const username = input.value.trim();
if (!username) {
error.textContent = 'Please enter a username';
error.style.display = 'block';
error.textContent = "Please enter a username";
error.style.display = "block";
return;
}
if (username.length < 3 || username.length > 20) {
error.textContent = 'Username must be 3-20 characters';
error.style.display = 'block';
error.textContent = "Username must be 3-20 characters";
error.style.display = "block";
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
error.textContent = 'Only letters, numbers, underscores, and hyphens allowed';
error.style.display = 'block';
error.textContent =
"Only letters, numbers, underscores, and hyphens allowed";
error.style.display = "block";
return;
}
error.style.display = 'none';
error.style.display = "none";
if (window.multiplayerClient) {
window.multiplayerClient.registerPlayer(username);
window.multiplayerClient.registerPlayer(username);
} else {
console.error("Multiplayer client not initialized");
error.textContent = "Error: Multiplayer not initialized. Refresh page.";
error.style.display = 'block';
console.error("Multiplayer client not initialized");
error.textContent =
"Error: Multiplayer not initialized. Refresh page.";
error.style.display = "block";
}
}
// Change username
function changeUsername() {
// Clear saved username from localStorage
localStorage.removeItem('connect5_username');
localStorage.removeItem("connect5_username");
// Show username modal
const modal = document.getElementById('usernameModal');
const modal = document.getElementById("usernameModal");
if (modal) {
modal.classList.add('active');
modal.classList.add("active");
// Clear the input field
document.getElementById('usernameInput').value = '';
document.getElementById("usernameInput").value = "";
}
}
// Allow Enter key to submit username
document.addEventListener('DOMContentLoaded', () => {
const input = document.getElementById('usernameInput');
document.addEventListener("DOMContentLoaded", () => {
const input = document.getElementById("usernameInput");
if (input) {
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
submitUsername();
}
});

View File

@@ -453,3 +453,200 @@
padding: 2rem 1.5rem;
}
}
/* Surrender Button */
.surrender-btn {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
border: none;
border-radius: 10px;
color: white;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s ease;
font-family: "Inter", sans-serif;
display: flex;
align-items: center;
gap: 0.5rem;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
.surrender-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.5);
}
/* Game Over Modal */
.game-over-modal {
max-width: 450px;
text-align: center;
}
.game-over-icon {
font-size: 4rem;
margin-bottom: 1rem;
animation: bounce 1s ease-in-out;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
.game-over-modal h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.game-over-subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
margin-bottom: 1.5rem;
}
.game-over-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
padding: 1.5rem;
background: var(--bg-tertiary);
border-radius: 12px;
margin-bottom: 1.5rem;
border: 1px solid var(--border-light);
}
.game-over-stats .stat-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
}
.game-over-stats .stat-label {
color: var(--text-secondary);
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
}
.game-over-stats .stat-value {
color: var(--accent-primary);
font-size: 1.5rem;
font-weight: 700;
}
.game-over-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.rematch-btn,
.lobby-btn {
padding: 1rem 2rem;
border: none;
border-radius: 12px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
font-family: "Inter", sans-serif;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.rematch-btn {
background: var(--accent-gradient);
color: var(--text-primary);
box-shadow: 0 4px 16px rgba(147, 51, 234, 0.3);
}
.rematch-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(147, 51, 234, 0.5);
}
.lobby-btn {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 2px solid var(--border-light);
}
.lobby-btn:hover {
border-color: var(--accent-primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(147, 51, 234, 0.2);
}
/* Surrender Confirmation Modal */
.surrender-modal {
max-width: 400px;
text-align: center;
}
.surrender-icon {
font-size: 4rem;
margin-bottom: 1rem;
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
.surrender-modal h2 {
font-size: 1.75rem;
margin-bottom: 0.75rem;
color: var(--danger);
}
.surrender-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.5rem;
}
.confirm-surrender-btn,
.cancel-surrender-btn {
flex: 1;
padding: 0.875rem 1.5rem;
border: none;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
font-family: "Inter", sans-serif;
}
.confirm-surrender-btn {
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
color: white;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
.confirm-surrender-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.5);
}
.cancel-surrender-btn {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 2px solid var(--border-light);
}
.cancel-surrender-btn:hover {
border-color: var(--accent-primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(147, 51, 234, 0.2);
}

View File

@@ -10,6 +10,9 @@ class MultiplayerClient {
this.activePlayers = [];
this.pendingChallenges = new Map();
this.selectedBoardSize = 15; // Default board size for multiplayer
this.opponent = null; // Track opponent for rematch
this.opponentId = null;
this.lastGameResult = null;
}

68
surrender-rematch.js Normal file
View File

@@ -0,0 +1,68 @@
// Global helper functions for surrender and rematch features
// Show surrender confirmation
function confirmSurrender() {
if (!window.multiplayerClient || !window.multiplayerClient.isMultiplayer) {
return;
}
const modal = document.getElementById('surrenderModal');
if (modal) {
modal.classList.add('active');
}
}
// Cancel surrender
function cancelSurrender() {
const modal = document.getElementById('surrenderModal');
if (modal) {
modal.classList.remove('active');
}
}
// Execute surrender
function executeSurrender() {
const modal = document.getElementById('surrenderModal');
if (modal) {
modal.classList.remove('active');
}
if (window.multiplayerClient) {
window.multiplayerClient.socket.emit('surrender', {
gameId: window.multiplayerClient.currentGameId
});
}
}
// Request rematch
function requestRematch() {
if (!window.multiplayerClient || !window.multiplayerClient.opponent) {
return;
}
window.multiplayerClient.socket.emit('send_rematch', {
opponentId: window.multiplayerClient.opponentId,
boardSize: window.multiplayerClient.selectedBoardSize || 15
});
if (window.multiplayerClient.showMessage) {
window.multiplayerClient.showMessage(`Rematch request sent to ${window.multiplayerClient.opponent}`, 'info');
}
const modal = document.getElementById('gameOverModal');
if (modal) {
modal.classList.remove('active');
}
}
// Return to lobby from game over modal
function returnToLobby() {
if (window.multiplayerClient) {
window.multiplayerClient.returnToLobby();
}
const modal = document.getElementById('gameOverModal');
if (modal) {
modal.classList.remove('active');
}
}

View File

@@ -0,0 +1,71 @@
// Surrender game
surrenderGame() {
if (!this.isMultiplayer || !this.currentGameId) {
this.showMessage('No active game to surrender', 'error');
return;
}
this.socket.emit('surrender', { gameId: this.currentGameId });
}
// Handle rematch request from opponent
handleRematchRequest(data) {
const notification = document.createElement('div');
notification.className = 'challenge-notification';
notification.innerHTML = `
<div class="challenge-content">
<h3>Rematch Request!</h3>
<p><strong>${data.from}</strong> wants a rematch</p>
<p>Board size: ${data.boardSize}×${data.boardSize}</p>
<div class="challenge-actions">
<button class="accept-btn" onclick="multiplayerClient.acceptRematch('${data.rematchId}')">
Accept
</button>
<button class="decline-btn" onclick="multiplayerClient.declineRematch('${data.rematchId}')">
Decline
</button>
</div>
</div>
`;
document.body.appendChild(notification);
setTimeout(() => notification.classList.add('active'), 10);
}
// Send rematch request
sendRematchRequest() {
if (!this.opponentId) {
this.showMessage('No opponent to challenge', 'error');
return;
}
this.socket.emit('send_rematch', {
opponentId: this.opponentId,
boardSize: this.selectedBoardSize
});
this.showMessage(`Rematch request sent to ${this.opponent}`, 'info');
document.getElementById('gameOverModal').classList.remove('active');
}
// Accept rematch
acceptRematch(rematchId) {
this.socket.emit('accept_rematch', { rematchId });
// Hide game over modal
document.getElementById('gameOverModal').classList.remove('active');
// Remove notification
const notifications = document.querySelectorAll('.challenge-notification');
notifications.forEach(n => n.remove());
}
// Decline rematch
declineRematch(rematchId) {
this.socket.emit('decline_rematch', { rematchId });
// Remove notification
const notifications = document.querySelectorAll('.challenge-notification');
notifications.forEach(n => n.remove());
}