advanced-games-library
Version:
Advanced Gaming Library for React Native - Four Complete Games with iOS Compatibility Fixes
588 lines (501 loc) • 17.3 kB
text/typescript
/**
* 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();
}
}