diff --git a/client/src/App.jsx b/client/src/App.jsx
index 836a30b..9b1257f 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -7,6 +7,7 @@ import HomeScreen from "./components/HomeScreen";
import SongSubmissionScreen from "./components/SongSubmissionScreen";
import VotingScreen from "./components/VotingScreen";
import ResultsScreen from "./components/ResultsScreen";
+import BattleResultScreen from "./components/BattleResultScreen";
import ConnectionStatus from "./components/ConnectionStatus";
const App = () => {
@@ -52,6 +53,8 @@ const App = () => {
return ;
case 'FINISHED':
return ;
+ case 'BATTLE':
+ return ;
default:
return ;
}
diff --git a/client/src/common/styles/components/battle-result-screen.sass b/client/src/common/styles/components/battle-result-screen.sass
new file mode 100644
index 0000000..276851a
--- /dev/null
+++ b/client/src/common/styles/components/battle-result-screen.sass
@@ -0,0 +1,243 @@
+// Battle Result Screen styles
+
+.battle-result-screen
+ display: flex
+ flex-direction: column
+ min-height: 100%
+ padding: 1.5rem
+ position: relative
+ overflow: hidden
+
+ header
+ display: flex
+ justify-content: center
+ align-items: center
+ flex-direction: column
+ margin-bottom: 2rem
+
+ h1
+ margin: 0
+ color: $accent
+ display: flex
+ align-items: center
+ gap: 0.75rem
+ font-size: 2.2rem
+ font-family: 'Press Start 2P', monospace
+ text-transform: uppercase
+ animation: winner-pulse 2s infinite alternate
+
+ svg
+ color: $accent
+ filter: drop-shadow(2px 2px 0 #000)
+
+ .countdown
+ margin-top: 1rem
+ font-family: 'Press Start 2P', monospace
+ font-size: 0.9rem
+ color: $text-muted
+ padding: 0.8rem 1.2rem
+ background-color: rgba(0, 0, 0, 0.3)
+ border-radius: 1rem
+ animation: pulse 1s infinite alternate
+ display: flex
+ align-items: center
+ gap: 0.5rem
+
+ svg
+ color: $accent
+ margin-right: 0.5rem
+
+ .winner-announcement
+ display: flex
+ flex-direction: column
+ align-items: center
+ margin-bottom: 2rem
+
+ .song-cards
+ display: flex
+ flex-direction: column
+ width: 100%
+ max-width: 700px
+ gap: 2rem
+
+ @media (min-width: 768px)
+ flex-direction: row
+ align-items: flex-start
+
+ .song-card
+ position: relative
+ background-color: rgba(0, 0, 0, 0.3)
+ padding: 1.5rem
+ border-radius: 1rem
+
+ &.winner
+ flex: 3
+ border: 6px solid $accent
+ box-shadow: 0 0 20px rgba($accent, 0.3)
+ transform: scale(1.05)
+ z-index: 1
+
+ .victory-badge
+ position: absolute
+ top: -12px
+ right: 10px
+ background-color: $accent
+ color: white
+ font-family: 'Press Start 2P', monospace
+ font-size: 0.7rem
+ padding: 0.5rem 1rem
+ border-radius: 0.5rem
+ display: flex
+ align-items: center
+ gap: 0.5rem
+ box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3)
+
+ svg
+ font-size: 0.8rem
+
+ &.loser
+ flex: 2
+ opacity: 0.7
+ filter: saturate(0.7)
+
+ .versus
+ position: absolute
+ top: 50%
+ left: -30px
+ font-family: 'Bangers', cursive
+ font-size: 2.5rem
+ color: $accent
+ transform: translateY(-50%)
+ text-shadow: 0 0 5px rgba($accent, 0.5)
+
+ @media (max-width: 767px)
+ top: -25px
+ left: 50%
+ transform: translateX(-50%)
+
+ .song-info
+ margin-bottom: 1.5rem
+
+ h2, h3
+ margin: 0 0 0.5rem 0
+ font-family: 'Press Start 2P', monospace
+
+ h2
+ font-size: 1.4rem
+ color: $text
+ margin-right: 70px
+
+ h3
+ font-size: 1.2rem
+ color: $text
+
+ .artist
+ color: $text-muted
+ font-size: 1.1rem
+ margin-bottom: 1rem
+
+ .submitter
+ font-size: 0.9rem
+ color: $text-muted
+ font-style: italic
+ margin-bottom: 0.5rem
+
+ .vote-count
+ display: inline-block
+ padding: 0.5rem 1rem
+ background-color: rgba(0, 0, 0, 0.3)
+ border-radius: 1rem
+ font-family: 'Press Start 2P', monospace
+ font-size: 0.8rem
+
+ .votes
+ color: $text
+
+ .winner-video
+ width: 100%
+ aspect-ratio: 16 / 9
+ border-radius: 0.5rem
+ overflow: hidden
+
+ .no-video
+ width: 100%
+ aspect-ratio: 16 / 9
+ display: flex
+ flex-direction: column
+ align-items: center
+ justify-content: center
+ background-color: rgba(0, 0, 0, 0.3)
+ border-radius: 0.5rem
+ color: $text-muted
+
+ .pulse-icon
+ font-size: 2rem
+ margin-bottom: 1rem
+ animation: pulse 2s infinite
+
+ .battle-actions
+ display: flex
+ justify-content: center
+ margin-top: auto
+ padding-top: 1.5rem
+
+ .btn
+ padding: 1rem 2rem
+ font-size: 1.1rem
+ display: flex
+ align-items: center
+ gap: 0.5rem
+
+ &.pixelated
+ position: relative
+
+ .pixel-corner
+ position: absolute
+ width: 8px
+ height: 8px
+ background-color: $primary
+
+ &.tl
+ top: -4px
+ left: -4px
+
+ &.tr
+ top: -4px
+ right: -4px
+
+ &.bl
+ bottom: -4px
+ left: -4px
+
+ &.br
+ bottom: -4px
+ right: -4px
+
+// Confetti animation
+.confetti
+ position: absolute
+ width: 10px
+ height: 20px
+ transform-origin: center bottom
+ animation: confetti-fall 3s linear forwards
+ z-index: -1
+
+@keyframes confetti-fall
+ 0%
+ transform: translateY(-100vh) rotate(0deg)
+ 100%
+ transform: translateY(100vh) rotate(360deg)
+
+@keyframes winner-pulse
+ 0%
+ transform: scale(1)
+ text-shadow: 2px 2px 0 #000, -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000
+ 100%
+ transform: scale(1.05)
+ text-shadow: 3px 3px 0 #000, -3px -3px 0 #000, 3px -3px 0 #000, -3px 3px 0 #000
+
+@keyframes pulse
+ 0%
+ opacity: 0.7
+ 100%
+ opacity: 1
diff --git a/client/src/common/styles/main.sass b/client/src/common/styles/main.sass
index 2416076..1129ac9 100644
--- a/client/src/common/styles/main.sass
+++ b/client/src/common/styles/main.sass
@@ -9,6 +9,7 @@
@import './components/song-submission-screen'
@import './components/voting-screen'
@import './components/results-screen'
+@import './components/battle-result-screen'
@import './components/youtube-embed'
@import './components/youtube-search'
@import './components/song-form-overlay'
diff --git a/client/src/components/BattleResultScreen.jsx b/client/src/components/BattleResultScreen.jsx
new file mode 100644
index 0000000..f94174d
--- /dev/null
+++ b/client/src/components/BattleResultScreen.jsx
@@ -0,0 +1,198 @@
+import { useEffect, useState } from 'react';
+import { useGame } from '../context/GameContext';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faTrophy, faMusic, faCrown, faClock } from '@fortawesome/free-solid-svg-icons';
+import YouTubeEmbed from './YouTubeEmbed';
+import { getDisplayName } from '../utils/playerUtils';
+
+const BattleResultScreen = () => {
+ const { lobby, currentPlayer, isHost, proceedToNextBattle } = useGame();
+ const [countdown, setCountdown] = useState(10); // Auto-advance after 10 seconds
+ const [showConfetti, setShowConfetti] = useState(true);
+
+ // Use previousBattle from the lobby state
+ const previousBattle = lobby?.previousBattle;
+
+ // Extract winning song
+ const winnerSongId = previousBattle?.winner;
+ const winningSong = winnerSongId === previousBattle?.song1?.id
+ ? previousBattle?.song1
+ : previousBattle?.song2;
+
+ const losingSong = winnerSongId === previousBattle?.song1?.id
+ ? previousBattle?.song2
+ : previousBattle?.song1;
+
+ const winningVotes = winnerSongId === previousBattle?.song1?.id
+ ? previousBattle?.song1Votes
+ : previousBattle?.song2Votes;
+
+ const losingVotes = winnerSongId === previousBattle?.song1?.id
+ ? previousBattle?.song2Votes
+ : previousBattle?.song1Votes;
+
+ // Get the player who submitted the winning song
+ const findSubmitter = (song) => {
+ if (!lobby || !song) return null;
+
+ const submitter = lobby.players.find(p => p.id === song.submittedById);
+ return submitter;
+ };
+
+ const winnerSubmitter = findSubmitter(winningSong);
+
+ // YouTube video ID extraction for the winner
+ const getYouTubeId = (url) => {
+ if (!url) return null;
+
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
+ const match = url.match(regExp);
+
+ return (match && match[2].length === 11) ? match[2] : null;
+ };
+
+ const winnerVideoId = getYouTubeId(winningSong?.youtubeLink);
+
+ // Auto-advance countdown
+ useEffect(() => {
+ if (countdown > 0) {
+ const timer = setInterval(() => {
+ setCountdown(prev => {
+ if (prev <= 1) {
+ clearInterval(timer);
+ // Only auto-advance if host, otherwise wait for host
+ if (isHost) {
+ proceedToNextBattle();
+ }
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }
+ }, [countdown, isHost, proceedToNextBattle]);
+
+ // Confetti effect
+ useEffect(() => {
+ if (showConfetti) {
+ const createConfetti = () => {
+ const confettiContainer = document.createElement('div');
+ confettiContainer.className = 'confetti';
+
+ // Random position, size, and color
+ const left = Math.random() * 100;
+ const colors = ['#f94144', '#f3722c', '#f8961e', '#f9c74f', '#90be6d', '#43aa8b', '#577590'];
+ const color = colors[Math.floor(Math.random() * colors.length)];
+ const size = Math.random() * 0.5 + 0.3; // Between 0.3 and 0.8rem
+ const rotation = Math.random() * 360; // Random rotation
+ const duration = Math.random() * 2 + 1; // Between 1 and 3 seconds
+
+ confettiContainer.style.left = `${left}%`;
+ confettiContainer.style.backgroundColor = color;
+ confettiContainer.style.width = `${size}rem`;
+ confettiContainer.style.height = `${size * 0.6}rem`;
+ confettiContainer.style.transform = `rotate(${rotation}deg)`;
+ confettiContainer.style.animation = `confetti-fall ${duration}s linear forwards`;
+
+ document.querySelector('.battle-result-screen').appendChild(confettiContainer);
+
+ // Remove after animation
+ setTimeout(() => {
+ confettiContainer.remove();
+ }, duration * 1000);
+ };
+
+ // Create confetti pieces
+ const confettiInterval = setInterval(() => {
+ for (let i = 0; i < 3; i++) {
+ createConfetti();
+ }
+ }, 300);
+
+ // Stop confetti after a few seconds
+ setTimeout(() => {
+ clearInterval(confettiInterval);
+ setShowConfetti(false);
+ }, 4000);
+
+ return () => {
+ clearInterval(confettiInterval);
+ };
+ }
+ }, [showConfetti]);
+
+ if (!previousBattle || !winningSong) {
+ return (
+
+
Nächster Kampf wird vorbereitet...
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ Gewinner
+
+
+
+
{winningSong.title}
+
{winningSong.artist}
+
+ {!lobby?.settings?.hidePlayerNames && winnerSubmitter && (
+
+ Eingereicht von: {getDisplayName(winnerSubmitter, lobby, currentPlayer)}
+
+ )}
+
+
+ {winningVotes} Stimmen
+
+
+
+ {winnerVideoId ? (
+
+
+
+ ) : (
+
+
+ Kein Video verfügbar
+
+ )}
+
+
+ {losingSong && (
+
+
VS
+
+
{losingSong.title}
+
{losingSong.artist}
+
+ {losingVotes} Stimmen
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default BattleResultScreen;
diff --git a/client/src/context/GameContext.jsx b/client/src/context/GameContext.jsx
index c14926e..8d4177a 100644
--- a/client/src/context/GameContext.jsx
+++ b/client/src/context/GameContext.jsx
@@ -439,6 +439,19 @@ export function GameProvider({ children }) {
});
};
+ // Handle battle ended and show result screen
+ const handleBattleEnded = (data) => {
+ console.log('Battle ended, showing result screen:', data);
+ setLobby(prevLobby => {
+ if (!prevLobby) return prevLobby;
+ return {
+ ...prevLobby,
+ state: 'BATTLE',
+ previousBattle: data.previousBattle
+ };
+ });
+ };
+
// Game finished
const handleGameFinished = (data) => {
setLobby(prevLobby => {
@@ -461,11 +474,13 @@ export function GameProvider({ children }) {
socket.on('songs_updated', handleSongsUpdated);
socket.on('player_status_changed', handlePlayerStatusChanged);
socket.on('vote_submitted', handleVoteSubmitted);
+ socket.on('battle_ended', handleBattleEnded);
socket.on('tournament_started', data => {
console.log('Tournament started event received:', data);
setLobby(prevLobby => prevLobby ? {...prevLobby, state: data.state} : prevLobby);
});
socket.on('new_battle', handleNewBattle);
+ socket.on('battle_ended', handleBattleEnded);
socket.on('game_finished', handleGameFinished);
// Clean up listeners on unmount
@@ -479,6 +494,7 @@ export function GameProvider({ children }) {
socket.off('player_status_changed', handlePlayerStatusChanged);
socket.off('vote_submitted', handleVoteSubmitted);
socket.off('new_battle', handleNewBattle);
+ socket.off('battle_ended', handleBattleEnded);
socket.off('game_finished', handleGameFinished);
};
}, [socket]);
@@ -706,6 +722,23 @@ export function GameProvider({ children }) {
setCurrentPlayer(null);
};
+ // Proceed to next battle after the battle result screen
+ const proceedToNextBattle = () => {
+ if (!socket || !isConnected) {
+ setError('Not connected to server');
+ return;
+ }
+
+ safeEmit('proceed_to_next_battle', {}, (response) => {
+ if (response.error) {
+ setError(response.error);
+ } else if (response.lobby) {
+ setLobby(response.lobby);
+ updateSavedGameData(response.lobby);
+ }
+ });
+ };
+
return (
{children}
diff --git a/server/game.js b/server/game.js
index e945020..74bf67b 100644
--- a/server/game.js
+++ b/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
}
}
diff --git a/server/index.js b/server/index.js
index 6937a87..6b10e35 100644
--- a/server/index.js
+++ b/server/index.js
@@ -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' });
}
});