// /server/game/GameManager.js const { v4: uuidv4 } = require('uuid'); const GameInstance = require('./instance/GameInstance'); const dataUtils = require('../data/dataUtils'); const GAME_CONFIG = require('../core/config'); class GameManager { constructor(io) { this.io = io; this.games = {}; // { gameId: GameInstance } this.userIdentifierToGameId = {}; // { userId: gameId } this.pendingPvPGames = []; // Массив gameId ожидающих PvP игр console.log("[GameManager] Initialized."); } _cleanupPreviousPendingGameForUser(identifier, reasonSuffix = 'unknown_cleanup_reason') { const oldPendingGameId = this.userIdentifierToGameId[identifier]; if (oldPendingGameId && this.games[oldPendingGameId]) { const gameToRemove = this.games[oldPendingGameId]; // Убеждаемся, что это именно ожидающая PvP игра этого пользователя if (gameToRemove.mode === 'pvp' && gameToRemove.ownerIdentifier === identifier && // Он владелец gameToRemove.playerCount === 1 && // В игре только он this.pendingPvPGames.includes(oldPendingGameId) && // Игра в списке ожидающих (!gameToRemove.gameState || !gameToRemove.gameState.isGameOver) // И она не завершена ) { console.log(`[GameManager._cleanupPreviousPendingGameForUser] User ${identifier} has an existing pending PvP game ${oldPendingGameId}. Removing it. Reason: ${reasonSuffix}`); this._cleanupGame(oldPendingGameId, `owner_action_removed_pending_pvp_game_${reasonSuffix}`); // _cleanupGame должен удалить запись из userIdentifierToGameId return true; // Успешно очистили } } return false; // Нечего было очищать или условия не совпали } createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) { console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey || 'Default'}`); const existingGameIdForUser = this.userIdentifierToGameId[identifier]; // 1. Проверить, не находится ли пользователь уже в какой-либо АКТИВНОЙ игре. if (existingGameIdForUser && this.games[existingGameIdForUser]) { const existingGame = this.games[existingGameIdForUser]; if (existingGame.gameState && existingGame.gameState.isGameOver) { console.warn(`[GameManager.createGame] User ${identifier} was in a finished game ${existingGameIdForUser}. Cleaning it up before creating new.`); this._cleanupGame(existingGameIdForUser, `stale_finished_on_create_${identifier}`); // После _cleanupGame, existingGameIdForUser в userIdentifierToGameId[identifier] должен быть удален } else { // Пользователь в активной игре. // Если это ЕГО ОЖИДАЮЩАЯ PvP игра, и он пытается создать НОВУЮ (любую), то ее нужно будет удалить ниже. // Если это ДРУГАЯ активная игра (не его ожидающая PvP), то отказать. const isHisOwnPendingPvp = existingGame.mode === 'pvp' && existingGame.ownerIdentifier === identifier && existingGame.playerCount === 1 && this.pendingPvPGames.includes(existingGameIdForUser); if (!isHisOwnPendingPvp) { // Он в другой активной игре (AI, или PvP с оппонентом, или PvP другого игрока) console.warn(`[GameManager.createGame] User ${identifier} is already in an active game ${existingGameIdForUser} (mode: ${existingGame.mode}, owner: ${existingGame.ownerIdentifier}). Cannot create new.`); socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' }); this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру return; } // Если это его ожидающая PvP, то _cleanupPreviousPendingGameForUser ниже ее удалит. } } // 2. Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он создает новую любую игру. // Это важно сделать ДО создания новой игры, чтобы освободить userIdentifierToGameId. const cleanedUp = this._cleanupPreviousPendingGameForUser(identifier, `creating_new_game_mode_${mode}`); if (cleanedUp) { console.log(`[GameManager.createGame] Successfully cleaned up previous pending PvP game for ${identifier}.`); } else { console.log(`[GameManager.createGame] No previous pending PvP game found or needed cleanup for ${identifier}.`); } console.log(`[GameManager.createGame] After potential cleanup, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`); // 3. Окончательная проверка: если ПОСЛЕ очистки пользователь все еще привязан к какой-то активной игре // (Это может случиться, если _cleanupPreviousPendingGameForUser не нашла ожидающую, но он был в другой игре, что было бы ошибкой логики выше) const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier]; if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) { console.error(`[GameManager.createGame] CRITICAL LOGIC ERROR: User ${identifier} still mapped to active game ${stillExistingGameIdAfterCleanup} after cleanup attempt. Denying creation.`); socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию.' }); this.handleRequestGameState(socket, identifier); return; } const gameId = uuidv4(); console.log(`[GameManager.createGame] New GameID: ${gameId}`); const game = new GameInstance(gameId, this.io, mode, this); this.games[gameId] = game; const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena'); if (game.addPlayer(socket, charKeyForPlayer, identifier)) { this.userIdentifierToGameId[identifier] = gameId; // Связываем пользователя с НОВОЙ игрой const playerInfo = Object.values(game.players).find(p => p.identifier === identifier); const assignedPlayerId = playerInfo?.id; const actualCharacterKey = playerInfo?.chosenCharacterKey; if (!assignedPlayerId || !actualCharacterKey) { console.error(`[GameManager.createGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} in game ${gameId}. Cleaning up.`); this._cleanupGame(gameId, 'player_info_missing_after_add_on_create'); socket.emit('gameError', { message: 'Ошибка сервера при создании роли в игре.' }); return; } console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId} as ${assignedPlayerId}. User map updated. Current map for ${identifier}: ${this.userIdentifierToGameId[identifier]}`); socket.emit('gameCreated', { gameId: gameId, mode: mode, yourPlayerId: assignedPlayerId, chosenCharacterKey: actualCharacterKey }); if (mode === 'ai') { if (game.initializeGame()) { console.log(`[GameManager.createGame] AI game ${gameId} initialized by GameManager, starting...`); game.startGame(); } else { console.error(`[GameManager.createGame] AI game ${gameId} init failed in GameManager. Cleaning up.`); this._cleanupGame(gameId, 'init_fail_ai_create_gm'); } } else if (mode === 'pvp') { if (game.initializeGame()) { // Для PvP инициализируем даже с одним игроком if (!this.pendingPvPGames.includes(gameId)) { this.pendingPvPGames.push(gameId); } socket.emit('waitingForOpponent'); this.broadcastAvailablePvPGames(); } else { console.error(`[GameManager.createGame] PvP game ${gameId} (single player) init failed. Cleaning up.`); this._cleanupGame(gameId, 'init_fail_pvp_create_gm_single_player'); } } } else { console.error(`[GameManager.createGame] game.addPlayer failed for ${identifier} in ${gameId}. Cleaning up.`); this._cleanupGame(gameId, 'player_add_failed_in_instance_gm_on_create'); // game.addPlayer должен был сам отправить ошибку клиенту } } joinGame(socket, gameIdToJoin, identifier, chosenCharacterKey = null) { console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join ${gameIdToJoin} with char ${chosenCharacterKey || 'Default'}`); const gameToJoin = this.games[gameIdToJoin]; if (!gameToJoin) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; } if (gameToJoin.gameState?.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); this._cleanupGame(gameIdToJoin, `attempt_join_finished_game_${identifier}`); return; } if (gameToJoin.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP).' }); return; } const playerInfoInTargetGame = Object.values(gameToJoin.players).find(p => p.identifier === identifier); if (gameToJoin.playerCount >= 2 && !playerInfoInTargetGame?.isTemporarilyDisconnected) { socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; } if (gameToJoin.ownerIdentifier === identifier && !playerInfoInTargetGame?.isTemporarilyDisconnected) { console.warn(`[GameManager.joinGame] User ${identifier} trying to join their own game ${gameIdToJoin} where they are owner and not disconnected. Treating as reconnect request.`); this.handleRequestGameState(socket, identifier); return; } // 1. Очистка завершенной игры пользователя, если такая есть const currentActiveGameIdUserIsIn = this.userIdentifierToGameId[identifier]; if (currentActiveGameIdUserIsIn && this.games[currentActiveGameIdUserIsIn] && this.games[currentActiveGameIdUserIsIn].gameState?.isGameOver) { console.warn(`[GameManager.joinGame] User ${identifier} was in a finished game ${currentActiveGameIdUserIsIn} while trying to join ${gameIdToJoin}. Cleaning old one.`); this._cleanupGame(currentActiveGameIdUserIsIn, `stale_finished_on_join_attempt_${identifier}`); } // 2. Если пользователь УЖЕ ПРИВЯЗАН к какой-то ДРУГОЙ АКТИВНОЙ игре (не той, к которой пытается присоединиться), // и это НЕ его собственная ожидающая PvP игра, то отказать. // Если это ЕГО ОЖИДАЮЩАЯ PvP игра, то ее нужно удалить. const stillExistingGameIdForUser = this.userIdentifierToGameId[identifier]; if (stillExistingGameIdForUser && stillExistingGameIdForUser !== gameIdToJoin && this.games[stillExistingGameIdForUser] && !this.games[stillExistingGameIdForUser].gameState?.isGameOver) { const usersCurrentGame = this.games[stillExistingGameIdForUser]; const isHisOwnPendingPvp = usersCurrentGame.mode === 'pvp' && usersCurrentGame.ownerIdentifier === identifier && usersCurrentGame.playerCount === 1 && this.pendingPvPGames.includes(stillExistingGameIdForUser); if (isHisOwnPendingPvp) { console.log(`[GameManager.joinGame] User ${identifier} is owner of pending game ${stillExistingGameIdForUser}, but wants to join ${gameIdToJoin}. Cleaning up old game.`); this._cleanupPreviousPendingGameForUser(identifier, `joining_another_game_${gameIdToJoin}`); } else { // Пользователь в другой активной игре (не своей ожидающей) console.warn(`[GameManager.joinGame] User ${identifier} is in another active game ${stillExistingGameIdForUser}. Cannot join ${gameIdToJoin}.`); socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' }); this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру return; } } console.log(`[GameManager.joinGame] After potential cleanup before join, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`); const charKeyForJoin = chosenCharacterKey || 'elena'; if (gameToJoin.addPlayer(socket, charKeyForJoin, identifier)) { this.userIdentifierToGameId[identifier] = gameIdToJoin; // Связываем пользователя с игрой, к которой он присоединился const joinedPlayerInfo = Object.values(gameToJoin.players).find(p => p.identifier === identifier); if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) { console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`); socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' }); if (this.userIdentifierToGameId[identifier] === gameIdToJoin) delete this.userIdentifierToGameId[identifier]; return; } console.log(`[GameManager.joinGame] Player ${identifier} added/reconnected to ${gameIdToJoin} as ${joinedPlayerInfo.id}. User map updated. Current map for ${identifier}: ${this.userIdentifierToGameId[identifier]}`); socket.emit('gameCreated', { gameId: gameIdToJoin, mode: gameToJoin.mode, yourPlayerId: joinedPlayerInfo.id, chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey }); if (gameToJoin.playerCount === 2) { console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`); if (gameToJoin.initializeGame()) { gameToJoin.startGame(); } else { this._cleanupGame(gameIdToJoin, 'full_init_fail_pvp_join_gm'); return; } const idx = this.pendingPvPGames.indexOf(gameIdToJoin); if (idx > -1) this.pendingPvPGames.splice(idx, 1); this.broadcastAvailablePvPGames(); } } else { console.warn(`[GameManager.joinGame] gameToJoin.addPlayer returned false for user ${identifier} in game ${gameIdToJoin}.`); // GameInstance должен был отправить причину } } findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`); const existingGameIdForUser = this.userIdentifierToGameId[identifier]; if (existingGameIdForUser && this.games[existingGameIdForUser]) { const existingGame = this.games[existingGameIdForUser]; if (existingGame.gameState && existingGame.gameState.isGameOver) { console.warn(`[GameManager.findRandomPvPGame] User ${identifier} was in a finished game ${existingGameIdForUser}. Cleaning it up.`); this._cleanupGame(existingGameIdForUser, `stale_finished_on_find_random_${identifier}`); } else { console.warn(`[GameManager.findRandomPvPGame] User ${identifier} is already in an active/pending game ${existingGameIdForUser}. Cannot find random.`); socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' }); this.handleRequestGameState(socket, identifier); return; } } // Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он ищет новую. this._cleanupPreviousPendingGameForUser(identifier, `finding_random_game`); console.log(`[GameManager.findRandomPvPGame] After potential cleanup, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`); // Если после очистки пользователь все еще привязан к какой-то *другой* активной игре const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier]; if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) { console.error(`[GameManager.findRandomPvPGame] CRITICAL LOGIC ERROR: User ${identifier} still mapped to active game ${stillExistingGameIdAfterCleanup} after cleanup attempt. Denying find random.`); socket.emit('gameError', { message: 'Ошибка: не удалось освободить предыдущую игровую сессию для поиска.' }); this.handleRequestGameState(socket, identifier); return; } let gameIdToJoin = null; for (const id of [...this.pendingPvPGames]) { // Итерируем копию const pendingGame = this.games[id]; if (pendingGame && pendingGame.mode === 'pvp' && pendingGame.playerCount === 1 && pendingGame.ownerIdentifier !== identifier && (!pendingGame.gameState || !pendingGame.gameState.isGameOver)) { gameIdToJoin = id; break; } else if (!pendingGame || (pendingGame.gameState && pendingGame.gameState.isGameOver)) { console.warn(`[GameManager.findRandomPvPGame] Found stale/finished pending game ${id}. Cleaning up.`); this._cleanupGame(id, `stale_finished_pending_on_find_random`); } } if (gameIdToJoin) { console.log(`[GameManager.findRandomPvPGame] Found pending game ${gameIdToJoin} for ${identifier}. Joining...`); const randomJoinCharKey = ['elena', 'almagest', 'balard'][Math.floor(Math.random() * 3)]; this.joinGame(socket, gameIdToJoin, identifier, randomJoinCharKey); } else { console.log(`[GameManager.findRandomPvPGame] No suitable pending game. Creating new PvP game for ${identifier}.`); this.createGame(socket, 'pvp', chosenCharacterKeyForCreation, identifier); } } handlePlayerAction(identifier, actionData) { const gameId = this.userIdentifierToGameId[identifier]; const game = this.games[gameId]; if (game) { if (game.gameState?.isGameOver) { const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket; if (playerSocket) { console.warn(`[GameManager.handlePlayerAction] Action from ${identifier} for game ${gameId}, but game is over. Requesting state.`); this.handleRequestGameState(playerSocket, identifier); } else { console.warn(`[GameManager.handlePlayerAction] Action from ${identifier} for game ${gameId}, game over, but no socket found for user.`); this._cleanupGame(gameId, `action_on_over_no_socket_gm_${identifier}`); } return; } game.processPlayerAction(identifier, actionData); } else { console.warn(`[GameManager.handlePlayerAction] No game found for user ${identifier} (mapped to game ${gameId}). Clearing map entry.`); delete this.userIdentifierToGameId[identifier]; const clientSocket = this._findClientSocketByIdentifier(identifier); if (clientSocket) clientSocket.emit('gameNotFound', { message: 'Ваша игровая сессия не найдена при совершении действия.' }); } } handlePlayerSurrender(identifier) { const gameId = this.userIdentifierToGameId[identifier]; console.log(`[GameManager.handlePlayerSurrender] User: ${identifier} surrendered. GameID from map: ${gameId}`); const game = this.games[gameId]; if (game) { if (game.gameState?.isGameOver) { console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} in game ${gameId} surrender, but game ALREADY OVER.`); // Не удаляем из userIdentifierToGameId здесь, _cleanupGame сделает это. return; } if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier); else { console.error(`[GameManager.handlePlayerSurrender] CRITICAL: GameInstance ${gameId} missing playerDidSurrender!`); this._cleanupGame(gameId, "surrender_missing_method_gm"); } } else { console.warn(`[GameManager.handlePlayerSurrender] No game found for user ${identifier}. Clearing map entry.`); if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier]; } } handleLeaveAiGame(identifier) { const gameId = this.userIdentifierToGameId[identifier]; console.log(`[GameManager.handleLeaveAiGame] User: ${identifier} leaving AI game. GameID from map: ${gameId}`); const game = this.games[gameId]; if (game) { if (game.gameState?.isGameOver) { console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} game ${gameId} leaving, but game ALREADY OVER.`); return; } if (game.mode === 'ai') { if (typeof game.playerExplicitlyLeftAiGame === 'function') { game.playerExplicitlyLeftAiGame(identifier); } else { console.error(`[GameManager.handleLeaveAiGame] CRITICAL: GameInstance ${gameId} missing playerExplicitlyLeftAiGame! Cleaning up directly.`); this._cleanupGame(gameId, "leave_ai_missing_method_gm"); } } else { console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} sent leaveAiGame, but game ${gameId} is not AI mode (${game.mode}).`); socket.emit('gameError', { message: 'Вы не в AI игре.' }); // Сообщить клиенту об ошибке } } else { console.warn(`[GameManager.handleLeaveAiGame] No game found for user ${identifier}. Clearing map entry.`); if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier]; // Сообщить клиенту, что игра не найдена const clientSocket = this._findClientSocketByIdentifier(identifier); if(clientSocket) clientSocket.emit('gameNotFound', { message: 'AI игра не найдена для выхода.' }); } } _findClientSocketByIdentifier(identifier) { for (const s of this.io.sockets.sockets.values()) { // Использование .values() для итератора if (s && s.userData && s.userData.userId === identifier && s.connected) return s; } return null; } handleDisconnect(socketId, identifier) { const gameIdFromMap = this.userIdentifierToGameId[identifier]; console.log(`[GameManager.handleDisconnect] Socket: ${socketId}, User: ${identifier}, GameID from map: ${gameIdFromMap}`); const game = gameIdFromMap ? this.games[gameIdFromMap] : null; if (game) { if (game.gameState?.isGameOver) { console.log(`[GameManager.handleDisconnect] Game ${gameIdFromMap} for user ${identifier} (socket ${socketId}) ALREADY OVER. Game will be cleaned up by its own logic or next relevant action.`); return; } const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); if (playerInfoInGame && playerInfoInGame.socket?.id === socketId && !playerInfoInGame.isTemporarilyDisconnected) { console.log(`[GameManager.handleDisconnect] Disconnecting socket ${socketId} matches active player ${identifier} (Role: ${playerInfoInGame.id}) in game ${gameIdFromMap}. Notifying GameInstance.`); if (typeof game.handlePlayerPotentiallyLeft === 'function') { game.handlePlayerPotentiallyLeft(playerInfoInGame.id, identifier, playerInfoInGame.chosenCharacterKey); } else { console.error(`[GameManager.handleDisconnect] CRITICAL: GameInstance ${gameIdFromMap} missing handlePlayerPotentiallyLeft!`); this._cleanupGame(gameIdFromMap, "missing_reconnect_logic_on_disconnect_gm"); } } else if (playerInfoInGame && playerInfoInGame.socket?.id !== socketId) { console.log(`[GameManager.handleDisconnect] Disconnected socket ${socketId} is STALE for user ${identifier}. Active socket in game: ${playerInfoInGame.socket?.id}. No action taken by GM.`); } else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) { console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while ALREADY temp disconnected. Reconnect timer in GameInstance handles final cleanup.`); } else if (!playerInfoInGame) { console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game.players. This might indicate a stale userIdentifierToGameId entry. Clearing map for this user.`); if (this.userIdentifierToGameId[identifier] === gameIdFromMap) { delete this.userIdentifierToGameId[identifier]; } } } else { if (this.userIdentifierToGameId[identifier]) { console.warn(`[GameManager.handleDisconnect] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`); delete this.userIdentifierToGameId[identifier]; } } } _cleanupGame(gameId, reason = 'unknown') { console.log(`[GameManager._cleanupGame] Attempting cleanup for GameID: ${gameId}, Reason: ${reason}`); const game = this.games[gameId]; if (!game) { console.warn(`[GameManager._cleanupGame] Game instance for ${gameId} not found in this.games. Cleaning up associated records.`); const pendingIdx = this.pendingPvPGames.indexOf(gameId); if (pendingIdx > -1) { this.pendingPvPGames.splice(pendingIdx, 1); console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`); } // Важно: итерируем по ключам, так как удаление может изменить объект Object.keys(this.userIdentifierToGameId).forEach(idKey => { if (this.userIdentifierToGameId[idKey] === gameId) { delete this.userIdentifierToGameId[idKey]; console.log(`[GameManager._cleanupGame] Removed mapping for user ${idKey} to game ${gameId}.`); } }); this.broadcastAvailablePvPGames(); return false; } console.log(`[GameManager._cleanupGame] Cleaning up game ${game.id}. Owner: ${game.ownerIdentifier}. Reason: ${reason}. Players in game: ${game.playerCount}`); if (typeof game.turnTimer?.clear === 'function') game.turnTimer.clear(); if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers(); if (game.gameState && !game.gameState.isGameOver) { console.log(`[GameManager._cleanupGame] Marking game ${game.id} as game over because it's being cleaned up while active.`); game.gameState.isGameOver = true; // Можно рассмотреть отправку gameOver, если игра прерывается извне // game.io.to(game.id).emit('gameOver', { reason: `game_cleanup_${reason}`, finalGameState: game.gameState, log: game.consumeLogBuffer() }); } Object.values(game.players).forEach(pInfo => { if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) { delete this.userIdentifierToGameId[pInfo.identifier]; console.log(`[GameManager._cleanupGame] Cleared userIdentifierToGameId for player ${pInfo.identifier}.`); } }); if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) { if (!Object.values(game.players).some(p => p.identifier === game.ownerIdentifier)) { delete this.userIdentifierToGameId[game.ownerIdentifier]; console.log(`[GameManager._cleanupGame] Cleared userIdentifierToGameId for owner ${game.ownerIdentifier} (was not in players list).`); } } const pendingIdx = this.pendingPvPGames.indexOf(gameId); if (pendingIdx > -1) { this.pendingPvPGames.splice(pendingIdx, 1); console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`); } delete this.games[gameId]; console.log(`[GameManager._cleanupGame] Game ${gameId} instance deleted. Games left: ${Object.keys(this.games).length}. Pending: ${this.pendingPvPGames.length}. User map size: ${Object.keys(this.userIdentifierToGameId).length}`); this.broadcastAvailablePvPGames(); return true; } getAvailablePvPGamesListForClient() { // Итерируем копию массива pendingPvPGames, так как _cleanupGame может его изменять return [...this.pendingPvPGames] .map(gameId => { const game = this.games[gameId]; if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) { const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected); let p1Username = 'Игрок'; let p1CharName = 'Неизвестный'; const ownerId = game.ownerIdentifier; // Это должен быть identifier создателя if (p1Entry && p1Entry.socket?.userData) { p1Username = p1Entry.socket.userData.username || `User#${String(p1Entry.identifier).substring(0,4)}`; const charData = dataUtils.getCharacterBaseStats(p1Entry.chosenCharacterKey); p1CharName = charData?.name || p1Entry.chosenCharacterKey || 'Не выбран'; } else if (ownerId){ const ownerSocket = this._findClientSocketByIdentifier(ownerId); p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`; const ownerCharKey = game.playerCharacterKey; // Это ключ персонажа для роли PLAYER_ID в этой игре const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null; p1CharName = charData?.name || ownerCharKey || 'Не выбран'; } return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId }; } else if (game && (game.playerCount !== 1 || game.gameState?.isGameOver)) { console.warn(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} is in pendingPvPGames but is not a valid pending game (players: ${game.playerCount}, over: ${game.gameState?.isGameOver}). Removing.`); this._cleanupGame(gameId, 'invalid_pending_game_in_list'); } return null; }) .filter(info => info !== null); } broadcastAvailablePvPGames() { const list = this.getAvailablePvPGamesListForClient(); this.io.emit('availablePvPGamesList', list); } handleRequestGameState(socket, identifier) { const gameIdFromMap = this.userIdentifierToGameId[identifier]; console.log(`[GameManager.handleRequestGameState] User: ${identifier} (Socket: ${socket.id}) requests state. GameID from map: ${gameIdFromMap}`); const game = gameIdFromMap ? this.games[gameIdFromMap] : null; if (game) { const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); if (playerInfoInGame) { if (game.gameState?.isGameOver) { socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' }); // _cleanupGame будет вызвана, когда игра фактически завершается. // Здесь не удаляем из userIdentifierToGameId, если игра еще есть в this.games. return; } if (typeof game.handlePlayerReconnected === 'function') { const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket); if (!reconnected) { console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected for ${identifier} in ${game.id} returned false.`); // GameInstance должен был отправить ошибку. } } else { console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} missing handlePlayerReconnected!`); this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm_on_request'); } } else { console.warn(`[GameManager.handleRequestGameState] User ${identifier} mapped to game ${gameIdFromMap}, but NOT FOUND in game.players. Cleaning map & sending gameNotFound.`); this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_but_mapped_on_request'); } } else { socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' }); if (this.userIdentifierToGameId[identifier]) { console.warn(`[GameManager.handleRequestGameState] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`); delete this.userIdentifierToGameId[identifier]; } } } _handleGameRecoveryError(socket, gameId, identifier, reasonCode) { console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`); socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры. Попробуйте войти снова.' }); if (gameId && this.games[gameId]) { this._cleanupGame(gameId, `recovery_error_gm_${reasonCode}_for_${identifier}`); } else if (this.userIdentifierToGameId[identifier]) { const problematicGameIdForUser = this.userIdentifierToGameId[identifier]; // Если игра была удалена, но пользователь к ней привязан, просто чистим карту delete this.userIdentifierToGameId[identifier]; console.log(`[GameManager._handleGameRecoveryError] Cleaned stale userIdentifierToGameId[${identifier}] pointing to ${problematicGameIdForUser}.`); } // Убедимся, что после всех очисток пользователь точно не привязан if (this.userIdentifierToGameId[identifier]) { delete this.userIdentifierToGameId[identifier]; console.warn(`[GameManager._handleGameRecoveryError] Force cleaned userIdentifierToGameId[${identifier}] as a final measure.`); } socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки. Пожалуйста, войдите снова.' }); } } module.exports = GameManager;