UNPKG

advanced-games-library

Version:

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

588 lines (501 loc) 17.3 kB
/** * Simple Puzzle Game Implementation * A sliding puzzle game where players arrange numbered tiles */ import { BaseGame } from '../base/BaseGame'; import { GameConfig, GameResult, GameEventType, GameDifficulty, GameStatus } from '../../core/types'; export interface PuzzleGameConfig extends GameConfig { gridSize: number; // 3x3, 4x4, etc. imageUrl?: any; useNumbers: boolean; moveLimit?: number; } export interface PuzzleTile { id: number; value: number | null; // null for empty tile position: { row: number; col: number }; correctPosition: { row: number; col: number }; imageSection?: string; // for image puzzles } export interface PuzzleGameState { tiles: PuzzleTile[]; emptyTilePosition: { row: number; col: number }; moves: number; isComplete: boolean; startTime: number; } /** * Simple Puzzle Game Class */ export class SimplePuzzleGame extends BaseGame { private gameState: PuzzleGameState; protected config!: PuzzleGameConfig; private moveHistory: Array<{ from: { row: number; col: number }; to: { row: number; col: number } }> = []; constructor() { super(); this.gameState = this.getInitialState(); // Set default config to ensure puzzle is always visible this.config = { gameId: 'simple-puzzle', difficulty: 'medium' as any, customization: { theme: {} as any }, rules: {}, gridSize: 3, useNumbers: true, // Default to numbers for visibility moveLimit: undefined }; } async initialize(config: PuzzleGameConfig): Promise<void> { await super.initialize(config); // Use image if provided, otherwise use numbers const useNumbers = config.imageUrl ? false : true; this.config = { ...config, useNumbers }; this.gameState = this.createInitialPuzzle(); this.shufflePuzzle(); // Verify initialization was successful if (!this.gameState.tiles || this.gameState.tiles.length === 0) { console.error('Failed to initialize puzzle - no tiles created'); throw new Error('Failed to initialize puzzle game'); } this.trackCustomEvent('puzzle_game_started', { gridSize: this.config.gridSize, useNumbers: this.config.useNumbers, difficulty: this.config.difficulty }); } async start(): Promise<void> { await super.start(); // Ensure game state is properly initialized if (!this.gameState.tiles || this.gameState.tiles.length === 0) { this.gameState = this.createInitialPuzzle(); this.shufflePuzzle(); } this.gameState.startTime = Date.now(); this.trackCustomEvent('puzzle_game_started', { gridSize: this.config.gridSize, useNumbers: this.config.useNumbers, difficulty: this.config.difficulty }); } /** * Handle tile move attempt */ moveTile(tilePosition: { row: number; col: number }): boolean { if (this.state.status !== 'playing') { return false; } const canMove = this.canMoveTile(tilePosition); if (!canMove) { this.trackCustomEvent('invalid_move_attempt', { position: tilePosition, moves: this.gameState.moves }); return false; } // Perform the move this.performMove(tilePosition); // Check if puzzle is complete if (this.isPuzzleComplete()) { this.completePuzzle(); } // Check move limit if (this.config.moveLimit && this.gameState.moves >= this.config.moveLimit) { this.failPuzzle(); } this.emit(GameEventType.PLAYER_ACTION, { action: 'tile_moved', position: tilePosition, moves: this.gameState.moves, isComplete: this.gameState.isComplete }); return true; } /** * Get current game state for UI */ getGameState(): PuzzleGameState { // Ensure we always have a valid game state with tiles if (!this.gameState.tiles || this.gameState.tiles.length === 0) { console.warn('Game state has no tiles, recreating puzzle...'); this.gameState = this.createInitialPuzzle(); this.shufflePuzzle(); } return { ...this.gameState }; } /** * Get tile at specific position */ getTileAt(row: number, col: number): PuzzleTile | null { if (!this.gameState.tiles || this.gameState.tiles.length === 0) { console.warn('getTileAt: No tiles in game state'); return null; } return this.gameState.tiles.find(tile => tile.position.row === row && tile.position.col === col ) || null; } /** * Check if tile can be moved */ canMoveTile(position: { row: number; col: number }): boolean { const { row, col } = position; const { row: emptyRow, col: emptyCol } = this.gameState.emptyTilePosition; // Check if tile is adjacent to empty space const isAdjacent = (Math.abs(row - emptyRow) === 1 && col === emptyCol) || (Math.abs(col - emptyCol) === 1 && row === emptyRow); return isAdjacent; } /** * Perform the actual move */ private performMove(tilePosition: { row: number; col: number }): void { const tile = this.getTileAt(tilePosition.row, tilePosition.col); if (!tile) return; const oldEmptyPosition = { ...this.gameState.emptyTilePosition }; // Move tile to empty position tile.position = { ...oldEmptyPosition }; // Update empty position this.gameState.emptyTilePosition = { ...tilePosition }; // Increment move counter this.gameState.moves++; // Add to move history this.moveHistory.push({ from: tilePosition, to: oldEmptyPosition }); this.trackCustomEvent('tile_moved', { from: tilePosition, to: oldEmptyPosition, moves: this.gameState.moves }); } /** * Check if puzzle is complete */ private isPuzzleComplete(): boolean { return this.gameState.tiles.every(tile => { if (tile.value === null) return true; // Empty tile return tile.position.row === tile.correctPosition.row && tile.position.col === tile.correctPosition.col; }); } /** * Handle puzzle completion */ private completePuzzle(): void { this.gameState.isComplete = true; const timeSpent = Date.now() - this.gameState.startTime; // Calculate score based on efficiency const optimalMoves = this.calculateOptimalMoves(); const efficiency = Math.max(0, (optimalMoves / this.gameState.moves) * 100); const timeBonus = Math.max(0, 1000 - Math.floor(timeSpent / 1000)); const finalScore = Math.round(efficiency * 10 + timeBonus); this.updateScore(finalScore); this.trackCustomEvent('puzzle_completed', { moves: this.gameState.moves, timeSpent, efficiency, score: finalScore, gridSize: this.config.gridSize }); this.state.status = GameStatus.COMPLETED; this.emit(GameEventType.GAME_COMPLETED, { gameId: this.config.gameId, completed: true, score: finalScore, moves: this.gameState.moves, timeSpent }); } /** * Handle puzzle failure (move limit reached) */ private failPuzzle(): void { const timeSpent = Date.now() - this.gameState.startTime; this.trackCustomEvent('puzzle_failed', { reason: 'move_limit_reached', moves: this.gameState.moves, timeSpent, gridSize: this.config.gridSize }); this.state.status = GameStatus.FAILED; this.emit(GameEventType.GAME_FAILED, { gameId: this.config.gameId, completed: false, score: 0, moves: this.gameState.moves, timeSpent }); } /** * Create initial solved puzzle */ private createInitialPuzzle(): PuzzleGameState { const tiles: PuzzleTile[] = []; const gridSize = this.config.gridSize || 3; // Fallback to 3x3 if not set // Create tiles (excluding bottom-right which is empty) for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { if (row === gridSize - 1 && col === gridSize - 1) { continue; // This will be the empty space } const tileNumber = row * gridSize + col + 1; tiles.push({ id: tileNumber, value: this.config.imageUrl ? null : tileNumber, // Use null for image tiles position: { row, col }, correctPosition: { row, col }, imageSection: this.config.imageUrl ? this.getImageSection(row, col) : undefined }); } } // Ensure we created the expected number of tiles const expectedTiles = gridSize * gridSize - 1; // -1 for empty space if (tiles.length !== expectedTiles) { console.error(`Expected ${expectedTiles} tiles but created ${tiles.length}`); } return { tiles, emptyTilePosition: { row: gridSize - 1, col: gridSize - 1 }, moves: 0, isComplete: false, startTime: 0 }; } /** * Shuffle the puzzle */ private shufflePuzzle(): void { const moves = this.config.gridSize * this.config.gridSize * 10; // Ensure solvability for (let i = 0; i < moves; i++) { const possibleMoves = this.getPossibleMoves(); if (possibleMoves.length > 0) { const randomMove = possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; this.performMoveForShuffle(randomMove); } } // Reset move counter after shuffling this.gameState.moves = 0; this.moveHistory = []; // Verify shuffle was successful if (this.gameState.tiles.length === 0) { console.error('Shuffle failed - no tiles in game state'); } } /** * Get possible moves for shuffling */ private getPossibleMoves(): Array<{ row: number; col: number }> { const { row, col } = this.gameState.emptyTilePosition; const moves: Array<{ row: number; col: number }> = []; const gridSize = this.config.gridSize; // Check all four directions const directions = [ { row: row - 1, col }, // Up { row: row + 1, col }, // Down { row, col: col - 1 }, // Left { row, col: col + 1 } // Right ]; directions.forEach(pos => { if (pos.row >= 0 && pos.row < gridSize && pos.col >= 0 && pos.col < gridSize) { moves.push(pos); } }); return moves; } /** * Perform move during shuffling (without tracking) */ private performMoveForShuffle(tilePosition: { row: number; col: number }): void { const tile = this.getTileAt(tilePosition.row, tilePosition.col); if (!tile) return; const oldEmptyPosition = { ...this.gameState.emptyTilePosition }; // Move tile to empty position tile.position = { ...oldEmptyPosition }; // Update empty position this.gameState.emptyTilePosition = { ...tilePosition }; } /** * Calculate optimal number of moves (simplified) */ private calculateOptimalMoves(): number { // This is a simplified calculation // In reality, calculating optimal moves for sliding puzzle is complex return this.config.gridSize * this.config.gridSize * 2; } /** * Get image section for tile (for image puzzles) */ private getImageSection(row: number, col: number): string { if (!this.config.imageUrl) return ''; const gridSize = this.config.gridSize; const sectionWidth = 100 / gridSize; const sectionHeight = 100 / gridSize; return `background-image: url(${this.config.imageUrl}); ` + `background-size: ${gridSize * 100}% ${gridSize * 100}%; ` + `background-position: ${col * sectionWidth}% ${row * sectionHeight}%;`; } /** * Get hint for next move */ getHint(): { row: number; col: number } | null { // Find the first tile that's not in correct position for (const tile of this.gameState.tiles) { if (tile.position.row !== tile.correctPosition.row || tile.position.col !== tile.correctPosition.col) { // Check if this tile can be moved towards its correct position if (this.canMoveTile(tile.position)) { return tile.position; } } } return null; } /** * Reset puzzle to initial shuffled state */ reset(): void { this.gameState = this.createInitialPuzzle(); this.shufflePuzzle(); this.moveHistory = []; // Verify reset was successful if (!this.gameState.tiles || this.gameState.tiles.length === 0) { console.error('Failed to reset puzzle - no tiles created'); // Force recreation this.gameState = this.createInitialPuzzle(); this.shufflePuzzle(); } this.trackCustomEvent('puzzle_reset', { gridSize: this.config.gridSize }); } /** * Get initial state */ private getInitialState(): PuzzleGameState { // Create a basic 3x3 puzzle as initial state const tiles: PuzzleTile[] = []; const gridSize = 3; for (let row = 0; row < gridSize; row++) { for (let col = 0; col < gridSize; col++) { if (row === gridSize - 1 && col === gridSize - 1) { continue; // Empty space } const tileNumber = row * gridSize + col + 1; tiles.push({ id: tileNumber, value: null, // Use null for image tiles position: { row, col }, correctPosition: { row, col } }); } } return { tiles, emptyTilePosition: { row: gridSize - 1, col: gridSize - 1 }, moves: 0, isComplete: false, startTime: 0 }; } end(): GameResult { const timeSpent = Date.now() - this.gameState.startTime; return { gameId: this.config.gameId, playerId: this.getCurrentPlayerId(), score: this.state.currentScore, maxScore: 2000, // Max possible score timeSpent, completed: this.gameState.isComplete, difficulty: this.config.difficulty, customData: { moves: this.gameState.moves, gridSize: this.config.gridSize, efficiency: this.gameState.moves > 0 ? (this.calculateOptimalMoves() / this.gameState.moves) * 100 : 0, moveHistory: this.moveHistory.slice(-20) // Last 20 moves }, timestamp: new Date(), sessionId: this.generateSessionId() }; } cleanup(): void { super.cleanup(); this.moveHistory = []; this.gameState = this.getInitialState(); } // --- Abstract properties required by BaseGame --- public readonly gameId = 'simple-puzzle'; public readonly name = 'פאזל הזזה'; public readonly description = 'סדר את המספרים או התמונה על ידי הזזת אריחים'; public readonly category = 'puzzle'; public readonly version = '1.0.0'; public readonly minDifficulty = GameDifficulty.EASY; public readonly maxDifficulty = GameDifficulty.HARD; public readonly estimatedDuration = 5; // --- Abstract methods required by BaseGame --- async initializeGameLogic(): Promise<void> { // Initialize the puzzle state and shuffle this.gameState = this.createInitialPuzzle(); this.shufflePuzzle(); } startGameLogic(): void { // Start the timer and set status this.gameState.startTime = Date.now(); this.state.status = GameStatus.PLAYING; } pauseGameLogic(): void { // No-op for now, could add timer pause logic this.state.status = GameStatus.PAUSED; } resumeGameLogic(): void { // No-op for now, could add timer resume logic this.state.status = GameStatus.PLAYING; } restartGameLogic(): void { // Reset and shuffle the puzzle this.reset(); this.state.status = GameStatus.NOT_STARTED; } endGameLogic(): GameResult { // Return the final game result return this.end(); } processPlayerAction(action: any): void { // Handle tile move action if (action.type === 'move_tile' && action.payload) { this.moveTile(action.payload); } } validateGameConfig(config: PuzzleGameConfig): boolean { // Validate grid size and useNumbers if (!config || typeof config.gridSize !== 'number' || config.gridSize < 2) return false; if (typeof config.useNumbers !== 'boolean') return false; return true; } // Helper to get current playerId (stub for now) private getCurrentPlayerId(): string { return 'player-1'; } } /** * Simple Puzzle Game Factory */ export class SimplePuzzleGameFactory { getGameInfo() { return { id: 'simple-puzzle', name: 'פאזל הזזה', description: 'סדר את המספרים או התמונה על ידי הזזת אריחים', category: 'puzzle', version: '1.0.0', thumbnail: '🧩' }; } createGame(): SimplePuzzleGame { return new SimplePuzzleGame(); } }