mirror of
https://github.com/DeNNiiInc/Connect-5.git
synced 2026-04-18 05:15:59 +00:00
Add surrender and rematch functionality - Part 2 (Complete)
Client-side (multiplayer.js): - Add socket listeners for rematch events - Update startMultiplayerGame to show surrender button and track opponent ID - Update handleGameEnded to show game-over modal with stats - Add handleRematchRequest method - Add global helper functions for rematch acceptance/decline Server-side (gameManager.js): - Add rematches Map to track rematch requests - Add handleSurrender method - Add sendRematch, acceptRematch, declineRematch methods - Handle surrender stats updates and game cleanup Server (server.js): - Add socket listeners for: surrender, send_rematch, accept_rematch, decline_rematch Features now fully functional: - Players can surrender during active games - Players can request rematches after games end - Opponents receive rematch notifications - Game-over modal shows stats and rematch option
This commit is contained in:
210
gameManager.js
210
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;
|
||||
|
||||
115
multiplayer.js
115
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 = `
|
||||
<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="acceptRematchFromNotification('${data.rematchId}')">
|
||||
Accept
|
||||
</button>
|
||||
<button class="decline-btn" onclick="declineRematchFromNotification('${data.rematchId}')">
|
||||
Decline
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
setTimeout(() => notification.classList.add('active'), 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Global helper functions for rematch from notifications
|
||||
function acceptRematchFromNotification(rematchId) {
|
||||
if (window.multiplayerClient && window.multiplayerClient.socket) {
|
||||
window.multiplayerClient.socket.emit('accept_rematch', { rematchId });
|
||||
|
||||
// Hide game over modal if visible
|
||||
const modal = document.getElementById('gameOverModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
|
||||
// Remove notification
|
||||
const notifications = document.querySelectorAll('.challenge-notification');
|
||||
notifications.forEach(n => n.remove());
|
||||
}
|
||||
}
|
||||
|
||||
function declineRematchFromNotification(rematchId) {
|
||||
if (window.multiplayerClient && window.multiplayerClient.socket) {
|
||||
window.multiplayerClient.socket.emit('decline_rematch', { rematchId });
|
||||
|
||||
// Remove notification
|
||||
const notifications = document.querySelectorAll('.challenge-notification');
|
||||
notifications.forEach(n => n.remove());
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize multiplayer client (will be used by game.js)
|
||||
|
||||
Reference in New Issue
Block a user