Перед шаблонизатором.

This commit is contained in:
PsiMagistr 2025-05-28 15:03:23 +03:00
parent 13f1308669
commit f8b648659f

View File

@ -13,18 +13,19 @@ class GameManager {
console.log("[GameManager] Initialized."); console.log("[GameManager] Initialized.");
} }
// Модифицированная функция: теперь она просто удаляет ожидающую игру пользователя, если находит.
// excludeGameId здесь больше не так критичен, так как логика вызова изменится.
_cleanupPreviousPendingGameForUser(identifier, reasonSuffix = 'unknown_cleanup_reason') { _cleanupPreviousPendingGameForUser(identifier, reasonSuffix = 'unknown_cleanup_reason') {
const oldPendingGameId = this.userIdentifierToGameId[identifier]; const oldPendingGameId = this.userIdentifierToGameId[identifier];
if (oldPendingGameId && this.games[oldPendingGameId]) { if (oldPendingGameId && this.games[oldPendingGameId]) {
const gameToRemove = this.games[oldPendingGameId]; const gameToRemove = this.games[oldPendingGameId];
// Убеждаемся, что это именно ожидающая PvP игра этого пользователя
if (gameToRemove.mode === 'pvp' && if (gameToRemove.mode === 'pvp' &&
gameToRemove.playerCount === 1 && // Убеждаемся, что это именно ожидающая игра с одним игроком gameToRemove.ownerIdentifier === identifier && // Он владелец
gameToRemove.ownerIdentifier === identifier && // И этот игрок - владелец gameToRemove.playerCount === 1 && // В игре только он
this.pendingPvPGames.includes(oldPendingGameId)) { this.pendingPvPGames.includes(oldPendingGameId) && // Игра в списке ожидающих
console.log(`[GameManager._cleanupPreviousPendingGameForUser] User ${identifier} performing new action. Removing previous pending PvP game: ${oldPendingGameId}. Reason: ${reasonSuffix}`); (!gameToRemove.gameState || !gameToRemove.gameState.isGameOver) // И она не завершена
this._cleanupGame(oldPendingGameId, `owner_action_removed_pending_game_${reasonSuffix}`); ) {
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 // _cleanupGame должен удалить запись из userIdentifierToGameId
return true; // Успешно очистили return true; // Успешно очистили
} }
@ -32,48 +33,59 @@ class GameManager {
return false; // Нечего было очищать или условия не совпали return false; // Нечего было очищать или условия не совпали
} }
createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) { createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) {
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey || 'Default'}`); console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey || 'Default'}`);
// 1. Проверить, не находится ли пользователь уже в ЗАВЕРШЕННОЙ, но не очищенной игре. const existingGameIdForUser = this.userIdentifierToGameId[identifier];
const existingGameId = this.userIdentifierToGameId[identifier];
if (existingGameId && this.games[existingGameId]) { // 1. Проверить, не находится ли пользователь уже в какой-либо АКТИВНОЙ игре.
const existingGame = this.games[existingGameId]; if (existingGameIdForUser && this.games[existingGameIdForUser]) {
const existingGame = this.games[existingGameIdForUser];
if (existingGame.gameState && existingGame.gameState.isGameOver) { if (existingGame.gameState && existingGame.gameState.isGameOver) {
console.warn(`[GameManager.createGame] User ${identifier} was in a finished game ${existingGameId}. Cleaning it up.`); console.warn(`[GameManager.createGame] User ${identifier} was in a finished game ${existingGameIdForUser}. Cleaning it up before creating new.`);
this._cleanupGame(existingGameId, `stale_finished_on_create_${identifier}`); this._cleanupGame(existingGameIdForUser, `stale_finished_on_create_${identifier}`);
// existingGameId в userIdentifierToGameId[identifier] должен был удалиться // После _cleanupGame, existingGameIdForUser в userIdentifierToGameId[identifier] должен быть удален
} else if (existingGame.mode === mode && // Если это та же самая игра, к которой он пытается "пересоздать" } else {
((mode === 'ai' && existingGame.ownerIdentifier === identifier) || // Пользователь в активной игре.
(mode === 'pvp' && existingGame.ownerIdentifier === identifier && existingGame.playerCount === 1) )) // Если это ЕГО ОЖИДАЮЩАЯ PvP игра, и он пытается создать НОВУЮ (любую), то ее нужно будет удалить ниже.
{ // Если это ДРУГАЯ активная игра (не его ожидающая PvP), то отказать.
console.warn(`[GameManager.createGame] User ${identifier} trying to recreate an existing identical game ${existingGameId}. Sending current state.`); const isHisOwnPendingPvp = existingGame.mode === 'pvp' &&
socket.emit('gameError', { message: mode === 'pvp' ? 'Вы уже создали PvP игру и ожидаете оппонента.' : 'Вы уже в игре с AI.' }); existingGame.ownerIdentifier === identifier &&
this.handleRequestGameState(socket, identifier); existingGame.playerCount === 1 &&
return; this.pendingPvPGames.includes(existingGameIdForUser);
} else if (existingGame.mode !== mode || existingGame.ownerIdentifier !== identifier) {
// Если он в другой активной игре (не своей ожидающей) if (!isHisOwnPendingPvp) {
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' }); // Он в другой активной игре (AI, или PvP с оппонентом, или PvP другого игрока)
this.handleRequestGameState(socket, identifier); 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; return;
} }
// Если это его собственная ожидающая PvP игра, мы ее удалим ниже. // Если это его ожидающая PvP, то _cleanupPreviousPendingGameForUser ниже ее удалит.
}
} }
// 2. Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он создает новую любую игру. // 2. Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он создает новую любую игру.
this._cleanupPreviousPendingGameForUser(identifier, `creating_new_game_mode_${mode}`); // Это важно сделать ДО создания новой игры, чтобы освободить 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 не нашла ожидающую, но он в другой игре. // 3. Окончательная проверка: если ПОСЛЕ очистки пользователь все еще привязан к какой-то активной игре
const stillExistingGameId = this.userIdentifierToGameId[identifier]; // (Это может случиться, если _cleanupPreviousPendingGameForUser не нашла ожидающую, но он был в другой игре, что было бы ошибкой логики выше)
if (stillExistingGameId && this.games[stillExistingGameId] && !this.games[stillExistingGameId].gameState?.isGameOver) { const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' }); 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); this.handleRequestGameState(socket, identifier);
return; return;
} }
const gameId = uuidv4(); const gameId = uuidv4();
console.log(`[GameManager.createGame] New GameID: ${gameId}`); console.log(`[GameManager.createGame] New GameID: ${gameId}`);
const game = new GameInstance(gameId, this.io, mode, this); const game = new GameInstance(gameId, this.io, mode, this);
@ -82,7 +94,7 @@ class GameManager {
const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena'); const charKeyForPlayer = mode === 'ai' ? (chosenCharacterKey || 'elena') : (chosenCharacterKey || 'elena');
if (game.addPlayer(socket, charKeyForPlayer, identifier)) { if (game.addPlayer(socket, charKeyForPlayer, identifier)) {
this.userIdentifierToGameId[identifier] = gameId; this.userIdentifierToGameId[identifier] = gameId; // Связываем пользователя с НОВОЙ игрой
const playerInfo = Object.values(game.players).find(p => p.identifier === identifier); const playerInfo = Object.values(game.players).find(p => p.identifier === identifier);
const assignedPlayerId = playerInfo?.id; const assignedPlayerId = playerInfo?.id;
const actualCharacterKey = playerInfo?.chosenCharacterKey; const actualCharacterKey = playerInfo?.chosenCharacterKey;
@ -94,7 +106,7 @@ class GameManager {
return; return;
} }
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId} as ${assignedPlayerId}. User map updated.`); 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', { socket.emit('gameCreated', {
gameId: gameId, gameId: gameId,
mode: mode, mode: mode,
@ -104,8 +116,10 @@ class GameManager {
if (mode === 'ai') { if (mode === 'ai') {
if (game.initializeGame()) { if (game.initializeGame()) {
console.log(`[GameManager.createGame] AI game ${gameId} initialized by GameManager, starting...`);
game.startGame(); game.startGame();
} else { } else {
console.error(`[GameManager.createGame] AI game ${gameId} init failed in GameManager. Cleaning up.`);
this._cleanupGame(gameId, 'init_fail_ai_create_gm'); this._cleanupGame(gameId, 'init_fail_ai_create_gm');
} }
} else if (mode === 'pvp') { } else if (mode === 'pvp') {
@ -116,12 +130,14 @@ class GameManager {
socket.emit('waitingForOpponent'); socket.emit('waitingForOpponent');
this.broadcastAvailablePvPGames(); this.broadcastAvailablePvPGames();
} else { } else {
console.error(`[GameManager.createGame] PvP game ${gameId} (single player) init failed. Cleaning up.`);
this._cleanupGame(gameId, 'init_fail_pvp_create_gm_single_player'); this._cleanupGame(gameId, 'init_fail_pvp_create_gm_single_player');
} }
} }
} else { } else {
console.error(`[GameManager.createGame] game.addPlayer failed for ${identifier} in ${gameId}. Cleaning up.`); console.error(`[GameManager.createGame] game.addPlayer failed for ${identifier} in ${gameId}. Cleaning up.`);
this._cleanupGame(gameId, 'player_add_failed_in_instance_gm_on_create'); this._cleanupGame(gameId, 'player_add_failed_in_instance_gm_on_create');
// game.addPlayer должен был сам отправить ошибку клиенту
} }
} }
@ -137,59 +153,56 @@ class GameManager {
if (gameToJoin.playerCount >= 2 && !playerInfoInTargetGame?.isTemporarilyDisconnected) { if (gameToJoin.playerCount >= 2 && !playerInfoInTargetGame?.isTemporarilyDisconnected) {
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return; socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
} }
// Нельзя присоединиться к своей же игре, если ты ее владелец и не отключен временно
if (gameToJoin.ownerIdentifier === identifier && !playerInfoInTargetGame?.isTemporarilyDisconnected) { 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.`);
// handleRequestGameState должен корректно обработать реконнект
console.warn(`[GameManager.joinGame] User ${identifier} trying to join their own game ${gameIdToJoin} as a new player. Treating as reconnect request.`);
this.handleRequestGameState(socket, identifier); this.handleRequestGameState(socket, identifier);
return; return;
} }
// 1. Проверить, не находится ли пользователь уже в ЗАВЕРШЕННОЙ, но не очищенной игре. // 1. Очистка завершенной игры пользователя, если такая есть
const currentActiveGameId = this.userIdentifierToGameId[identifier]; const currentActiveGameIdUserIsIn = this.userIdentifierToGameId[identifier];
if (currentActiveGameId && this.games[currentActiveGameId] && this.games[currentActiveGameId].gameState?.isGameOver) { if (currentActiveGameIdUserIsIn && this.games[currentActiveGameIdUserIsIn] && this.games[currentActiveGameIdUserIsIn].gameState?.isGameOver) {
console.warn(`[GameManager.joinGame] User ${identifier} was in a finished game ${currentActiveGameId} while trying to join ${gameIdToJoin}. Cleaning old one.`); console.warn(`[GameManager.joinGame] User ${identifier} was in a finished game ${currentActiveGameIdUserIsIn} while trying to join ${gameIdToJoin}. Cleaning old one.`);
this._cleanupGame(currentActiveGameId, `stale_finished_on_join_attempt_${identifier}`); this._cleanupGame(currentActiveGameIdUserIsIn, `stale_finished_on_join_attempt_${identifier}`);
} }
// 2. Если пользователь УЖЕ ПРИВЯЗАН к какой-то ДРУГОЙ АКТИВНОЙ игре (не той, к которой пытается присоединиться), // 2. Если пользователь УЖЕ ПРИВЯЗАН к какой-то ДРУГОЙ АКТИВНОЙ игре (не той, к которой пытается присоединиться),
// и это НЕ его собственная ожидающая PvP игра, то отказать. // и это НЕ его собственная ожидающая PvP игра, то отказать.
// Если это ЕГО ОЖИДАЮЩАЯ PvP игра, то ее нужно удалить. // Если это ЕГО ОЖИДАЮЩАЯ PvP игра, то ее нужно удалить.
if (currentActiveGameId && currentActiveGameId !== gameIdToJoin && this.games[currentActiveGameId] && !this.games[currentActiveGameId].gameState?.isGameOver) { const stillExistingGameIdForUser = this.userIdentifierToGameId[identifier];
const usersCurrentGame = this.games[currentActiveGameId]; if (stillExistingGameIdForUser && stillExistingGameIdForUser !== gameIdToJoin && this.games[stillExistingGameIdForUser] && !this.games[stillExistingGameIdForUser].gameState?.isGameOver) {
if (usersCurrentGame.mode === 'pvp' && const usersCurrentGame = this.games[stillExistingGameIdForUser];
usersCurrentGame.playerCount === 1 && const isHisOwnPendingPvp = usersCurrentGame.mode === 'pvp' &&
usersCurrentGame.ownerIdentifier === identifier && usersCurrentGame.ownerIdentifier === identifier &&
this.pendingPvPGames.includes(currentActiveGameId)) { usersCurrentGame.playerCount === 1 &&
console.log(`[GameManager.joinGame] User ${identifier} is owner of pending game ${currentActiveGameId}, but wants to join ${gameIdToJoin}. Cleaning up old game.`); 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}`); this._cleanupPreviousPendingGameForUser(identifier, `joining_another_game_${gameIdToJoin}`);
} else { } else {
// Пользователь в другой активной игре (не своей ожидающей) // Пользователь в другой активной игре (не своей ожидающей)
console.warn(`[GameManager.joinGame] User ${identifier} is in another active game ${stillExistingGameIdForUser}. Cannot join ${gameIdToJoin}.`);
socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' }); socket.emit('gameError', { message: 'Вы уже находитесь в другой активной игре.' });
this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру this.handleRequestGameState(socket, identifier); // Попытаться вернуть в ту игру
return; return;
} }
} }
console.log(`[GameManager.joinGame] After potential cleanup before join, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
const charKeyForJoin = chosenCharacterKey || 'elena'; const charKeyForJoin = chosenCharacterKey || 'elena';
if (gameToJoin.addPlayer(socket, charKeyForJoin, identifier)) { if (gameToJoin.addPlayer(socket, charKeyForJoin, identifier)) {
this.userIdentifierToGameId[identifier] = gameIdToJoin; this.userIdentifierToGameId[identifier] = gameIdToJoin; // Связываем пользователя с игрой, к которой он присоединился
const joinedPlayerInfo = Object.values(gameToJoin.players).find(p => p.identifier === identifier); const joinedPlayerInfo = Object.values(gameToJoin.players).find(p => p.identifier === identifier);
if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) { if (!joinedPlayerInfo || !joinedPlayerInfo.id || !joinedPlayerInfo.chosenCharacterKey) {
console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`); console.error(`[GameManager.joinGame] CRITICAL: Failed to get player role/charKey after addPlayer for ${identifier} joining ${gameIdToJoin}.`);
// Здесь важно НЕ удалять gameToJoin, так как в ней мог быть первый игрок.
// addPlayer должен был вернуть false и не изменить playerCount игры, если что-то пошло не так критично.
// Если addPlayer вернул true, но инфо нет, это проблема в addPlayer или GameInstance.
socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' }); socket.emit('gameError', { message: 'Ошибка сервера при назначении роли в игре.' });
// Возможно, стоит удалить игрока из userIdentifierToGameId, если он не смог корректно добавиться
if (this.userIdentifierToGameId[identifier] === gameIdToJoin) delete this.userIdentifierToGameId[identifier]; if (this.userIdentifierToGameId[identifier] === gameIdToJoin) delete this.userIdentifierToGameId[identifier];
return; return;
} }
console.log(`[GameManager.joinGame] Player ${identifier} added/reconnected to ${gameIdToJoin} as ${joinedPlayerInfo.id}.`); 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', { // Используем gameCreated для консистентности, т.к. клиент ожидает это для установки ID игры socket.emit('gameCreated', {
gameId: gameIdToJoin, gameId: gameIdToJoin,
mode: gameToJoin.mode, mode: gameToJoin.mode,
yourPlayerId: joinedPlayerInfo.id, yourPlayerId: joinedPlayerInfo.id,
@ -198,7 +211,6 @@ class GameManager {
if (gameToJoin.playerCount === 2) { if (gameToJoin.playerCount === 2) {
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`); console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`);
// Убедимся, что игра еще раз инициализируется с обоими игроками, если нужно
if (gameToJoin.initializeGame()) { if (gameToJoin.initializeGame()) {
gameToJoin.startGame(); gameToJoin.startGame();
} else { } else {
@ -209,50 +221,49 @@ class GameManager {
this.broadcastAvailablePvPGames(); this.broadcastAvailablePvPGames();
} }
} else { } else {
// addPlayer вернул false, GameInstance должен был отправить причину через gameError
console.warn(`[GameManager.joinGame] gameToJoin.addPlayer returned false for user ${identifier} in game ${gameIdToJoin}.`); console.warn(`[GameManager.joinGame] gameToJoin.addPlayer returned false for user ${identifier} in game ${gameIdToJoin}.`);
// GameInstance должен был отправить причину
} }
} }
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) { findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`); console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
// 1. Проверить, не находится ли пользователь уже в ЗАВЕРШЕННОЙ, но не очищенной игре. const existingGameIdForUser = this.userIdentifierToGameId[identifier];
const existingGameId = this.userIdentifierToGameId[identifier]; if (existingGameIdForUser && this.games[existingGameIdForUser]) {
if (existingGameId && this.games[existingGameId]) { const existingGame = this.games[existingGameIdForUser];
const existingGame = this.games[existingGameId];
if (existingGame.gameState && existingGame.gameState.isGameOver) { if (existingGame.gameState && existingGame.gameState.isGameOver) {
this._cleanupGame(existingGameId, `stale_finished_on_find_random_${identifier}`); 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 { } else {
// Если он уже в активной игре (своей ожидающей или другой) console.warn(`[GameManager.findRandomPvPGame] User ${identifier} is already in an active/pending game ${existingGameIdForUser}. Cannot find random.`);
socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' }); socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' });
this.handleRequestGameState(socket, identifier); return; this.handleRequestGameState(socket, identifier); return;
} }
} }
// 2. Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он ищет новую. // Удалить предыдущую ОЖИДАЮЩУЮ PvP игру этого пользователя, если он ищет новую.
this._cleanupPreviousPendingGameForUser(identifier, `finding_random_game`); this._cleanupPreviousPendingGameForUser(identifier, `finding_random_game`);
console.log(`[GameManager.findRandomPvPGame] After potential cleanup, user ${identifier} mapping: ${this.userIdentifierToGameId[identifier]}`);
// 3. Если после очистки пользователь все еще привязан к какой-то *другой* активной игре // Если после очистки пользователь все еще привязан к какой-то *другой* активной игре
const stillExistingGameId = this.userIdentifierToGameId[identifier]; const stillExistingGameIdAfterCleanup = this.userIdentifierToGameId[identifier];
if (stillExistingGameId && this.games[stillExistingGameId] && !this.games[stillExistingGameId].gameState?.isGameOver) { if (stillExistingGameIdAfterCleanup && this.games[stillExistingGameIdAfterCleanup] && !this.games[stillExistingGameIdAfterCleanup].gameState?.isGameOver) {
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' }); 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); this.handleRequestGameState(socket, identifier);
return; return;
} }
let gameIdToJoin = null; let gameIdToJoin = null;
// Итерируем копию массива, чтобы избежать проблем при удалении элементов из pendingPvPGames внутри цикла for (const id of [...this.pendingPvPGames]) { // Итерируем копию
for (const id of [...this.pendingPvPGames]) {
const pendingGame = this.games[id]; const pendingGame = this.games[id];
if (pendingGame && pendingGame.mode === 'pvp' && if (pendingGame && pendingGame.mode === 'pvp' &&
pendingGame.playerCount === 1 && pendingGame.playerCount === 1 &&
pendingGame.ownerIdentifier !== identifier && // Не присоединяться к своей же игре через "случайный поиск" pendingGame.ownerIdentifier !== identifier &&
!pendingGame.gameState?.isGameOver) { (!pendingGame.gameState || !pendingGame.gameState.isGameOver)) {
gameIdToJoin = id; break; gameIdToJoin = id; break;
} else if (!pendingGame || pendingGame.gameState?.isGameOver) { // Очистка "мертвых" ожидающих игр } else if (!pendingGame || (pendingGame.gameState && pendingGame.gameState.isGameOver)) {
// Это может случиться, если игра была удалена, но ID остался в pendingPvPGames
console.warn(`[GameManager.findRandomPvPGame] Found stale/finished pending game ${id}. Cleaning up.`); console.warn(`[GameManager.findRandomPvPGame] Found stale/finished pending game ${id}. Cleaning up.`);
this._cleanupGame(id, `stale_finished_pending_on_find_random`); this._cleanupGame(id, `stale_finished_pending_on_find_random`);
} }
@ -260,7 +271,7 @@ class GameManager {
if (gameIdToJoin) { if (gameIdToJoin) {
console.log(`[GameManager.findRandomPvPGame] Found pending game ${gameIdToJoin} for ${identifier}. Joining...`); console.log(`[GameManager.findRandomPvPGame] Found pending game ${gameIdToJoin} for ${identifier}. Joining...`);
const randomJoinCharKey = ['elena', 'almagest', 'balard'][Math.floor(Math.random() * 3)]; // TODO: Сделать выбор персонажа более умным const randomJoinCharKey = ['elena', 'almagest', 'balard'][Math.floor(Math.random() * 3)];
this.joinGame(socket, gameIdToJoin, identifier, randomJoinCharKey); this.joinGame(socket, gameIdToJoin, identifier, randomJoinCharKey);
} else { } else {
console.log(`[GameManager.findRandomPvPGame] No suitable pending game. Creating new PvP game for ${identifier}.`); console.log(`[GameManager.findRandomPvPGame] No suitable pending game. Creating new PvP game for ${identifier}.`);
@ -270,7 +281,6 @@ class GameManager {
handlePlayerAction(identifier, actionData) { handlePlayerAction(identifier, actionData) {
const gameId = this.userIdentifierToGameId[identifier]; const gameId = this.userIdentifierToGameId[identifier];
// console.log(`[GameManager.handlePlayerAction] User: ${identifier}, Action: ${actionData?.actionType}, GameID: ${gameId}`);
const game = this.games[gameId]; const game = this.games[gameId];
if (game) { if (game) {
if (game.gameState?.isGameOver) { if (game.gameState?.isGameOver) {
@ -278,6 +288,9 @@ class GameManager {
if (playerSocket) { if (playerSocket) {
console.warn(`[GameManager.handlePlayerAction] Action from ${identifier} for game ${gameId}, but game is over. Requesting state.`); console.warn(`[GameManager.handlePlayerAction] Action from ${identifier} for game ${gameId}, but game is over. Requesting state.`);
this.handleRequestGameState(playerSocket, identifier); 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; return;
} }
@ -297,7 +310,7 @@ class GameManager {
if (game) { if (game) {
if (game.gameState?.isGameOver) { if (game.gameState?.isGameOver) {
console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} in game ${gameId} surrender, but game ALREADY OVER.`); console.warn(`[GameManager.handlePlayerSurrender] User ${identifier} in game ${gameId} surrender, but game ALREADY OVER.`);
// Не удаляем из userIdentifierToGameId здесь, _cleanupGame сделает это, если игра действительно должна быть удалена. // Не удаляем из userIdentifierToGameId здесь, _cleanupGame сделает это.
return; return;
} }
if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier); if (typeof game.playerDidSurrender === 'function') game.playerDidSurrender(identifier);
@ -326,16 +339,19 @@ class GameManager {
} }
} else { } else {
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} sent leaveAiGame, but game ${gameId} is not AI mode (${game.mode}).`); console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} sent leaveAiGame, but game ${gameId} is not AI mode (${game.mode}).`);
socket.emit('gameError', { message: 'Вы не в AI игре.' }); // Сообщить клиенту об ошибке
} }
} else { } else {
console.warn(`[GameManager.handleLeaveAiGame] No game found for user ${identifier}. Clearing map entry.`); console.warn(`[GameManager.handleLeaveAiGame] No game found for user ${identifier}. Clearing map entry.`);
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier]; if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
// Сообщить клиенту, что игра не найдена
const clientSocket = this._findClientSocketByIdentifier(identifier);
if(clientSocket) clientSocket.emit('gameNotFound', { message: 'AI игра не найдена для выхода.' });
} }
} }
_findClientSocketByIdentifier(identifier) { _findClientSocketByIdentifier(identifier) {
for (const sid of this.io.sockets.sockets.keys()) { for (const s of this.io.sockets.sockets.values()) { // Использование .values() для итератора
const s = this.io.sockets.sockets.get(sid);
if (s && s.userData && s.userData.userId === identifier && s.connected) return s; if (s && s.userData && s.userData.userId === identifier && s.connected) return s;
} }
return null; return null;
@ -349,7 +365,6 @@ class GameManager {
if (game) { if (game) {
if (game.gameState?.isGameOver) { 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.`); 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.`);
// Не удаляем из userIdentifierToGameId здесь, пусть это сделает _cleanupGame по завершению игры.
return; return;
} }
@ -368,15 +383,12 @@ class GameManager {
} else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) { } 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.`); console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while ALREADY temp disconnected. Reconnect timer in GameInstance handles final cleanup.`);
} else if (!playerInfoInGame) { } else if (!playerInfoInGame) {
// Это странная ситуация: пользователь привязан к игре, но его нет в списке игроков этой игры.
// Это может случиться, если игра была очищена, но userIdentifierToGameId не обновился.
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.`); 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) { if (this.userIdentifierToGameId[identifier] === gameIdFromMap) {
delete this.userIdentifierToGameId[identifier]; delete this.userIdentifierToGameId[identifier];
} }
} }
} else { } else {
// Если игра не найдена, но пользователь был к ней привязан, значит, карта устарела.
if (this.userIdentifierToGameId[identifier]) { if (this.userIdentifierToGameId[identifier]) {
console.warn(`[GameManager.handleDisconnect] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`); console.warn(`[GameManager.handleDisconnect] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`);
delete this.userIdentifierToGameId[identifier]; delete this.userIdentifierToGameId[identifier];
@ -389,44 +401,40 @@ class GameManager {
const game = this.games[gameId]; const game = this.games[gameId];
if (!game) { if (!game) {
// Если игры уже нет в this.games, но она есть в pendingPvPGames или userIdentifierToGameId,
// нужно все равно почистить эти структуры.
console.warn(`[GameManager._cleanupGame] Game instance for ${gameId} not found in this.games. Cleaning up associated records.`); console.warn(`[GameManager._cleanupGame] Game instance for ${gameId} not found in this.games. Cleaning up associated records.`);
const pendingIdx = this.pendingPvPGames.indexOf(gameId); const pendingIdx = this.pendingPvPGames.indexOf(gameId);
if (pendingIdx > -1) { if (pendingIdx > -1) {
this.pendingPvPGames.splice(pendingIdx, 1); this.pendingPvPGames.splice(pendingIdx, 1);
console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`); console.log(`[GameManager._cleanupGame] Removed ${gameId} from pendingPvPGames.`);
} }
for (const idKey in this.userIdentifierToGameId) { // Важно: итерируем по ключам, так как удаление может изменить объект
Object.keys(this.userIdentifierToGameId).forEach(idKey => {
if (this.userIdentifierToGameId[idKey] === gameId) { if (this.userIdentifierToGameId[idKey] === gameId) {
delete this.userIdentifierToGameId[idKey]; delete this.userIdentifierToGameId[idKey];
console.log(`[GameManager._cleanupGame] Removed mapping for user ${idKey} to game ${gameId}.`); console.log(`[GameManager._cleanupGame] Removed mapping for user ${idKey} to game ${gameId}.`);
} }
} });
this.broadcastAvailablePvPGames(); // Обновляем список, так как ожидающая игра могла быть удалена this.broadcastAvailablePvPGames();
return false; // Игры не было для основной очистки return false;
} }
console.log(`[GameManager._cleanupGame] Cleaning up game ${game.id}. Owner: ${game.ownerIdentifier}. Reason: ${reason}. Players in game: ${game.playerCount}`); 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.turnTimer?.clear === 'function') game.turnTimer.clear();
if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers(); if (typeof game.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers();
// Убедимся, что игра помечена как завершенная, если она еще не была
if (game.gameState && !game.gameState.isGameOver) { if (game.gameState && !game.gameState.isGameOver) {
console.log(`[GameManager._cleanupGame] Marking game ${game.id} as game over.`); console.log(`[GameManager._cleanupGame] Marking game ${game.id} as game over because it's being cleaned up while active.`);
game.gameState.isGameOver = true; game.gameState.isGameOver = true;
// Можно также отправить финальное событие gameOver, если это не было сделано ранее // Можно рассмотреть отправку gameOver, если игра прерывается извне
// game.io.to(game.id).emit('gameOver', { /* ... данные ... */ }); // game.io.to(game.id).emit('gameOver', { reason: `game_cleanup_${reason}`, finalGameState: game.gameState, log: game.consumeLogBuffer() });
} }
// Очищаем всех игроков этой игры из глобальной карты userIdentifierToGameId
Object.values(game.players).forEach(pInfo => { Object.values(game.players).forEach(pInfo => {
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) { if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
delete this.userIdentifierToGameId[pInfo.identifier]; delete this.userIdentifierToGameId[pInfo.identifier];
console.log(`[GameManager._cleanupGame] Cleared userIdentifierToGameId for player ${pInfo.identifier}.`); console.log(`[GameManager._cleanupGame] Cleared userIdentifierToGameId for player ${pInfo.identifier}.`);
} }
}); });
// Дополнительная проверка для ownerIdentifier, если он не был в game.players
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) { if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId) {
if (!Object.values(game.players).some(p => p.identifier === game.ownerIdentifier)) { if (!Object.values(game.players).some(p => p.identifier === game.ownerIdentifier)) {
delete this.userIdentifierToGameId[game.ownerIdentifier]; delete this.userIdentifierToGameId[game.ownerIdentifier];
@ -434,7 +442,6 @@ class GameManager {
} }
} }
const pendingIdx = this.pendingPvPGames.indexOf(gameId); const pendingIdx = this.pendingPvPGames.indexOf(gameId);
if (pendingIdx > -1) { if (pendingIdx > -1) {
this.pendingPvPGames.splice(pendingIdx, 1); this.pendingPvPGames.splice(pendingIdx, 1);
@ -443,41 +450,36 @@ class GameManager {
delete this.games[gameId]; 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}`); 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(); // Обновляем список, т.к. ожидающая игра могла быть удалена this.broadcastAvailablePvPGames();
return true; return true;
} }
getAvailablePvPGamesListForClient() { getAvailablePvPGamesListForClient() {
return this.pendingPvPGames // Итерируем копию массива pendingPvPGames, так как _cleanupGame может его изменять
return [...this.pendingPvPGames]
.map(gameId => { .map(gameId => {
const game = this.games[gameId]; const game = this.games[gameId];
// Убедимся, что игра существует и действительно ожидает
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) { 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); const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
let p1Username = 'Игрок'; let p1Username = 'Игрок';
let p1CharName = 'Неизвестный'; let p1CharName = 'Неизвестный';
const ownerId = game.ownerIdentifier; const ownerId = game.ownerIdentifier; // Это должен быть identifier создателя
if (p1Entry && p1Entry.socket?.userData) { // Приоритет данным из сокета, если есть if (p1Entry && p1Entry.socket?.userData) {
p1Username = p1Entry.socket.userData.username || `User#${String(p1Entry.identifier).substring(0,4)}`; p1Username = p1Entry.socket.userData.username || `User#${String(p1Entry.identifier).substring(0,4)}`;
const charData = dataUtils.getCharacterBaseStats(p1Entry.chosenCharacterKey); const charData = dataUtils.getCharacterBaseStats(p1Entry.chosenCharacterKey);
p1CharName = charData?.name || p1Entry.chosenCharacterKey || 'Не выбран'; p1CharName = charData?.name || p1Entry.chosenCharacterKey || 'Не выбран';
} else if (ownerId){ // Фоллбэк на данные по ownerId } else if (ownerId){
const ownerSocket = this._findClientSocketByIdentifier(ownerId); // Попытаться найти сокет владельца const ownerSocket = this._findClientSocketByIdentifier(ownerId);
p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`; p1Username = ownerSocket?.userData?.username || `Owner#${String(ownerId).substring(0,4)}`;
const ownerCharKey = game.playerCharacterKey; const ownerCharKey = game.playerCharacterKey; // Это ключ персонажа для роли PLAYER_ID в этой игре
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null; const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
p1CharName = charData?.name || ownerCharKey || 'Не выбран'; p1CharName = charData?.name || ownerCharKey || 'Не выбран';
} else if (p1Entry) { // Если есть p1Entry, но нет userData (маловероятно для создателя)
p1Username = `User#${String(p1Entry.identifier).substring(0,4)}`;
const charData = dataUtils.getCharacterBaseStats(p1Entry.chosenCharacterKey);
p1CharName = charData?.name || p1Entry.chosenCharacterKey || 'Не выбран';
} }
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId }; return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
} else if (game && (game.playerCount !== 1 || game.gameState?.isGameOver)) { } else if (game && (game.playerCount !== 1 || game.gameState?.isGameOver)) {
// Если игра есть в pending, но не соответствует условиям, ее нужно оттуда удалить
console.warn(`[GameManager.getAvailablePvPGamesListForClient] Game ${gameId} is in pendingPvPGames but is not a valid pending game (players: ${game.playerCount}, over: ${game.gameState?.isGameOver}). Removing.`); 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'); // Это вызовет broadcastAvailablePvPGames снова, поэтому мы делаем map на копии. this._cleanupGame(gameId, 'invalid_pending_game_in_list');
} }
return null; return null;
}) })
@ -496,38 +498,31 @@ class GameManager {
if (game) { if (game) {
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier); const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
// console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found. PlayerInfo in game.players: ${playerInfoInGame ? `Role: ${playerInfoInGame.id}, TempDisco: ${playerInfoInGame.isTemporarilyDisconnected}` : 'Not found in game.players'}`);
if (playerInfoInGame) { // Игрок действительно является частью этой игры if (playerInfoInGame) {
if (game.gameState?.isGameOver) { if (game.gameState?.isGameOver) {
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' }); socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
// _cleanupGame должна быть вызвана, когда игра фактически завершается, // _cleanupGame будет вызвана, когда игра фактически завершается.
// здесь мы не должны удалять из userIdentifierToGameId, если игра еще есть в this.games. // Здесь не удаляем из userIdentifierToGameId, если игра еще есть в this.games.
// Если игра уже очищена, то game будет null.
return; return;
} }
if (typeof game.handlePlayerReconnected === 'function') { if (typeof game.handlePlayerReconnected === 'function') {
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket); const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
// reconnected может вернуть false, если реконнект не удался по внутренней причине GameInstance
if (!reconnected) { if (!reconnected) {
console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected for ${identifier} in ${game.id} returned false.`); console.warn(`[GameManager.handleRequestGameState] game.handlePlayerReconnected for ${identifier} in ${game.id} returned false.`);
// GameInstance должен был отправить ошибку клиенту, если нужно. // GameInstance должен был отправить ошибку.
// Можно рассмотреть _handleGameRecoveryError, если это критично.
} }
} else { } else {
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} missing handlePlayerReconnected!`); console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} missing handlePlayerReconnected!`);
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm_on_request'); this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm_on_request');
} }
} else { } else {
// Пользователь привязан к этой игре в userIdentifierToGameId, но его нет в списке игроков game.players
// Это может означать, что он был удален из игры (например, таймаут реконнекта), но userIdentifierToGameId не очистился.
console.warn(`[GameManager.handleRequestGameState] User ${identifier} mapped to game ${gameIdFromMap}, but NOT FOUND in game.players. Cleaning map & sending gameNotFound.`); 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'); this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_but_mapped_on_request');
} }
} else { } else {
// Игра не найдена в this.games, но могла быть в userIdentifierToGameId
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' }); socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
if (this.userIdentifierToGameId[identifier]) { // Если привязка была, удаляем ее if (this.userIdentifierToGameId[identifier]) {
console.warn(`[GameManager.handleRequestGameState] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`); console.warn(`[GameManager.handleRequestGameState] No game instance found for gameId ${gameIdFromMap} (user ${identifier}). Clearing stale map entry.`);
delete this.userIdentifierToGameId[identifier]; delete this.userIdentifierToGameId[identifier];
} }
@ -538,23 +533,19 @@ class GameManager {
console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`); console.error(`[GameManager._handleGameRecoveryError] Error recovering game (ID: ${gameId || 'N/A'}) for user ${identifier}. Reason: ${reasonCode}.`);
socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры. Попробуйте войти снова.' }); socket.emit('gameError', { message: 'Ошибка сервера при восстановлении состояния игры. Попробуйте войти снова.' });
// Очищаем игру, если она существует и вызвала ошибку
if (gameId && this.games[gameId]) { if (gameId && this.games[gameId]) {
this._cleanupGame(gameId, `recovery_error_gm_${reasonCode}_for_${identifier}`); this._cleanupGame(gameId, `recovery_error_gm_${reasonCode}_for_${identifier}`);
} else if (this.userIdentifierToGameId[identifier]) { } else if (this.userIdentifierToGameId[identifier]) {
// Если игра уже удалена, но пользователь все еще к ней привязан в карте
const problematicGameIdForUser = this.userIdentifierToGameId[identifier]; const problematicGameIdForUser = this.userIdentifierToGameId[identifier];
if (this.games[problematicGameIdForUser]) { // Если она все же есть (маловероятно, если gameId был null) // Если игра была удалена, но пользователь к ней привязан, просто чистим карту
this._cleanupGame(problematicGameIdForUser, `recovery_error_stale_map_gm_${identifier}_reason_${reasonCode}`);
} else { // Если ее нет, просто чистим карту
delete this.userIdentifierToGameId[identifier]; delete this.userIdentifierToGameId[identifier];
console.log(`[GameManager._handleGameRecoveryError] Cleaned stale userIdentifierToGameId[${identifier}] pointing to ${problematicGameIdForUser}.`);
} }
} // Убедимся, что после всех очисток пользователь точно не привязан
// Если после _cleanupGame пользователь все еще привязан (маловероятно, но для гарантии)
if (this.userIdentifierToGameId[identifier]) { if (this.userIdentifierToGameId[identifier]) {
delete this.userIdentifierToGameId[identifier]; delete this.userIdentifierToGameId[identifier];
console.warn(`[GameManager._handleGameRecoveryError] Force cleaned userIdentifierToGameId[${identifier}] as a final measure.`);
} }
// Отправляем gameNotFound, чтобы клиент перешел на экран логина или выбора игры
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки. Пожалуйста, войдите снова.' }); socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки. Пожалуйста, войдите снова.' });
} }
} }