Skip to content

Algoritmo de Alocação de Quadras

Sport Tech Club - Sistema Inteligente de Gestão de Quadras

Visão Geral

O algoritmo de alocação de quadras é o coração do Sport Tech Club, responsável por:

  • Gestão dinâmica de filas de espera
  • Sistema "winner stays" (vencedor fica)
  • Balanceamento de jogadores por nível de habilidade
  • Otimização de ocupação das quadras

1. Modelo de Domínio

1.1 Entidades Principais

typescript
// Quadra
interface Court {
  id: string;
  arenaId: string;
  name: string;
  sport: Sport;
  surface: Surface;
  status: CourtStatus;
  currentSession?: GameSession;
  queue: WaitingQueue;
  config: CourtConfig;
}

// Sessão de Jogo
interface GameSession {
  id: string;
  courtId: string;
  teams: Team[];
  startTime: Date;
  maxDuration: number; // minutos
  score: Score;
  status: SessionStatus;
  winnerStays: boolean;
}

// Time
interface Team {
  id: string;
  players: Player[];
  skillRating: number; // média do time
  waitingSince?: Date;
  gamesPlayed: number; // na sessão atual
}

// Jogador
interface Player {
  id: string;
  userId: string;
  name: string;
  skillLevel: SkillLevel;
  skillRating: number; // 1-100
  preferences: PlayerPreferences;
  stats: PlayerStats;
}

// Fila de Espera
interface WaitingQueue {
  id: string;
  courtId: string;
  entries: QueueEntry[];
  config: QueueConfig;
  estimatedWaitTime: number;
}

// Entrada na Fila
interface QueueEntry {
  id: string;
  team: Team;
  joinedAt: Date;
  priority: Priority;
  status: QueueStatus;
  estimatedPlayTime: Date;
}

1.2 Enumerações

typescript
enum Sport {
  BEACH_TENNIS = 'BEACH_TENNIS',
  BEACH_VOLLEY = 'BEACH_VOLLEY',
  FUTEVOLEI = 'FUTEVOLEI',
  PADEL = 'PADEL'
}

enum CourtStatus {
  AVAILABLE = 'AVAILABLE',
  IN_USE = 'IN_USE',
  MAINTENANCE = 'MAINTENANCE',
  RESERVED = 'RESERVED'
}

enum SessionStatus {
  WAITING_PLAYERS = 'WAITING_PLAYERS',
  IN_PROGRESS = 'IN_PROGRESS',
  PAUSED = 'PAUSED',
  COMPLETED = 'COMPLETED'
}

enum SkillLevel {
  BEGINNER = 'BEGINNER',      // 1-25
  INTERMEDIATE = 'INTERMEDIATE', // 26-50
  ADVANCED = 'ADVANCED',      // 51-75
  PROFESSIONAL = 'PROFESSIONAL' // 76-100
}

enum Priority {
  NORMAL = 0,
  MEMBER = 1,
  VIP = 2,
  TOURNAMENT = 3
}

2. Algoritmo de Fila de Espera

2.1 Priority Queue Implementation

typescript
class PriorityWaitingQueue {
  private entries: QueueEntry[] = [];
  private config: QueueConfig;

  constructor(config: QueueConfig) {
    this.config = config;
  }

  /**
   * Adiciona um time à fila com prioridade calculada
   */
  enqueue(team: Team, basePriority: Priority): QueueEntry {
    const entry: QueueEntry = {
      id: generateId(),
      team,
      joinedAt: new Date(),
      priority: this.calculateEffectivePriority(team, basePriority),
      status: QueueStatus.WAITING,
      estimatedPlayTime: this.estimatePlayTime()
    };

    // Inserção ordenada por prioridade
    const insertIndex = this.findInsertIndex(entry);
    this.entries.splice(insertIndex, 0, entry);

    this.recalculateEstimates();
    return entry;
  }

