chore: Limpiar archivos basura de git y actualizar .gitignore
Some checks failed
CI/CD - Francia Ocupada (La Resistencia) / build-and-deploy (push) Failing after 8s

This commit is contained in:
Resistencia Dev
2025-12-22 18:59:03 +01:00
parent f4a557acdb
commit b11186943b
264 changed files with 12518 additions and 11 deletions

83
server/dist/server/src/db.js vendored Normal file
View File

@@ -0,0 +1,83 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getGameHistory = exports.logGameEnd = exports.updateGamePlayers = exports.logGameStart = exports.initDb = void 0;
const pg_1 = require("pg");
const dotenv_1 = __importDefault(require("dotenv"));
dotenv_1.default.config();
const pool = new pg_1.Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:password@db:5432/resistencia',
});
// Inicializar base de datos
const initDb = async () => {
const client = await pool.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS game_logs (
id SERIAL PRIMARY KEY,
room_id UUID NOT NULL,
room_name TEXT NOT NULL,
host_name TEXT NOT NULL,
players TEXT NOT NULL, -- Lista de nombres separada por comas
max_players INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
finished_at TIMESTAMP,
winner TEXT, -- 'resistance' o 'spies'
status TEXT DEFAULT 'active' -- 'active', 'finished', 'aborted'
);
`);
console.log('[DB] Base de datos inicializada correctamente');
}
catch (err) {
console.error('[DB] Error al inicializar base de datos:', err);
}
finally {
client.release();
}
};
exports.initDb = initDb;
// Registrar inicio de partida
const logGameStart = async (roomId, roomName, hostName, maxPlayers) => {
try {
await pool.query('INSERT INTO game_logs (room_id, room_name, host_name, players, max_players, status) VALUES ($1, $2, $3, $4, $5, $6)', [roomId, roomName, hostName, hostName, maxPlayers, 'active']);
}
catch (err) {
console.error('[DB] Error al registrar inicio de partida:', err);
}
};
exports.logGameStart = logGameStart;
// Actualizar lista de jugadores en tiempo real (opcional, pero útil para el histórico final)
const updateGamePlayers = async (roomId, players) => {
try {
await pool.query('UPDATE game_logs SET players = $1 WHERE room_id = $2 AND status = $3', [players.join(', '), roomId, 'active']);
}
catch (err) {
console.error('[DB] Error al actualizar jugadores en log:', err);
}
};
exports.updateGamePlayers = updateGamePlayers;
// Registrar fin de partida
const logGameEnd = async (roomId, winner = null, aborted = false) => {
try {
await pool.query('UPDATE game_logs SET finished_at = CURRENT_TIMESTAMP, winner = $1, status = $2 WHERE room_id = $3 AND status = $4', [winner, aborted ? 'aborted' : 'finished', roomId, 'active']);
}
catch (err) {
console.error('[DB] Error al registrar fin de partida:', err);
}
};
exports.logGameEnd = logGameEnd;
// Obtener historial
const getGameHistory = async () => {
try {
const res = await pool.query('SELECT * FROM game_logs ORDER BY created_at DESC LIMIT 50');
return res.rows;
}
catch (err) {
console.error('[DB] Error al obtener historial:', err);
return [];
}
};
exports.getGameHistory = getGameHistory;
exports.default = pool;

437
server/dist/server/src/index.js vendored Normal file
View File

@@ -0,0 +1,437 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const http_1 = __importDefault(require("http"));
const socket_io_1 = require("socket.io");
const cors_1 = __importDefault(require("cors"));
const dotenv_1 = __importDefault(require("dotenv"));
const crypto_1 = __importDefault(require("crypto"));
const Game_1 = require("./models/Game");
const types_1 = require("../../shared/types");
const db_1 = require("./db");
dotenv_1.default.config();
// Inicializar DB
(0, db_1.initDb)();
const app = (0, express_1.default)();
const port = process.env.PORT || 4000;
const allowedOrigins = [
process.env.CORS_ORIGIN || "http://localhost:3000",
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://192.168.1.131:3000"
];
app.use((0, cors_1.default)({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
}
else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ["GET", "POST"]
}));
const server = http_1.default.createServer(app);
const io = new socket_io_1.Server(server, {
cors: {
origin: allowedOrigins,
methods: ["GET", "POST"]
}
});
// ALMACÉN DE PARTIDAS (En memoria por ahora)
// En el futuro esto podría estar en Redis o Postgres
const games = {};
// Almacén de timers para auto-resolución de votaciones
const voteTimers = {};
// --- LOBBY MANAGEMENT ---
const MISSION_NAMES = [
"Operación Overlord", "Operación Market Garden", "Operación Barbarroja",
"Operación Valkiria", "Operación Torch", "Operación Husky",
"Batalla de Stalingrado", "Dunkerque", "El Alamein", "Midway",
"Operación Ciudadela", "Operación Dragoon", "Operación Fortaleza",
"Operación Eiche", "Operación León Marino", "Operación Urano"
];
// Helper para iniciar timer de votación de líder (10 segundos)
function startLeaderVoteTimer(roomId) {
if (voteTimers[roomId])
clearTimeout(voteTimers[roomId]);
voteTimers[roomId] = setTimeout(() => {
const g = games[roomId];
if (g && g.state.phase === 'vote_leader') {
// 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 (líder rechazado, nuevo líder), reiniciar timer
if (g.state.phase === 'vote_leader') {
startLeaderVoteTimer(roomId);
}
}
}, 10000); // 10 segundos
}
const generateRoomName = () => {
const idx = Math.floor(Math.random() * MISSION_NAMES.length);
const suffix = Math.floor(100 + Math.random() * 900); // 3 digit code
return `${MISSION_NAMES[idx]} #${suffix}`;
};
const getAdminData = async () => {
const activeGamesData = Object.values(games).map(g => ({
id: g.state.roomId,
name: g.roomName,
status: g.state.phase,
currentPlayers: g.state.players.length,
maxPlayers: g.maxPlayers,
players: g.state.players.map(p => ({ id: p.id, name: p.name }))
}));
const history = await (0, db_1.getGameHistory)();
return {
activeGames: activeGamesData,
history: history
};
};
const broadcastAdminUpdate = async () => {
const data = await getAdminData();
io.to('admin-room').emit('admin_data', data);
};
io.on('connection', (socket) => {
console.log('Cliente conectado:', socket.id);
// Enviar lista de salas al conectar
socket.emit('rooms_list', getRoomsList());
// A. CREAR SALA
socket.on('create_game', ({ hostName, maxPlayers, password }) => {
console.log(`[CREATE_GAME] Host: ${hostName}, Players: ${maxPlayers}`);
const roomId = crypto_1.default.randomUUID();
const roomName = generateRoomName();
const newGame = new Game_1.Game(roomId, roomName, socket.id, maxPlayers, password);
games[roomId] = newGame;
// Unir al creador automáticamente
newGame.addPlayer(socket.id, hostName);
socket.join(roomId);
// Notificar al creador
socket.emit('game_joined', { roomId, state: newGame.state });
// Actualizar lista a todos
io.emit('rooms_list', getRoomsList());
// LOG EN DB
(0, db_1.logGameStart)(roomId, roomName, hostName, maxPlayers);
broadcastAdminUpdate();
});
// B. UNIRSE A SALA
socket.on('join_game', ({ roomId, playerName, password }) => {
const game = games[roomId];
if (!game) {
socket.emit('error', 'La partida no existe');
return;
}
if (game.password && game.password !== password) {
socket.emit('error', 'Contraseña incorrecta');
return;
}
if (game.state.players.length >= game.maxPlayers) {
socket.emit('error', 'La partida está llena');
return;
}
// Evitar duplicados
const existingPlayer = game.state.players.find(p => p.id === socket.id);
if (!existingPlayer) {
game.addPlayer(socket.id, playerName);
}
socket.join(roomId);
// Enviar estado actualizado a la sala
io.to(roomId).emit('game_state', game.state);
// Avisar al usuario que entró OK
socket.emit('game_joined', { roomId, state: game.state });
// Actualizar lista de salas (cambió contador de jugadores)
io.emit('rooms_list', getRoomsList());
// ACTUALIZAR LOG EN DB
(0, db_1.updateGamePlayers)(roomId, game.state.players.map(p => p.name));
broadcastAdminUpdate();
});
// C. REFRESCAR LISTA
socket.on('get_rooms', () => {
socket.emit('rooms_list', getRoomsList());
});
// --- GAMEPLAY DE SIEMPRE (Adaptado a roomId dinámico) ---
// 2. INICIAR PARTIDA - FASE INTRO
socket.on('start_game', ({ roomId }) => {
const game = games[roomId];
// Solo el host puede iniciar
if (game && game.hostId === socket.id && game.state.phase === 'lobby') {
game.state.phase = 'intro';
io.to(roomId).emit('game_state', game.state);
io.emit('rooms_list', getRoomsList());
}
});
// 2.1 PASAR INTRO -> REVEAL ROLE
socket.on('finish_intro', ({ roomId }) => {
const game = games[roomId];
if (game && game.hostId === socket.id && game.state.phase === 'intro') {
// AQUI DISTRIBUIMOS ROLES
if (game.startGame()) {
// startGame pone phase en TEAM_BUILDING, pero nosotros queremos REVEAL_ROLE primero
game.state.phase = 'reveal_role';
io.to(roomId).emit('game_state', game.state);
}
}
});
// 2.2 REVEAL ROLE -> ROLL CALL
socket.on('finish_reveal', ({ roomId }) => {
const game = games[roomId];
// Cualquiera puede llamar a esto si es auto-timer, o solo host?
// El usuario dijo "Habrá una cuenta atrás de 5 segundos".
// Lo ideal es que el Host controle el tiempo para sincronizar.
if (game && game.hostId === socket.id && game.state.phase === 'reveal_role') {
game.state.phase = 'roll_call';
io.to(roomId).emit('game_state', game.state);
}
});
// 2.3 FINALIZAR ROLL CALL -> PRIMER TURNO DE JUEGO
socket.on('finish_roll_call', ({ roomId }) => {
const game = games[roomId];
if (game && game.hostId === socket.id && game.state.phase === 'roll_call') {
// ERROR CORREGIDO: No llamar a startGame() de nuevo porque re-baraja los roles.
// Simplemente avanzamos a la fase de votación de líder que ya estaba configurada.
game.state.phase = 'vote_leader';
// Iniciar timer de 11 segundos para forzar cambio de líder
startLeaderVoteTimer(roomId);
io.to(roomId).emit('game_state', game.state);
}
});
// 2.4 VOTAR LÍDER
socket.on('vote_leader', ({ roomId, approve }) => {
const game = games[roomId];
if (game) {
const previousPhase = game.state.phase;
game.voteLeader(socket.id, approve);
io.to(roomId).emit('game_state', game.state);
// Si cambió de fase (líder aprobado o rechazado)
if (game.state.phase !== previousPhase) {
// Limpiar timer actual
if (voteTimers[roomId]) {
clearTimeout(voteTimers[roomId]);
delete voteTimers[roomId];
}
// Si pasó a vote_leader de nuevo (líder rechazado), iniciar nuevo timer
if (game.state.phase === 'vote_leader') {
startLeaderVoteTimer(roomId);
}
}
}
});
// 3. PROPONER EQUIPO
socket.on('propose_team', ({ roomId, teamIds }) => {
const game = games[roomId];
if (game && game.proposeTeam(teamIds)) {
io.to(roomId).emit('game_state', game.state);
}
});
// 4. VOTAR EQUIPO
socket.on('vote_team', ({ roomId, approve }) => {
const game = games[roomId];
if (game) {
game.voteForTeam(socket.id, approve);
io.to(roomId).emit('game_state', game.state);
}
});
// 5. VOTAR MISIÓN
socket.on('vote_mission', ({ roomId, success }) => {
const game = games[roomId];
if (game) {
game.voteMission(success);
io.to(roomId).emit('game_state', game.state);
}
});
// 5.1 FINALIZAR REVELACIÓN DE CARTAS
socket.on('finish_reveal', ({ roomId }) => {
const game = games[roomId];
// Permitir a cualquiera avanzar para evitar bloqueos
if (game && game.state.phase === 'mission_reveal') {
game.finishReveal();
io.to(roomId).emit('game_state', game.state);
}
});
// 5.2 FINALIZAR PANTALLA DE RESULTADO
socket.on('finish_mission_result', ({ roomId }) => {
const game = games[roomId];
if (game && game.hostId === socket.id && game.state.phase === types_1.GamePhase.MISSION_RESULT) {
game.finishMissionResult();
io.to(roomId).emit('game_state', game.state);
// Si volvió a vote_leader (nueva ronda), iniciar timer
// TypeScript no detecta que finishMissionResult() cambia la fase, usamos type assertion
if (game.state.phase === types_1.GamePhase.VOTE_LEADER) {
startLeaderVoteTimer(roomId);
}
}
});
// 6. ASESINATO FINAL
socket.on('assassin_kill', ({ roomId, targetId }) => {
const game = games[roomId];
if (game) {
game.assassinKill(targetId);
io.to(roomId).emit('game_state', game.state);
}
});
// 7. REINICIAR PARTIDA
socket.on('restart_game', ({ roomId }) => {
const game = games[roomId];
if (game && game.hostId === socket.id) {
game.restartGame();
io.to(roomId).emit('game_state', game.state);
}
});
// 8. FINALIZAR Y EXPULSAR JUGADORES
socket.on('finalize_game', ({ roomId }) => {
const game = games[roomId];
if (game && game.hostId === socket.id) {
// Notificar a todos los jugadores que la partida ha sido finalizada
io.to(roomId).emit('game_finalized');
// Eliminar la partida inmediatamente del registro
delete games[roomId];
// Actualizar lista de salas para todos los clientes
io.emit('rooms_list', getRoomsList());
// Desconectar a todos los jugadores de la sala
io.in(roomId).socketsLeave(roomId);
// LOG EN DB
(0, db_1.logGameEnd)(roomId, game.state.winner, false);
broadcastAdminUpdate();
}
});
// 8. ABANDONAR PARTIDA (cualquier jugador)
socket.on('leave_game', ({ roomId }) => {
const game = games[roomId];
if (game) {
// Encontrar el jugador que se va
const leavingPlayer = game.state.players.find(p => p.id === socket.id);
const playerName = leavingPlayer?.name || 'Un jugador';
// Notificar a todos los jugadores
io.to(roomId).emit('player_left_game', { playerName });
io.to(roomId).emit('game_finalized');
// Eliminar la partida de la base de datos
delete games[roomId];
// Limpiar timer si existe
if (voteTimers[roomId]) {
clearTimeout(voteTimers[roomId]);
delete voteTimers[roomId];
}
// Actualizar lista de salas
io.emit('rooms_list', getRoomsList());
// Desconectar a todos de la sala
io.in(roomId).socketsLeave(roomId);
// LOG EN DB COMO ABORTADA
(0, db_1.logGameEnd)(roomId, null, true);
broadcastAdminUpdate();
console.log(`[LEAVE_GAME] ${playerName} abandonó la partida ${roomId}. Partida eliminada.`);
}
});
// 9. RECONECTAR SESIÓN
socket.on('reconnect_session', ({ playerName, roomId }) => {
console.log(`[RECONNECT_SESSION] Intento de reconexión: ${playerName} a sala ${roomId}`);
if (roomId) {
const game = games[roomId];
if (game) {
// Buscar si el jugador existe en la partida
const existingPlayer = game.state.players.find(p => p.name === playerName);
if (existingPlayer) {
// Actualizar el socket ID del jugador
existingPlayer.id = socket.id;
// Unir al socket a la sala
socket.join(roomId);
// Enviar estado actualizado
socket.emit('game_joined', { roomId, state: game.state });
io.to(roomId).emit('game_state', game.state);
console.log(`[RECONNECT_SESSION] ${playerName} reconectado exitosamente a ${roomId}`);
}
else {
console.log(`[RECONNECT_SESSION] Jugador ${playerName} no encontrado en partida ${roomId}`);
socket.emit('error', 'No se pudo reconectar a la partida');
}
}
else {
console.log(`[RECONNECT_SESSION] Partida ${roomId} no existe`);
socket.emit('error', 'La partida ya no existe');
}
}
});
// 10. FINALIZAR Y EXPULSAR JUGADORES (solo host)
socket.on('finalize_game', ({ roomId }) => {
const game = games[roomId];
if (game && game.hostId === socket.id) {
// Notificar a todos los jugadores que la partida ha sido finalizada
io.to(roomId).emit('game_finalized');
// Eliminar la partida inmediatamente del registro
delete games[roomId];
// Actualizar lista de salas para todos los clientes
io.emit('rooms_list', getRoomsList());
// Desconectar a todos los jugadores de la sala
io.in(roomId).socketsLeave(roomId);
}
});
// 11. DESCONEXIÓN
socket.on('disconnect', () => {
// Buscar en qué partida estaba y sacarlo (opcional, por ahora solo notificamos)
console.log('Desconectado:', socket.id);
});
// --- ADMIN COMMANDS ---
socket.on('admin_get_data', async () => {
console.log('[ADMIN] Agente administrativo conectado');
socket.join('admin-room');
const data = await getAdminData();
socket.emit('admin_data', data);
});
socket.on('admin_close_game', async ({ roomId }) => {
const game = games[roomId];
if (game) {
io.to(roomId).emit('game_finalized');
delete games[roomId];
if (voteTimers[roomId]) {
clearTimeout(voteTimers[roomId]);
delete voteTimers[roomId];
}
io.emit('rooms_list', getRoomsList());
io.in(roomId).socketsLeave(roomId);
// Log como abortada por admin
await (0, db_1.logGameEnd)(roomId, null, true);
socket.emit('admin_action_success');
broadcastAdminUpdate();
}
});
socket.on('admin_kick_player', ({ roomId, targetSocketId }) => {
const game = games[roomId];
if (game) {
const playerIndex = game.state.players.findIndex(p => p.id === targetSocketId);
if (playerIndex !== -1) {
const playerName = game.state.players[playerIndex].name;
game.state.players.splice(playerIndex, 1);
// Notificar al jugador expulsado
io.to(targetSocketId).emit('game_finalized');
const targetSocket = io.sockets.sockets.get(targetSocketId);
targetSocket?.leave(roomId);
// Notificar al resto
io.to(roomId).emit('player_left_game', { playerName: `${playerName} (Expulsado)` });
io.to(roomId).emit('game_state', game.state);
io.emit('rooms_list', getRoomsList());
(0, db_1.updateGamePlayers)(roomId, game.state.players.map(p => p.name));
socket.emit('admin_action_success');
broadcastAdminUpdate();
}
}
});
});
const getRoomsList = () => {
return Object.values(games).map(g => ({
id: g.state.roomId,
name: g.roomName,
hostId: g.hostId,
currentPlayers: g.state.players.length,
maxPlayers: g.maxPlayers,
isPrivate: !!g.password,
status: g.state.phase === 'lobby' ? 'waiting' : 'playing'
}));
};
app.get('/', (req, res) => {
res.send('Servidor de La Resistencia funcionando 🚀');
});
server.listen(port, () => {
console.log(`[server]: Servidor corriendo en http://localhost:${port}`);
});

