Actualizar sistema de admin y mejoras en lobby

This commit is contained in:
2025-08-15 12:04:12 -06:00
parent e14bdddc62
commit 232c159baf
6 changed files with 385 additions and 0 deletions

View File

@@ -82,6 +82,13 @@
<div class="control-group"> <div class="control-group">
<h3>Player Management</h3> <h3>Player Management</h3>
<div class="control-buttons"> <div class="control-buttons">
<button
@click="shufflePlayers"
class="btn btn-shuffle"
:disabled="isLoadingGlobal"
>
🎲 Shuffle Players
</button>
<button <button
@click="sendAllToLobby" @click="sendAllToLobby"
class="btn btn-lobby-all" class="btn btn-lobby-all"
@@ -386,6 +393,30 @@ async function changeGlobalVariant() {
} }
} }
async function shufflePlayers() {
if (!confirm('Are you sure you want to SHUFFLE all players? This will randomly redistribute players between rooms and assign new roles!')) return;
isLoadingGlobal.value = true;
try {
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api'}/admin/shuffle-players`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) throw new Error('Failed to shuffle players');
const result = await response.json();
console.log('Players shuffled successfully:', result.message);
alert(`Shuffle completed! ${result.message}`);
await fetchData();
} catch (error) {
console.error('Failed to shuffle players:', error);
alert('Failed to shuffle players. Check console for details.');
} finally {
isLoadingGlobal.value = false;
}
}
async function sendAllToLobby() { async function sendAllToLobby() {
if (!confirm('Are you sure you want to send ALL players back to the lobby? This will end all active games!')) return; if (!confirm('Are you sure you want to send ALL players back to the lobby? This will end all active games!')) return;
@@ -683,6 +714,11 @@ const selectedRoom = computed(() => {
color: white; color: white;
} }
.btn-shuffle {
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
color: white;
}
.btn-lobby-all { .btn-lobby-all {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%); background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
color: white; color: white;

View File

@@ -211,6 +211,23 @@ onMounted(() => {
// Handle room closure/disconnection // Handle room closure/disconnection
room.onLeave((code) => { room.onLeave((code) => {
console.log('[DemoGame] Room disconnected with code:', code); console.log('[DemoGame] Room disconnected with code:', code);
// Handle shuffle disconnection specially
if (code === 1002) {
console.log('[DemoGame] Disconnected for player shuffle - will redirect to lobby');
try {
if (typeof window !== 'undefined') {
window.localStorage.removeItem('snatch.game.roomId');
window.localStorage.removeItem('snatch.game.sessionId');
}
} catch {}
// Redirect to lobby and let it handle the shuffle redirect
router.push('/');
return;
}
// Normal disconnection handling
// Always clean up local storage when room closes // Always clean up local storage when room closes
try { try {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {

View File

@@ -1,6 +1,7 @@
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { matchMaker } from "colyseus"; import { matchMaker } from "colyseus";
import { GameRoom } from "./rooms/GameRoom"; import { GameRoom } from "./rooms/GameRoom";
import { NameManager } from "./utils/nameManager";
// SSE connections storage // SSE connections storage
const sseClients = new Set<Response>(); const sseClients = new Set<Response>();
@@ -292,6 +293,145 @@ adminRouter.post("/admin/send-all-to-lobby", async (req: Request, res: Response)
} }
}); });
adminRouter.post("/admin/shuffle-players", async (req: Request, res: Response) => {
try {
console.log("[AdminAPI] Starting player shuffle...");
// 1. Get all game rooms and their players
const gameRooms = await matchMaker.query({ name: "game" });
if (gameRooms.length === 0) {
return res.json({ success: true, message: "No game rooms to shuffle" });
}
console.log(`[AdminAPI] Found ${gameRooms.length} game rooms for shuffle`);
// 2. Extract all players from all rooms
const allPlayers: any[] = [];
const roomsInfo: { roomId: string; variant: string }[] = [];
for (const room of gameRooms) {
try {
const roomState = await matchMaker.remoteRoomCall(room.roomId, "getState");
roomsInfo.push({
roomId: room.roomId,
variant: roomState.variant || 'G1'
});
// Collect players with their full info
if (roomState.players && roomState.players.length > 0) {
roomState.players.forEach((player: any) => {
allPlayers.push({
uuid: player.uuid || player.sessionId, // Use actual UUID if available
name: player.name,
color: player.color,
sessionId: player.sessionId
});
});
}
} catch (error) {
console.error(`Failed to get state for room ${room.roomId}:`, error);
}
}
console.log(`[AdminAPI] Collected ${allPlayers.length} players for shuffle`);
if (allPlayers.length === 0) {
return res.json({ success: true, message: "No players found to shuffle" });
}
// 3. Shuffle players array
const shuffledPlayers = [...allPlayers];
for (let i = shuffledPlayers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledPlayers[i], shuffledPlayers[j]] = [shuffledPlayers[j], shuffledPlayers[i]];
}
// 4. Create groups of 2 players
const playerGroups: any[][] = [];
const extraPlayers: any[] = [];
for (let i = 0; i < shuffledPlayers.length; i += 2) {
if (i + 1 < shuffledPlayers.length) {
playerGroups.push([shuffledPlayers[i], shuffledPlayers[i + 1]]);
} else {
extraPlayers.push(shuffledPlayers[i]);
}
}
console.log(`[AdminAPI] Created ${playerGroups.length} player groups, ${extraPlayers.length} extra players`);
// 5. Start shuffle process
NameManager.getInstance().startShuffle();
// 6. Assign players to rooms and roles randomly
const assignments: { [roomId: string]: { p1: any; p2: any } } = {};
for (let i = 0; i < playerGroups.length && i < roomsInfo.length; i++) {
const group = playerGroups[i];
const roomInfo = roomsInfo[i];
// Randomly assign P1 and P2 roles
const [player1, player2] = Math.random() < 0.5 ? [group[0], group[1]] : [group[1], group[0]];
assignments[roomInfo.roomId] = {
p1: { ...player1, role: 'P1' },
p2: { ...player2, role: 'P2' }
};
// Store assignments in NameManager for lobby redirects
NameManager.getInstance().assignPlayerToRoom(player1.uuid, roomInfo.roomId, 'P1');
NameManager.getInstance().assignPlayerToRoom(player2.uuid, roomInfo.roomId, 'P2');
}
// 7. Clear all rooms first
console.log("[AdminAPI] Clearing all game rooms for shuffle...");
const clearPromises = gameRooms.map(room =>
matchMaker.remoteRoomCall(room.roomId, "executeAdminCommand", ["clearForShuffle"])
.catch(error => console.error(`Failed to clear room ${room.roomId}:`, error))
);
await Promise.allSettled(clearPromises);
// 8. Wait a bit for rooms to clear, then assign new players
setTimeout(async () => {
console.log("[AdminAPI] Assigning shuffled players to rooms...");
const assignPromises = Object.entries(assignments).map(([roomId, assignment]) =>
matchMaker.remoteRoomCall(roomId, "executeAdminCommand", ["assignShuffledPlayers", assignment])
.catch(error => console.error(`Failed to assign players to room ${roomId}:`, error))
);
await Promise.allSettled(assignPromises);
// Handle extra players - they go to lobby
extraPlayers.forEach(player => {
console.log(`[AdminAPI] Player ${player.name} will remain in lobby (odd number of players)`);
});
console.log("[AdminAPI] Player shuffle completed!");
// Cleanup after a delay
setTimeout(() => {
NameManager.getInstance().endShuffle();
broadcastDashboardUpdate();
}, 10000); // 10 seconds for all players to reconnect
}, 3000); // 3 seconds delay
res.json({
success: true,
message: `Shuffle initiated for ${allPlayers.length} players across ${gameRooms.length} rooms. ${playerGroups.length} pairs created, ${extraPlayers.length} players will go to lobby.`
});
} catch (error) {
console.error("[AdminAPI] Error shuffling players:", error);
NameManager.getInstance().endShuffle(); // Cleanup on error
res.status(500).json({ error: "Failed to shuffle players" });
}
});
// SSE endpoint for real-time dashboard updates // SSE endpoint for real-time dashboard updates
adminRouter.get("/dashboard-stream", (req: Request, res: Response) => { adminRouter.get("/dashboard-stream", (req: Request, res: Response) => {
// Set SSE headers // Set SSE headers

View File

@@ -8,6 +8,11 @@ export class GameRoom extends Room<GameState> {
maxClients = 2; maxClients = 2;
private gameInterval?: NodeJS.Timeout; private gameInterval?: NodeJS.Timeout;
private recentSystemMessage: { text: string; kind: string; timestamp: number } | null = null; private recentSystemMessage: { text: string; kind: string; timestamp: number } | null = null;
// For shuffle functionality
private expectedShuffledPlayers: { p1?: any; p2?: any } = {};
private isWaitingForShuffledPlayers: boolean = false;
private sessionToUuid: Map<string, string> = new Map();
private sysChat(text: string, kind: string) { private sysChat(text: string, kind: string) {
const timestamp = Date.now(); const timestamp = Date.now();
@@ -228,12 +233,23 @@ export class GameRoom extends Room<GameState> {
onJoin(client: Client, options: any) { onJoin(client: Client, options: any) {
console.log(`[GameRoom] ${client.sessionId} joined room ${this.roomId} with name: ${options.playerName}`); console.log(`[GameRoom] ${client.sessionId} joined room ${this.roomId} with name: ${options.playerName}`);
// Special handling for shuffled players
if (this.isWaitingForShuffledPlayers && options.uuid) {
return this.handleShuffledPlayerJoin(client, options);
}
// Prevent new joins if game already started or two players are registered // Prevent new joins if game already started or two players are registered
if (this.state.gameStatus !== GameStatus.WAITING || this.state.players.size >= 2) { if (this.state.gameStatus !== GameStatus.WAITING || this.state.players.size >= 2) {
try { client.leave(1000); } catch {} try { client.leave(1000); } catch {}
return; return;
} }
// Store UUID mapping if provided
if (options.uuid) {
this.sessionToUuid.set(client.sessionId, options.uuid);
}
// Use the playerName passed from the lobby - don't generate a new one! // Use the playerName passed from the lobby - don't generate a new one!
const playerName = options.playerName || "player"; const playerName = options.playerName || "player";
const playerColor = (options.playerColor && typeof options.playerColor === 'string') ? options.playerColor : "#667eea"; const playerColor = (options.playerColor && typeof options.playerColor === 'string') ? options.playerColor : "#667eea";
@@ -568,11 +584,124 @@ export class GameRoom extends Room<GameState> {
} }
break; break;
case 'clearForShuffle':
this.handleClearForShuffle();
break;
case 'assignShuffledPlayers':
const playerAssignments = args[0];
this.handleAssignShuffledPlayers(playerAssignments);
break;
default: default:
console.warn(`[GameRoom] Unknown admin command: ${command}`); console.warn(`[GameRoom] Unknown admin command: ${command}`);
} }
} }
private handleClearForShuffle() {
console.log(`[GameRoom] Clearing room ${this.roomId} for shuffle`);
this.sysChat('🎲 Admin está reorganizando jugadores...', 'admin_shuffle');
// Give players a moment to see the message
setTimeout(() => {
// Disconnect all clients with special code for shuffle
this.clients.forEach(client => {
try {
client.leave(1002); // Special code for shuffle
} catch (error) {
console.error(`Failed to disconnect client ${client.sessionId}:`, error);
}
});
// Reset room state but keep variant
const currentVariant = this.state.currentVariant;
this.state.restartGame();
this.state.currentVariant = currentVariant;
// Prepare for new players
this.isWaitingForShuffledPlayers = true;
this.expectedShuffledPlayers = {};
broadcastDashboardUpdate();
}, 1000);
}
private handleAssignShuffledPlayers(assignments: { p1?: any; p2?: any }) {
console.log(`[GameRoom] Assigning shuffled players to room ${this.roomId}:`, assignments);
this.expectedShuffledPlayers = assignments;
this.isWaitingForShuffledPlayers = true;
// Update metadata
this.setMetadata({
gameStatus: 'waiting',
currentRound: 1,
currentVariant: this.state.currentVariant
});
}
private handleShuffledPlayerJoin(client: Client, options: any) {
const uuid = options.uuid;
console.log(`[GameRoom] Shuffled player ${uuid} trying to join room ${this.roomId}`);
// Check if this player is expected in this room
let expectedRole: 'P1' | 'P2' | null = null;
if (this.expectedShuffledPlayers.p1?.uuid === uuid) {
expectedRole = 'P1';
} else if (this.expectedShuffledPlayers.p2?.uuid === uuid) {
expectedRole = 'P2';
}
if (!expectedRole) {
console.log(`[GameRoom] Player ${uuid} not expected in room ${this.roomId}, rejecting`);
try { client.leave(1000); } catch {}
return;
}
// Get player info from expected data
const expectedPlayerData = expectedRole === 'P1' ? this.expectedShuffledPlayers.p1 : this.expectedShuffledPlayers.p2;
console.log(`[GameRoom] Adding shuffled player ${uuid} as ${expectedRole} in room ${this.roomId}`);
// Add player with the expected role
const player = this.state.addPlayer(client.sessionId, expectedPlayerData.name);
player.role = expectedRole;
player.color = expectedPlayerData.color || "#667eea";
// Set role IDs
if (expectedRole === 'P1') {
this.state.p1Id = client.sessionId;
} else {
this.state.p2Id = client.sessionId;
}
client.send("playerInfo", {
sessionId: client.sessionId,
name: expectedPlayerData.name,
roomId: this.roomId
});
// Remove from NameManager assignment once joined
NameManager.getInstance().removePlayerRoomAssignment(uuid);
// Check if room is complete
if (this.state.players.size === 2) {
this.isWaitingForShuffledPlayers = false;
this.expectedShuffledPlayers = {};
this.sysChat(`🎲 Reorganización completa - ¡comenzando partida!`, 'shuffle_complete');
setTimeout(() => {
this.startGame();
}, 1000);
} else {
this.sysChat(`🎲 Esperando más jugadores reorganizados...`, 'shuffle_waiting');
}
broadcastDashboardUpdate();
}
private getConnectedPlayersCount(): number { private getConnectedPlayersCount(): number {
let count = 0; let count = 0;
this.state.players.forEach(player => { this.state.players.forEach(player => {
@@ -586,6 +715,7 @@ export class GameRoom extends Room<GameState> {
roomId: this.roomId, roomId: this.roomId,
players: Array.from(this.state.players.values()).map(p => ({ players: Array.from(this.state.players.values()).map(p => ({
sessionId: p.sessionId, sessionId: p.sessionId,
uuid: this.sessionToUuid.get(p.sessionId) || p.sessionId, // Include UUID for shuffle
name: p.name, name: p.name,
role: p.role, role: p.role,
pavoTokens: p.pavoTokens, pavoTokens: p.pavoTokens,

View File

@@ -38,6 +38,33 @@ export class LobbyRoom extends Room<LobbyState> {
if (options.uuid) { if (options.uuid) {
this.sessionToUuid.set(client.sessionId, options.uuid); this.sessionToUuid.set(client.sessionId, options.uuid);
// Check for shuffle redirect FIRST
if (NameManager.getInstance().isShuffleInProgress()) {
const assignment = NameManager.getInstance().getPlayerRoomAssignment(options.uuid);
if (assignment) {
console.log(`[LobbyRoom] Redirecting shuffled player ${options.uuid} to room ${assignment.roomId} as ${assignment.role}`);
// Add player temporarily to lobby state
const existingName = NameManager.getInstance().getPlayerName(options.uuid);
this.state.addPlayer(client.sessionId, existingName || "");
// Send welcome first
client.send("welcome", {
sessionId: client.sessionId,
name: existingName || "",
color: this.state.players.get(client.sessionId)?.color || "#667eea"
});
// Then immediately redirect to assigned room
setTimeout(() => {
this.handleJoinRoom(client, assignment.roomId);
}, 500);
return;
}
}
// Normal lobby join flow
// Check if this UUID already has a name // Check if this UUID already has a name
const existingName = NameManager.getInstance().getPlayerName(options.uuid); const existingName = NameManager.getInstance().getPlayerName(options.uuid);
this.state.addPlayer(client.sessionId, existingName || ""); this.state.addPlayer(client.sessionId, existingName || "");

View File

@@ -1,6 +1,10 @@
export class NameManager { export class NameManager {
private static instance: NameManager; private static instance: NameManager;
private uuidToName: Map<string, string> = new Map(); private uuidToName: Map<string, string> = new Map();
// For shuffle functionality
private roomAssignments: Map<string, { roomId: string; role: 'P1' | 'P2' }> = new Map();
private shuffleInProgress: boolean = false;
private constructor() {} private constructor() {}
@@ -55,4 +59,35 @@ export class NameManager {
getAllActivePlayers(): string[] { getAllActivePlayers(): string[] {
return Array.from(this.uuidToName.values()); return Array.from(this.uuidToName.values());
} }
// Shuffle functionality methods
startShuffle(): void {
this.shuffleInProgress = true;
this.roomAssignments.clear();
}
endShuffle(): void {
this.shuffleInProgress = false;
this.roomAssignments.clear();
}
isShuffleInProgress(): boolean {
return this.shuffleInProgress;
}
assignPlayerToRoom(uuid: string, roomId: string, role: 'P1' | 'P2'): void {
this.roomAssignments.set(uuid, { roomId, role });
}
getPlayerRoomAssignment(uuid: string): { roomId: string; role: 'P1' | 'P2' } | undefined {
return this.roomAssignments.get(uuid);
}
removePlayerRoomAssignment(uuid: string): void {
this.roomAssignments.delete(uuid);
}
getAllRoomAssignments(): Map<string, { roomId: string; role: 'P1' | 'P2' }> {
return new Map(this.roomAssignments);
}
} }