  /**
   * Remove e retorna o próximo time da fila
   */
  dequeue(): QueueEntry | null {
    if (this.entries.length === 0) return null;

    const entry = this.entries.shift()!;
    entry.status = QueueStatus.CALLED;

    this.recalculateEstimates();
    return entry;
  }

  /**
   * Calcula prioridade efetiva considerando múltiplos fatores
   */
  private calculateEffectivePriority(
    team: Team,
    basePriority: Priority
  ): number {
    let score = basePriority * 1000; // Base priority weight

    // Fator de tempo de espera (aging)
    const waitingMinutes = team.waitingSince
      ? (Date.now() - team.waitingSince.getTime()) / 60000
      : 0;
    score += waitingMinutes * this.config.agingFactor;

    // Fator de jogos jogados (fairness)
    if (team.gamesPlayed > 0) {
      score -= team.gamesPlayed * this.config.fairnessPenalty;
    }

    // Bonus para times completos
    if (this.isTeamComplete(team)) {
      score += this.config.completeTeamBonus;
    }

    return score;
  }

  /**
   * Encontra posição de inserção mantendo ordem
   */
  private findInsertIndex(entry: QueueEntry): number {
    for (let i = 0; i < this.entries.length; i++) {
      if (entry.priority > this.entries[i].priority) {
        return i;
      }
    }
    return this.entries.length;
  }

  /**
   * Recalcula tempos estimados para toda a fila
   */
  private recalculateEstimates(): void {
    let accumulatedTime = 0;

    for (const entry of this.entries) {
      entry.estimatedPlayTime = new Date(
        Date.now() + accumulatedTime + this.config.averageGameDuration
      );
      accumulatedTime += this.config.averageGameDuration;
    }
  }
}

2.2 Configuração da Fila

typescript
interface QueueConfig {
  // Tempo médio de jogo (ms)
  averageGameDuration: number; // default: 15 * 60 * 1000 (15 min)

  // Máximo de jogos consecutivos (winner stays)
  maxConsecutiveGames: number; // default: 3

  // Fator de envelhecimento (pontos por minuto de espera)
  agingFactor: number; // default: 10

  // Penalidade de fairness (pontos por jogo jogado)
  fairnessPenalty: number; // default: 50

  // Bonus para times completos
  completeTeamBonus: number; // default: 100

  // Máximo de entradas na fila
  maxQueueSize: number; // default: 20

  // Timeout para responder chamada (ms)
  callTimeout: number; // default: 2 * 60 * 1000 (2 min)
}

3. Sistema Winner Stays

3.1 Lógica Principal

typescript
class WinnerStaysManager {
  constructor(
    private courtService: CourtService,
    private queueService: QueueService,
    private notificationService: NotificationService
  ) {}

  /**
   * Processa fim de jogo e determina próxima partida
   */
  async processGameEnd(session: GameSession, result: GameResult): Promise<void> {
    const court = await this.courtService.findById(session.courtId);
    const config = court.config;

    // Determina vencedor e perdedor
    const { winner, loser } = this.determineTeams(session, result);

    // Verifica se vencedor pode ficar
    const canWinnerStay = this.canTeamStay(winner, config);

    if (canWinnerStay) {
      // Vencedor fica, busca próximo desafiante
      const challenger = await this.getNextChallenger(court, winner);

      if (challenger) {
        // Inicia nova partida
        await this.startNewGame(court, winner, challenger.team);

        // Perdedor vai para o fim da fila
        await this.queueService.enqueue(court.queue.id, loser, Priority.NORMAL);

        // Remove desafiante da fila
        await this.queueService.remove(challenger.id);
      } else {
        // Sem desafiantes, vencedor continua ou libera quadra
        await this.handleNoChallenger(court, winner, loser);
      }
    } else {
      // Vencedor atingiu limite, ambos vão para fila
      await this.handleWinnerLimitReached(court, winner, loser);
    }
  }