338
server/dist/server/src/models/Game.js vendored Normal file
View File

@@ -0,0 +1,338 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Game = void 0;
const types_1 = require("../../../shared/types");
class Game {
constructor(roomId, roomName, hostId, maxPlayers, password) {
this.roomName = roomName;
this.hostId = hostId;
this.maxPlayers = maxPlayers;
this.password = password;
this.state = {
roomId,
roomName,
phase: types_1.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, name) {
// 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 = {
id,
name,
avatar: avatarStr,
isLeader: false
};
this.state.players.push(player);
this.log(`${name} se ha unido a la partida.`);
return player;
}
removePlayer(id) {
this.state.players = this.state.players.filter(p => p.id !== id);
}
startGame() {
const count = this.state.players.length;
if (count < 5 || count > 10)
return false;
const config = types_1.GAME_CONFIG[count];
// 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 = types_1.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 ...
assignRoles(goodCount, evilCount) {
// Roles obligatorios
const roles = [types_1.Role.MARLENE, types_1.Role.FRANCOTIRADOR]; // Updated roles
// Rellenar resto de malos
for (let i = 0; i < evilCount - 1; i++)
roles.push(types_1.Role.COLABORACIONISTA); // Updated role
// Rellenar resto de buenos
for (let i = 0; i < goodCount - 1; i++)
roles.push(types_1.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 ([types_1.Role.MARLENE, types_1.Role.PARTISANO].includes(player.role)) { // Updated roles
player.faction = types_1.Faction.ALIADOS;
}
else {
player.faction = types_1.Faction.ALEMANES;
}
});
}
// --- LOGICA DE VOTACIÓN DE LÍDER ---
voteLeader(playerId, approve) {
// 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();
}
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(types_1.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 = types_1.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(types_1.Faction.ALEMANES, 'Se han rechazado 5 líderes consecutivos.');
}
else {
this.nextLeader(); // Esto pondrá phase en VOTE_LEADER de nuevo
}
}
}
proposeTeam(playerIds) {
// Validar tamaño del equipo según la ronda
const config = types_1.GAME_CONFIG[this.state.players.length];
const requiredSize = config.quests[this.state.currentRound - 1];
if (playerIds.length !== requiredSize)
return false;
this.state.proposedTeam = playerIds;
this.state.phase = types_1.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, approve) {
this.state.teamVotes[playerId] = approve;
// Comprobar si todos han votado
if (Object.keys(this.state.teamVotes).length === this.state.players.length) {
this.resolveTeamVote();
}
}
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 = types_1.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(types_1.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) {
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();
}
}
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 = types_1.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 = types_1.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 = types_1.Faction.ALEMANES;
this.state.phase = types_1.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 = types_1.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) {
const target = this.state.players.find(p => p.id === targetId);
if (target && target.role === types_1.Role.MARLENE) {
// El Francotirador acierta: Nazis ganan
this.state.winner = types_1.Faction.ALEMANES;
this.state.phase = types_1.GamePhase.NAZIS_WIN;
this.log('¡El Francotirador ha eliminado a Marlene! Los Nazis ganan.');
}
else {
// El Francotirador falla: Aliados ganan
this.state.winner = types_1.Faction.ALIADOS;
this.state.phase = types_1.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 = types_1.GamePhase.VOTE_LEADER;
this.state.leaderVotes = {};
}
endGame(winner, reason) {
this.state.winner = winner;
this.state.phase = types_1.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 = types_1.GAME_CONFIG[count];
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 = types_1.GamePhase.REVEAL_ROLE;
this.log('Nueva partida iniciada. Revelando roles...');
}
// Método para finalizar definitivamente y preparar expulsión
finalizeGame() {
this.state.phase = types_1.GamePhase.GAME_OVER;
this.log('Partida finalizada. Los jugadores serán expulsados.');
}
log(message) {
this.state.history.push(message);
// Mantener solo los últimos 50 mensajes
if (this.state.history.length > 50)
this.state.history.shift();
}
}
exports.Game = Game;

