diff --git a/gameManager.js b/gameManager.js index 4fe2183..302c334 100644 --- a/gameManager.js +++ b/gameManager.js @@ -449,4 +449,214 @@ class GameManager { } } + // Handle player surrender + async handleSurrender(socket, data) { + const player = this.players.get(socket.id); + if (!player) { + socket.emit('error', { message: 'Player not found' }); + return; + } + + const game = this.games.get(data.gameId); + if (!game) { + socket.emit('error', { message: 'Game not found' }); + return; + } + + // Determine winner (the opponent) + const winnerId = game.player1_id === player.id ? game.player2_id : game.player1_id; + const winnerUsername = game.player1_id === player.id ? game.player2_username : game.player1_username; + + // Update database - mark as completed with winner + try { + await this.db.abandonGame(data.gameId, winnerId); + + // Update player stats + await this.db.incrementLosses(player.id); + await this.db.incrementWins(winnerId); + + // Get updated stats + const loserStats = await this.db.getPlayerStats(player.id); + const winnerStats = await this.db.getPlayerStats(winnerId); + + // Find winner's socket + let winnerSocket = null; + for (const [socketId, p] of this.players.entries()) { + if (p.id === winnerId) { + winnerSocket = this.io.sockets.sockets.get(socketId); + break; + } + } + + // Notify both players + socket.emit('game_ended', { + reason: 'surrender', + message: 'You surrendered', + stats: loserStats + }); + + if (winnerSocket) { + winnerSocket.emit('game_ended', { + reason: 'win', + message: `${player.username} surrendered`, + stats: winnerStats + }); + } + + // Clean up + this.games.delete(data.gameId); + + } catch (error) { + console.error('Error handling surrender:', error); + socket.emit('error', { message: 'Failed to process surrender' }); + } + } + + // Send rematch request + sendRematch(socket, data) { + const player = this.players.get(socket.id); + if (!player) return; + + // Find opponent's socket + let opponentSocket = null; + for (const [socketId, p] of this.players.entries()) { + if (p.id === data.opponentId) { + opponentSocket = this.io.sockets.sockets.get(socketId); + break; + } + } + + if (!opponentSocket) { + socket.emit('error', { message: 'Opponent not online' }); + return; + } + + const rematchId = `rematch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + this.rematches.set(rematchId, { + rematchId, + challenger: player.id, + challengerUsername: player.username, + challenged: data.opponentId, + boardSize: data.boardSize || 15, + timestamp: Date.now() + }); + + opponentSocket.emit('rematch_request', { + rematchId, + from: player.username, + boardSize: data.boardSize || 15 + }); + } + + // Accept rematch + async acceptRematch(socket, data) { + const player = this.players.get(socket.id); + if (!player) return; + + const rematch = this.rematches.get(data.rematchId); + if (!rematch) { + socket.emit('error', { message: 'Rematch request expired' }); + return; + } + + // Verify this player is the challenged one + if (rematch.challenged !== player.id) { + socket.emit('error', { message: 'Invalid rematch accept' }); + return; + } + + // Find challenger's socket + let challengerSocket = null; + for (const [socketId, p] of this.players.entries()) { + if (p.id === rematch.challenger) { + challengerSocket = this.io.sockets.sockets.get(socketId); + break; + } + } + + if (!challengerSocket) { + socket.emit('error', { message: 'Challenger no longer online' }); + this.rematches.delete(data.rematchId); + return; + } + + // Create new game (similar to acceptChallenge logic) + try { + const gameData = await this.db.createGame( + rematch.challenger, + player.id, + rematch.challengerUsername, + player.username, + rematch.boardSize + ); + + const gameState = { + gameId: gameData.id, + player1_id: rematch.challenger, + player2_id: player.id, + player1_username: rematch.challengerUsername, + player2_username: player.username, + currentTurn: rematch.challenger, + boardSize: rematch.boardSize, + board: Array(rematch.boardSize).fill(null).map(() => Array(rematch.boardSize).fill(null)), + moveCount: 0 + }; + + this.games.set(gameData.id, gameState); + + // Notify both players + challengerSocket.emit('rematch_accepted', { + gameId: gameData.id, + opponent: player.username, + opponentId: player.id, + yourSymbol: 'X', + yourTurn: true, + boardSize: rematch.boardSize + }); + + socket.emit('game_started', { + gameId: gameData.id, + opponent: rematch.challengerUsername, + opponentId: rematch.challenger, + yourSymbol: 'O', + yourTurn: false, + boardSize: rematch.boardSize + }); + + this.rematches.delete(data.rematchId); + + } catch (error) { + console.error('Error accepting rematch:', error); + socket.emit('error', { message: 'Failed to start rematch' }); + } + } + + // Decline rematch + declineRematch(socket, data) { + const player = this.players.get(socket.id); + if (!player) return; + + const rematch = this.rematches.get(data.rematchId); + if (!rematch) return; + + // Find challenger's socket + let challengerSocket = null; + for (const [socketId, p] of this.players.entries()) { + if (p.id === rematch.challenger) { + challengerSocket = this.io.sockets.sockets.get(socketId); + break; + } + } + + if (challengerSocket) { + challengerSocket.emit('rematch_declined', { + by: player.username + }); + } + + this.rematches.delete(data.rematchId); + } + + module.exports = GameManager; diff --git a/multiplayer.js b/multiplayer.js index 3dcba42..63fcbeb 100644 --- a/multiplayer.js +++ b/multiplayer.js @@ -173,6 +173,20 @@ class MultiplayerClient { this.handleGameEnded(data); }); + socket.on('rematch_request', (data) => { + this.handleRematchRequest(data); + }); + + socket.on('rematch_accepted', (data) => { + this.startMultiplayerGame(data); + }); + + socket.on('rematch_declined', (data) => { + this.showMessage(`${data.by} declined the rematch`, 'error'); + document.getElementById('gameOverModal').classList.remove('active'); + this.returnToLobby(); + }); + socket.on('opponent_disconnected', (data) => { this.showMessage(data.message + '. Waiting for reconnection...', 'warning'); }); @@ -354,8 +368,15 @@ class MultiplayerClient { this.currentGameId = data.gameId; this.mySymbol = data.yourSymbol; this.opponent = data.opponent; + this.opponentId = data.opponentId; // Store for rematch this.myTurn = data.yourTurn; + // Show surrender button + const surrenderBtn = document.getElementById('surrenderBtn'); + if (surrenderBtn) { + surrenderBtn.style.display = 'flex'; + } + // Update UI - hide multiplayer lobby, show game board document.getElementById('multiplayerPanel').style.display = 'none'; document.getElementById('gameControls').style.display = 'grid'; @@ -495,13 +516,44 @@ class MultiplayerClient { document.getElementById('playerDraws').textContent = data.stats.draws; } - this.showMessage(message, 'success'); + // Hide surrender button + const surrenderBtn = document.getElementById('surrenderBtn'); + if (surrenderBtn) { + surrenderBtn.style.display = 'none'; + } - // Show multiplayer panel again - setTimeout(() => { - document.getElementById('multiplayerPanel').style.display = 'block'; - this.socket.emit('request_active_players'); - }, 3000); + // Show game-over modal with stats + const modal = document.getElementById('gameOverModal'); + const icon = document.getElementById('gameOverIcon'); + const title = document.getElementById('gameOverTitle'); + const subtitle = document.getElementById('gameOverMessage'); + + if (modal && icon && title && subtitle) { + // Set icon and title based on result + if (data.reason === 'win' || data.reason === 'opponent_abandoned') { + icon.textContent = '🏆'; + title.textContent = 'Victory!'; + subtitle.textContent = data.reason === 'opponent_abandoned' ? 'Opponent disconnected' : 'Great game!'; + } else if (data.reason === 'loss' || data.reason === 'surrender') { + icon.textContent = '😔'; + title.textContent = 'Defeat'; + subtitle.textContent = data.reason === 'surrender' ? 'You surrendered' : 'Better luck next time!'; + } else { + icon.textContent = '🤝'; + title.textContent = "It's a Draw!"; + subtitle.textContent = 'Well played!'; + } + + // Update stats in modal + if (data.stats) { + document.getElementById('gameOverWins').textContent = data.stats.wins; + document.getElementById('gameOverLosses').textContent = data.stats.losses; + document.getElementById('gameOverDraws').textContent = data.stats.draws; + } + + // Show modal + modal.classList.add('active'); + } } // Handle disconnect @@ -538,6 +590,57 @@ class MultiplayerClient { messageEl.className = `status-text-small ${type}`; } } + + // Handle rematch request from opponent + handleRematchRequest(data) { + const notification = document.createElement('div'); + notification.className = 'challenge-notification'; + notification.innerHTML = ` +
${data.from} wants a rematch
+Board size: ${data.boardSize}×${data.boardSize}
+