  /**
   * Verifica se time pode continuar na quadra
   */
  private canTeamStay(team: Team, config: CourtConfig): boolean {
    // Limite de jogos consecutivos
    if (team.gamesPlayed >= config.maxConsecutiveGames) {
      return false;
    }

    // Limite de tempo na quadra
    const timeOnCourt = Date.now() - team.enteredCourtAt.getTime();
    if (timeOnCourt >= config.maxTimeOnCourt) {
      return false;
    }

    return true;
  }

  /**
   * Busca próximo desafiante com skill matching
   */
  private async getNextChallenger(
    court: Court,
    currentTeam: Team
  ): Promise<QueueEntry | null> {
    const queue = await this.queueService.getQueue(court.queue.id);

    if (queue.entries.length === 0) {
      return null;
    }

    // Se skill matching está ativo
    if (court.config.skillMatching) {
      return this.findBestSkillMatch(queue.entries, currentTeam);
    }

    // Caso contrário, retorna primeiro da fila
    return queue.entries[0];
  }

  /**
   * Encontra melhor match por habilidade
   */
  private findBestSkillMatch(
    entries: QueueEntry[],
    currentTeam: Team
  ): QueueEntry {
    const targetRating = currentTeam.skillRating;
    const tolerance = 15; // diferença máxima de rating

    // Filtra times dentro da tolerância
    const matchingEntries = entries.filter(e =>
      Math.abs(e.team.skillRating - targetRating) <= tolerance
    );

    if (matchingEntries.length > 0) {
      // Retorna o que está esperando há mais tempo
      return matchingEntries.reduce((oldest, current) =>
        current.joinedAt < oldest.joinedAt ? current : oldest
      );
    }

    // Se não há match, retorna primeiro da fila
    return entries[0];
  }
}

3.2 Fluxograma do Algoritmo

┌─────────────────┐
│   Fim do Jogo   │
└────────┬────────┘


┌─────────────────────────┐
│ Determinar Vencedor     │
│ e Perdedor              │
└────────┬────────────────┘


┌─────────────────────────┐     Não
│ Vencedor pode ficar?    │────────┐
│ (< max jogos e tempo)   │        │
└────────┬────────────────┘        │
         │ Sim                     │
         ▼                         │
┌─────────────────────────┐        │
│ Há desafiantes na fila? │        │
└────────┬────────────────┘        │
         │                         │
    ┌────┴────┐                    │
    │         │                    │
   Sim       Não                   │
    │         │                    │
    ▼         ▼                    ▼
┌─────────┐ ┌───────────┐ ┌───────────────┐
│ Buscar  │ │ Vencedor  │ │ Ambos times   │
│ próximo │ │ continua  │ │ vão para fila │
│ na fila │ │ treinando │ └───────┬───────┘
└────┬────┘ └─────┬─────┘         │
     │            │               │
     ▼            │               │
┌───────────────┐ │               │
│ Skill Match?  │ │               │
└───────┬───────┘ │               │
        │         │               │
   ┌────┴────┐    │               │
  Sim       Não   │               │
   │         │    │               │
   ▼         ▼    │               │
┌───────┐ ┌─────┐ │               │
│ Busca │ │FIFO │ │               │
│ match │ │     │ │               │
└───┬───┘ └──┬──┘ │               │
    │        │    │               │
    └───┬────┘    │               │
        │         │               │
        ▼         │               │
┌───────────────┐ │               │
│ Nova partida: │ │               │
│ Vencedor vs   │ │               │
│ Desafiante    │ │               │
└───────┬───────┘ │               │
        │         │               │
        ▼         ▼               │
┌───────────────────┐             │
│ Perdedor vai      │             │
│ para fim da fila  │◄────────────┘
└───────────────────┘

4. Skill Matching

4.1 Cálculo de Skill Rating

