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
Some checks failed
CI/CD - Francia Ocupada (La Resistencia) / build-and-deploy (push) Failing after 8s
This commit is contained in:
83
server/dist/server/src/db.js
vendored
Normal file
83
server/dist/server/src/db.js
vendored
Normal 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
437
server/dist/server/src/index.js
vendored
Normal 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
338
server/dist/server/src/models/Game.js
vendored
Normal 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
47
server/dist/shared/types.js
vendored
Normal 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] },
|
||||
};
|
||||
Reference in New Issue
Block a user