UNPKG

advanced-games-library

Version:

Advanced Gaming Library for React Native - Four Complete Games with iOS Compatibility Fixes

674 lines (577 loc) 18.1 kB
/** * Multiplayer Puzzle Game Implementation * Extends the simple puzzle game with real-time multiplayer support */ import { SimplePuzzleGame, PuzzleGameState, PuzzleTile } from './SimplePuzzleGame'; import { MultiplayerService } from '../../features/multiplayer'; import { GameConfig, GameEventType } from '../../core/types'; export interface MultiplayerPuzzleGameState extends PuzzleGameState { players: MultiplayerPlayer[]; currentTurn: string; // playerId gameMode: 'cooperative' | 'competitive' | 'race'; syncedState: boolean; moveHistory: MultiplayerMove[]; startTime: number; // Add missing startTime property } export interface MultiplayerPlayer { id: string; name: string; avatar?: string; score: number; moves: number; isReady: boolean; isOnline: boolean; color: string; // For visual distinction lastMoveTime: number; } export interface MultiplayerMove { playerId: string; moveId: string; from: { row: number; col: number }; to: { row: number; col: number }; timestamp: number; successful: boolean; } export interface MultiplayerPuzzleConfig extends GameConfig { maxPlayers: number; gameMode: 'cooperative' | 'competitive' | 'race'; turnTimeout: number; // seconds allowSpectators: boolean; gridSize: number; useNumbers: boolean; imageUrl?: string; } /** * Multiplayer Puzzle Game class */ export class MultiplayerPuzzleGame extends SimplePuzzleGame { private multiplayerState!: MultiplayerPuzzleGameState; private multiplayerService: MultiplayerService; protected config!: MultiplayerPuzzleConfig; // Change to protected to match parent private turnTimer?: number; // Change from NodeJS.Timeout to number private currentPlayerId: string; constructor(playerId: string) { super(); this.currentPlayerId = playerId; this.multiplayerService = MultiplayerService.getInstance(); this.setupMultiplayerHandlers(); } // Add missing emit method protected emit(eventType: GameEventType, data: any): void { super.emit(eventType, data); } // Add missing getTileAt method getTileAt(row: number, col: number): PuzzleTile | null { return super.getTileAt(row, col); } async initialize(config: MultiplayerPuzzleConfig): Promise<void> { this.config = { ...config, maxPlayers: config.maxPlayers || 2, gameMode: config.gameMode || 'competitive', turnTimeout: config.turnTimeout || 30, allowSpectators: config.allowSpectators !== undefined ? config.allowSpectators : true, gridSize: config.gridSize || 3, useNumbers: config.useNumbers !== undefined ? config.useNumbers : true }; // Initialize base game await super.initialize(config); // Initialize multiplayer state this.multiplayerState = { ...this.getGameState(), players: [], currentTurn: '', gameMode: this.config.gameMode, syncedState: false, moveHistory: [], startTime: Date.now() // Initialize startTime }; // Initialize multiplayer service await this.multiplayerService.initialize(this.currentPlayerId); } /** * Join a multiplayer game room */ async joinRoom(roomId: string, playerName: string): Promise<void> { try { await this.multiplayerService.joinRoom(roomId); this.emit(GameEventType.MULTIPLAYER_JOINED, { roomId, playerId: this.currentPlayerId }); } catch (error) { this.emit(GameEventType.MULTIPLAYER_ERROR, { error: 'Failed to join room', details: error }); } } /** * Create a new multiplayer game room */ async createRoom(playerName: string): Promise<string> { try { const room = await this.multiplayerService.createRoom( 'simple-puzzle', `Puzzle Game - ${playerName}`, this.config.maxPlayers, false // isPrivate ); await this.joinRoom(room.id, playerName); return room.id; } catch (error) { this.emit(GameEventType.MULTIPLAYER_ERROR, { error: 'Failed to create room', details: error }); throw error; } } /** * Start multiplayer game */ async startMultiplayerGame(): Promise<void> { if (this.multiplayerState.players.length < 2) { throw new Error('Need at least 2 players to start'); } // Check if all players are ready const allReady = this.multiplayerState.players.every(p => p.isReady); if (!allReady) { throw new Error('All players must be ready'); } await super.start(); // Set first turn based on game mode if (this.config.gameMode === 'cooperative') { this.multiplayerState.currentTurn = this.multiplayerState.players[0].id; } else if (this.config.gameMode === 'competitive') { this.multiplayerState.currentTurn = this.multiplayerState.players[0].id; this.startTurnTimer(); } // In race mode, all players can move simultaneously this.broadcastGameState(); } /** * Handle multiplayer tile move */ async multiplayerMoveTile( position: { row: number; col: number }, playerId: string ): Promise<boolean> { // Validate move permission if (!this.canPlayerMove(playerId)) { return false; } const moveId = this.generateMoveId(); const oldEmptyPosition = { ...this.multiplayerState.emptyTilePosition }; // Attempt the move const success = this.moveTile(position); // Record the move const move: MultiplayerMove = { playerId, moveId, from: position, to: oldEmptyPosition, timestamp: Date.now(), successful: success }; this.multiplayerState.moveHistory.push(move); if (success) { // Update player stats const player = this.multiplayerState.players.find(p => p.id === playerId); if (player) { player.moves++; player.lastMoveTime = Date.now(); } // Send move to other players this.multiplayerService.sendGameAction('move', move); // Check for game completion - use public method const gameState = this.getGameState(); if (gameState.isComplete) { await this.handleMultiplayerCompletion(); } else { // Switch turn if in competitive mode if (this.config.gameMode === 'competitive') { this.nextTurn(); } } } return success; } /** * Check if player can make a move */ private canPlayerMove(playerId: string): boolean { // Check if it's the player's turn if (this.config.gameMode === 'competitive' && this.multiplayerState.currentTurn !== playerId) { return false; } // Check if game is in playing state if (this.state.status !== 'playing') return false; // Check if player is online and ready const player = this.multiplayerState.players.find(p => p.id === playerId); if (!player || !player.isOnline || !player.isReady) { return false; } return true; } /** * Handle turn progression */ private nextTurn(): void { if (this.config.gameMode === 'race') return; const currentIndex = this.multiplayerState.players.findIndex( p => p.id === this.multiplayerState.currentTurn ); const nextIndex = (currentIndex + 1) % this.multiplayerState.players.length; this.multiplayerState.currentTurn = this.multiplayerState.players[nextIndex].id; this.clearTurnTimer(); this.startTurnTimer(); this.emit(GameEventType.MULTIPLAYER_TURN_CHANGED, { currentTurn: this.multiplayerState.currentTurn, nextPlayer: this.multiplayerState.players[nextIndex] }); } /** * Start turn timer */ private startTurnTimer(): void { if (this.config.turnTimeout <= 0) return; this.turnTimer = setTimeout(() => { this.handleTurnTimeout(); }, this.config.turnTimeout * 1000) as unknown as number; } /** * Clear turn timer */ private clearTurnTimer(): void { if (this.turnTimer) { clearTimeout(this.turnTimer); this.turnTimer = undefined; } } /** * Handle turn timeout */ private handleTurnTimeout(): void { this.emit(GameEventType.MULTIPLAYER_TURN_TIMEOUT, { playerId: this.multiplayerState.currentTurn }); // Skip to next turn this.nextTurn(); } /** * Handle multiplayer game completion */ private async handleMultiplayerCompletion(): Promise<void> { const results = this.calculateGameResults(); this.emit(GameEventType.MULTIPLAYER_GAME_COMPLETED, { gameId: this.gameId, results, gameTime: Date.now() - this.multiplayerState.startTime }); // Send completion to other players this.multiplayerService.sendGameAction('game_completed', results); } /** * Calculate cooperative game results */ private calculateCooperativeResults(): any { const totalMoves = this.multiplayerState.players.reduce((sum, p) => sum + p.moves, 0); const teamScore = Math.max(0, 1000 - (totalMoves * 10)); return { success: true, teamScore, totalMoves, players: this.multiplayerState.players.map(p => ({ id: p.id, name: p.name, moves: p.moves, contribution: p.moves / totalMoves * 100 })) }; } /** * Calculate competitive game results */ private calculateCompetitiveResults(): any { const sortedPlayers = [...this.multiplayerState.players].sort((a, b) => b.score - a.score); return { winner: sortedPlayers[0], rankings: sortedPlayers.map((player, index) => ({ rank: index + 1, player, score: player.score, moves: player.moves })) }; } /** * Calculate race mode results */ private calculateRaceResults(): any { // In race mode, the first to solve wins const completingPlayer = this.multiplayerState.players.find(p => p.lastMoveTime === Math.max(...this.multiplayerState.players.map(pl => pl.lastMoveTime)) ); return { winner: completingPlayer, completionTime: Date.now() - this.multiplayerState.startTime, totalMoves: this.multiplayerState.moveHistory.length }; } /** * Calculate score for a move */ private calculateMoveScore(): number { // Base score varies by game mode and move efficiency let baseScore = 10; // Bonus for good moves (moving towards correct position) // This would be calculated based on puzzle state return baseScore; } /** * Broadcast game state to all players */ private async broadcastGameState(): Promise<void> { this.multiplayerService.updateGameState({ tiles: this.multiplayerState.tiles, emptyTilePosition: this.multiplayerState.emptyTilePosition, moves: this.multiplayerState.moves, isComplete: this.multiplayerState.isComplete, currentTurn: this.multiplayerState.currentTurn, players: this.multiplayerState.players }); } /** * Broadcast a move to all players */ private async broadcastMove(move: MultiplayerMove): Promise<void> { this.multiplayerService.sendGameAction('move', move); } /** * Broadcast game completion */ private async broadcastGameCompletion(results: any): Promise<void> { this.multiplayerService.sendGameAction('game_completed', results); } /** * Calculate game results based on game mode */ private calculateGameResults(): any { switch (this.config.gameMode) { case 'cooperative': return this.calculateCooperativeResults(); case 'competitive': return this.calculateCompetitiveResults(); case 'race': return this.calculateRaceResults(); default: return this.calculateCompetitiveResults(); } } /** * Setup multiplayer event handlers */ private setupMultiplayerHandlers(): void { this.multiplayerService.on('player_joined', (data: any) => { this.handlePlayerJoined(data); }); this.multiplayerService.on('player_left', (data: any) => { this.handlePlayerLeft(data); }); this.multiplayerService.on('player_ready', (data: any) => { this.handlePlayerReady(data); }); this.multiplayerService.on('move_received', (data: any) => { this.handleMoveReceived(data); }); this.multiplayerService.on('game_state_sync', (data: any) => { this.handleGameStateSync(data); }); this.multiplayerService.on('connection_lost', () => { this.handleConnectionLost(); }); this.multiplayerService.on('connection_restored', () => { this.handleConnectionRestored(); }); } /** * Handle player joined event */ private handlePlayerJoined(data: any): void { const newPlayer: MultiplayerPlayer = { id: data.playerId, name: data.playerName, avatar: data.avatar, score: 0, moves: 0, isReady: false, isOnline: true, color: this.assignPlayerColor(), lastMoveTime: 0 }; this.multiplayerState.players.push(newPlayer); this.emit(GameEventType.MULTIPLAYER_PLAYER_JOINED, { player: newPlayer, totalPlayers: this.multiplayerState.players.length }); } /** * Handle player left event */ private handlePlayerLeft(data: any): void { const playerIndex = this.multiplayerState.players.findIndex(p => p.id === data.playerId); if (playerIndex >= 0) { const leftPlayer = this.multiplayerState.players[playerIndex]; this.multiplayerState.players.splice(playerIndex, 1); // Handle current turn if leaving player was active if (this.multiplayerState.currentTurn === data.playerId) { this.nextTurn(); } this.emit(GameEventType.MULTIPLAYER_PLAYER_LEFT, { player: leftPlayer, totalPlayers: this.multiplayerState.players.length }); } } /** * Handle player ready event */ private handlePlayerReady(data: any): void { const player = this.multiplayerState.players.find(p => p.id === data.playerId); if (player) { player.isReady = data.isReady; this.emit(GameEventType.MULTIPLAYER_PLAYER_READY, { player, allReady: this.multiplayerState.players.every(p => p.isReady) }); } } /** * Handle received move from other players */ private handleMoveReceived(data: any): void { if (data.move.playerId !== this.currentPlayerId) { // Apply the move to local state this.applyRemoteMove(data.move); this.emit(GameEventType.MULTIPLAYER_MOVE_RECEIVED, { move: data.move, gameState: data.gameState }); } } /** * Apply a move received from another player */ private applyRemoteMove(move: MultiplayerMove): void { if (move.successful) { // Apply the move to the local game state const tile = this.getTileAt(move.from.row, move.from.col); if (tile) { tile.position = { ...move.to }; this.multiplayerState.emptyTilePosition = { ...move.from }; this.multiplayerState.moves++; } // Update player stats const player = this.multiplayerState.players.find(p => p.id === move.playerId); if (player) { player.moves++; player.lastMoveTime = move.timestamp; } // Add to move history this.multiplayerState.moveHistory.push(move); } } /** * Handle game state synchronization */ private handleGameStateSync(data: any): void { this.multiplayerState = { ...this.multiplayerState, ...data.gameState }; this.multiplayerState.syncedState = true; this.emit(GameEventType.MULTIPLAYER_STATE_SYNCED, { gameState: this.multiplayerState }); } /** * Handle connection lost */ private handleConnectionLost(): void { this.emit(GameEventType.MULTIPLAYER_CONNECTION_LOST, { gameId: this.gameId, timestamp: Date.now() }); } /** * Handle connection restored */ private handleConnectionRestored(): void { // Request current game state from server this.multiplayerService.sendGameAction('request_state', {}); this.emit(GameEventType.MULTIPLAYER_CONNECTION_RESTORED, { gameId: this.gameId, timestamp: Date.now() }); } /** * Set player ready status */ async setPlayerReady(isReady: boolean): Promise<void> { await this.multiplayerService.setPlayerReady(isReady); } /** * Leave multiplayer game */ async leaveGame(): Promise<void> { this.clearTurnTimer(); await this.multiplayerService.leaveRoom(); } /** * Get multiplayer game state */ getMultiplayerGameState(): MultiplayerPuzzleGameState { return { ...this.multiplayerState }; } /** * Utility methods */ private generateAvatar(): string { const avatars = ['🐸', '🐧', '🦊', '🐨', '🐼', '🦁', '🐯', '🐸']; return avatars[Math.floor(Math.random() * avatars.length)]; } private assignPlayerColor(): string { const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD']; const usedColors = this.multiplayerState.players.map(p => p.color); const availableColors = colors.filter(c => !usedColors.includes(c)); return availableColors[0] || colors[0]; } private generateMoveId(): string { return `move_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } cleanup(): void { if (this.turnTimer) { clearTimeout(this.turnTimer); } this.multiplayerService.destroy(); super.cleanup(); } } /** * Multiplayer Puzzle Game Factory */ export class MultiplayerPuzzleGameFactory { getGameInfo() { return { id: 'multiplayer-puzzle', name: 'פאזל מרובה משתתפים', description: 'שחק פאזל הזזה עם חברים', category: 'multiplayer-puzzle', version: '1.0.0', thumbnail: '🧩👥', maxPlayers: 4, minPlayers: 2 }; } createGame(playerId: string): MultiplayerPuzzleGame { return new MultiplayerPuzzleGame(playerId); } }