typescript
class SkillRatingCalculator {
  // Constantes do sistema ELO modificado
  private readonly K_FACTOR = 32;
  private readonly BASE_RATING = 1500;

  /**
   * Calcula novo rating após uma partida
   */
  calculateNewRating(
    player: Player,
    opponent: Player,
    result: MatchResult
  ): number {
    const expectedScore = this.expectedScore(
      player.skillRating,
      opponent.skillRating
    );

    const actualScore = this.getActualScore(result);

    const newRating = player.skillRating +
      this.K_FACTOR * (actualScore - expectedScore);

    // Limita entre 1 e 100 (escala normalizada)
    return Math.max(1, Math.min(100, newRating));
  }

  /**
   * Probabilidade esperada de vitória
   */
  private expectedScore(ratingA: number, ratingB: number): number {
    return 1 / (1 + Math.pow(10, (ratingB - ratingA) / 400));
  }

  /**
   * Pontuação real do resultado
   */
  private getActualScore(result: MatchResult): number {
    switch (result) {
      case MatchResult.WIN: return 1;
      case MatchResult.DRAW: return 0.5;
      case MatchResult.LOSS: return 0;
    }
  }

  /**
   * Calcula rating médio de um time
   */
  calculateTeamRating(team: Team): number {
    if (team.players.length === 0) return this.BASE_RATING;

    const sum = team.players.reduce((acc, p) => acc + p.skillRating, 0);
    return sum / team.players.length;
  }

  /**
   * Determina nível de habilidade baseado no rating
   */
  getSkillLevel(rating: number): SkillLevel {
    if (rating <= 25) return SkillLevel.BEGINNER;
    if (rating <= 50) return SkillLevel.INTERMEDIATE;
    if (rating <= 75) return SkillLevel.ADVANCED;
    return SkillLevel.PROFESSIONAL;
  }
}

4.2 Algoritmo de Matching

typescript
class SkillMatchingService {
  constructor(private ratingCalculator: SkillRatingCalculator) {}

  /**
   * Encontra o melhor oponente na fila
   */
  findBestMatch(
    currentTeam: Team,
    waitingTeams: Team[],
    config: MatchingConfig
  ): Team | null {
    if (waitingTeams.length === 0) return null;

    const currentRating = this.ratingCalculator.calculateTeamRating(currentTeam);

    // Calcula score de match para cada time
    const scoredTeams = waitingTeams.map(team => ({
      team,
      score: this.calculateMatchScore(currentTeam, team, config)
    }));

    // Ordena por melhor score
    scoredTeams.sort((a, b) => b.score - a.score);

    // Retorna melhor match se score mínimo for atingido
    const best = scoredTeams[0];
    if (best.score >= config.minimumMatchScore) {
      return best.team;
    }

    // Fallback para FIFO se não há bom match
    return waitingTeams[0];
  }

  /**
   * Calcula score de compatibilidade entre times
   */
  private calculateMatchScore(
    teamA: Team,
    teamB: Team,
    config: MatchingConfig
  ): number {
    let score = 100;

    // Penalidade por diferença de skill
    const ratingDiff = Math.abs(
      this.ratingCalculator.calculateTeamRating(teamA) -
      this.ratingCalculator.calculateTeamRating(teamB)
    );
    score -= ratingDiff * config.skillDiffPenalty;

    // Bonus por tempo de espera do oponente
    const waitingMinutes = teamB.waitingSince
      ? (Date.now() - teamB.waitingSince.getTime()) / 60000
      : 0;
    score += waitingMinutes * config.waitingTimeBonus;

    // Penalidade se já jogaram juntos recentemente
    if (this.playedRecently(teamA, teamB)) {
      score -= config.recentMatchPenalty;
    }

    return Math.max(0, score);
  }
}

interface MatchingConfig {
  skillDiffPenalty: number;    // default: 2 (por ponto de diferença)
  waitingTimeBonus: number;    // default: 5 (por minuto)
  recentMatchPenalty: number;  // default: 30
  minimumMatchScore: number;   // default: 50
}

