import { GameState, Player, GamePhase, Role, Faction, GAME_CONFIG } from '../../../shared/types'; export class Game { public state: GameState; public roomName: string; public hostId: string; public maxPlayers: number; public password?: string; constructor( roomId: string, roomName: string, hostId: string, maxPlayers: number, password?: string ) { this.roomName = roomName; this.hostId = hostId; this.maxPlayers = maxPlayers; this.password = password; this.state = { roomId, roomName, phase: GamePhase.LOBBY, players: [], currentRound: 1, failedVotesCount: 0, questResults: [null, null, null, null, null], currentLeaderId: '', leaderVotes: {}, proposedTeam: [], teamVotes: {}, missionVotes: [], missionHistory: [], revealedVotes: [], history: [], hostId: hostId }; } addPlayer(id: string, name: string): Player { // Asignar avatar aleatorio sin repetir (rebel001.jpg - rebel010.jpg) // Obtener avatares ya usados const usedAvatars = this.state.players.map(p => p.avatar); // Crear lista de avatares disponibles const allAvatars = Array.from({ length: 10 }, (_, i) => `rebel${(i + 1).toString().padStart(3, '0')}.jpg` ); const availableAvatars = allAvatars.filter(av => !usedAvatars.includes(av)); // Si no hay avatares disponibles (más de 10 jugadores), usar cualquiera const avatarStr = availableAvatars.length > 0 ? availableAvatars[Math.floor(Math.random() * availableAvatars.length)] : allAvatars[Math.floor(Math.random() * allAvatars.length)]; const player: Player = { id, name, avatar: avatarStr, isLeader: false }; this.state.players.push(player); this.log(`${name} se ha unido a la partida.`); return player; } removePlayer(id: string) { this.state.players = this.state.players.filter(p => p.id !== id); } startGame(): boolean { const count = this.state.players.length; if (count < 5 || count > 10) return false; const config = GAME_CONFIG[count as keyof typeof GAME_CONFIG]; // 1. Asignar Roles this.assignRoles(config.good, config.evil); // 2. Asignar Líder inicial aleatorio const leaderIndex = Math.floor(Math.random() * count); this.state.players[leaderIndex].isLeader = true; this.state.currentLeaderId = this.state.players[leaderIndex].id; // 3. Iniciar juego - Fase Votación de Líder this.state.phase = GamePhase.VOTE_LEADER; this.state.leaderVotes = {}; // Reset votes this.state.currentRound = 1; this.log('¡La partida ha comenzado! Ahora deben votar para confirmar al Líder.'); return true; } // ... assignRoles se mantiene igual ... private assignRoles(goodCount: number, evilCount: number) { // Roles obligatorios const roles: Role[] = [Role.MARLENE, Role.FRANCOTIRADOR]; // Updated roles // Rellenar resto de malos for (let i = 0; i < evilCount - 1; i++) roles.push(Role.COLABORACIONISTA); // Updated role // Rellenar resto de buenos for (let i = 0; i < goodCount - 1; i++) roles.push(Role.PARTISANO); // Updated role // Barajar roles const shuffledRoles = roles.sort(() => Math.random() - 0.5); // Asignar a jugadores this.state.players.forEach((player, index) => { player.role = shuffledRoles[index]; // Asignar facción basada en el rol if ([Role.MARLENE, Role.PARTISANO].includes(player.role)) { // Updated roles player.faction = Faction.ALIADOS; } else { player.faction = Faction.ALEMANES; } }); } // --- LOGICA DE VOTACIÓN DE LÍDER --- voteLeader(playerId: string, approve: boolean) { // Solo registrar el voto si el jugador existe y aún no ha votado const player = this.state.players.find(p => p.id === playerId); if (!player) return; this.state.leaderVotes[playerId] = approve; // Comprobar si todos han votado if (Object.keys(this.state.leaderVotes).length === this.state.players.length) { this.resolveLeaderVote(); } } // Método para forzar la resolución de votos cuando se acaba el tiempo forceResolveLeaderVote() { // Si nadie votó o no todos votaron, se considera fracaso this.resolveLeaderVote(); } private resolveLeaderVote() { const totalPlayers = this.state.players.length; const votesCount = Object.keys(this.state.leaderVotes).length; // Si nadie votó, rechazar automáticamente if (votesCount === 0) { this.log(`Votación de Líder: nadie votó. Líder rechazado.`); this.state.failedVotesCount++; if (this.state.failedVotesCount >= 5) { this.endGame(Faction.ALEMANES, 'Se han rechazado 5 líderes consecutivos.'); } else { this.nextLeader(); } return; } // Contar votos de aprobación y rechazo const approves = Object.values(this.state.leaderVotes).filter(v => v === true).length; const rejects = Object.values(this.state.leaderVotes).filter(v => v === false).length; // La mayoría se calcula sobre los que votaron, no sobre el total const isSuccess = approves > rejects; this.log(`Votación de Líder: ${approves} a favor, ${rejects} en contra (${votesCount}/${totalPlayers} votaron)`); if (isSuccess) { // Líder Aprobado -> Fase de Construcción de Equipo this.state.phase = GamePhase.TEAM_BUILDING; this.state.proposedTeam = []; // Reset team selection this.state.failedVotesCount = 0; // Reset contador al aprobar líder this.log(`Líder ${this.state.players.find(p => p.id === this.state.currentLeaderId)?.name} confirmado con ${approves} votos a favor.`); } else { // Líder Rechazado -> Incrementar contador y pasar al siguiente líder this.state.failedVotesCount++; this.log(`Líder rechazado: ${rejects} votos en contra superan a ${approves} a favor. Pasando turno al siguiente jugador.`); // Verificar si se alcanzó el límite de rechazos (5 votos fallidos = alemanes ganan) if (this.state.failedVotesCount >= 5) { this.endGame(Faction.ALEMANES, 'Se han rechazado 5 líderes consecutivos.'); } else { this.nextLeader(); // Esto pondrá phase en VOTE_LEADER de nuevo } } } proposeTeam(playerIds: string[]): boolean { // Validar tamaño del equipo según la ronda const config = GAME_CONFIG[this.state.players.length as keyof typeof GAME_CONFIG]; const requiredSize = config.quests[this.state.currentRound - 1]; if (playerIds.length !== requiredSize) return false; this.state.proposedTeam = playerIds; this.state.phase = GamePhase.MISSION; // Ir directamente a la misión this.state.missionVotes = []; // Resetear votos de misión this.state.failedVotesCount = 0; // Reset contador de rechazos this.log(`El líder ha seleccionado un equipo de ${playerIds.length} personas. ¡Comienza la misión!`); return true; } voteForTeam(playerId: string, approve: boolean) { this.state.teamVotes[playerId] = approve; // Comprobar si todos han votado if (Object.keys(this.state.teamVotes).length === this.state.players.length) { this.resolveTeamVote(); } } private resolveTeamVote() { const votes = Object.values(this.state.teamVotes); const approves = votes.filter(v => v).length; const rejects = votes.length - approves; this.log(`Votación completada: ${approves} A favor - ${rejects} En contra.`); if (approves > rejects) { // Equipo Aprobado this.state.phase = GamePhase.MISSION; this.state.missionVotes = []; this.state.failedVotesCount = 0; this.log('El equipo ha sido aprobado. ¡Comienza la misión!'); } else { // Equipo Rechazado this.state.failedVotesCount++; this.state.proposedTeam = []; if (this.state.failedVotesCount >= 5) { this.endGame(Faction.ALEMANES, 'Se han rechazado 5 equipos consecutivos.'); // Updated Faction } else { this.nextLeader(); // Pasa a VOTE_LEADER this.log('El equipo fue rechazado. El liderazgo pasa al siguiente jugador.'); } } } voteMission(success: boolean) { this.state.missionVotes.push(success); // Comprobar si todos los miembros del equipo han votado if (this.state.missionVotes.length === this.state.proposedTeam.length) { this.resolveMission(); } } private resolveMission() { const fails = this.state.missionVotes.filter(v => !v).length; const successes = this.state.missionVotes.filter(v => v).length; const playerCount = this.state.players.length; const round = this.state.currentRound; // Regla especial: 4ta misión con 7+ jugadores necesita 2 fallos let failsRequired = 1; if (playerCount >= 7 && round === 4) { failsRequired = 2; } const isSuccess = fails < failsRequired; // 1. BARAJAR los votos para que no se sepa quién votó qué const shuffledVotes = [...this.state.missionVotes].sort(() => Math.random() - 0.5); // 2. Guardar en el histórico const missionRecord = { round, team: [...this.state.proposedTeam], votes: shuffledVotes, successes, fails, isSuccess, leaderId: this.state.currentLeaderId }; this.state.missionHistory.push(missionRecord); // 3. Actualizar resultado de la quest this.state.questResults[round - 1] = isSuccess; // 4. Pasar a fase MISSION_REVEAL (mostrar cartas una a una) this.state.phase = GamePhase.MISSION_REVEAL; this.state.revealedVotes = shuffledVotes; // Las cartas a revelar this.log(`Misión ${round} completada. Revelando votos...`); // El cliente controlará el avance a MISSION_RESULT con su timer } // Método para avanzar desde MISSION_REVEAL a MISSION_RESULT finishReveal() { this.state.phase = GamePhase.MISSION_RESULT; } // Método para avanzar desde MISSION_RESULT y continuar el juego finishMissionResult() { const round = this.state.currentRound; const isSuccess = this.state.questResults[round - 1]; this.log(`Misión ${round}: ${isSuccess ? 'ÉXITO' : 'FRACASO'}`); // Comprobar condiciones de victoria const successes = this.state.questResults.filter(r => r === true).length; const failures = this.state.questResults.filter(r => r === false).length; if (failures >= 3) { // Los Nazis ganan directamente this.state.winner = Faction.ALEMANES; this.state.phase = GamePhase.NAZIS_WIN; this.log('¡Los Nazis han ganado! Tres misiones han fracasado.'); } else if (successes >= 3) { // Los Aliados han completado 3 misiones, pero el Asesino tiene una oportunidad this.state.phase = GamePhase.ASSASSIN_PHASE; this.log('¡La Resistencia ha triunfado! Pero el Francotirador tiene una última oportunidad...'); } else { // Siguiente ronda this.state.currentRound++; this.nextLeader(); // Esto pone phase = VOTE_LEADER this.state.proposedTeam = []; this.state.missionVotes = []; this.state.revealedVotes = []; } } assassinKill(targetId: string) { const target = this.state.players.find(p => p.id === targetId); if (target && target.role === Role.MARLENE) { // El Francotirador acierta: Nazis ganan this.state.winner = Faction.ALEMANES; this.state.phase = GamePhase.NAZIS_WIN; this.log('¡El Francotirador ha eliminado a Marlene! Los Nazis ganan.'); } else { // El Francotirador falla: Aliados ganan this.state.winner = Faction.ALIADOS; this.state.phase = GamePhase.ALLIED_WIN; this.log('El Francotirador ha fallado. ¡La Resistencia gana!'); } } nextLeader() { const currentIdx = this.state.players.findIndex(p => p.id === this.state.currentLeaderId); const nextIdx = (currentIdx + 1) % this.state.players.length; this.state.players[currentIdx].isLeader = false; this.state.players[nextIdx].isLeader = true; this.state.currentLeaderId = this.state.players[nextIdx].id; // Fase de confirmar al nuevo líder this.state.phase = GamePhase.VOTE_LEADER; this.state.leaderVotes = {}; } private endGame(winner: Faction, reason: string) { this.state.winner = winner; this.state.phase = GamePhase.GAME_OVER; this.log(`FIN DEL JUEGO. Victoria para ${winner}. Razón: ${reason}`); } // Método para reiniciar la partida (volver a REVEAL_ROLE) restartGame() { this.log('=== REINICIANDO PARTIDA ==='); // Resetear variables de juego this.state.currentRound = 1; this.state.failedVotesCount = 0; this.state.questResults = [null, null, null, null, null]; this.state.leaderVotes = {}; this.state.proposedTeam = []; this.state.teamVotes = {}; this.state.missionVotes = []; this.state.revealedVotes = []; this.state.missionHistory = []; this.state.winner = undefined; // Reasignar roles const count = this.state.players.length; const config = GAME_CONFIG[count as keyof typeof GAME_CONFIG]; this.assignRoles(config.good, config.evil); // Asignar nuevo líder aleatorio const leaderIndex = Math.floor(Math.random() * count); this.state.players.forEach((p, i) => p.isLeader = i === leaderIndex); this.state.currentLeaderId = this.state.players[leaderIndex].id; // Volver a REVEAL_ROLE this.state.phase = GamePhase.REVEAL_ROLE; this.log('Nueva partida iniciada. Revelando roles...'); } // Método para finalizar definitivamente y preparar expulsión finalizeGame() { this.state.phase = GamePhase.GAME_OVER; this.log('Partida finalizada. Los jugadores serán expulsados.'); } private log(message: string) { this.state.history.push(message); // Mantener solo los últimos 50 mensajes if (this.state.history.length > 50) this.state.history.shift(); } }