219 lines
8.6 KiB
TypeScript
219 lines
8.6 KiB
TypeScript
import { JoinGameCommand } from './JoinGameCommand';
|
|
import { GameAggregate, GameState, LoginType } from '../../../Domain/Game/GameAggregate';
|
|
import { IGameRepository } from '../../../Domain/IRepository/IGameRepository';
|
|
import { DIContainer } from '../../Services/DIContainer';
|
|
import { RedisService } from '../../Services/RedisService';
|
|
import { logOther, logError } from '../../Services/Logger';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
export interface GamePlayerData {
|
|
playerId: string;
|
|
playerName?: string;
|
|
joinedAt: Date;
|
|
isOnline: boolean;
|
|
position?: number; // For game board position (to be used later)
|
|
}
|
|
|
|
export interface ActiveGameData {
|
|
gameId: string;
|
|
gameCode: string;
|
|
hostId?: string;
|
|
maxPlayers: number;
|
|
currentPlayers: GamePlayerData[];
|
|
state: GameState;
|
|
createdAt: Date;
|
|
startedAt?: Date;
|
|
currentTurn?: string; // Player ID whose turn it is
|
|
websocketRoom: string; // WebSocket room name for real-time updates
|
|
}
|
|
|
|
export class JoinGameCommandHandler {
|
|
private gameRepository: IGameRepository;
|
|
private redisService: RedisService;
|
|
|
|
constructor() {
|
|
this.gameRepository = DIContainer.getInstance().gameRepository;
|
|
this.redisService = RedisService.getInstance();
|
|
}
|
|
|
|
async handle(command: JoinGameCommand): Promise<GameAggregate> {
|
|
const startTime = performance.now();
|
|
|
|
try {
|
|
logOther('Joining game', `gameCode: ${command.gameCode}, playerId: ${command.playerId || 'anonymous'}, loginType: ${command.loginType}`);
|
|
|
|
// Find the game by game code
|
|
const game = await this.gameRepository.findByGameCode(command.gameCode);
|
|
if (!game) {
|
|
throw new Error(`Game with code ${command.gameCode} not found`);
|
|
}
|
|
|
|
// Generate player ID for public games or use provided one
|
|
// For anonymous players (no playerId), use playerName as the identifier to allow rejoining
|
|
const actualPlayerId = command.playerId || `guest_${command.playerName}`;
|
|
|
|
// Validate game joinability (authentication/org checks done in router)
|
|
this.validateGameJoinability(game, actualPlayerId, command);
|
|
|
|
// Add player to database
|
|
const updatedGame = await this.gameRepository.addPlayerToGame(game.id, actualPlayerId);
|
|
if (!updatedGame) {
|
|
throw new Error('Failed to add player to game');
|
|
}
|
|
|
|
// Update Redis with the new player
|
|
await this.updateGameInRedis(updatedGame, { ...command, playerId: actualPlayerId });
|
|
|
|
const endTime = performance.now();
|
|
logOther('Player joined game successfully', {
|
|
gameId: game.id,
|
|
gameCode: game.gamecode,
|
|
playerId: actualPlayerId,
|
|
playerCount: updatedGame.players.length,
|
|
maxPlayers: updatedGame.maxplayers,
|
|
loginType: game.logintype,
|
|
executionTime: Math.round(endTime - startTime)
|
|
});
|
|
|
|
return updatedGame;
|
|
|
|
} catch (error) {
|
|
const endTime = performance.now();
|
|
logError('Failed to join game', error instanceof Error ? error : new Error(String(error)));
|
|
logOther('Game join failed', {
|
|
gameCode: command.gameCode,
|
|
playerId: command.playerId || 'anonymous',
|
|
loginType: command.loginType,
|
|
executionTime: Math.round(endTime - startTime)
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private validateGameJoinability(game: GameAggregate, playerId: string, command: JoinGameCommand): void {
|
|
// Check if game is in waiting state
|
|
if (game.state !== GameState.WAITING) {
|
|
throw new Error('Game is not accepting new players');
|
|
}
|
|
|
|
// Check if player is already in the game
|
|
if (game.players.includes(playerId)) {
|
|
throw new Error('Player is already in this game');
|
|
}
|
|
|
|
// Check if game is full
|
|
if (game.players.length >= game.maxplayers) {
|
|
throw new Error('Game is full');
|
|
}
|
|
|
|
// Note: Login type validation is now handled in the router before reaching this handler
|
|
// This ensures proper authentication and organization membership checks are done first
|
|
|
|
logOther('Game join validation passed', {
|
|
gameId: game.id,
|
|
gameCode: game.gamecode,
|
|
currentPlayers: game.players.length,
|
|
maxPlayers: game.maxplayers,
|
|
gameState: game.state,
|
|
loginType: game.logintype,
|
|
playerId: playerId,
|
|
isAuthenticated: !!command.playerId
|
|
});
|
|
}
|
|
|
|
private async updateGameInRedis(game: GameAggregate, command: JoinGameCommand & { playerId: string }): Promise<void> {
|
|
try {
|
|
const redisKey = `game:${game.gamecode}`;
|
|
|
|
// Get existing game data from Redis or create new
|
|
let gameData: ActiveGameData;
|
|
const existingData = await this.redisService.get(redisKey);
|
|
|
|
if (existingData) {
|
|
gameData = JSON.parse(existingData) as ActiveGameData;
|
|
} else {
|
|
// Create new game data structure
|
|
gameData = {
|
|
gameId: game.id,
|
|
gameCode: game.gamecode,
|
|
maxPlayers: game.maxplayers,
|
|
currentPlayers: [],
|
|
state: game.state,
|
|
createdAt: game.createdate,
|
|
websocketRoom: `game_${game.gamecode}`
|
|
};
|
|
}
|
|
|
|
// Add the new player
|
|
const newPlayer: GamePlayerData = {
|
|
playerId: command.playerId,
|
|
playerName: command.playerName,
|
|
joinedAt: new Date(),
|
|
isOnline: true
|
|
};
|
|
|
|
// Check if player name is already in use by a different player
|
|
const existingPlayerWithName = gameData.currentPlayers.find(
|
|
p => p.playerName === command.playerName && p.playerId !== command.playerId
|
|
);
|
|
|
|
if (existingPlayerWithName) {
|
|
throw new Error(`Player name "${command.playerName}" is already in use in this game`);
|
|
}
|
|
|
|
// Update players list (remove if exists, then add)
|
|
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== command.playerId);
|
|
gameData.currentPlayers.push(newPlayer);
|
|
|
|
// Update game state and player count
|
|
gameData.state = game.state;
|
|
|
|
// Store updated data in Redis with TTL (24 hours)
|
|
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
|
|
|
|
logOther('Game data updated in Redis', {
|
|
gameId: game.id,
|
|
gameCode: game.gamecode,
|
|
redisKey,
|
|
playerCount: gameData.currentPlayers.length,
|
|
websocketRoom: gameData.websocketRoom,
|
|
playerId: command.playerId
|
|
});
|
|
|
|
} catch (error) {
|
|
logError('Failed to update game in Redis', error instanceof Error ? error : new Error(String(error)));
|
|
// Don't throw error here - Redis failure shouldn't prevent game join
|
|
logOther('Game join completed despite Redis error', {
|
|
gameId: game.id,
|
|
playerId: command.playerId
|
|
});
|
|
}
|
|
}
|
|
|
|
async getGameFromRedis(gameCode: string): Promise<ActiveGameData | null> {
|
|
try {
|
|
const redisKey = `game:${gameCode}`;
|
|
const data = await this.redisService.get(redisKey);
|
|
return data ? JSON.parse(data) as ActiveGameData : null;
|
|
} catch (error) {
|
|
logError('Failed to get game from Redis', error instanceof Error ? error : new Error(String(error)));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async removePlayerFromRedis(gameCode: string, playerId: string): Promise<void> {
|
|
try {
|
|
const redisKey = `game:${gameCode}`;
|
|
const existingData = await this.redisService.get(redisKey);
|
|
|
|
if (existingData) {
|
|
const gameData = JSON.parse(existingData) as ActiveGameData;
|
|
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== playerId);
|
|
|
|
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
|
|
}
|
|
} catch (error) {
|
|
logError('Failed to remove player from Redis', error instanceof Error ? error : new Error(String(error)));
|
|
}
|
|
}
|
|
} |