5. Otimização de Ocupação

5.1 Court Allocation Optimizer

typescript
class CourtAllocationOptimizer {
  /**
   * Otimiza alocação de jogadores em múltiplas quadras
   */
  async optimizeAllocation(
    arena: Arena,
    availableCourts: Court[],
    waitingPlayers: Player[]
  ): Promise<AllocationPlan> {
    const plan: AllocationPlan = {
      allocations: [],
      unallocatedPlayers: [],
      metrics: {
        courtUtilization: 0,
        averageWaitTime: 0,
        skillMatchQuality: 0
      }
    };

    // Agrupa jogadores por esporte preferido
    const playersBySport = this.groupBySport(waitingPlayers);

    // Para cada esporte
    for (const [sport, players] of playersBySport) {
      const courtsForSport = availableCourts.filter(c => c.sport === sport);

      // Forma times balanceados
      const teams = this.formBalancedTeams(players, sport);

      // Aloca times às quadras
      const allocations = this.allocateTeamsToCourts(teams, courtsForSport);

      plan.allocations.push(...allocations);
    }

    // Calcula métricas
    plan.metrics = this.calculateMetrics(plan);

    return plan;
  }

  /**
   * Forma times balanceados por skill
   */
  private formBalancedTeams(players: Player[], sport: Sport): Team[] {
    const teamSize = this.getTeamSize(sport);
    const teams: Team[] = [];

    // Ordena por skill
    const sortedPlayers = [...players].sort(
      (a, b) => b.skillRating - a.skillRating
    );

    // Usa algoritmo serpentine para balanceamento
    while (sortedPlayers.length >= teamSize) {
      const team: Player[] = [];

      for (let i = 0; i < teamSize; i++) {
        // Alterna entre pegar do início e do fim
        const player = i % 2 === 0
          ? sortedPlayers.shift()!
          : sortedPlayers.pop()!;
        team.push(player);
      }

      teams.push({
        id: generateId(),
        players: team,
        skillRating: this.calculateTeamRating(team),
        gamesPlayed: 0
      });
    }

    return teams;
  }

  /**
   * Aloca times às quadras disponíveis
   */
  private allocateTeamsToCourts(
    teams: Team[],
    courts: Court[]
  ): CourtAllocation[] {
    const allocations: CourtAllocation[] = [];

    // Ordena quadras por prioridade (menos ocupadas primeiro)
    const sortedCourts = [...courts].sort(
      (a, b) => a.queue.entries.length - b.queue.entries.length
    );

    // Distribui times uniformemente
    for (let i = 0; i < teams.length; i++) {
      const court = sortedCourts[i % sortedCourts.length];

      allocations.push({
        teamId: teams[i].id,
        courtId: court.id,
        action: court.status === CourtStatus.AVAILABLE
          ? 'START_GAME'
          : 'ADD_TO_QUEUE'
      });
    }

    return allocations;
  }
}

5.2 Métricas de Eficiência

typescript
interface AllocationMetrics {
  // Taxa de ocupação das quadras (0-100%)
  courtUtilization: number;

  // Tempo médio de espera (minutos)
  averageWaitTime: number;

  // Qualidade do skill matching (0-100)
  skillMatchQuality: number;

  // Jogadores não alocados
  unallocatedCount: number;

  // Eficiência geral (0-100)
  overallEfficiency: number;
}

class MetricsCalculator {
  calculateMetrics(
    courts: Court[],
    allocations: CourtAllocation[]
  ): AllocationMetrics {
    const courtUtilization = this.calculateCourtUtilization(courts);
    const averageWaitTime = this.calculateAverageWaitTime(courts);
    const skillMatchQuality = this.calculateSkillMatchQuality(courts);

    const overallEfficiency = (
      courtUtilization * 0.4 +
      (100 - Math.min(averageWaitTime, 60) / 60 * 100) * 0.3 +
      skillMatchQuality * 0.3
    );

    return {
      courtUtilization,
      averageWaitTime,
      skillMatchQuality,
      unallocatedCount: 0, // calculado separadamente
      overallEfficiency
    };
  }

