UNPKG

advanced-games-library

Version:

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

539 lines (465 loc) 16.9 kB
/** * Competition Management Service * Handles competitions, leaderboards, and tournaments */ import { PlayerData, GameResult, AnalyticsEvent, GameLibraryError, GameErrorCode } from '../../core/types'; export interface Competition { id: string; name: string; description: string; gameId: string; type: 'leaderboard' | 'tournament' | 'challenge'; status: 'upcoming' | 'active' | 'completed'; startDate: Date; endDate: Date; maxParticipants?: number; currentParticipants: number; rules: CompetitionRules; prizes: Prize[]; createdBy: string; organizerInfo?: { name: string; logo?: string; description?: string; }; } export interface CompetitionRules { scoringMethod: 'highest_score' | 'lowest_time' | 'best_average' | 'total_points'; maxAttempts?: number; timeLimit?: number; // seconds difficultyLevel?: 'easy' | 'medium' | 'hard'; customRules?: Record<string, any>; } export interface Prize { position: number; // 1 = first place, 2 = second place, etc. title: string; description?: string; type: 'points' | 'badge' | 'physical' | 'digital'; value: string | number; icon?: string; } export interface LeaderboardEntry { rank: number; playerId: string; playerName: string; score: number; attempts: number; bestTime?: number; lastSubmission: Date; isCurrentPlayer?: boolean; } export interface CompetitionParticipation { competitionId: string; playerId: string; joinedAt: Date; submissions: CompetitionSubmission[]; bestScore: number; totalAttempts: number; currentRank?: number; } export interface CompetitionSubmission { id: string; playerId: string; competitionId: string; gameResult: GameResult; submittedAt: Date; isValid: boolean; rank?: number; } /** * Competition Service for managing tournaments and leaderboards */ export class CompetitionService { private static instance: CompetitionService; private competitions = new Map<string, Competition>(); private participations = new Map<string, CompetitionParticipation[]>(); // competitionId -> participations private submissions = new Map<string, CompetitionSubmission[]>(); // competitionId -> submissions private eventListeners = new Map<string, Function[]>(); private currentPlayerId?: string; private constructor() {} static getInstance(): CompetitionService { if (!CompetitionService.instance) { CompetitionService.instance = new CompetitionService(); } return CompetitionService.instance; } /** * Initialize the competition service */ async initialize(playerId: string): Promise<void> { this.currentPlayerId = playerId; // Load sample competitions await this.loadSampleCompetitions(); this.emit('service_initialized', { playerId }); } /** * Get all available competitions */ getCompetitions(status?: Competition['status']): Competition[] { let competitions = Array.from(this.competitions.values()); if (status) { competitions = competitions.filter(comp => comp.status === status); } return competitions.sort((a, b) => { // Active competitions first, then by start date if (a.status === 'active' && b.status !== 'active') return -1; if (b.status === 'active' && a.status !== 'active') return 1; return b.startDate.getTime() - a.startDate.getTime(); }); } /** * Get a specific competition */ getCompetition(competitionId: string): Competition | null { return this.competitions.get(competitionId) || null; } /** * Join a competition */ async joinCompetition(competitionId: string): Promise<void> { if (!this.currentPlayerId) { throw new GameLibraryError('Player not initialized', GameErrorCode.PLAYER_NOT_SET); } const competition = this.competitions.get(competitionId); if (!competition) { throw new GameLibraryError('Competition not found', GameErrorCode.GAME_NOT_FOUND); } if (competition.status !== 'active') { throw new GameLibraryError('Competition is not active', GameErrorCode.INVALID_CONFIG); } if (competition.maxParticipants && competition.currentParticipants >= competition.maxParticipants) { throw new GameLibraryError('Competition is full', GameErrorCode.INVALID_CONFIG); } // Check if already participating const participations = this.participations.get(competitionId) || []; if (participations.find(p => p.playerId === this.currentPlayerId)) { return; // Already participating } // Create participation record const participation: CompetitionParticipation = { competitionId, playerId: this.currentPlayerId, joinedAt: new Date(), submissions: [], bestScore: 0, totalAttempts: 0 }; participations.push(participation); this.participations.set(competitionId, participations); // Update competition participant count competition.currentParticipants++; this.emit('competition_joined', { competition, participation }); } /** * Leave a competition */ async leaveCompetition(competitionId: string): Promise<void> { if (!this.currentPlayerId) return; const participations = this.participations.get(competitionId) || []; const participationIndex = participations.findIndex(p => p.playerId === this.currentPlayerId); if (participationIndex === -1) return; participations.splice(participationIndex, 1); this.participations.set(competitionId, participations); // Update competition participant count const competition = this.competitions.get(competitionId); if (competition) { competition.currentParticipants--; } this.emit('competition_left', { competitionId }); } /** * Submit a game result to a competition */ async submitResult(competitionId: string, gameResult: GameResult): Promise<CompetitionSubmission> { if (!this.currentPlayerId) { throw new GameLibraryError('Player not initialized', GameErrorCode.PLAYER_NOT_SET); } const competition = this.competitions.get(competitionId); if (!competition) { throw new GameLibraryError('Competition not found', GameErrorCode.GAME_NOT_FOUND); } if (competition.status !== 'active') { throw new GameLibraryError('Competition is not active', GameErrorCode.INVALID_CONFIG); } // Validate game matches competition if (gameResult.gameId !== competition.gameId) { throw new GameLibraryError('Game result does not match competition', GameErrorCode.INVALID_CONFIG); } // Check attempt limits const participation = this.getPlayerParticipation(competitionId, this.currentPlayerId); if (!participation) { throw new GameLibraryError('Player not participating in competition', GameErrorCode.INVALID_CONFIG); } if (competition.rules.maxAttempts && participation.totalAttempts >= competition.rules.maxAttempts) { throw new GameLibraryError('Maximum attempts reached', GameErrorCode.INVALID_CONFIG); } // Create submission const submission: CompetitionSubmission = { id: this.generateSubmissionId(), playerId: this.currentPlayerId, competitionId, gameResult, submittedAt: new Date(), isValid: this.validateSubmission(competition, gameResult) }; // Store submission const submissions = this.submissions.get(competitionId) || []; submissions.push(submission); this.submissions.set(competitionId, submissions); // Update participation participation.submissions.push(submission); participation.totalAttempts++; if (submission.isValid) { const newScore = this.calculateCompetitionScore(competition.rules, gameResult); if (newScore > participation.bestScore) { participation.bestScore = newScore; } } // Recalculate leaderboard this.updateLeaderboard(competitionId); this.emit('result_submitted', { competition, submission }); return submission; } /** * Get leaderboard for a competition */ getLeaderboard(competitionId: string, limit: number = 100): LeaderboardEntry[] { const competition = this.competitions.get(competitionId); if (!competition) return []; const participations = this.participations.get(competitionId) || []; const validParticipations = participations.filter(p => p.bestScore > 0); // Sort by best score according to competition rules const sorted = validParticipations.sort((a, b) => { switch (competition.rules.scoringMethod) { case 'highest_score': case 'total_points': return b.bestScore - a.bestScore; case 'lowest_time': case 'best_average': return a.bestScore - b.bestScore; default: return b.bestScore - a.bestScore; } }); return sorted.slice(0, limit).map((participation, index) => { const bestSubmission = participation.submissions .filter(s => s.isValid) .sort((a, b) => { const aScore = this.calculateCompetitionScore(competition.rules, a.gameResult); const bScore = this.calculateCompetitionScore(competition.rules, b.gameResult); return competition.rules.scoringMethod === 'lowest_time' ? aScore - bScore : bScore - aScore; })[0]; return { rank: index + 1, playerId: participation.playerId, playerName: this.getPlayerName(participation.playerId), score: participation.bestScore, attempts: participation.totalAttempts, bestTime: bestSubmission?.gameResult.timeSpent, lastSubmission: participation.submissions[participation.submissions.length - 1]?.submittedAt || participation.joinedAt, isCurrentPlayer: participation.playerId === this.currentPlayerId }; }); } /** * Get player's rank in a competition */ getPlayerRank(competitionId: string, playerId: string): number | null { const leaderboard = this.getLeaderboard(competitionId); const entry = leaderboard.find(e => e.playerId === playerId); return entry?.rank || null; } /** * Get player's participation in a competition */ getPlayerParticipation(competitionId: string, playerId: string): CompetitionParticipation | null { const participations = this.participations.get(competitionId) || []; return participations.find(p => p.playerId === playerId) || null; } /** * Check if player is participating in a competition */ isPlayerParticipating(competitionId: string, playerId: string): boolean { return !!this.getPlayerParticipation(competitionId, playerId); } /** * Create a new competition (admin/organizer function) */ async createCompetition(competitionData: Omit<Competition, 'id' | 'currentParticipants'>): Promise<Competition> { const competition: Competition = { ...competitionData, id: this.generateCompetitionId(), currentParticipants: 0 }; this.competitions.set(competition.id, competition); this.participations.set(competition.id, []); this.submissions.set(competition.id, []); this.emit('competition_created', competition); return competition; } /** * Event subscription */ on(event: string, callback: Function): void { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event)!.push(callback); } off(event: string, callback: Function): void { const listeners = this.eventListeners.get(event); if (listeners) { const index = listeners.indexOf(callback); if (index > -1) { listeners.splice(index, 1); } } } /** * Private methods */ private emit(event: string, data: any): void { const listeners = this.eventListeners.get(event) || []; listeners.forEach(callback => { try { callback(data); } catch (error) { console.warn('Event listener error:', error); } }); } private async loadSampleCompetitions(): Promise<void> { const now = new Date(); const weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); const monthFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); const sampleCompetitions: Competition[] = [ { id: 'weekly_memory_challenge', name: 'אתגר הזיכרון השבועי', description: 'תחרות זיכרון שבועית - מי יזכור הכי הרבה?', gameId: 'memory-match', type: 'leaderboard', status: 'active', startDate: now, endDate: weekFromNow, maxParticipants: 100, currentParticipants: 0, rules: { scoringMethod: 'highest_score', maxAttempts: 3, difficultyLevel: 'medium' }, prizes: [ { position: 1, title: 'מקום ראשון', type: 'points', value: 1000, icon: '🥇' }, { position: 2, title: 'מקום שני', type: 'points', value: 500, icon: '🥈' }, { position: 3, title: 'מקום שלישי', type: 'points', value: 250, icon: '🥉' } ], createdBy: 'system' }, { id: 'speed_quiz_tournament', name: 'טורניר חידון מהיר', description: 'מי הכי מהיר בחידון? הצטרפו לטורניר!', gameId: 'quiz', type: 'tournament', status: 'upcoming', startDate: new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000), endDate: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000), maxParticipants: 50, currentParticipants: 0, rules: { scoringMethod: 'lowest_time', maxAttempts: 1, timeLimit: 300, difficultyLevel: 'hard' }, prizes: [ { position: 1, title: 'אלוף הטורניר', type: 'badge', value: 'tournament_champion', icon: '👑' }, { position: 2, title: 'סגן אלוף', type: 'badge', value: 'tournament_runner_up', icon: '🏆' } ], createdBy: 'system' }, { id: 'reaction_time_monthly', name: 'אתגר זמן התגובה החודשי', description: 'אתגר חודשי למהירות התגובה הטובה ביותר', gameId: 'reaction-time', type: 'challenge', status: 'active', startDate: new Date(now.getFullYear(), now.getMonth(), 1), endDate: monthFromNow, rules: { scoringMethod: 'best_average', maxAttempts: 10 }, prizes: [ { position: 1, title: 'המהיר ביותר', type: 'points', value: 2000, icon: '⚡' } ], createdBy: 'system', currentParticipants: 0 } ]; sampleCompetitions.forEach(comp => { this.competitions.set(comp.id, comp); this.participations.set(comp.id, []); this.submissions.set(comp.id, []); }); } private validateSubmission(competition: Competition, gameResult: GameResult): boolean { // Check if game result meets competition requirements if (competition.rules.difficultyLevel && gameResult.difficulty !== competition.rules.difficultyLevel) { return false; } if (competition.rules.timeLimit && gameResult.timeSpent > competition.rules.timeLimit * 1000) { return false; } return gameResult.completed; } private calculateCompetitionScore(rules: CompetitionRules, gameResult: GameResult): number { switch (rules.scoringMethod) { case 'highest_score': case 'total_points': return gameResult.score; case 'lowest_time': return gameResult.timeSpent; case 'best_average': // For reaction time games, lower is better return gameResult.customData?.averageTime || gameResult.timeSpent; default: return gameResult.score; } } private updateLeaderboard(competitionId: string): void { const participations = this.participations.get(competitionId) || []; participations.forEach(participation => { const rank = this.getPlayerRank(competitionId, participation.playerId); participation.currentRank = rank; }); } private getPlayerName(playerId: string): string { // In a real implementation, this would fetch from player service return `שחקן ${playerId.slice(-4)}`; } private generateCompetitionId(): string { return `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } private generateSubmissionId(): string { return `sub_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Cleanup */ destroy(): void { this.competitions.clear(); this.participations.clear(); this.submissions.clear(); this.eventListeners.clear(); this.currentPlayerId = undefined; } }