Skip to main content

Event Reporting

Event Reporting enables the Game Platform to send real-time tournament events to external systems via AWS SQS queues. This provides reliable, asynchronous communication for webhook delivery, analytics, and business logic integration.

SQS Event Architecture

Event Flow

  1. Game Platform generates tournament events during gameplay
  2. SQS Queue receives events for reliable, ordered processing

Tournament Platform Event Notifications

New Feature: Tournament Platform now sends automatic event notifications to Game Platform for tournament lifecycle events.
The Tournament Platform proactively notifies the Game Platform about important tournament events through HTTP webhooks. This enables real-time synchronization and automatic game room management.

Configuration

Add the Game Platform URL to your Tournament Platform environment:
# .env
GAME_PLATFORM_URL=https://gameplatform.example.com

Supported Events

Tournament Start Event

Sent when a tournament begins (manual start or scheduled start):
POST {GAME_PLATFORM_URL}/internal/tournament/events
Content-Type: application/json

{
  "event": "tournament.started",
  "tournament_id": "12345",
  "tournament_name": "Daily Championship",
  "game_slug": "crash-classic",
  "started_at": "2024-01-15T20:00:00Z",
  "player_count": 87,
  "tournament_config": {
    "tournament_type": "points_based",
    "max_players": 100,
    "entry_settings": {
      "rebuy_enabled": true,
      "max_entries_per_player": 3
    }
  }
}

Tournament Cancellation Event

Sent when a tournament is cancelled (scheduled or in-progress):
POST {GAME_PLATFORM_URL}/internal/tournament/events
Content-Type: application/json

{
  "event": "tournament.cancelled",
  "tournament_id": "12345",
  "tournament_name": "Daily Championship", 
  "game_slug": "crash-classic",
  "cancelled_at": "2024-01-15T19:45:00Z",
  "reason": "Insufficient player registration",
  "status_before_cancellation": "scheduled"
}

Event Reliability

  • Non-blocking: Event notifications don’t block tournament operations
  • Error Handling: Failed notifications are logged but don’t affect tournaments
  • Retry Logic: Automatic retries with exponential backoff
  • Comprehensive Logging: All event notifications are logged for debugging
  1. Event Processor consumes events and triggers appropriate actions
  2. External Systems receive notifications via webhooks or API calls

SQS Event Types

Tournament Lifecycle Events

Tournament Started

{
  "eventType": "room.tournament.started",
  "timestamp": "2024-01-15T20:02:15Z",
  "tournamentId": "12345",
  "roomId": "room_abc123",
  "operatorId": "op_1234567890abcdef",
  "data": {
    "actualStartTime": "2024-01-15T20:02:15Z",
    "playerCount": 87,
    "gameSlug": "crash-classic",
    "estimatedDuration": 4500
  },
  "metadata": {
    "gameVersion": "1.2.3",
    "serverRegion": "us-east-1"
  }
}

Tournament Completed

{
  "eventType": "room.tournament.completed",
  "timestamp": "2024-01-15T21:15:30Z",
  "tournamentId": "12345",
  "roomId": "room_abc123",
  "operatorId": "op_1234567890abcdef",
  "data": {
    "completedAt": "2024-01-15T21:15:30Z",
    "duration": 4395,
    "finalPlayerCount": 87,
    "totalRounds": 5,
    "prizePoolTotal": 870.00,
    "topPlayers": [
      {
        "playerId": "tp_player_456",
        "position": 1,
        "points": 2150,
        "prizeAmount": 435.00
      },
      {
        "playerId": "tp_player_789",
        "position": 2,
        "points": 1980,
        "prizeAmount": 261.00
      }
    ]
  }
}

Player Events

Player Connected

{
  "eventType": "room.player.connected",
  "timestamp": "2024-01-15T19:45:20Z",
  "tournamentId": "12345",
  "playerId": "tp_player_456",
  "roomId": "room_abc123",
  "operatorId": "op_1234567890abcdef",
  "data": {
    "displayName": "PlayerName",
    "connectionTime": "2024-01-15T19:45:20Z",
    "ipAddress": "192.168.1.100",
    "userAgent": "Mozilla/5.0...",
    "entryCount": 2,
    "totalPaid": 20.00
  }
}

Player Score Updated

