Estado actual con errores de sintaxis en GameBoard.tsx
This commit is contained in:
24
server/Dockerfile
Normal file
24
server/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy shared module first (as a sibling to server)
|
||||
COPY shared ./shared
|
||||
|
||||
# Setup Server directory
|
||||
WORKDIR /app/server
|
||||
|
||||
# Install dependencies
|
||||
COPY server/package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Copy server source code
|
||||
COPY server/src ./src
|
||||
COPY server/tsconfig.json ./
|
||||
|
||||
# Expose port
|
||||
EXPOSE 4000
|
||||
|
||||
# Run with nodemon watching both src and shared
|
||||
CMD ["npx", "nodemon", "--watch", "src", "--watch", "../shared", "--exec", "npx ts-node", "src/index.ts"]
|
||||
28
server/package.json
Normal file
28
server/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "resistencia-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend para el juego La Resistencia",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"dev": "npx nodemon --watch src --exec \"npx ts-node\" src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"socket.io": "^4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.10.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
254
server/src/index.ts
Normal file
254
server/src/index.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import crypto from 'crypto';
|
||||
import { Game } from './models/Game';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 4000;
|
||||
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
|
||||
methods: ["GET", "POST"]
|
||||
}));
|
||||
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
// ALMACÉN DE PARTIDAS (En memoria por ahora)
|
||||
// En el futuro esto podría estar en Redis o Postgres
|
||||
const games: Record<string, Game> = {};
|
||||
|
||||
// --- 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"
|
||||
];
|
||||
|
||||
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}`;
|
||||
};
|
||||
|
||||
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.randomUUID();
|
||||
const roomName = generateRoomName();
|
||||
|
||||
const newGame = new 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());
|
||||
});
|
||||
|
||||
// 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());
|
||||
});
|
||||
|
||||
// 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' as any;
|
||||
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' as any;
|
||||
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' as any;
|
||||
io.to(roomId).emit('game_state', game.state);
|
||||
}
|
||||
});
|
||||
|
||||
// 2.3 FINALIZAR ROLL CALL -> PRIMER TURNO DE JUEGO (TEAM_BUILDING)
|
||||
socket.on('finish_roll_call', ({ roomId }) => {
|
||||
const game = games[roomId];
|
||||
if (game && game.hostId === socket.id && game.state.phase === 'roll_call') {
|
||||
// Ir a VOTE_LEADER (ya que startGame lo inicializa a VOTE_LEADER en el modelo, y nextLeader tambien)
|
||||
// Solo debemos asegurarnos que el GameState se sincronice.
|
||||
if (game.startGame()) {
|
||||
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) {
|
||||
game.voteLeader(socket.id, approve);
|
||||
io.to(roomId).emit('game_state', game.state);
|
||||
}
|
||||
});
|
||||
|
||||
// 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];
|
||||
if (game && game.hostId === socket.id && 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 === 'mission_result') {
|
||||
game.finishMissionResult();
|
||||
io.to(roomId).emit('game_state', game.state);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 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. DESCONEXIÓN
|
||||
socket.on('disconnect', () => {
|
||||
// Buscar en qué partida estaba y sacarlo (opcional, por ahora solo notificamos)
|
||||
console.log('Desconectado:', socket.id);
|
||||
// TODO: Eliminar de la partida si está en LOBBY para liberar hueco
|
||||
});
|
||||
});
|
||||
|
||||
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}`);
|
||||
});
|
||||
311
server/src/models/Game.ts
Normal file
311
server/src/models/Game.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
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,
|
||||
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 persistente (rebel001.jpg - rebel010.jpg)
|
||||
const avatarIdx = Math.floor(Math.random() * 10) + 1;
|
||||
const avatarStr = `rebel${avatarIdx.toString().padStart(3, '0')}.jpg`;
|
||||
|
||||
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.MERLIN, Role.ASSASSIN];
|
||||
|
||||
// Rellenar resto de malos
|
||||
for (let i = 0; i < evilCount - 1; i++) roles.push(Role.MINION);
|
||||
|
||||
// Rellenar resto de buenos
|
||||
for (let i = 0; i < goodCount - 1; i++) roles.push(Role.LOYAL_SERVANT);
|
||||
|
||||
// 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.MERLIN, Role.PERCIVAL, Role.LOYAL_SERVANT].includes(player.role)) {
|
||||
player.faction = Faction.RESISTANCE;
|
||||
} else {
|
||||
player.faction = Faction.SPIES;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- LOGICA DE VOTACIÓN DE LÍDER ---
|
||||
voteLeader(playerId: string, approve: boolean | null) {
|
||||
this.state.leaderVotes[playerId] = approve;
|
||||
|
||||
// Comprobar si todos han votado
|
||||
if (Object.keys(this.state.leaderVotes).length === this.state.players.length) {
|
||||
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).length;
|
||||
// Los nulos (timeout) no suman a ninguno, o cuentan como reject implícito?
|
||||
// "Si llega a 0... su voto no cuenta". Simplemente no suma.
|
||||
|
||||
this.log(`Votación de Líder: ${approves} A favor - ${rejects} En contra.`);
|
||||
|
||||
if (approves > rejects) {
|
||||
// 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.');
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
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.VOTING_TEAM;
|
||||
this.state.teamVotes = {}; // Resetear votos
|
||||
this.log(`El líder ha propuesto un equipo de ${playerIds.length} personas.`);
|
||||
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.SPIES, 'Se han rechazado 5 equipos consecutivos.');
|
||||
} 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...`);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
this.endGame(Faction.SPIES, 'Tres misiones han fracasado.');
|
||||
} else if (successes >= 3) {
|
||||
this.state.phase = GamePhase.ASSASSIN_PHASE;
|
||||
this.log('¡La Resistencia ha triunfado! Pero el Asesino 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.MERLIN) {
|
||||
this.endGame(Faction.SPIES, '¡El Asesino ha eliminado a Marlenne (Merlín)!');
|
||||
} else {
|
||||
this.endGame(Faction.RESISTANCE, 'El Asesino ha fallado. ¡La Resistencia gana!');
|
||||
}
|
||||
}
|
||||
|
||||
private 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}`);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
15
server/tsconfig.json
Normal file
15
server/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../shared/**/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user