Liedkampf/server/game.js
Mathias Wagner f2712bdcec
All checks were successful
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 2m3s
Fix automatic advance bug
2025-05-14 19:27:21 +02:00

918 lines
28 KiB
JavaScript

const { v4: uuidv4 } = require('uuid');
const youtubeAPI = require('./youtube-api');
/**
* Game state and logic manager for Song Battle application
*/
class GameManager {
constructor() {
this.lobbies = new Map(); // Map of lobbyId -> lobby object
this.playerToLobby = new Map(); // Map of playerId -> lobbyId for quick lookup
}
/**
* Create a new game lobby
* @param {string} hostId - ID of the host player
* @param {string} hostName - Name of the host player
* @returns {Object} New lobby data and ID
*/
createLobby(hostId, hostName) {
// Generate a simple 6-character lobby code
const lobbyId = this._generateLobbyCode();
// Create lobby object
const lobby = {
id: lobbyId,
hostId: hostId,
state: 'LOBBY', // LOBBY -> SONG_SUBMISSION -> VOTING -> FINISHED
settings: {
songsPerPlayer: 3,
maxPlayers: 10,
minPlayers: 3
},
players: [{
id: hostId,
name: hostName,
isConnected: true,
isReady: false,
songs: [],
songCount: 0
}],
songs: [], // All submitted songs
currentBattle: null,
battles: [], // History of all battles
finalWinner: null
};
// Store lobby and player-to-lobby mapping
this.lobbies.set(lobbyId, lobby);
this.playerToLobby.set(hostId, lobbyId);
return { lobby, lobbyId };
}
/**
* Join an existing lobby
* @param {string} playerId - ID of joining player
* @param {string} playerName - Name of joining player
* @param {string} lobbyId - ID of lobby to join
* @returns {Object} Lobby data or error
*/
joinLobby(playerId, playerName, lobbyId) {
// Check if lobby exists
if (!this.lobbies.has(lobbyId)) {
return { error: 'Lobby not found' };
}
const lobby = this.lobbies.get(lobbyId);
// Check if lobby is in correct state
if (lobby.state !== 'LOBBY') {
return { error: 'Cannot join: game already in progress' };
}
// Check if player limit is reached
if (lobby.players.length >= lobby.settings.maxPlayers) {
return { error: 'Lobby is full' };
}
// Check if player name is already taken
if (lobby.players.some(p => p.name === playerName && p.isConnected)) {
return { error: 'Name already taken' };
}
// Check if player is rejoining
const existingPlayerIndex = lobby.players.findIndex(p => p.name === playerName && !p.isConnected);
if (existingPlayerIndex >= 0) {
// Update player ID and connection status
lobby.players[existingPlayerIndex].id = playerId;
lobby.players[existingPlayerIndex].isConnected = true;
this.playerToLobby.set(playerId, lobbyId);
return { lobby, lobbyId };
}
// Add new player
lobby.players.push({
id: playerId,
name: playerName,
isConnected: true,
isReady: false,
songs: [],
songCount: 0
});
// Update map
this.playerToLobby.set(playerId, lobbyId);
return { lobby, lobbyId };
}
/**
* Handle player reconnection to a lobby
* @param {string} playerId - New ID of the reconnecting player
* @param {string} lobbyId - ID of the lobby
* @param {string} playerName - Name of the player
* @returns {Object} Lobby data or error
*/
handleReconnect(playerId, lobbyId, playerName) {
// Check if lobby exists
if (!this.lobbies.has(lobbyId)) {
return { error: 'Lobby not found' };
}
const lobby = this.lobbies.get(lobbyId);
// Find player by name
const playerIndex = lobby.players.findIndex(p => p.name === playerName);
if (playerIndex === -1) {
return { error: 'Player not found in lobby' };
}
// Update player ID and connection status
lobby.players[playerIndex].id = playerId;
lobby.players[playerIndex].isConnected = true;
// Update map
this.playerToLobby.set(playerId, lobbyId);
return { lobby, lobbyId };
}
/**
* Update lobby settings
* @param {string} playerId - ID of the host player
* @param {Object} settings - New settings object
* @returns {Object} Updated lobby data or error
*/
updateSettings(playerId, settings) {
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 update settings' };
}
// Validate settings
if (settings.songsPerPlayer < 1 || settings.songsPerPlayer > 10) {
return { error: 'Songs per player must be between 1 and 10' };
}
if (settings.maxPlayers < 3 || settings.maxPlayers > 20) {
return { error: 'Max players must be between 3 and 20' };
}
// Update settings
lobby.settings = {
...lobby.settings,
...settings
};
return { lobby, lobbyId };
}
/**
* Start the game from lobby state
* @param {string} playerId - ID of the host player
* @returns {Object} Updated lobby data or error
*/
startGame(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 start the game' };
}
// Check if in correct state
if (lobby.state !== 'LOBBY') {
return { error: 'Game already started' };
}
// Check if enough players
if (lobby.players.length < lobby.settings.minPlayers) {
return { error: `Need at least ${lobby.settings.minPlayers} players to start` };
}
// Move to song submission state
lobby.state = 'SONG_SUBMISSION';
return { lobby, lobbyId };
}
/**
* Add a song for a player
* @param {string} playerId - ID of the player
* @param {Object} song - Song data (title, artist, youtubeLink)
* @returns {Object} Updated lobby data or error
*/
async addSong(playerId, song) {
console.log(`[DEBUG] addSong called for player ID: ${playerId}`);
// 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
let foundLobby = null;
let foundPlayerIndex = -1;
for (const [id, lobby] of this.lobbies.entries()) {
const playerIndex = lobby.players.findIndex(p => p.id === playerId);
if (playerIndex !== -1) {
foundLobby = lobby;
lobbyId = id;
foundPlayerIndex = playerIndex;
console.log(`[DEBUG] Found player in lobby ${id} at index ${playerIndex}`);
// Fix the mapping for future requests
this.playerToLobby.set(playerId, id);
break;
}
}
if (!foundLobby) {
console.log(`[DEBUG] Player not found in any lobby. Available lobbies: ${Array.from(this.lobbies.keys())}`);
return { error: 'Player not in a lobby' };
}
}
const lobby = this.lobbies.get(lobbyId);
if (!lobby) {
console.log(`[DEBUG] Lobby ID ${lobbyId} not found`);
return { error: 'Lobby not found' };
}
// Log lobby state for debugging
console.log(`[DEBUG] Lobby ${lobbyId} state: ${lobby.state}`);
console.log(`[DEBUG] Lobby players: ${JSON.stringify(lobby.players.map(p => ({id: p.id, name: p.name})))}`);
// Check if in correct state
if (lobby.state !== 'SONG_SUBMISSION') {
return { error: 'Cannot add songs at this time' };
}
// Get player
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' };
}
// Check if player can add more songs
if (lobby.players[playerIndex].songs.length >= lobby.settings.songsPerPlayer) {
return { error: 'Maximum number of songs reached' };
}
// If we have a YouTube link, validate it
if (song.youtubeLink) {
const videoId = await youtubeAPI.extractVideoId(song.youtubeLink);
if (!videoId) {
return { error: 'Invalid YouTube link' };
}
}
// Handle async metadata fetching
return this._addSongToPlayer(playerIndex, lobby, lobbyId, song, playerId);
}
/**
* Search for songs on YouTube
* @param {string} query - Search query
* @returns {Promise<Array>} Search results
*/
async searchYouTube(query) {
try {
return await youtubeAPI.searchYouTube(query);
} catch (error) {
console.error('Error searching YouTube:', error);
return { error: 'Failed to search YouTube' };
}
}
/**
* Get metadata for a single YouTube video
* @param {string} videoId - YouTube video ID
* @returns {Promise<Object>} Video metadata
*/
async getYouTubeVideoMetadata(videoId) {
try {
return await youtubeAPI.getVideoMetadata(videoId);
} catch (error) {
console.error('Error getting YouTube video metadata:', error);
return { error: 'Failed to get video metadata' };
}
}
/**
* Helper method to add a song to a player
* @param {number} playerIndex - Index of the player in the lobby
* @param {Object} lobby - The lobby object
* @param {string} lobbyId - ID of the lobby
* @param {Object} song - Song data to add
* @param {string} playerId - ID of the player adding the song
* @returns {Object|Promise<Object>} Updated lobby data
* @private
*/
async _addSongToPlayer(playerIndex, lobby, lobbyId, song, playerId) {
// Generate song ID
const songId = uuidv4();
// Prepare song data
let title = song.title;
let artist = song.artist;
let thumbnail = song.thumbnail || null; // Use client-provided thumbnail if available
// If YouTube link is provided but title/artist are empty, try to fetch metadata
if (song.youtubeLink && (!title || !artist || title === 'Unknown' || artist === 'Unknown' || !thumbnail)) {
try {
// Extract video ID from YouTube link
const videoId = youtubeAPI.extractVideoId(song.youtubeLink);
if (videoId) {
console.log(`Getting metadata for YouTube video: ${videoId}`);
// Fetch metadata from YouTube API
const metadata = await youtubeAPI.getVideoMetadata(videoId);
// Only update if we don't have values or they're generic
if (!title || title === 'Unknown') title = metadata.title || 'Unknown';
if (!artist || artist === 'Unknown') artist = metadata.artist || 'Unknown';
if (!thumbnail) thumbnail = metadata.thumbnail;
console.log(`Fetched metadata: "${title}" by ${artist}`);
}
} catch (error) {
console.error('Error fetching YouTube metadata:', error.message);
// Continue with user-provided data if API fails
}
}
// Add song to player and global song list
const newSong = {
id: songId,
title: title,
artist: artist,
youtubeLink: song.youtubeLink || '',
thumbnail: thumbnail,
submittedById: playerId,
submittedByName: lobby.players[playerIndex].name
};
// Add song to both player's list and global list
console.log(`[DEBUG] Adding song "${title}" to player at index ${playerIndex} in lobby ${lobbyId}`);
lobby.players[playerIndex].songs.push(newSong);
lobby.players[playerIndex].songCount = lobby.players[playerIndex].songs.length;
lobby.songs.push(newSong);
console.log(`[DEBUG] Updated player song count: ${lobby.players[playerIndex].songCount}`);
console.log(`[DEBUG] Total songs in lobby: ${lobby.songs.length}`);
// Make sure we're returning a properly formed result with both lobby and lobbyId
const result = { lobby, lobbyId };
// Validate that the result contains what we expect before returning
if (!result.lobby || !result.lobbyId) {
console.log(`[DEBUG] CRITICAL ERROR: Result is missing lobby or lobbyId after adding song`);
console.log(`[DEBUG] Result keys: ${Object.keys(result)}`);
} else {
console.log(`[DEBUG] Song successfully added, returning result with valid lobby and lobbyId`);
}
return result;
}
/**
* Remove a song for a player
* @param {string} playerId - ID of the player
* @param {string} songId - ID of the song to remove
* @returns {Object} Updated lobby data or error
*/
removeSong(playerId, songId) {
const lobbyId = this.playerToLobby.get(playerId);
if (!lobbyId) {
return { error: 'Player not in a lobby' };
}
const lobby = this.lobbies.get(lobbyId);
// Check if in correct state
if (lobby.state !== 'SONG_SUBMISSION') {
return { error: 'Cannot remove songs at this time' };
}
// Get player
const playerIndex = lobby.players.findIndex(p => p.id === playerId);
if (playerIndex === -1) {
return { error: 'Player not found in lobby' };
}
// Find song in player's songs
const songIndex = lobby.players[playerIndex].songs.findIndex(s => s.id === songId);
if (songIndex === -1) {
return { error: 'Song not found' };
}
// Remove song from player's list
lobby.players[playerIndex].songs.splice(songIndex, 1);
lobby.players[playerIndex].songCount = lobby.players[playerIndex].songs.length;
// Remove from global song list
const globalSongIndex = lobby.songs.findIndex(s => s.id === songId);
if (globalSongIndex !== -1) {
lobby.songs.splice(globalSongIndex, 1);
}
// If player was ready, set to not ready
if (lobby.players[playerIndex].isReady) {
lobby.players[playerIndex].isReady = false;
}
return { lobby, lobbyId };
}
/**
* Set player ready status in song submission phase
* @param {string} playerId - ID of the player
* @returns {Object} Updated lobby data or error
*/
setPlayerReady(playerId) {
const lobbyId = this.playerToLobby.get(playerId);
if (!lobbyId) {
return { error: 'Player not in a lobby' };
}
const lobby = this.lobbies.get(lobbyId);
// Check if in correct state
if (lobby.state !== 'SONG_SUBMISSION') {
return { error: 'Cannot set ready status at this time' };
}
// Get player
const playerIndex = lobby.players.findIndex(p => p.id === playerId);
if (playerIndex === -1) {
return { error: 'Player not found in lobby' };
}
// Check if player has submitted enough songs
if (lobby.players[playerIndex].songs.length < lobby.settings.songsPerPlayer) {
return { error: 'Must submit all songs before ready' };
}
// Set player as ready
lobby.players[playerIndex].isReady = true;
// Check if all players are ready
const allReady = lobby.players.every(p => p.isReady || !p.isConnected);
if (allReady) {
console.log('All players ready, starting tournament...');
// Start the tournament by creating brackets
this._startTournament(lobby);
// Return an indicator that the tournament has started
return {
lobby,
lobbyId,
tournamentStarted: true
};
}
return { lobby, lobbyId };
}
/**
* 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
* @returns {Object} Updated lobby data or error
*/
submitVote(playerId, songId) {
const lobbyId = this.playerToLobby.get(playerId);
if (!lobbyId) {
return { error: 'Player not in a lobby' };
}
const lobby = this.lobbies.get(lobbyId);
// Check if in correct state
if (lobby.state !== 'VOTING') {
return { error: 'Cannot vote at this time' };
}
// Check if there's an active battle
if (!lobby.currentBattle) {
return { error: 'No active battle' };
}
// For bye battles (automatic advancement), only allow the host to vote/advance
if (lobby.currentBattle.bye === true && playerId !== lobby.hostId) {
return { error: 'Only the host can advance bye rounds' };
}
// Check if player has already voted in this battle
if (lobby.currentBattle.votes && lobby.currentBattle.votes.has(playerId)) {
return { error: 'Already voted in this battle' };
}
// Check if the voted song is part of the current battle
if (songId !== lobby.currentBattle.song1.id &&
(lobby.currentBattle.song2 && songId !== lobby.currentBattle.song2.id)) {
return { error: 'Invalid song ID' };
}
// Find player name for display purposes
const player = lobby.players.find(p => p.id === playerId);
const playerName = player ? player.name : 'Unknown Player';
// Initialize votes Map if it doesn't exist
if (!lobby.currentBattle.votes) {
lobby.currentBattle.votes = new Map();
}
// Record the vote with player name for UI display
lobby.currentBattle.votes.set(playerId, {
songId,
playerName
});
// Update vote counts
if (songId === lobby.currentBattle.song1.id) {
lobby.currentBattle.song1Votes = (lobby.currentBattle.song1Votes || 0) + 1;
} else if (lobby.currentBattle.song2) {
lobby.currentBattle.song2Votes = (lobby.currentBattle.song2Votes || 0) + 1;
}
// Add a voteCount attribute for easier UI rendering
lobby.currentBattle.voteCount = lobby.currentBattle.votes.size;
// For bye battles, the host's vote is all that's needed
if (lobby.currentBattle.bye === true && playerId === lobby.hostId) {
// Determine winner (in bye battles, it's always song1)
const winnerSongId = lobby.currentBattle.song1.id;
lobby.currentBattle.winner = winnerSongId;
// Save battle to history
lobby.battles.push({
round: lobby.currentBattle.round,
song1: lobby.currentBattle.song1,
song2: lobby.currentBattle.song2,
song1Votes: lobby.currentBattle.song1Votes,
song2Votes: 0,
winner: winnerSongId
});
// Move to next battle or finish tournament
this._moveToNextBattle(lobby);
return { lobby, lobbyId };
}
// 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;
if (voteCount >= connectedPlayers) {
// Determine winner
const winnerSongId = lobby.currentBattle.song1Votes > lobby.currentBattle.song2Votes
? lobby.currentBattle.song1.id
: lobby.currentBattle.song2.id;
lobby.currentBattle.winner = winnerSongId;
// Save battle to history
lobby.battles.push({
round: lobby.currentBattle.round,
song1: lobby.currentBattle.song1,
song2: lobby.currentBattle.song2,
song1Votes: lobby.currentBattle.song1Votes,
song2Votes: lobby.currentBattle.song2Votes,
winner: winnerSongId
});
// Move to next battle or finish tournament
this._moveToNextBattle(lobby);
}
return { lobby, lobbyId };
}
/**
* Handle player leaving a lobby
* @param {string} playerId - ID of the player
* @returns {Object} Updated lobby data or null if lobby is removed
*/
leaveLobby(playerId) {
const lobbyId = this.playerToLobby.get(playerId);
if (!lobbyId) {
return null; // Player not in a lobby
}
const lobby = this.lobbies.get(lobbyId);
// Remove player from lobby
const playerIndex = lobby.players.findIndex(p => p.id === playerId);
if (playerIndex !== -1) {
// If game hasn't started, remove player completely
if (lobby.state === 'LOBBY') {
lobby.players.splice(playerIndex, 1);
} else {
// Otherwise just mark as disconnected
lobby.players[playerIndex].isConnected = false;
}
}
// Remove from map
this.playerToLobby.delete(playerId);
// If it's the host leaving and game hasn't started, assign new host or delete lobby
if (lobby.hostId === playerId && lobby.state === 'LOBBY') {
if (lobby.players.length > 0) {
lobby.hostId = lobby.players[0].id;
} else {
this.lobbies.delete(lobbyId);
return null;
}
}
// If all players have left, remove the lobby
const connectedPlayers = lobby.players.filter(p => p.isConnected).length;
if (connectedPlayers === 0) {
this.lobbies.delete(lobbyId);
return null;
}
// 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 remainingPlayers = lobby.players.filter(p => p.isConnected).length;
if (totalVotes >= remainingPlayers && totalVotes > 0) {
// Determine winner
const winnerSongId = lobby.currentBattle.song1Votes > lobby.currentBattle.song2Votes
? lobby.currentBattle.song1.id
: lobby.currentBattle.song2.id;
lobby.currentBattle.winner = winnerSongId;
// Save battle to history
lobby.battles.push({
round: lobby.currentBattle.round,
song1: lobby.currentBattle.song1,
song2: lobby.currentBattle.song2,
song1Votes: lobby.currentBattle.song1Votes,
song2Votes: lobby.currentBattle.song2Votes,
winner: winnerSongId
});
// Move to next battle or finish tournament
this._moveToNextBattle(lobby);
}
}
return { lobby, lobbyId };
}
/**
* Handle player disconnection
* @param {string} playerId - ID of the disconnected player
* @returns {Object|null} Updated lobby data or null if no lobby found
*/
handleDisconnect(playerId) {
return this.leaveLobby(playerId);
}
/**
* Generate a unique lobby code
* @returns {string} 6-character lobby code
* @private
*/
_generateLobbyCode() {
const characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Removed ambiguous characters
let result = '';
// Generate code until unique
do {
result = '';
for (let i = 0; i < 6; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
} while (this.lobbies.has(result));
return result;
}
/**
* Start the tournament by creating brackets and first battle
* @param {Object} lobby - Lobby object
* @private
*/
_startTournament(lobby) {
// Collect all submitted songs
const songs = [...lobby.songs];
// Shuffle songs
this._shuffleArray(songs);
// Create tournament brackets
const brackets = [];
let round = 0;
// Handle odd number of songs by giving a bye to one song
if (songs.length % 2 !== 0 && songs.length > 1) {
const byeSong = songs.pop();
brackets.push({
round,
song1: byeSong,
song2: null, // Bye
bye: true,
winner: null, // Set to null until host advances it
song1Votes: 0,
song2Votes: 0,
votes: new Map() // Initialize votes as a Map
});
}
// Create initial bracket pairs
for (let i = 0; i < songs.length; i += 2) {
if (i + 1 < songs.length) {
brackets.push({
round,
song1: songs[i],
song2: songs[i + 1],
song1Votes: 0,
song2Votes: 0,
winner: null,
votes: new Map()
});
}
}
// Store brackets and transition to voting state
lobby.brackets = brackets;
lobby.currentBracketIndex = 0;
if (brackets.length > 0) {
// Set the first battle
lobby.state = 'VOTING';
// 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(),
voteCount: 0
};
console.log('Starting first battle:', lobby.currentBattle);
} else {
// Edge case: only one song submitted
if (songs.length === 1) {
lobby.state = 'FINISHED';
lobby.finalWinner = songs[0];
}
}
}
/**
* Move to next battle or finish tournament
* @param {Object} lobby - Lobby object
* @private
*/
_moveToNextBattle(lobby) {
// 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;
}
// Current round complete, check if tournament is finished
const winners = lobby.brackets
.filter(b => !b.bye) // Skip byes
.map(b => {
const winningSong = b.song1.id === b.winner ? b.song1 : b.song2;
return winningSong;
});
// Add byes to winners
const byes = lobby.brackets
.filter(b => b.bye)
.map(b => b.song1);
const nextRoundSongs = [...winners, ...byes];
// If only one song remains, we have a winner
if (nextRoundSongs.length <= 1) {
lobby.state = 'FINISHED';
lobby.finalWinner = nextRoundSongs[0];
lobby.currentBattle = null;
return;
}
// Create brackets for next round
const nextRound = [];
const round = lobby.brackets[0].round + 1;
// Handle odd number of songs by giving a bye to one song
if (nextRoundSongs.length % 2 !== 0) {
const byeSong = nextRoundSongs.pop();
nextRound.push({
round,
song1: byeSong,
song2: null,
bye: true,
winner: null, // Set to null until host advances it
song1Votes: 0,
song2Votes: 0,
votes: new Map() // Initialize votes as a 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: new Map()
});
}
}
// Update brackets and reset index
lobby.brackets = nextRound;
lobby.currentBracketIndex = 0;
// Set first battle of new round
if (nextRound.length > 0) {
lobby.currentBattle = nextRound[0];
// Ensure votes is initialized as a Map
if (!lobby.currentBattle.votes) {
lobby.currentBattle.votes = new Map();
}
// Initialize vote count for UI
lobby.currentBattle.voteCount = 0;
}
}
/**
* 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]];
}
}
}
// Export GameManager class
module.exports = GameManager;