47
server/dist/shared/types.js vendored Normal file
View File

@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GAME_CONFIG = exports.GamePhase = exports.Faction = exports.Role = void 0;
var Role;
(function (Role) {
// Bando Aliado (Resistencia Francesa)
Role["MARLENE"] = "marlene";
Role["CAPITAN_PHILIPPE"] = "capitan_philippe";
Role["PARTISANO"] = "partisano";
// Bando Alemán (Ocupación Nazi)
Role["COMANDANTE_SCHMIDT"] = "comandante_schmidt";
Role["FRANCOTIRADOR"] = "francotirador";
Role["AGENTE_DOBLE"] = "agente_doble";
Role["INFILTRADO"] = "infiltrado";
Role["COLABORACIONISTA"] = "colaboracionista";
})(Role || (exports.Role = Role = {}));
var Faction;
(function (Faction) {
Faction["ALIADOS"] = "aliados";
Faction["ALEMANES"] = "alemanes";
})(Faction || (exports.Faction = Faction = {}));
var GamePhase;
(function (GamePhase) {
GamePhase["LOBBY"] = "lobby";
GamePhase["INTRO"] = "intro";
GamePhase["REVEAL_ROLE"] = "reveal_role";
GamePhase["ROLL_CALL"] = "roll_call";
GamePhase["VOTE_LEADER"] = "vote_leader";
GamePhase["TEAM_BUILDING"] = "team_building";
GamePhase["VOTING_TEAM"] = "voting_team";
GamePhase["MISSION"] = "mission";
GamePhase["MISSION_REVEAL"] = "mission_reveal";
GamePhase["MISSION_RESULT"] = "mission_result";
GamePhase["ASSASSIN_PHASE"] = "assassin_phase";
GamePhase["NAZIS_WIN"] = "nazis_win";
GamePhase["ALLIED_WIN"] = "allied_win";
GamePhase["GAME_OVER"] = "game_over";
})(GamePhase || (exports.GamePhase = GamePhase = {}));
// Configuración de jugadores por partida (según tus reglas)
exports.GAME_CONFIG = {
5: { good: 3, evil: 2, quests: [2, 3, 2, 3, 3] },
6: { good: 4, evil: 2, quests: [2, 3, 4, 3, 4] },
7: { good: 4, evil: 3, quests: [2, 3, 3, 4, 4] }, // Nota: 4ta misión requiere 2 fallos
8: { good: 5, evil: 3, quests: [3, 4, 4, 5, 5] },
9: { good: 6, evil: 3, quests: [3, 4, 4, 5, 5] },
10: { good: 6, evil: 4, quests: [3, 4, 4, 5, 5] },
};