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