// /server_modules/gameManager.js const { v4: uuidv4 } = require('uuid'); const GameInstance = require('./gameInstance'); const gameData = require('./data'); class GameManager { constructor(io) { this.io = io; this.games = {}; // { gameId: GameInstance } this.socketToGame = {}; // { socket.id: gameId } this.pendingPvPGames = []; // [gameId] this.userToPendingGame = {}; // { userId: gameId } или { socketId: gameId } } /** * Удаляет предыдущие ожидающие PvP игры, созданные этим же пользователем/сокетом. * @param {string} currentSocketId - ID текущего сокета игрока. * @param {number|string} [identifier] - userId игрока или socketId. * @param {string} [excludeGameId] - ID игры, которую НЕ нужно удалять. */ _removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) { const keyToUse = identifier || currentSocketId; const oldPendingGameId = this.userToPendingGame[keyToUse]; if (oldPendingGameId && oldPendingGameId !== excludeGameId) { const gameToRemove = this.games[oldPendingGameId]; if (gameToRemove && gameToRemove.mode === 'pvp' && gameToRemove.playerCount === 1 && this.pendingPvPGames.includes(oldPendingGameId)) { const playersInOldGame = Object.values(gameToRemove.players); // Убеждаемся, что единственный игрок в старой игре - это действительно тот, кто сейчас создает/присоединяется // Либо по ID сокета, либо по userId, если он был владельцем const isOwnerBySocket = playersInOldGame.length === 1 && playersInOldGame[0].socket.id === currentSocketId; const isOwnerByUserId = identifier && gameToRemove.ownerUserId === identifier; if (isOwnerBySocket || isOwnerByUserId) { console.log(`[GameManager] Пользователь ${keyToUse} (сокет: ${currentSocketId}) создал/присоединился к новой игре. Удаляем его предыдущую ожидающую игру: ${oldPendingGameId}`); delete this.games[oldPendingGameId]; const pendingIndex = this.pendingPvPGames.indexOf(oldPendingGameId); if (pendingIndex > -1) this.pendingPvPGames.splice(pendingIndex, 1); // Удаляем привязки для старого сокета, если он там был if (playersInOldGame.length === 1 && this.socketToGame[playersInOldGame[0].socket.id] === oldPendingGameId) { delete this.socketToGame[playersInOldGame[0].socket.id]; } delete this.userToPendingGame[keyToUse]; // Удаляем по ключу, который использовали для поиска this.broadcastAvailablePvPGames(); } } else if (oldPendingGameId === excludeGameId) { // Это та же игра, ничего не делаем } else { // Запись в userToPendingGame устарела или не соответствует условиям, чистим delete this.userToPendingGame[keyToUse]; } } } createGame(socket, mode = 'ai', chosenCharacterKey = 'elena', userId = null) { const identifier = userId || socket.id; this._removePreviousPendingGames(socket.id, identifier); // Удаляем старые перед созданием новой const gameId = uuidv4(); const game = new GameInstance(gameId, this.io, mode); if (userId) game.ownerUserId = userId; // Устанавливаем владельца игры this.games[gameId] = game; const charKeyForInstance = (mode === 'pvp') ? chosenCharacterKey : 'elena'; if (game.addPlayer(socket, charKeyForInstance)) { // addPlayer теперь сам установит userId в game.ownerUserId если это первый игрок this.socketToGame[socket.id] = gameId; // Устанавливаем привязку после успешного добавления console.log(`[GameManager] Игра создана: ${gameId} (режим: ${mode}) игроком ${socket.id} (userId: ${userId}, выбран: ${charKeyForInstance})`); const assignedPlayerId = game.players[socket.id]?.id; if (!assignedPlayerId) { delete this.games[gameId]; if(this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id]; socket.emit('gameError', { message: 'Ошибка сервера при создании игры (ID игрока).' }); return; } socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId }); if (mode === 'pvp') { if (!this.pendingPvPGames.includes(gameId)) this.pendingPvPGames.push(gameId); this.userToPendingGame[identifier] = gameId; this.broadcastAvailablePvPGames(); } } else { delete this.games[gameId]; // game.addPlayer вернул false, чистим // socketToGame не должен был быть установлен, если addPlayer вернул false if (this.socketToGame[socket.id] === gameId) delete this.socketToGame[socket.id]; socket.emit('gameError', { message: 'Не удалось создать игру или добавить игрока.' }); } } joinGame(socket, gameId, userId = null) { const identifier = userId || socket.id; const game = this.games[gameId]; if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; } if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'Эта игра не является PvP игрой.' }); return; } if (game.playerCount >= 2) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; } if (game.players[socket.id]) { socket.emit('gameError', { message: 'Вы уже в этой игре (попытка двойного присоединения).' }); return;} // Доп. проверка // Удаляем предыдущие ожидающие игры этого пользователя this._removePreviousPendingGames(socket.id, identifier, gameId); if (game.addPlayer(socket)) { this.socketToGame[socket.id] = gameId; // console.log(`[GameManager] Игрок ${socket.id} (userId: ${userId}) присоединился к PvP игре ${gameId}`); const gameIndex = this.pendingPvPGames.indexOf(gameId); if (gameIndex > -1) this.pendingPvPGames.splice(gameIndex, 1); // Очищаем запись о создателе из userToPendingGame, так как игра началась if (game.ownerUserId && this.userToPendingGame[game.ownerUserId] === gameId) { delete this.userToPendingGame[game.ownerUserId]; } else { // Если ownerUserId не был или не совпал, пробуем по socketId первого игрока const firstPlayerSocketId = Object.keys(game.players).find(sId => game.players[sId].id === GAME_CONFIG.PLAYER_ID && game.players[sId].socket.id !== socket.id); if (firstPlayerSocketId && this.userToPendingGame[firstPlayerSocketId] === gameId) { delete this.userToPendingGame[firstPlayerSocketId]; } } this.broadcastAvailablePvPGames(); } else { socket.emit('gameError', { message: 'Не удалось присоединиться к игре (внутренняя ошибка).' }); } } findAndJoinRandomPvPGame(socket, chosenCharacterKey = 'elena', userId = null) { const identifier = userId || socket.id; this._removePreviousPendingGames(socket.id, identifier); // Удаляем старые перед поиском/созданием let gameIdToJoin = null; const preferredOpponentKey = chosenCharacterKey === 'elena' ? 'almagest' : 'elena'; for (const id of this.pendingPvPGames) { const pendingGame = this.games[id]; if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') { const firstPlayerInfo = Object.values(pendingGame.players)[0]; const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id); if (isMyOwnGame) continue; if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey === preferredOpponentKey) { gameIdToJoin = id; break; } } } if (!gameIdToJoin && this.pendingPvPGames.length > 0) { for (const id of this.pendingPvPGames) { const pendingGame = this.games[id]; if (pendingGame && pendingGame.playerCount === 1 && pendingGame.mode === 'pvp') { const firstPlayerInfo = Object.values(pendingGame.players)[0]; const isMyOwnGame = (userId && pendingGame.ownerUserId === userId) || (firstPlayerInfo.socket.id === socket.id); if (isMyOwnGame) continue; gameIdToJoin = id; break; } } } if (gameIdToJoin) { this.joinGame(socket, gameIdToJoin, userId); } else { this.createGame(socket, 'pvp', chosenCharacterKey, userId); socket.emit('noPendingGamesFound', { message: 'Свободных PvP игр не найдено. Создана новая игра для вас. Ожидайте противника.', }); } } handlePlayerAction(socketId, actionData) { const gameIdFromSocket = this.socketToGame[socketId]; const game = this.games[gameIdFromSocket]; if (game) { game.processPlayerAction(socketId, actionData); } else { const playerSocket = this.io.sockets.sockets.get(socketId); if (playerSocket) playerSocket.emit('gameError', { message: 'Ошибка: игровая сессия потеряна.' }); } } requestRestart(socketId, gameId) { const game = this.games[gameId]; if (game && game.players[socketId]) { game.handleVoteRestart(socketId); } else { const playerSocket = this.io.sockets.sockets.get(socketId); if (playerSocket) playerSocket.emit('gameError', { message: 'Не удалось перезапустить: сессия не найдена.' }); } } handleDisconnect(socketId, userId = null) { const identifier = userId || socketId; const gameId = this.socketToGame[socketId]; if (gameId && this.games[gameId]) { const game = this.games[gameId]; console.log(`[GameManager] Игрок ${socketId} (userId: ${userId}) отключился от игры ${gameId}.`); game.removePlayer(socketId); if (game.playerCount === 0) { console.log(`[GameManager] Игра ${gameId} пуста и будет удалена (после дисконнекта).`); delete this.games[gameId]; const gameIndexPending = this.pendingPvPGames.indexOf(gameId); if (gameIndexPending > -1) this.pendingPvPGames.splice(gameIndexPending, 1); // Удаляем из userToPendingGame, если игра была там по любому ключу for (const key in this.userToPendingGame) { if (this.userToPendingGame[key] === gameId) delete this.userToPendingGame[key]; } this.broadcastAvailablePvPGames(); } else if (game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) { if (!this.pendingPvPGames.includes(gameId)) { this.pendingPvPGames.push(gameId); } // Обновляем ownerUserId и userToPendingGame для оставшегося игрока const remainingPlayerSocketId = Object.keys(game.players)[0]; const remainingPlayerSocket = game.players[remainingPlayerSocketId]?.socket; const remainingUserId = remainingPlayerSocket?.userData?.userId; const newIdentifier = remainingUserId || remainingPlayerSocketId; game.ownerUserId = remainingUserId; // Устанавливаем нового владельца (может быть null) this.userToPendingGame[newIdentifier] = gameId; // Связываем нового владельца // Удаляем старую привязку отключившегося, если она была и не совпадает с новой if (identifier !== newIdentifier && this.userToPendingGame[identifier] === gameId) { delete this.userToPendingGame[identifier]; } console.log(`[GameManager] Игра ${gameId} возвращена в список ожидания PvP. Новый владелец: ${newIdentifier}`); this.broadcastAvailablePvPGames(); } } else { // Если игрок не был в активной игре, но мог иметь ожидающую this._removePreviousPendingGames(socketId, identifier); } delete this.socketToGame[socketId]; // Всегда удаляем эту связь } getAvailablePvPGamesListForClient() { return this.pendingPvPGames .map(gameId => { const game = this.games[gameId]; if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) { let firstPlayerName = 'Игрок'; if (game.players && Object.keys(game.players).length > 0) { const firstPlayerSocketId = Object.keys(game.players)[0]; const firstPlayerInfo = game.players[firstPlayerSocketId]; if (firstPlayerInfo && firstPlayerInfo.chosenCharacterKey) { const charData = gameData[firstPlayerInfo.chosenCharacterKey + 'BaseStats']; if (charData) firstPlayerName = charData.name; } } return { id: gameId, status: `Ожидает 1 игрока (Создал: ${firstPlayerName})` }; } return null; }) .filter(info => info !== null); } broadcastAvailablePvPGames() { this.io.emit('availablePvPGamesList', this.getAvailablePvPGamesListForClient()); } getActiveGamesList() { return Object.values(this.games).map(game => { let playerSlotChar = game.gameState?.player?.name || (game.playerCharacterKey ? gameData[game.playerCharacterKey + 'BaseStats']?.name : 'N/A'); let opponentSlotChar = game.gameState?.opponent?.name || (game.opponentCharacterKey ? gameData[game.opponentCharacterKey + 'BaseStats']?.name : 'N/A'); if (game.mode === 'pvp' && game.playerCount === 1 && !game.opponentCharacterKey) opponentSlotChar = 'Ожидание...'; return { id: game.id, mode: game.mode, playerCount: game.playerCount, isGameOver: game.gameState ? game.gameState.isGameOver : 'N/A', playerSlot: playerSlotChar, opponentSlot: opponentSlotChar, ownerUserId: game.ownerUserId || 'N/A' }; }); } } module.exports = GameManager;