void }) {
+// Subcomponente para el Timer de Votación (solo visual, el servidor controla el timeout real)
+function VotingTimer() {
const [timeLeft, setTimeLeft] = useState(10);
useEffect(() => {
if (timeLeft <= 0) {
- onTimeout();
- return;
+ return; // El servidor se encargará de forzar la resolución
}
const interval = setInterval(() => setTimeLeft(t => t - 1), 1000);
return () => clearInterval(interval);
- }, [timeLeft, onTimeout]);
+ }, [timeLeft]);
return (
diff --git a/client/src/hooks/useSocket.ts b/client/src/hooks/useSocket.ts
index e991ba4..b341751 100644
--- a/client/src/hooks/useSocket.ts
+++ b/client/src/hooks/useSocket.ts
@@ -97,7 +97,7 @@ export const useSocket = () => {
proposeTeam,
voteTeam,
voteMission,
- voteLeader: (approve: boolean | null) => socket?.emit('vote_leader', { roomId: gameState?.roomId, approve }),
+ voteLeader: (approve: boolean) => socket?.emit('vote_leader', { roomId: gameState?.roomId, approve }),
assassinKill,
finishIntro: () => socket?.emit('finish_intro', { roomId: gameState?.roomId }),
finishReveal: () => socket?.emit('finish_reveal', { roomId: gameState?.roomId }),
diff --git a/server/src/index.ts b/server/src/index.ts
index 38c90a7..97d239b 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -41,22 +41,22 @@ const MISSION_NAMES = [
"Operación Eiche", "Operación León Marino", "Operación Urano"
];
-// Helper para iniciar timer de votación de líder
+// Helper para iniciar timer de votación de líder (10 segundos)
function startLeaderVoteTimer(roomId: string) {
if (voteTimers[roomId]) clearTimeout(voteTimers[roomId]);
voteTimers[roomId] = setTimeout(() => {
const g = games[roomId];
if (g && g.state.phase === 'vote_leader') {
- // Pasar al siguiente líder sin resolver (rechazar implícitamente)
- g.nextLeader();
+ // Forzar resolución de votos cuando se acaba el tiempo
+ g.forceResolveLeaderVote();
io.to(roomId).emit('game_state', g.state);
- // Si sigue en vote_leader (nuevo líder), reiniciar timer
+ // Si sigue en vote_leader (líder rechazado, nuevo líder), reiniciar timer
if (g.state.phase === 'vote_leader') {
startLeaderVoteTimer(roomId);
}
}
- }, 11000); // 11 segundos
+ }, 10000); // 10 segundos
}
const generateRoomName = () => {
diff --git a/server/src/models/Game.ts b/server/src/models/Game.ts
index 0ec1eb9..fc8f951 100644
--- a/server/src/models/Game.ts
+++ b/server/src/models/Game.ts
@@ -128,7 +128,11 @@ export class Game {
}
// --- LOGICA DE VOTACIÓN DE LÍDER ---
- voteLeader(playerId: string, approve: boolean | null) {
+ 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
@@ -137,33 +141,55 @@ export class Game {
}
}
- // Método para forzar la resolución de votos (llamado por timeout)
+ // Método para forzar la resolución de votos cuando se acaba el tiempo
forceResolveLeaderVote() {
- // Registrar votos null para los que no votaron
- this.state.players.forEach(player => {
- if (!(player.id in this.state.leaderVotes)) {
- this.state.leaderVotes[player.id] = null;
- }
- });
+ // Si nadie votó o no todos votaron, se considera fracaso
this.resolveLeaderVote();
}
private resolveLeaderVote() {
- const votes = Object.values(this.state.leaderVotes);
- const approves = votes.filter(v => v === true).length;
- const rejects = votes.filter(v => v === false || v === null).length; // null cuenta como rechazo
+ const totalPlayers = this.state.players.length;
+ const votesCount = Object.keys(this.state.leaderVotes).length;
- this.log(`Votación de Líder: ${approves} A favor - ${rejects} En contra.`);
+ // 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 (approves > rejects) {
+ 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.log('Líder confirmado. Ahora debe proponer un equipo.');
+ 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 -> Siguiente líder
- this.log('Líder rechazado. Pasando turno al siguiente jugador.');
- this.nextLeader(); // Esto pondrá phase en VOTE_LEADER de nuevo
+ // 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
+ }
}
}