{
  "eventType": "player.score.updated",
  "timestamp": "2024-01-15T20:15:45Z",
  "tournamentId": "12345",
  "playerId": "tp_player_456",
  "roomId": "room_abc123",
  "operatorId": "op_1234567890abcdef",
  "data": {
    "previousScore": 1100,
    "newScore": 1250,
    "scoreDelta": 150,
    "currentPosition": 15,
    "previousPosition": 18,
    "round": 3,
    "gameAction": "successful_cashout"
  }
}

Player Eliminated

{
  "eventType": "player.eliminated",
  "timestamp": "2024-01-15T20:45:30Z",
  "tournamentId": "12345",
  "playerId": "tp_player_789",
  "roomId": "room_abc123",
  "operatorId": "op_1234567890abcdef",
  "data": {
    "eliminatedAt": "2024-01-15T20:45:30Z",
    "finalPosition": 23,
    "finalScore": 850,
    "eliminationRound": 3,
    "eliminationReason": "insufficient_score",
    "survivalTime": 2610
  }
}

Game Events

Round Started

{
  "eventType": "game.round.started",
  "timestamp": "2024-01-15T20:20:00Z",
  "tournamentId": "12345",
  "roomId": "room_abc123",
  "operatorId": "op_1234567890abcdef",
  "data": {
    "roundNumber": 3,
    "roundStartTime": "2024-01-15T20:20:00Z",
    "activePlayers": 67,
    "eliminatedPlayers": 20,
    "roundDuration": 300,
    "gameConfig": {
      "eliminationRate": 0.3,
      "minCrashMultiplier": 1.2
    }
  }
}

Event Publishing Implementation

SQS Event Publisher

const AWS = require('aws-sdk');

class SQSEventPublisher {
  constructor(queueUrl, region = 'us-east-1') {
    this.sqs = new AWS.SQS({ region });
    this.queueUrl = queueUrl;
    this.batchSize = 10; // SQS batch limit
    this.messageQueue = [];
  }
  
  async publishEvent(eventType, tournamentId, data, metadata = {}) {
    const event = {
      eventType: eventType,
      timestamp: new Date().toISOString(),
      tournamentId: tournamentId,
      eventId: this.generateEventId(),
      data: data,
      metadata: {
        source: 'game-platform',
        version: '1.0',
        ...metadata
      }
    };
    
    try {
      await this.sendToSQS(event);
      console.log(`Published event: ${eventType} for tournament ${tournamentId}`);
    } catch (error) {
      console.error('Failed to publish event:', error);
      throw error;
    }
  }
  
  async sendToSQS(event) {
    const params = {
      QueueUrl: this.queueUrl,
      MessageBody: JSON.stringify(event),
      MessageAttributes: {
        'EventType': {
          DataType: 'String',
          StringValue: event.eventType
        },
        'TournamentId': {
          DataType: 'String', 
          StringValue: event.tournamentId
        },
        'OperatorId': {
          DataType: 'String',
          StringValue: event.operatorId || 'unknown'
        }
      },
      MessageGroupId: event.tournamentId, // For FIFO queues
      MessageDeduplicationId: event.eventId
    };
    
    return await this.sqs.sendMessage(params).promise();
  }
  
