Add Battle Result Screen component and associated styles for displaying battle outcomes
Some checks failed
Publish Docker image / Push Docker image to Docker Hub (push) Failing after 6m59s
Some checks failed
Publish Docker image / Push Docker image to Docker Hub (push) Failing after 6m59s
This commit is contained in:
283
server/game.js
283
server/game.js
@ -635,11 +635,24 @@ class GameManager {
|
||||
song2: lobby.currentBattle.song2,
|
||||
song1Votes: lobby.currentBattle.song1Votes,
|
||||
song2Votes: 0,
|
||||
winner: winnerSongId
|
||||
winner: winnerSongId,
|
||||
bye: true
|
||||
});
|
||||
|
||||
// Move to next battle or finish tournament
|
||||
this._moveToNextBattle(lobby);
|
||||
// Store current battle for potential future reference
|
||||
lobby.previousBattle = {
|
||||
...lobby.currentBattle,
|
||||
winner: winnerSongId
|
||||
};
|
||||
|
||||
// Skip battle result screen for byes - go straight to the next battle
|
||||
lobby.state = 'VOTING';
|
||||
const tournamentFinished = this._moveToNextBattle(lobby);
|
||||
|
||||
// Check if the tournament has finished
|
||||
if (tournamentFinished || lobby.state === 'FINISHED') {
|
||||
console.log(`Tournament has finished after bye battle! Winner: ${lobby.finalWinner ? lobby.finalWinner.title : 'No winner'}`);
|
||||
}
|
||||
|
||||
return { lobby, lobbyId };
|
||||
}
|
||||
@ -666,8 +679,132 @@ class GameManager {
|
||||
winner: winnerSongId
|
||||
});
|
||||
|
||||
// Move to next battle or finish tournament
|
||||
this._moveToNextBattle(lobby);
|
||||
// Store current battle as previousBattle and set state to BATTLE_RESULT
|
||||
lobby.previousBattle = {
|
||||
...lobby.currentBattle,
|
||||
winner: winnerSongId
|
||||
};
|
||||
|
||||
// Change state to BATTLE to show the battle result screen
|
||||
lobby.state = 'BATTLE';
|
||||
}
|
||||
|
||||
return { lobby, lobbyId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceed to the next battle after showing the battle result screen
|
||||
* @param {string} playerId - ID of the player (should be host)
|
||||
* @returns {Object} Updated lobby data or error
|
||||
*/
|
||||
proceedToNextBattle(playerId) {
|
||||
const lobbyId = this.playerToLobby.get(playerId);
|
||||
if (!lobbyId) {
|
||||
return { error: 'Player not in a lobby' };
|
||||
}
|
||||
|
||||
const lobby = this.lobbies.get(lobbyId);
|
||||
|
||||
// Check if player is the host
|
||||
if (lobby.hostId !== playerId) {
|
||||
return { error: 'Only the host can proceed to the next battle' };
|
||||
}
|
||||
|
||||
// Check if we're in the battle result state
|
||||
// Allow proceeding from either BATTLE or VOTING state to handle race conditions
|
||||
if (lobby.state !== 'BATTLE' && lobby.state !== 'VOTING') {
|
||||
console.log(`Warning: Attempting to proceed to next battle from state ${lobby.state}`);
|
||||
return { error: 'Not in a valid state to proceed to next battle' };
|
||||
}
|
||||
|
||||
// If we're in VOTING state, ensure we have a winner before proceeding
|
||||
if (lobby.state === 'VOTING' && lobby.currentBattle && !lobby.currentBattle.winner) {
|
||||
console.log('Warning: Attempting to proceed without a winner determined');
|
||||
|
||||
// Check if this is a bye battle (only one song, automatic advancement)
|
||||
if (lobby.currentBattle.bye === true || !lobby.currentBattle.song2) {
|
||||
console.log('Processing bye battle - automatic advancement');
|
||||
// For bye battles, the winner is always song1
|
||||
const winnerSongId = lobby.currentBattle.song1.id;
|
||||
|
||||
lobby.currentBattle.winner = winnerSongId;
|
||||
lobby.currentBattle.song1Votes = 1; // Add a dummy vote for song1
|
||||
|
||||
// Store as previous battle for history
|
||||
lobby.previousBattle = {
|
||||
...lobby.currentBattle,
|
||||
winner: winnerSongId
|
||||
};
|
||||
|
||||
// Add to battle history
|
||||
lobby.battles.push({
|
||||
round: lobby.currentBattle.round,
|
||||
song1: lobby.currentBattle.song1,
|
||||
song2: lobby.currentBattle.song2,
|
||||
song1Votes: 1, // Always at least one vote for the bye song
|
||||
song2Votes: 0,
|
||||
winner: winnerSongId,
|
||||
bye: true
|
||||
});
|
||||
}
|
||||
// Determine winner based on votes if present for regular battles
|
||||
else if (lobby.currentBattle.votes && Object.keys(lobby.currentBattle.votes).length > 0) {
|
||||
const winnerSongId = (lobby.currentBattle.song1Votes || 0) > (lobby.currentBattle.song2Votes || 0)
|
||||
? lobby.currentBattle.song1.id
|
||||
: lobby.currentBattle.song2.id;
|
||||
|
||||
lobby.currentBattle.winner = winnerSongId;
|
||||
|
||||
// Store as previous battle for history
|
||||
lobby.previousBattle = {
|
||||
...lobby.currentBattle,
|
||||
winner: winnerSongId
|
||||
};
|
||||
|
||||
// Add to battle history
|
||||
lobby.battles.push({
|
||||
round: lobby.currentBattle.round,
|
||||
song1: lobby.currentBattle.song1,
|
||||
song2: lobby.currentBattle.song2,
|
||||
song1Votes: lobby.currentBattle.song1Votes || 0,
|
||||
song2Votes: lobby.currentBattle.song2Votes || 0,
|
||||
winner: winnerSongId
|
||||
});
|
||||
} else {
|
||||
// For regular battles with no votes, force a winner to avoid getting stuck
|
||||
console.log('No votes recorded but proceeding anyway to avoid blocking the game');
|
||||
const winnerSongId = lobby.currentBattle.song1.id; // Default to song1 as winner
|
||||
|
||||
lobby.currentBattle.winner = winnerSongId;
|
||||
lobby.currentBattle.song1Votes = 1; // Add a dummy vote
|
||||
|
||||
// Store as previous battle for history
|
||||
lobby.previousBattle = {
|
||||
...lobby.currentBattle,
|
||||
winner: winnerSongId
|
||||
};
|
||||
|
||||
// Add to battle history
|
||||
lobby.battles.push({
|
||||
round: lobby.currentBattle.round,
|
||||
song1: lobby.currentBattle.song1,
|
||||
song2: lobby.currentBattle.song2,
|
||||
song1Votes: 1,
|
||||
song2Votes: 0,
|
||||
winner: winnerSongId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set state to VOTING for the next battle
|
||||
lobby.state = 'VOTING';
|
||||
|
||||
// Move to next battle or finish tournament - capture if the tournament has finished
|
||||
const tournamentFinished = this._moveToNextBattle(lobby);
|
||||
|
||||
// Check if the game has ended after trying to move to the next battle
|
||||
if (tournamentFinished || lobby.state === 'FINISHED') {
|
||||
console.log(`Tournament has finished! Winner: ${lobby.finalWinner ? lobby.finalWinner.title : 'No winner'}`);
|
||||
}
|
||||
|
||||
return { lobby, lobbyId };
|
||||
@ -854,18 +991,81 @@ class GameManager {
|
||||
/**
|
||||
* Move to next battle or finish tournament
|
||||
* @param {Object} lobby - Lobby object
|
||||
* @returns {boolean} true if tournament has finished, false otherwise
|
||||
* @private
|
||||
*/
|
||||
_moveToNextBattle(lobby) {
|
||||
// If coming from BATTLE state, reset to VOTING for the next battle
|
||||
if (lobby.state === 'BATTLE') {
|
||||
lobby.state = 'VOTING';
|
||||
}
|
||||
|
||||
// Check if there are more battles in current round
|
||||
lobby.currentBracketIndex++;
|
||||
|
||||
if (lobby.currentBracketIndex < lobby.brackets.length) {
|
||||
// Move to next battle
|
||||
lobby.currentBattle = lobby.brackets[lobby.currentBracketIndex];
|
||||
return;
|
||||
|
||||
// Ensure votes is initialized as an object
|
||||
if (!lobby.currentBattle.votes) {
|
||||
lobby.currentBattle.votes = {};
|
||||
}
|
||||
|
||||
// Initialize vote count for UI
|
||||
lobby.currentBattle.voteCount = 0;
|
||||
|
||||
// Check if this is a bye battle that should be auto-advanced
|
||||
if (lobby.currentBattle.bye === true) {
|
||||
console.log('Auto-advancing bye battle');
|
||||
// Auto-advance logic for bye battles
|
||||
const winnerSongId = lobby.currentBattle.song1.id;
|
||||
|
||||
lobby.currentBattle.winner = winnerSongId;
|
||||
lobby.currentBattle.song1Votes = 1; // Add a dummy vote
|
||||
|
||||
// Store to history
|
||||
lobby.battles.push({
|
||||
round: lobby.currentBattle.round,
|
||||
song1: lobby.currentBattle.song1,
|
||||
song2: null,
|
||||
song1Votes: 1,
|
||||
song2Votes: 0,
|
||||
winner: winnerSongId,
|
||||
bye: true
|
||||
});
|
||||
|
||||
// Immediately advance to the next battle
|
||||
return this._moveToNextBattle(lobby);
|
||||
}
|
||||
|
||||
return false; // Tournament continues
|
||||
}
|
||||
|
||||
// No more battles in current round, set up the next round
|
||||
return this._setUpNextRound(lobby);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle array in-place using Fisher-Yates algorithm
|
||||
* @param {Array} array - Array to shuffle
|
||||
* @private
|
||||
*/
|
||||
_shuffleArray(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set up the next tournament round
|
||||
* Similar logic to end of _moveToNextBattle but extracted for reuse
|
||||
* @param {Object} lobby - Lobby object
|
||||
* @returns {boolean} true if tournament has finished, false otherwise
|
||||
* @private
|
||||
*/
|
||||
_setUpNextRound(lobby) {
|
||||
// Current round complete, check if tournament is finished
|
||||
const winners = lobby.brackets
|
||||
.filter(b => !b.bye) // Skip byes
|
||||
@ -884,9 +1084,10 @@ class GameManager {
|
||||
// If only one song remains, we have a winner
|
||||
if (nextRoundSongs.length <= 1) {
|
||||
lobby.state = 'FINISHED';
|
||||
lobby.finalWinner = nextRoundSongs[0];
|
||||
lobby.finalWinner = nextRoundSongs.length > 0 ? nextRoundSongs[0] : null;
|
||||
lobby.currentBattle = null;
|
||||
return;
|
||||
console.log(`Tournament has finished! Final winner: ${lobby.finalWinner ? lobby.finalWinner.title : 'No winner'}`);
|
||||
return true; // Return true to indicate tournament has finished
|
||||
}
|
||||
|
||||
// Create brackets for next round
|
||||
@ -906,20 +1107,22 @@ class GameManager {
|
||||
song2Votes: 0,
|
||||
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: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@ -936,19 +1139,33 @@ class GameManager {
|
||||
|
||||
// Initialize vote count for UI
|
||||
lobby.currentBattle.voteCount = 0;
|
||||
|
||||
// Check if this first battle is a bye - if so, auto-advance
|
||||
if (lobby.currentBattle.bye === true) {
|
||||
console.log('Auto-advancing bye battle in new round');
|
||||
// Auto-advance logic similar to above
|
||||
const winnerSongId = lobby.currentBattle.song1.id;
|
||||
|
||||
lobby.currentBattle.winner = winnerSongId;
|
||||
lobby.currentBattle.song1Votes = 1;
|
||||
|
||||
// Store to history
|
||||
lobby.battles.push({
|
||||
round: lobby.currentBattle.round,
|
||||
song1: lobby.currentBattle.song1,
|
||||
song2: null,
|
||||
song1Votes: 1,
|
||||
song2Votes: 0,
|
||||
winner: winnerSongId,
|
||||
bye: true
|
||||
});
|
||||
|
||||
// Move to next battle
|
||||
this._moveToNextBattle(lobby);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle array in-place using Fisher-Yates algorithm
|
||||
* @param {Array} array - Array to shuffle
|
||||
* @private
|
||||
*/
|
||||
_shuffleArray(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
|
||||
return false; // Tournament continues
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,38 +302,69 @@ io.on('connection', (socket) => {
|
||||
// Submit a vote in a battle
|
||||
socket.on('submit_vote', ({ songId }, callback) => {
|
||||
try {
|
||||
console.log(`Player ${socket.id} voting for song ${songId}`);
|
||||
const result = gameManager.submitVote(socket.id, songId);
|
||||
|
||||
if (result.error) {
|
||||
if (callback) callback({ error: result.error });
|
||||
} else {
|
||||
// Broadcast updated game state to all players in the lobby
|
||||
socket.to(result.lobbyId).emit('vote_submitted', result);
|
||||
|
||||
// Check if we've entered the BATTLE state (all votes are in and battle has ended)
|
||||
const lobby = result.lobby;
|
||||
if (lobby.state === 'BATTLE') {
|
||||
// Broadcast battle result to all players in the lobby
|
||||
io.to(result.lobbyId).emit('battle_ended', {
|
||||
previousBattle: lobby.previousBattle,
|
||||
state: 'BATTLE'
|
||||
});
|
||||
}
|
||||
// Check if the game has finished (edge case - last battle with automatic winner)
|
||||
else if (lobby.state === 'FINISHED') {
|
||||
console.log('Game finished after vote submission');
|
||||
io.to(result.lobbyId).emit('game_finished', {
|
||||
winner: lobby.finalWinner,
|
||||
battles: lobby.battles
|
||||
});
|
||||
}
|
||||
|
||||
if (callback) callback(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify all players about vote count
|
||||
io.to(result.lobbyId).emit('vote_submitted', {
|
||||
lobby: result.lobby
|
||||
});
|
||||
|
||||
// If battle is finished, notify about new battle
|
||||
if (result.lobby.currentBattle && result.lobby.currentBattle !== result.lobby.battles[result.lobby.battles.length - 1]) {
|
||||
io.to(result.lobbyId).emit('new_battle', {
|
||||
battle: result.lobby.currentBattle
|
||||
});
|
||||
}
|
||||
|
||||
// If game is finished, notify about the winner
|
||||
if (result.lobby.state === 'FINISHED') {
|
||||
io.to(result.lobbyId).emit('game_finished', {
|
||||
winner: result.lobby.finalWinner,
|
||||
battles: result.lobby.battles
|
||||
});
|
||||
}
|
||||
|
||||
// Send response to client
|
||||
if (callback) callback(result);
|
||||
} catch (error) {
|
||||
console.error('Error submitting vote:', error);
|
||||
if (callback) callback({ error: 'Failed to submit vote' });
|
||||
if (callback) callback({ error: 'Server error while submitting vote' });
|
||||
}
|
||||
});
|
||||
|
||||
// Proceed to next battle after the battle result screen
|
||||
socket.on('proceed_to_next_battle', (data, callback) => {
|
||||
try {
|
||||
const result = gameManager.proceedToNextBattle(socket.id);
|
||||
|
||||
if (result.error) {
|
||||
if (callback) callback({ error: result.error });
|
||||
} else {
|
||||
// Check if the game has finished
|
||||
if (result.lobby.state === 'FINISHED') {
|
||||
console.log('Game finished, broadcasting final winner');
|
||||
io.to(result.lobbyId).emit('game_finished', {
|
||||
winner: result.lobby.finalWinner,
|
||||
battles: result.lobby.battles
|
||||
});
|
||||
} else {
|
||||
// Broadcast updated game state to all players in the lobby
|
||||
io.to(result.lobbyId).emit('new_battle', {
|
||||
battle: result.lobby.currentBattle,
|
||||
state: result.lobby.state
|
||||
});
|
||||
}
|
||||
|
||||
if (callback) callback(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error proceeding to next battle:', error);
|
||||
if (callback) callback({ error: 'Server error while proceeding to next battle' });
|
||||
}
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user