  private calculateCourtUtilization(courts: Court[]): number {
    const inUse = courts.filter(c => c.status === CourtStatus.IN_USE).length;
    return (inUse / courts.length) * 100;
  }

  private calculateAverageWaitTime(courts: Court[]): number {
    let totalWait = 0;
    let count = 0;

    for (const court of courts) {
      for (const entry of court.queue.entries) {
        const waitTime = (Date.now() - entry.joinedAt.getTime()) / 60000;
        totalWait += waitTime;
        count++;
      }
    }

    return count > 0 ? totalWait / count : 0;
  }
}

6. Integração com Eventos

6.1 Event-Driven Architecture

typescript
// Eventos emitidos pelo sistema de alocação
interface AllocationEvents {
  'queue.player.joined': {
    queueId: string;
    playerId: string;
    position: number;
    estimatedWaitTime: number;
  };

  'queue.player.called': {
    queueId: string;
    playerId: string;
    courtId: string;
    timeToRespond: number;
  };

  'queue.player.timeout': {
    queueId: string;
    playerId: string;
    reason: 'no_response' | 'declined';
  };

  'game.started': {
    courtId: string;
    sessionId: string;
    teams: Team[];
    isWinnerStays: boolean;
  };

  'game.ended': {
    courtId: string;
    sessionId: string;
    winner: Team;
    loser: Team;
    duration: number;
    score: Score;
  };

  'court.status.changed': {
    courtId: string;
    previousStatus: CourtStatus;
    newStatus: CourtStatus;
  };
}

// Handler de eventos
class AllocationEventHandler {
  constructor(
    private eventBus: EventBus,
    private notificationService: NotificationService,
    private analyticsService: AnalyticsService
  ) {
    this.registerHandlers();
  }

  private registerHandlers(): void {
    this.eventBus.on('queue.player.called', async (event) => {
      // Notifica jogador via push notification
      await this.notificationService.sendPush(event.playerId, {
        title: 'Sua vez de jogar!',
        body: `Dirija-se à quadra. Você tem ${event.timeToRespond / 60}min.`,
        action: 'COURT_READY'
      });
    });

    this.eventBus.on('game.ended', async (event) => {
      // Registra analytics
      await this.analyticsService.trackGameEnd({
        courtId: event.courtId,
        duration: event.duration,
        ...event
      });
    });
  }
}

7. Casos de Uso

7.1 Beach Tennis - Winner Stays

typescript
// Configuração típica para beach tennis
const beachTennisConfig: CourtConfig = {
  sport: Sport.BEACH_TENNIS,
  teamSize: 2,
  maxConsecutiveGames: 3,
  maxTimeOnCourt: 45 * 60 * 1000, // 45 minutos
  winnerStays: true,
  skillMatching: true,
  skillMatchTolerance: 15,
  averageGameDuration: 15 * 60 * 1000, // 15 minutos
};

// Exemplo de fluxo
async function beachTennisFlow(court: Court) {
  // 1. Times A e B jogando
  // 2. Time A vence (6x4)
  // 3. Time A já jogou 2 partidas (pode ficar mais 1)
  // 4. Time C é o próximo na fila (skill compatível)
  // 5. Time B vai para o fim da fila
  // 6. Time A vs Time C começa
}

7.2 Futevôlei - Rotação

typescript
// Configuração para futevôlei com rotação
const futevoleiConfig: CourtConfig = {
  sport: Sport.FUTEVOLEI,
  teamSize: 2,
  maxConsecutiveGames: 2, // menos jogos por ser mais cansativo
  maxTimeOnCourt: 30 * 60 * 1000, // 30 minutos
  winnerStays: false, // rotação após cada jogo
  skillMatching: false, // mais casual
  averageGameDuration: 12 * 60 * 1000, // 12 minutos
};

