- Mover timer de votación a esquina superior izquierda (fixed, 20px margen) - Eliminar contador de votos rechazados en resultado de misión - Ajustar posiciones de tokens de victoria/fracaso en el mapa - Mantener mapa visible durante toda la fase MISSION_RESULT (eliminar timeout de 7s) - Cambiar título intro de 'Guerra Total' a 'Traidores en París' - Ajustar tamaño de cartas aceptar/rechazar líder a cuadradas (w-32 h-32) TODO: Afinar posiciones de tokens 3, 4 y 5 en el mapa
411 lines
15 KiB
TypeScript
411 lines
15 KiB
TypeScript
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();
|
|
}
|
|
}
|