mirror of
https://github.com/DeNNiiInc/Connect-5.git
synced 2026-04-17 22:46:00 +00:00
Reliability: Fix disconnect logic, add game restoration, and prevent race conditions
- gameManager.js: - Added disconnectTimeouts Map to track/clear pending timeouts correctly - Updated handleDisconnect to not abandon game if player reconnects - Updated registerPlayer to clear timeouts and restore active game state - Added race condition checks to handleSurrender and acceptRematch - multiplayer.js: - Updated handleRegistration to resume active game if data provided - Updated startMultiplayerGame to restore board state from server data
This commit is contained in:
@@ -9,7 +9,10 @@ class GameManager {
|
||||
this.challenges = new Map(); // challengeId -> challengeData
|
||||
this.activeGames = new Map(); // gameId -> gameData
|
||||
this.playerSockets = new Map(); // playerId -> socketId
|
||||
this.activeGames = new Map(); // gameId -> gameData
|
||||
this.playerSockets = new Map(); // playerId -> socketId
|
||||
this.rematches = new Map(); // rematchId -> rematchData
|
||||
this.disconnectTimeouts = new Map(); // playerId -> timeoutId
|
||||
}
|
||||
|
||||
// Validate and register player
|
||||
@@ -57,17 +60,51 @@ class GameManager {
|
||||
await this.broadcastActivePlayers();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
player: {
|
||||
id: playerId,
|
||||
username: username,
|
||||
stats: {
|
||||
wins: player.total_wins,
|
||||
losses: player.total_losses,
|
||||
draws: player.total_draws
|
||||
}
|
||||
}
|
||||
},
|
||||
activeGame: null // Will be populated if they are reconnecting
|
||||
};
|
||||
|
||||
// Check if player is in an active game (reconnection)
|
||||
for (const [gameId, game] of this.activeGames.entries()) {
|
||||
if (game.state === 'active' && (game.player1Id === playerId || game.player2Id === playerId)) {
|
||||
// Restore game state for player
|
||||
const playerEntry = this.players.get(socket.id);
|
||||
if (playerEntry) {
|
||||
playerEntry.currentGameId = gameId;
|
||||
}
|
||||
|
||||
// Clear any pending disconnect timeout
|
||||
if (this.disconnectTimeouts.has(playerId)) {
|
||||
clearTimeout(this.disconnectTimeouts.get(playerId));
|
||||
this.disconnectTimeouts.delete(playerId);
|
||||
}
|
||||
|
||||
// Notify opponent of reconnection
|
||||
const opponentId = game.player1Id === playerId ? game.player2Id : game.player1Id;
|
||||
const opponentSocket = this.playerSockets.get(opponentId);
|
||||
if (opponentSocket) {
|
||||
this.io.to(opponentSocket).emit('opponent_reconnected', {
|
||||
message: `${username} reconnected!`
|
||||
});
|
||||
}
|
||||
|
||||
// Add game data to response so client can restore board
|
||||
response.activeGame = {
|
||||
gameId: gameId,
|
||||
opponent: game.player1Id === playerId ? game.player2Username : game.player1Username,
|
||||
opponentId: game.player1Id === playerId ? game.player2Id : game.player1Id,
|
||||
yourSymbol: game.player1Id === playerId ? game.player1Symbol : game.player2Symbol,
|
||||
boardSize: game.boardSize,
|
||||
yourTurn: game.currentTurn === playerId,
|
||||
board: game.board,
|
||||
currentTurnSymbol: game.currentTurn === game.player1Id ? game.player1Symbol : game.player2Symbol
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error registering player:', error);
|
||||
return { success: false, error: 'Registration failed' };
|
||||
@@ -95,9 +132,13 @@ class GameManager {
|
||||
}
|
||||
|
||||
// Set timeout for game abandonment
|
||||
setTimeout(async () => {
|
||||
const stillDisconnected = !this.players.has(socket.id);
|
||||
if (stillDisconnected && game.state === 'active') {
|
||||
const timeoutId = setTimeout(async () => {
|
||||
// Check if player has reconnected (is in playerSockets)
|
||||
// We check this.playerSockets because if they reconnected, registerPlayer
|
||||
// would have put them back in there.
|
||||
const hasReconnected = this.playerSockets.has(player.id);
|
||||
|
||||
if (!hasReconnected && game.state === 'active') {
|
||||
// Award win to opponent
|
||||
await this.db.abandonGame(player.currentGameId, opponentId);
|
||||
|
||||
@@ -116,7 +157,11 @@ class GameManager {
|
||||
|
||||
this.activeGames.delete(player.currentGameId);
|
||||
}
|
||||
|
||||
this.disconnectTimeouts.delete(player.id);
|
||||
}, 30000); // 30 second grace period
|
||||
|
||||
this.disconnectTimeouts.set(player.id, timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,6 +510,14 @@ class GameManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.state !== 'active') {
|
||||
socket.emit('error', { message: 'Game is already ending' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Set state immediately to prevent double-surrender race conditions
|
||||
game.state = 'completed';
|
||||
|
||||
// Determine winner (the opponent)
|
||||
const winnerId = game.player1Id === player.id ? game.player2Id : game.player1Id;
|
||||
|
||||
@@ -561,6 +614,12 @@ class GameManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify players are not already in a game
|
||||
if (player.currentGameId) {
|
||||
socket.emit('error', { message: 'You are already in a game' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify this player is the challenged one
|
||||
if (rematch.challenged !== player.id) {
|
||||
socket.emit('error', { message: 'Invalid rematch accept' });
|
||||
@@ -576,6 +635,13 @@ class GameManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const challenger = this.players.get(challengerSocket);
|
||||
if (challenger && challenger.currentGameId) {
|
||||
socket.emit('error', { message: 'Challenger is already in a game' });
|
||||
this.rematches.delete(data.rematchId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new game (similar to acceptChallenge logic)
|
||||
try {
|
||||
const gameId = await this.db.createGame(
|
||||
|
||||
@@ -190,6 +190,10 @@ class MultiplayerClient {
|
||||
socket.on('opponent_disconnected', (data) => {
|
||||
this.showMessage(data.message + '. Waiting for reconnection...', 'warning');
|
||||
});
|
||||
|
||||
socket.on('opponent_reconnected', (data) => {
|
||||
this.showMessage(data.message, 'success');
|
||||
});
|
||||
|
||||
// Send heartbeat every 30 seconds
|
||||
setInterval(() => {
|
||||
@@ -266,6 +270,12 @@ class MultiplayerClient {
|
||||
|
||||
// Request active players
|
||||
this.socket.emit('request_active_players');
|
||||
|
||||
// Check for active game restoration
|
||||
if (data.activeGame) {
|
||||
console.log(' restoring active game:', data.activeGame);
|
||||
this.startMultiplayerGame(data.activeGame);
|
||||
}
|
||||
} else {
|
||||
// Registration failed - clear saved username and show modal
|
||||
localStorage.removeItem('connect5_username');
|
||||
@@ -431,6 +441,35 @@ class MultiplayerClient {
|
||||
}
|
||||
|
||||
console.log(`✅ Game started! You are ${this.mySymbol}, ${this.myTurn ? 'your turn' : 'waiting'}`);
|
||||
|
||||
// Restore board state if provided (reconnection)
|
||||
if (data.board) {
|
||||
this.game.board = data.board;
|
||||
// Re-render board
|
||||
const cells = this.game.boardElement.children;
|
||||
for (let r = 0; r < data.boardSize; r++) {
|
||||
for (let c = 0; c < data.boardSize; c++) {
|
||||
const symbol = data.board[r][c];
|
||||
if (symbol) {
|
||||
const index = r * data.boardSize + c;
|
||||
const cell = cells[index];
|
||||
if (cell) {
|
||||
cell.classList.add('occupied', symbol.toLowerCase());
|
||||
cell.textContent = ''; // CSS handles the X/O appearance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update current player for game logic
|
||||
if (data.currentTurnSymbol) {
|
||||
this.game.currentPlayer = data.currentTurnSymbol;
|
||||
const currentPlayerDisplay = document.getElementById('currentPlayer');
|
||||
if (currentPlayerDisplay) {
|
||||
currentPlayerDisplay.textContent = this.game.currentPlayer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make move in multiplayer game
|
||||
|
||||
Reference in New Issue
Block a user