8. API Reference

8.1 Endpoints

yaml
# Gestão de Filas
POST   /api/v1/courts/{courtId}/queue          # Entrar na fila
DELETE /api/v1/courts/{courtId}/queue/{entryId} # Sair da fila
GET    /api/v1/courts/{courtId}/queue          # Listar fila
POST   /api/v1/courts/{courtId}/queue/call     # Chamar próximo

# Sessões de Jogo
POST   /api/v1/courts/{courtId}/sessions       # Iniciar sessão
PUT    /api/v1/courts/{courtId}/sessions/{id}  # Atualizar placar
POST   /api/v1/courts/{courtId}/sessions/{id}/end # Finalizar

# Alocação
POST   /api/v1/arenas/{arenaId}/allocate       # Auto-alocação
GET    /api/v1/arenas/{arenaId}/allocation/metrics # Métricas

8.2 WebSocket Events

typescript
// Cliente se conecta ao WebSocket da quadra
ws.connect(`/ws/courts/${courtId}`);

// Eventos recebidos pelo cliente
ws.on('queue:updated', (data: QueueUpdate) => {
  // Atualiza UI da fila
});

ws.on('player:called', (data: PlayerCalled) => {
  // Mostra notificação
});

ws.on('game:started', (data: GameStarted) => {
  // Atualiza status da quadra
});

ws.on('score:updated', (data: ScoreUpdate) => {
  // Atualiza placar em tempo real
});

9. Performance Considerations

9.1 Otimizações

typescript
// Cache de filas em Redis
class CachedQueueService {
  private readonly QUEUE_TTL = 3600; // 1 hora

  async getQueue(courtId: string): Promise<WaitingQueue> {
    const cached = await this.redis.get(`queue:${courtId}`);
    if (cached) {
      return JSON.parse(cached);
    }

    const queue = await this.db.queues.findByCourtId(courtId);
    await this.redis.setex(
      `queue:${courtId}`,
      this.QUEUE_TTL,
      JSON.stringify(queue)
    );

    return queue;
  }

  async invalidateCache(courtId: string): Promise<void> {
    await this.redis.del(`queue:${courtId}`);
    // Publica evento para outros pods
    await this.redis.publish('queue:invalidated', courtId);
  }
}

9.2 Índices de Banco

sql
-- Índices para queries frequentes
CREATE INDEX idx_queue_entries_court_status
  ON queue_entries(court_id, status)
  WHERE status = 'WAITING';

CREATE INDEX idx_queue_entries_priority
  ON queue_entries(court_id, priority DESC, joined_at ASC);

CREATE INDEX idx_game_sessions_court_active
  ON game_sessions(court_id)
  WHERE status = 'IN_PROGRESS';

10. Testes

10.1 Cenários de Teste

gherkin
Feature: Winner Stays Algorithm

  Scenario: Vencedor permanece após vitória
    Given uma quadra com "winner stays" ativo
    And Time A e Time B estão jogando
    And Time C está na fila
    When Time A vence a partida
    And Time A jogou menos de 3 partidas
    Then Time A deve permanecer na quadra
    And Time B deve ir para o fim da fila
    And Time C deve ser chamado para jogar

  Scenario: Vencedor sai após limite de jogos
    Given uma quadra com max_consecutive_games = 3
    And Time A venceu 3 partidas consecutivas
    When Time A vence mais uma partida
    Then Time A deve ir para o fim da fila
    And próximo time da fila deve iniciar novo ciclo

Este documento detalha o algoritmo de alocação de quadras do Sport Tech Club, cobrindo desde a estrutura de dados até a implementação do sistema winner-stays e otimização de ocupação.