mirror of
https://github.com/DeNNiiInc/Connect-5.git
synced 2026-04-20 15:46:01 +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;
|
module.exports = GameManager;
|
||||||
|
|||||||
115
multiplayer.js
115
multiplayer.js
@@ -173,6 +173,20 @@ class MultiplayerClient {
|
|||||||
this.handleGameEnded(data);
|
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) => {
|
socket.on('opponent_disconnected', (data) => {
|
||||||
this.showMessage(data.message + '. Waiting for reconnection...', 'warning');
|
this.showMessage(data.message + '. Waiting for reconnection...', 'warning');
|
||||||
});
|
});
|
||||||
@@ -354,8 +368,15 @@ class MultiplayerClient {
|
|||||||
this.currentGameId = data.gameId;
|
this.currentGameId = data.gameId;
|
||||||
this.mySymbol = data.yourSymbol;
|
this.mySymbol = data.yourSymbol;
|
||||||
this.opponent = data.opponent;
|
this.opponent = data.opponent;
|
||||||
|
this.opponentId = data.opponentId; // Store for rematch
|
||||||
this.myTurn = data.yourTurn;
|
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
|
// Update UI - hide multiplayer lobby, show game board
|
||||||
document.getElementById('multiplayerPanel').style.display = 'none';
|
document.getElementById('multiplayerPanel').style.display = 'none';
|
||||||
document.getElementById('gameControls').style.display = 'grid';
|
document.getElementById('gameControls').style.display = 'grid';
|
||||||
@@ -495,13 +516,44 @@ class MultiplayerClient {
|
|||||||
document.getElementById('playerDraws').textContent = data.stats.draws;
|
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
|
// Show game-over modal with stats
|
||||||
setTimeout(() => {
|
const modal = document.getElementById('gameOverModal');
|
||||||
document.getElementById('multiplayerPanel').style.display = 'block';
|
const icon = document.getElementById('gameOverIcon');
|
||||||
this.socket.emit('request_active_players');
|
const title = document.getElementById('gameOverTitle');
|
||||||
}, 3000);
|
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
|
// Handle disconnect
|
||||||
@@ -538,6 +590,57 @@ class MultiplayerClient {
|
|||||||
messageEl.className = `status-text-small ${type}`;
|
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)
|
// Initialize multiplayer client (will be used by game.js)
|
||||||
|
|||||||
Reference in New Issue
Block a user