Add connection status component and improve socket reconnection handling
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 2m20s
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 2m20s
This commit is contained in:
149
server/game.js
149
server/game.js
@ -112,9 +112,10 @@ class GameManager {
|
||||
* @param {string} playerId - New ID of the reconnecting player
|
||||
* @param {string} lobbyId - ID of the lobby
|
||||
* @param {string} playerName - Name of the player
|
||||
* @param {Object} lastKnownState - Last known game state from client
|
||||
* @returns {Object} Lobby data or error
|
||||
*/
|
||||
handleReconnect(playerId, lobbyId, playerName) {
|
||||
handleReconnect(playerId, lobbyId, playerName, lastKnownState = null) {
|
||||
// Check if lobby exists
|
||||
if (!this.lobbies.has(lobbyId)) {
|
||||
return { error: 'Lobby not found' };
|
||||
@ -135,6 +136,65 @@ class GameManager {
|
||||
// Update map
|
||||
this.playerToLobby.set(playerId, lobbyId);
|
||||
|
||||
// Handle state reconciliation if lastKnownState is provided
|
||||
if (lastKnownState && lastKnownState.gameState && lastKnownState.timestamp) {
|
||||
console.log(`Reconciling player state based on last known state: ${lastKnownState.gameState} from ${new Date(lastKnownState.timestamp).toISOString()}`);
|
||||
|
||||
// If the client's last known state was VOTING and the current battle has votes
|
||||
if (lastKnownState.gameState === 'VOTING' &&
|
||||
lastKnownState.currentBattle &&
|
||||
lastKnownState.playerState &&
|
||||
lastKnownState.playerState.hasVoted) {
|
||||
|
||||
// The player already voted in this battle according to their local state
|
||||
// If we still have the same battle, ensure their vote is counted
|
||||
if (lobby.currentBattle &&
|
||||
lastKnownState.currentBattle.round === lobby.currentBattle.round) {
|
||||
|
||||
console.log(`Player ${playerName} had voted in battle round ${lastKnownState.currentBattle.round}, checking if vote needs restoration`);
|
||||
|
||||
// Check if their vote is missing
|
||||
const hasVote = lobby.currentBattle.votes &&
|
||||
(lobby.currentBattle.votes.has(playerId) ||
|
||||
Object.keys(lobby.currentBattle.votes).includes(playerId));
|
||||
|
||||
// If their vote is missing, we need to determine which song they voted for
|
||||
if (!hasVote && lastKnownState.currentBattle.votes) {
|
||||
// Find which song they voted for
|
||||
const votedSongId = lastKnownState.currentBattle.votes[lobby.players[playerIndex].id];
|
||||
if (votedSongId) {
|
||||
console.log(`Restoring player ${playerName}'s vote for song ${votedSongId}`);
|
||||
// Recreate their vote
|
||||
this.submitVote(playerId, votedSongId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the client's last known state was SONG_SUBMISSION and they were ready
|
||||
if (lastKnownState.gameState === 'SONG_SUBMISSION' &&
|
||||
lastKnownState.playerState &&
|
||||
lastKnownState.playerState.isReady &&
|
||||
lobby.state === 'SONG_SUBMISSION' &&
|
||||
!lobby.players[playerIndex].isReady) {
|
||||
|
||||
console.log(`Restoring ready status for player ${playerName}`);
|
||||
lobby.players[playerIndex].isReady = true;
|
||||
|
||||
// Check if all players are now ready
|
||||
const allReady = lobby.players.every(p => p.isReady || !p.isConnected);
|
||||
if (allReady) {
|
||||
console.log('All players ready after reconnection, starting tournament...');
|
||||
this._startTournament(lobby);
|
||||
return {
|
||||
lobby,
|
||||
lobbyId,
|
||||
tournamentStarted: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { lobby, lobbyId };
|
||||
}
|
||||
|
||||
@ -220,7 +280,6 @@ class GameManager {
|
||||
// Check if player is in a lobby
|
||||
let lobbyId = this.playerToLobby.get(playerId);
|
||||
if (!lobbyId) {
|
||||
// If no mapping exists, try to find the player in any lobby
|
||||
console.log(`[DEBUG] No lobby mapping found for player ID: ${playerId}, trying to locate player...`);
|
||||
|
||||
// Search all lobbies for this player
|
||||
@ -266,31 +325,6 @@ class GameManager {
|
||||
const playerIndex = lobby.players.findIndex(p => p.id === playerId);
|
||||
if (playerIndex === -1) {
|
||||
console.log(`[DEBUG] Player ID ${playerId} not found in lobby ${lobbyId}.`);
|
||||
|
||||
// First try: Find the host if this is a host request
|
||||
if (playerId === lobby.hostId || lobby.players.some(p => p.id === lobby.hostId)) {
|
||||
console.log('[DEBUG] This appears to be the host. Looking for host player...');
|
||||
const hostIndex = lobby.players.findIndex(p => p.id === lobby.hostId);
|
||||
if (hostIndex !== -1) {
|
||||
console.log(`[DEBUG] Found host at index ${hostIndex}`);
|
||||
return this._addSongToPlayer(hostIndex, lobby, lobbyId, song, playerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Second try: Match any connected player as fallback
|
||||
console.log('[DEBUG] Trying to find any connected player...');
|
||||
const connectedIndex = lobby.players.findIndex(p => p.isConnected);
|
||||
if (connectedIndex !== -1) {
|
||||
console.log(`[DEBUG] Found connected player at index ${connectedIndex}`);
|
||||
return this._addSongToPlayer(connectedIndex, lobby, lobbyId, song, playerId);
|
||||
}
|
||||
|
||||
// If we still can't find a match, use the first player as last resort
|
||||
if (lobby.players.length > 0) {
|
||||
console.log('[DEBUG] Using first player as last resort');
|
||||
return this._addSongToPlayer(0, lobby, lobbyId, song, playerId);
|
||||
}
|
||||
|
||||
return { error: 'Player not found in lobby' };
|
||||
}
|
||||
|
||||
@ -519,9 +553,10 @@ class GameManager {
|
||||
* Submit a vote for a song in a battle
|
||||
* @param {string} playerId - ID of the voting player
|
||||
* @param {string} songId - ID of the voted song
|
||||
* @param {boolean} isRestoredVote - Whether this is a restored vote during reconnection
|
||||
* @returns {Object} Updated lobby data or error
|
||||
*/
|
||||
submitVote(playerId, songId) {
|
||||
submitVote(playerId, songId, isRestoredVote = false) {
|
||||
const lobbyId = this.playerToLobby.get(playerId);
|
||||
if (!lobbyId) {
|
||||
return { error: 'Player not in a lobby' };
|
||||
@ -545,7 +580,7 @@ class GameManager {
|
||||
}
|
||||
|
||||
// Check if player has already voted in this battle
|
||||
if (lobby.currentBattle.votes && lobby.currentBattle.votes.has(playerId)) {
|
||||
if (lobby.currentBattle.votes && Object.prototype.hasOwnProperty.call(lobby.currentBattle.votes, playerId) && !isRestoredVote) {
|
||||
return { error: 'Already voted in this battle' };
|
||||
}
|
||||
|
||||
@ -559,16 +594,16 @@ class GameManager {
|
||||
const player = lobby.players.find(p => p.id === playerId);
|
||||
const playerName = player ? player.name : 'Unknown Player';
|
||||
|
||||
// Initialize votes Map if it doesn't exist
|
||||
// Initialize votes Object if it doesn't exist
|
||||
if (!lobby.currentBattle.votes) {
|
||||
lobby.currentBattle.votes = new Map();
|
||||
lobby.currentBattle.votes = {};
|
||||
}
|
||||
|
||||
// Record the vote with player name for UI display
|
||||
lobby.currentBattle.votes.set(playerId, {
|
||||
lobby.currentBattle.votes[playerId] = {
|
||||
songId,
|
||||
playerName
|
||||
});
|
||||
};
|
||||
|
||||
// Update vote counts
|
||||
if (songId === lobby.currentBattle.song1.id) {
|
||||
@ -578,7 +613,7 @@ class GameManager {
|
||||
}
|
||||
|
||||
// Add a voteCount attribute for easier UI rendering
|
||||
lobby.currentBattle.voteCount = lobby.currentBattle.votes.size;
|
||||
lobby.currentBattle.voteCount = Object.keys(lobby.currentBattle.votes).length;
|
||||
|
||||
// For bye battles, the host's vote is all that's needed
|
||||
if (lobby.currentBattle.bye === true && playerId === lobby.hostId) {
|
||||
@ -605,7 +640,7 @@ class GameManager {
|
||||
|
||||
// For regular battles, check if all connected players have voted
|
||||
const connectedPlayers = lobby.players.filter(p => p.isConnected).length;
|
||||
const voteCount = lobby.currentBattle.votes.size;
|
||||
const voteCount = Object.keys(lobby.currentBattle.votes).length;
|
||||
|
||||
if (voteCount >= connectedPlayers) {
|
||||
// Determine winner
|
||||
@ -680,7 +715,7 @@ class GameManager {
|
||||
// For voting state, check if we need to progress
|
||||
if (lobby.state === 'VOTING' && lobby.currentBattle) {
|
||||
// Add null check for votes property
|
||||
const totalVotes = lobby.currentBattle.votes?.size || 0;
|
||||
const totalVotes = lobby.currentBattle.votes ? Object.keys(lobby.currentBattle.votes).length : 0;
|
||||
const remainingPlayers = lobby.players.filter(p => p.isConnected).length;
|
||||
|
||||
if (totalVotes >= remainingPlayers && totalVotes > 0) {
|
||||
@ -765,7 +800,7 @@ class GameManager {
|
||||
winner: null, // Set to null until host advances it
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
votes: new Map() // Initialize votes as a Map
|
||||
votes: {} // Initialize votes as an empty object
|
||||
});
|
||||
}
|
||||
|
||||
@ -779,7 +814,7 @@ class GameManager {
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
winner: null,
|
||||
votes: new Map()
|
||||
votes: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -795,8 +830,8 @@ class GameManager {
|
||||
// Ensure we create a proper battle object with all required fields
|
||||
lobby.currentBattle = {
|
||||
...brackets[0],
|
||||
// Make sure votes is a Map (it might be serialized incorrectly)
|
||||
votes: brackets[0].votes || new Map(),
|
||||
// Make sure votes is an object (not a Map)
|
||||
votes: brackets[0].votes || {},
|
||||
voteCount: 0
|
||||
};
|
||||
|
||||
@ -863,24 +898,22 @@ class GameManager {
|
||||
winner: null, // Set to null until host advances it
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
votes: new Map() // Initialize votes as a Map
|
||||
votes: {} // Initialize votes as an object
|
||||
});
|
||||
}
|
||||
|
||||
// Create pairs for next round
|
||||
for (let i = 0; i < nextRoundSongs.length; i += 2) {
|
||||
if (i + 1 < nextRoundSongs.length) {
|
||||
nextRound.push({
|
||||
round,
|
||||
song1: nextRoundSongs[i],
|
||||
song2: nextRoundSongs[i + 1],
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
winner: null,
|
||||
votes: new Map()
|
||||
});
|
||||
} // Create pairs for next round
|
||||
for (let i = 0; i < nextRoundSongs.length; i += 2) {
|
||||
if (i + 1 < nextRoundSongs.length) {
|
||||
nextRound.push({
|
||||
round,
|
||||
song1: nextRoundSongs[i],
|
||||
song2: nextRoundSongs[i + 1],
|
||||
song1Votes: 0,
|
||||
song2Votes: 0,
|
||||
winner: null,
|
||||
votes: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update brackets and reset index
|
||||
lobby.brackets = nextRound;
|
||||
@ -890,9 +923,9 @@ class GameManager {
|
||||
if (nextRound.length > 0) {
|
||||
lobby.currentBattle = nextRound[0];
|
||||
|
||||
// Ensure votes is initialized as a Map
|
||||
// Ensure votes is initialized as an object
|
||||
if (!lobby.currentBattle.votes) {
|
||||
lobby.currentBattle.votes = new Map();
|
||||
lobby.currentBattle.votes = {};
|
||||
}
|
||||
|
||||
// Initialize vote count for UI
|
||||
|
@ -73,9 +73,9 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
|
||||
// Attempt to reconnect to a lobby
|
||||
socket.on('reconnect_to_lobby', ({ lobbyId, playerName }, callback) => {
|
||||
socket.on('reconnect_to_lobby', ({ lobbyId, playerName, lastKnownState }, callback) => {
|
||||
try {
|
||||
const result = gameManager.handleReconnect(socket.id, lobbyId, playerName);
|
||||
const result = gameManager.handleReconnect(socket.id, lobbyId, playerName, lastKnownState);
|
||||
|
||||
if (result.error) {
|
||||
if (callback) callback(result);
|
||||
@ -337,7 +337,7 @@ io.on('connection', (socket) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
// Handle player disconnection
|
||||
socket.on('disconnect', () => {
|
||||
try {
|
||||
const result = gameManager.handleDisconnect(socket.id);
|
||||
@ -355,6 +355,13 @@ io.on('connection', (socket) => {
|
||||
console.error('Error handling disconnect:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Simple ping handler to help with connection testing
|
||||
socket.on('ping', (callback) => {
|
||||
if (callback && typeof callback === 'function') {
|
||||
callback({ success: true, timestamp: Date.now() });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.on('error', (error) => {
|
||||
|
Reference in New Issue
Block a user