  generateEventId() {
    return `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
  
  // Batch publishing for high-volume events
  queueEvent(event) {
    this.messageQueue.push(event);
    
    if (this.messageQueue.length >= this.batchSize) {
      this.flushBatch();
    }
  }
  
  async flushBatch() {
    if (this.messageQueue.length === 0) return;
    
    const batch = this.messageQueue.splice(0, this.batchSize);
    
    const params = {
      QueueUrl: this.queueUrl,
      Entries: batch.map((event, index) => ({
        Id: index.toString(),
        MessageBody: JSON.stringify(event),
        MessageGroupId: event.tournamentId,
        MessageDeduplicationId: event.eventId,
        MessageAttributes: {
          'EventType': {
            DataType: 'String',
            StringValue: event.eventType
          }
        }
      }))
    };
    
    try {
      await this.sqs.sendMessageBatch(params).promise();
      console.log(`Batch published ${batch.length} events`);
    } catch (error) {
      console.error('Batch publish failed:', error);
      // Re-queue failed events
      this.messageQueue.unshift(...batch);
    }
  }
}

Tournament Event Manager

class TournamentEventManager {
  constructor(publisher) {
    this.publisher = publisher;
    this.eventBuffer = new Map();
    this.flushInterval = setInterval(() => {
      this.flushBufferedEvents();
    }, 1000); // Flush every second
  }
  
  // Tournament lifecycle events
  async tournamentStarted(tournamentId, roomId, startData) {
    await this.publisher.publishEvent(
      'room.tournament.started',
      tournamentId,
      {
        roomId: roomId,
        actualStartTime: new Date().toISOString(),
        playerCount: startData.playerCount,
        gameSlug: startData.gameSlug,
        estimatedDuration: startData.estimatedDuration
      }
    );
  }
  
  async tournamentCompleted(tournamentId, roomId, results) {
    await this.publisher.publishEvent(
      'room.tournament.completed',
      tournamentId,
      {
        roomId: roomId,
        completedAt: new Date().toISOString(),
        duration: results.duration,
        finalPlayerCount: results.playerCount,
        totalRounds: results.rounds,
        prizePoolTotal: results.prizePool,
        topPlayers: results.winners.slice(0, 3) // Top 3 players
      }
    );
  }
  
  // Player events
  async playerConnected(tournamentId, playerId, connectionData) {
    await this.publisher.publishEvent(
      'room.player.connected',
      tournamentId,
      {
        playerId: playerId,
        displayName: connectionData.displayName,
        connectionTime: new Date().toISOString(),
        ipAddress: connectionData.ipAddress,
        entryCount: connectionData.entryCount,
        totalPaid: connectionData.totalPaid
      }
    );
  }
  
  // Buffered score updates to reduce event volume
  bufferScoreUpdate(tournamentId, playerId, scoreData) {
    const key = `${tournamentId}_${playerId}`;
    
    this.eventBuffer.set(key, {
      eventType: 'player.score.updated',
      tournamentId: tournamentId,
      playerId: playerId,
      data: scoreData,
      lastUpdate: new Date()
    });
  }
  
  async flushBufferedEvents() {
    const events = Array.from(this.eventBuffer.values());
    this.eventBuffer.clear();
    
    for (const event of events) {
      try {
        await this.publisher.publishEvent(
          event.eventType,
          event.tournamentId,
          {
            playerId: event.playerId,
            timestamp: event.lastUpdate.toISOString(),
            ...event.data
          }
        );
      } catch (error) {
        console.error('Failed to flush buffered event:', error);
      }
    }
  }
  
  async playerEliminated(tournamentId, playerId, eliminationData) {
    await this.publisher.publishEvent(
      'player.eliminated',
      tournamentId,
      {
        playerId: playerId,
        eliminatedAt: new Date().toISOString(),
        finalPosition: eliminationData.position,
        finalScore: eliminationData.score,
        eliminationRound: eliminationData.round,
        eliminationReason: eliminationData.reason,
        survivalTime: eliminationData.survivalTime
      }
    );
  }
}

Game Integration Example

class CrashGameEventIntegration {
  constructor(eventManager) {
    this.eventManager = eventManager;
  }
  
  // Game-specific event handlers
  async onGameRoundStart(tournamentId, roundData) {
    await this.eventManager.publisher.publishEvent(
      'game.round.started',
      tournamentId,
      {
        roundNumber: roundData.roundNumber,
        roundStartTime: new Date().toISOString(),
        activePlayers: roundData.activePlayers,
        eliminatedPlayers: roundData.eliminatedPlayers,
        roundDuration: roundData.duration,
        gameConfig: roundData.config
      }
    );
  }
  
  async onCrashEvent(tournamentId, crashData) {
    await this.eventManager.publisher.publishEvent(
      'game.crash.occurred',
      tournamentId,
      {
        crashMultiplier: crashData.multiplier,
        crashTime: crashData.crashTime,
        playersActive: crashData.playersActive,
        playersCashedOut: crashData.playersCashedOut,
        totalWinnings: crashData.totalWinnings
      }
    );
  }
  
  onPlayerAction(tournamentId, playerId, action, actionData) {
    // Buffer high-frequency events
    this.eventManager.bufferScoreUpdate(tournamentId, playerId, {
      action: action,
      actionData: actionData,
      timestamp: new Date().toISOString()
    });
  }
}

Error Handling & Reliability

Dead Letter Queue Handling

class ReliableEventPublisher extends SQSEventPublisher {
  constructor(queueUrl, dlqUrl, region = 'us-east-1') {
    super(queueUrl, region);
    this.dlqUrl = dlqUrl;
    this.retryAttempts = 3;
    this.retryDelay = 1000; // 1 second
  }
  
  async publishEventWithRetry(eventType, tournamentId, data, metadata = {}) {
    let lastError;
    
    for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
      try {
        await this.publishEvent(eventType, tournamentId, data, metadata);
        return; // Success
      } catch (error) {
        lastError = error;
        console.warn(`Event publish attempt ${attempt} failed:`, error.message);
        
        if (attempt < this.retryAttempts) {
          await this.delay(this.retryDelay * Math.pow(2, attempt - 1)); // Exponential backoff
        }
      }
    }
    
    // All retries failed - send to DLQ
    console.error(`Event publish failed after ${this.retryAttempts} attempts:`, lastError);
    await this.sendToDeadLetterQueue({
      originalEvent: { eventType, tournamentId, data, metadata },
      error: lastError.message,
      attempts: this.retryAttempts,
      timestamp: new Date().toISOString()
    });
  }
  
  async sendToDeadLetterQueue(failedEvent) {
    try {
      await this.sqs.sendMessage({
        QueueUrl: this.dlqUrl,
        MessageBody: JSON.stringify(failedEvent)
      }).promise();
      
      console.log('Event sent to dead letter queue');
    } catch (error) {
      console.error('Failed to send to dead letter queue:', error);
    }
  }
  
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Event Monitoring

class EventMetrics {
  constructor() {
    this.metrics = {
      published: 0,
      failed: 0,
      retries: 0,
      averageLatency: 0,
      eventTypes: new Map()
    };
    
    this.startTime = Date.now();
    
    // Report metrics every 60 seconds
    setInterval(() => {
      this.reportMetrics();
    }, 60000);
  }
  
  recordPublished(eventType, latencyMs) {
    this.metrics.published++;
    
    // Track by event type
    const typeCount = this.metrics.eventTypes.get(eventType) || 0;
    this.metrics.eventTypes.set(eventType, typeCount + 1);
    
    // Update average latency
    this.updateAverageLatency(latencyMs);
  }
  
  recordFailed(eventType, error) {
    this.metrics.failed++;
    console.error(`Event publication failed - ${eventType}:`, error);
  }
  
  recordRetry(eventType) {
    this.metrics.retries++;
  }
  
  updateAverageLatency(latencyMs) {
    const count = this.metrics.published;
    this.metrics.averageLatency = 
      (this.metrics.averageLatency * (count - 1) + latencyMs) / count;
  }
  
  reportMetrics() {
    const uptime = Date.now() - this.startTime;
    const publishRate = (this.metrics.published / (uptime / 1000)).toFixed(2);
    
    console.log('Event Metrics Report:');
    console.log(`  Published: ${this.metrics.published}`);
    console.log(`  Failed: ${this.metrics.failed}`);
    console.log(`  Retries: ${this.metrics.retries}`);
    console.log(`  Publish Rate: ${publishRate} events/second`);
    console.log(`  Average Latency: ${this.metrics.averageLatency.toFixed(2)}ms`);
    
    // Reset counters
    Object.assign(this.metrics, {
      published: 0,
      failed: 0,
      retries: 0,
      eventTypes: new Map()
    });
  }
}

Testing & Debugging

Event Testing Framework

class EventTestFramework {
  constructor() {
    this.publishedEvents = [];
    this.mockPublisher = {
      publishEvent: this.mockPublishEvent.bind(this)
    };
  }
  
  async mockPublishEvent(eventType, tournamentId, data, metadata) {
    const event = {
      eventType,
      tournamentId,
      data,
      metadata,
      timestamp: new Date().toISOString()
    };
    
    this.publishedEvents.push(event);
    console.log('Mock published event:', event);
    return event;
  }
  
  getPublishedEvents(eventType = null) {
    if (eventType) {
      return this.publishedEvents.filter(e => e.eventType === eventType);
    }
    return [...this.publishedEvents];
  }
  
  clearEvents() {
    this.publishedEvents = [];
  }
  
  assertEventPublished(eventType, tournamentId) {
    const found = this.publishedEvents.find(e => 
      e.eventType === eventType && e.tournamentId === tournamentId
    );
    
    if (!found) {
      throw new Error(`Expected event ${eventType} for tournament ${tournamentId} was not published`);
    }
    
    return found;
  }
}

// Usage in tests
const testFramework = new EventTestFramework();
const eventManager = new TournamentEventManager(testFramework.mockPublisher);

// Test tournament started event
await eventManager.tournamentStarted('12345', 'room_abc123', {
  playerCount: 50,
  gameSlug: 'crash-classic',
  estimatedDuration: 3600
});

// Assert event was published
const event = testFramework.assertEventPublished('room.tournament.started', '12345');
console.log('Event published successfully:', event);

Next Steps