Actualizar sistema de admin y mejoras en lobby
This commit is contained in:
@@ -82,6 +82,13 @@
|
||||
<div class="control-group">
|
||||
<h3>Player Management</h3>
|
||||
<div class="control-buttons">
|
||||
<button
|
||||
@click="shufflePlayers"
|
||||
class="btn btn-shuffle"
|
||||
:disabled="isLoadingGlobal"
|
||||
>
|
||||
🎲 Shuffle Players
|
||||
</button>
|
||||
<button
|
||||
@click="sendAllToLobby"
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
|
||||
.btn-shuffle {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-lobby-all {
|
||||
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
|
||||
color: white;
|
||||
|
||||
@@ -211,6 +211,23 @@ onMounted(() => {
|
||||
// Handle room closure/disconnection
|
||||
room.onLeave((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
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Request, Response, Router } from "express";
|
||||
import { matchMaker } from "colyseus";
|
||||
import { GameRoom } from "./rooms/GameRoom";
|
||||
import { NameManager } from "./utils/nameManager";
|
||||
|
||||
// SSE connections storage
|
||||
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
|
||||
adminRouter.get("/dashboard-stream", (req: Request, res: Response) => {
|
||||
// Set SSE headers
|
||||
|
||||
@@ -8,6 +8,11 @@ export class GameRoom extends Room<GameState> {
|
||||
maxClients = 2;
|
||||
private gameInterval?: NodeJS.Timeout;
|
||||
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) {
|
||||
const timestamp = Date.now();
|
||||
@@ -228,12 +233,23 @@ export class GameRoom extends Room<GameState> {
|
||||
|
||||
onJoin(client: Client, options: any) {
|
||||
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
|
||||
if (this.state.gameStatus !== GameStatus.WAITING || this.state.players.size >= 2) {
|
||||
try { client.leave(1000); } catch {}
|
||||
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!
|
||||
const playerName = options.playerName || "player";
|
||||
const playerColor = (options.playerColor && typeof options.playerColor === 'string') ? options.playerColor : "#667eea";
|
||||
@@ -568,11 +584,124 @@ export class GameRoom extends Room<GameState> {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'clearForShuffle':
|
||||
this.handleClearForShuffle();
|
||||
break;
|
||||
|
||||
case 'assignShuffledPlayers':
|
||||
const playerAssignments = args[0];
|
||||
this.handleAssignShuffledPlayers(playerAssignments);
|
||||
break;
|
||||
|
||||
default:
|
||||
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 {
|
||||
let count = 0;
|
||||
this.state.players.forEach(player => {
|
||||
@@ -586,6 +715,7 @@ export class GameRoom extends Room<GameState> {
|
||||
roomId: this.roomId,
|
||||
players: Array.from(this.state.players.values()).map(p => ({
|
||||
sessionId: p.sessionId,
|
||||
uuid: this.sessionToUuid.get(p.sessionId) || p.sessionId, // Include UUID for shuffle
|
||||
name: p.name,
|
||||
role: p.role,
|
||||
pavoTokens: p.pavoTokens,
|
||||
|
||||
@@ -38,6 +38,33 @@ export class LobbyRoom extends Room<LobbyState> {
|
||||
if (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
|
||||
const existingName = NameManager.getInstance().getPlayerName(options.uuid);
|
||||
this.state.addPlayer(client.sessionId, existingName || "");
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export class NameManager {
|
||||
private static instance: NameManager;
|
||||
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() {}
|
||||
|
||||
@@ -55,4 +59,35 @@ export class NameManager {
|
||||
getAllActivePlayers(): string[] {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user