bc/server/game/GameManager.js

436 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// /server/game/GameManager.js
const { v4: uuidv4 } = require('uuid');
const GameInstance = require('./instance/GameInstance'); // Путь к 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.");
}
_removePreviousPendingGames(currentSocketId, identifier, excludeGameId = null) {
console.log(`[GameManager._removePreviousPendingGames] User: ${identifier}, Socket: ${currentSocketId}, Exclude: ${excludeGameId}`);
const oldPendingGameId = this.userIdentifierToGameId[identifier];
if (oldPendingGameId && oldPendingGameId !== excludeGameId && this.games[oldPendingGameId]) {
const gameToRemove = this.games[oldPendingGameId];
// Используем gameToRemove.playerCount (через геттер)
if (gameToRemove.mode === 'pvp' &&
gameToRemove.playerCount === 1 &&
gameToRemove.ownerIdentifier === identifier &&
this.pendingPvPGames.includes(oldPendingGameId)) {
console.log(`[GameManager._removePreviousPendingGames] User ${identifier} creating/joining new. Removing previous pending PvP game: ${oldPendingGameId}`);
this._cleanupGame(oldPendingGameId, 'owner_action_removed_pending_game');
}
}
}
createGame(socket, mode = 'ai', chosenCharacterKey = null, identifier) {
console.log(`[GameManager.createGame] User: ${identifier} (Socket: ${socket.id}), Mode: ${mode}, Char: ${chosenCharacterKey || 'Default'}`);
const existingGameId = this.userIdentifierToGameId[identifier];
if (existingGameId && this.games[existingGameId]) {
const existingGame = this.games[existingGameId];
// Используем existingGame.playerCount (через геттер)
console.warn(`[GameManager.createGame] User ${identifier} already in game ${existingGameId}. Mode: ${existingGame.mode}, Players: ${existingGame.playerCount}, Owner: ${existingGame.ownerIdentifier}, GameOver: ${existingGame.gameState?.isGameOver}`);
if (existingGame.gameState && !existingGame.gameState.isGameOver) {
// Используем existingGame.playerCount (через геттер)
if (existingGame.mode === 'pvp' && existingGame.playerCount === 1 && existingGame.ownerIdentifier === identifier) {
socket.emit('gameError', { message: 'Вы уже создали PvP игру и ожидаете оппонента.' });
} else {
socket.emit('gameError', { message: 'Вы уже находитесь в активной игре.' });
}
this.handleRequestGameState(socket, identifier);
return;
} else {
this._cleanupGame(existingGameId, `stale_finished_on_create_${identifier}`);
}
}
this._removePreviousPendingGames(socket.id, identifier);
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;
// Получаем роль и актуальный ключ из GameInstance через геттер game.players
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');
socket.emit('gameError', { message: 'Ошибка сервера при создании роли в игре.' });
return;
}
console.log(`[GameManager.createGame] Player ${identifier} added to game ${gameId} as ${assignedPlayerId}. User map updated.`);
socket.emit('gameCreated', {
gameId: gameId,
mode: mode,
yourPlayerId: assignedPlayerId,
chosenCharacterKey: actualCharacterKey
});
if (mode === 'ai') {
if (game.initializeGame()) {
game.startGame();
} else {
this._cleanupGame(gameId, 'init_fail_ai_create_gm');
}
} else if (mode === 'pvp') {
game.initializeGame();
if (!this.pendingPvPGames.includes(gameId)) {
this.pendingPvPGames.push(gameId);
}
socket.emit('waitingForOpponent');
this.broadcastAvailablePvPGames();
}
} else {
console.error(`[GameManager.createGame] game.addPlayer (instance method) failed for ${identifier} in ${gameId}. Cleaning up.`);
this._cleanupGame(gameId, 'player_add_failed_in_instance_gm');
}
}
joinGame(socket, gameIdToJoin, identifier, chosenCharacterKey = null) {
console.log(`[GameManager.joinGame] User: ${identifier} (Socket: ${socket.id}) attempts to join ${gameIdToJoin} with char ${chosenCharacterKey || 'Default'}`);
const game = this.games[gameIdToJoin];
if (!game) { socket.emit('gameError', { message: 'Игра с таким ID не найдена.' }); return; }
if (game.gameState?.isGameOver) { socket.emit('gameError', { message: 'Эта игра уже завершена.' }); this._cleanupGame(gameIdToJoin, `attempt_join_finished_${identifier}`); return; }
if (game.mode !== 'pvp') { socket.emit('gameError', { message: 'К этой игре нельзя присоединиться (не PvP).' }); return; }
// Используем геттер game.players
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
// Используем game.playerCount (через геттер)
if (game.playerCount >= 2 && !playerInfoInGame?.isTemporarilyDisconnected) {
socket.emit('gameError', { message: 'Эта PvP игра уже заполнена.' }); return;
}
if (game.ownerIdentifier === identifier && !playerInfoInGame?.isTemporarilyDisconnected) {
socket.emit('gameError', { message: 'Вы не можете присоединиться к своей же ожидающей игре как новый игрок.' }); this.handleRequestGameState(socket, identifier); return;
}
const existingGameIdOfUser = this.userIdentifierToGameId[identifier];
if (existingGameIdOfUser && existingGameIdOfUser !== gameIdToJoin) {
const otherGame = this.games[existingGameIdOfUser];
if (otherGame && !otherGame.gameState?.isGameOver) { socket.emit('gameError', { message: 'Вы уже в другой активной игре.' }); this.handleRequestGameState(socket, identifier); return; }
else if (otherGame?.gameState?.isGameOver) this._cleanupGame(existingGameIdOfUser, `stale_finished_on_join_${identifier}`);
}
this._removePreviousPendingGames(socket.id, identifier, gameIdToJoin);
const charKeyForJoin = chosenCharacterKey || 'elena';
if (game.addPlayer(socket, charKeyForJoin, identifier)) {
this.userIdentifierToGameId[identifier] = gameIdToJoin;
// Используем геттер game.players
const joinedPlayerInfo = Object.values(game.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: 'Ошибка сервера при назначении роли в игре.' });
return;
}
console.log(`[GameManager.joinGame] Player ${identifier} added/reconnected to ${gameIdToJoin} as ${joinedPlayerInfo.id}.`);
socket.emit('gameCreated', {
gameId: gameIdToJoin,
mode: game.mode,
yourPlayerId: joinedPlayerInfo.id,
chosenCharacterKey: joinedPlayerInfo.chosenCharacterKey
});
// Используем game.playerCount (через геттер)
if (game.playerCount === 2) {
console.log(`[GameManager.joinGame] Game ${gameIdToJoin} is now full. Initializing and starting.`);
if (game.initializeGame()) {
game.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();
}
}
}
findAndJoinRandomPvPGame(socket, chosenCharacterKeyForCreation = 'elena', identifier) {
console.log(`[GameManager.findRandomPvPGame] User: ${identifier} (Socket: ${socket.id}), CharForCreation: ${chosenCharacterKeyForCreation}`);
const existingGameId = this.userIdentifierToGameId[identifier];
if (existingGameId && this.games[existingGameId]) {
const existingGame = this.games[existingGameId];
if (!existingGame.gameState?.isGameOver) {
socket.emit('gameError', { message: 'Вы уже в активной или ожидающей игре.' });
this.handleRequestGameState(socket, identifier); return;
} else {
this._cleanupGame(existingGameId, `stale_finished_on_find_random_${identifier}`);
}
}
this._removePreviousPendingGames(socket.id, identifier);
let gameIdToJoin = null;
for (const id of [...this.pendingPvPGames]) {
const pendingGame = this.games[id];
// Используем pendingGame.playerCount (через геттер)
if (pendingGame && pendingGame.mode === 'pvp' &&
pendingGame.playerCount === 1 &&
pendingGame.ownerIdentifier !== identifier &&
!pendingGame.gameState?.isGameOver) {
gameIdToJoin = id; break;
} else if (pendingGame?.gameState?.isGameOver) {
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];
console.log(`[GameManager.handlePlayerAction] User: ${identifier}, Action: ${actionData?.actionType}, GameID: ${gameId}`);
const game = this.games[gameId];
if (game) {
if (game.gameState?.isGameOver) {
// Используем геттер game.players
const playerSocket = Object.values(game.players).find(p => p.identifier === identifier)?.socket;
if (playerSocket) this.handleRequestGameState(playerSocket, identifier);
return;
}
game.processPlayerAction(identifier, actionData);
} else {
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.`);
if (this.userIdentifierToGameId[identifier] === gameId) delete this.userIdentifierToGameId[identifier];
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"); }
} else {
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");
}
} else {
console.warn(`[GameManager.handleLeaveAiGame] User ${identifier} sent leaveAiGame, but game ${gameId} is not AI mode (${game.mode}).`);
}
} else {
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
}
}
_findClientSocketByIdentifier(identifier) {
for (const sid of this.io.sockets.sockets.keys()) {
const s = this.io.sockets.sockets.get(sid);
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.`);
return;
}
// Используем геттер game.players
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}.`);
} else if (playerInfoInGame && playerInfoInGame.isTemporarilyDisconnected) {
console.log(`[GameManager.handleDisconnect] User ${identifier} (socket ${socketId}) disconnected while already temp disconnected. Reconnect timer handles final cleanup.`);
} else if (!playerInfoInGame) {
console.warn(`[GameManager.handleDisconnect] User ${identifier} mapped to game ${gameIdFromMap}, but not found in game's player list. Clearing map.`);
if (this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier];
}
} else {
if (this.userIdentifierToGameId[identifier]) {
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) {
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
if (pendingIdx > -1) { this.pendingPvPGames.splice(pendingIdx, 1); this.broadcastAvailablePvPGames(); }
for (const idKey in this.userIdentifierToGameId) { if (this.userIdentifierToGameId[idKey] === gameId) delete this.userIdentifierToGameId[idKey]; }
return false;
}
// Используем 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.clearAllReconnectTimers === 'function') game.clearAllReconnectTimers();
if (game.gameState && !game.gameState.isGameOver) {
game.gameState.isGameOver = true;
}
let playersCleanedFromMap = 0;
// Используем геттер game.players
Object.values(game.players).forEach(pInfo => {
if (pInfo?.identifier && this.userIdentifierToGameId[pInfo.identifier] === gameId) {
delete this.userIdentifierToGameId[pInfo.identifier];
playersCleanedFromMap++;
}
});
// Используем геттер game.players
if (game.ownerIdentifier && this.userIdentifierToGameId[game.ownerIdentifier] === gameId &&
!Object.values(game.players).some(p=>p.identifier === game.ownerIdentifier)) {
delete this.userIdentifierToGameId[game.ownerIdentifier];
playersCleanedFromMap++;
}
const pendingIdx = this.pendingPvPGames.indexOf(gameId);
if (pendingIdx > -1) this.pendingPvPGames.splice(pendingIdx, 1);
delete this.games[gameId];
console.log(`[GameManager._cleanupGame] Game ${game.id} 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() {
return this.pendingPvPGames
.map(gameId => {
const game = this.games[gameId];
// Используем game.playerCount (через геттер)
if (game && game.mode === 'pvp' && game.playerCount === 1 && (!game.gameState || !game.gameState.isGameOver)) {
// Используем геттер game.players
const p1Entry = Object.values(game.players).find(p => p.id === GAME_CONFIG.PLAYER_ID && !p.isTemporarilyDisconnected);
let p1Username = 'Игрок';
let p1CharName = 'Неизвестный';
const ownerId = game.ownerIdentifier;
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;
const charData = ownerCharKey ? dataUtils.getCharacterBaseStats(ownerCharKey) : null;
p1CharName = charData?.name || ownerCharKey || 'Не выбран';
}
return { id: gameId, status: `Ожидает (${p1Username} за ${p1CharName})`, ownerIdentifier: ownerId };
}
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) {
// Используем геттер game.players
const playerInfoInGame = Object.values(game.players).find(p => p.identifier === identifier);
console.log(`[GameManager.handleRequestGameState] Game ${gameIdFromMap} found. PlayerInfo: ${playerInfoInGame ? `Role: ${playerInfoInGame.id}, TempDisco: ${playerInfoInGame.isTemporarilyDisconnected}` : 'Not found in game.players'}`);
if (playerInfoInGame) {
if (game.gameState?.isGameOver) {
socket.emit('gameNotFound', { message: 'Ваша предыдущая игра уже завершена.' });
if(this.userIdentifierToGameId[identifier] === gameIdFromMap) delete this.userIdentifierToGameId[identifier];
return;
}
if (typeof game.handlePlayerReconnected === 'function') {
const reconnected = game.handlePlayerReconnected(playerInfoInGame.id, socket);
// ... (обработка результата reconnected, если нужно)
} else {
console.error(`[GameManager.handleRequestGameState] CRITICAL: GameInstance ${game.id} missing handlePlayerReconnected!`);
this._handleGameRecoveryError(socket, game.id, identifier, 'gi_missing_reconnect_method_gm');
}
} else {
this._handleGameRecoveryError(socket, gameIdFromMap, identifier, 'player_not_in_gi_players_reconnect_gm');
}
} else {
socket.emit('gameNotFound', { message: 'Активная игровая сессия не найдена.' });
if (this.userIdentifierToGameId[identifier]) 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_${reasonCode}_for_${identifier}`);
} else if (this.userIdentifierToGameId[identifier]) {
const problematicGameId = this.userIdentifierToGameId[identifier];
if (this.games[problematicGameId]) {
this._cleanupGame(problematicGameId, `recovery_error_stale_map_${identifier}_reason_${reasonCode}`);
} else {
delete this.userIdentifierToGameId[identifier];
}
}
if (this.userIdentifierToGameId[identifier]) delete this.userIdentifierToGameId[identifier];
socket.emit('gameNotFound', { message: 'Ваша игровая сессия была завершена из-за ошибки.' });
}
}
module.exports = GameManager;