From 970e89b2e5e426e99b21ee6774d36d859089a6a7 Mon Sep 17 00:00:00 2001 From: Raz Date: Sun, 4 May 2025 20:20:32 +0200 Subject: [PATCH] fix issue with event --- .../Extensions/Tournament+Extensions.swift | 3 +- PadelClub/Views/Cashier/Event/EventView.swift | 2 +- .../Views/Tournament/AnimationMeleeView.swift | 18 + .../Views/Tournament/FileImportView.swift | 10 +- .../PlayerRotationTournamentGenerator.swift | 358 ++++++++++++++++++ 5 files changed, 384 insertions(+), 7 deletions(-) create mode 100644 PadelClub/Views/Tournament/AnimationMeleeView.swift create mode 100644 PadelClub/Views/Tournament/PlayerRotationTournamentGenerator.swift diff --git a/PadelClub/Extensions/Tournament+Extensions.swift b/PadelClub/Extensions/Tournament+Extensions.swift index 7b09900..50c7c92 100644 --- a/PadelClub/Extensions/Tournament+Extensions.swift +++ b/PadelClub/Extensions/Tournament+Extensions.swift @@ -225,7 +225,8 @@ extension Tournament { if let tournamentStore = self.tournamentStore { tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teamsToImport) - tournamentStore.playerRegistrations.addOrUpdate(contentOfs: teams.flatMap { $0.players }) + let playersToImport = teams.flatMap { $0.players } + tournamentStore.playerRegistrations.addOrUpdate(contentOfs: playersToImport) } if state() == .build && groupStageCount > 0 && groupStageTeams().isEmpty { diff --git a/PadelClub/Views/Cashier/Event/EventView.swift b/PadelClub/Views/Cashier/Event/EventView.swift index 576bb50..99ae840 100644 --- a/PadelClub/Views/Cashier/Event/EventView.swift +++ b/PadelClub/Views/Cashier/Event/EventView.swift @@ -95,7 +95,7 @@ struct EventView: View { case .tournaments(let event): EventTournamentsView(event: event) case .cashier: - CashierDetailView(tournaments: event.tournaments.filter({ $0.isFree() == false })) + CashierDetailView(tournaments: event.paidTournaments()) } } } diff --git a/PadelClub/Views/Tournament/AnimationMeleeView.swift b/PadelClub/Views/Tournament/AnimationMeleeView.swift new file mode 100644 index 0000000..88f1d89 --- /dev/null +++ b/PadelClub/Views/Tournament/AnimationMeleeView.swift @@ -0,0 +1,18 @@ +// +// AnimationMeleeView.swift +// PadelClub +// +// Created by razmig on 03/05/2025. +// + +import SwiftUI + +struct AnimationMeleeView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + AnimationMeleeView() +} diff --git a/PadelClub/Views/Tournament/FileImportView.swift b/PadelClub/Views/Tournament/FileImportView.swift index d1d26f2..7e71726 100644 --- a/PadelClub/Views/Tournament/FileImportView.swift +++ b/PadelClub/Views/Tournament/FileImportView.swift @@ -192,7 +192,7 @@ struct FileImportView: View { } } - if let event = tournament.eventObject(), event.tournaments.count > 1, fileProvider == .frenchFederation { + if let event = tournament.eventObject(), event.federalTournaments().count > 1, fileProvider == .frenchFederation { Section { RowButtonView("Importer pour tous les tournois") { multiImport = true @@ -322,7 +322,7 @@ struct FileImportView: View { let _filteredTeams = filteredTeams let previousTeams = tournament.sortedTeams(selectedSortedTeams: tournament.selectedSortedTeams()) - if multiImport, fileProvider == .frenchFederation, let tournaments = tournament.eventObject()?.tournaments, tournaments.count > 1 { + if multiImport, fileProvider == .frenchFederation, let tournaments = tournament.eventObject()?.federalTournaments(), tournaments.count > 1 { ForEach(tournaments) { tournament in let tournamentFilteredTeams = self.filteredTeams(tournament: tournament) @@ -471,7 +471,7 @@ struct FileImportView: View { ButtonValidateView(title: (multiImport ? "Tout Valider" : "Valider")) { validationInProgress = true Task { - if let tournaments = tournament.eventObject()?.tournaments, tournaments.count > 1, multiImport { + if let tournaments = tournament.eventObject()?.federalTournaments(), tournaments.count > 1, multiImport { for tournament in tournaments { if validatedTournamentIds.contains(tournament.id) == false { await _validate(tournament: tournament) @@ -538,9 +538,9 @@ struct FileImportView: View { } let event: Event? = tournament.eventObject() - if let event, event.tournaments.count > 1 { + if let event, event.federalTournaments().count > 1 { var categoriesDone: [CombinedCategory] = [] - for someTournament in event.tournaments { + for someTournament in event.federalTournaments() { let combinedCategory = CombinedCategory(tournamentCategory: someTournament.tournamentCategory, federalTournamentAge: someTournament.federalTournamentAge) if categoriesDone.contains(combinedCategory) == false { let _teams = try await FileImportManager.shared.createTeams(from: fileContent, tournament: someTournament, fileProvider: fileProvider, checkingCategoryDisabled: false, chunkByParameter: chunkByParameter) diff --git a/PadelClub/Views/Tournament/PlayerRotationTournamentGenerator.swift b/PadelClub/Views/Tournament/PlayerRotationTournamentGenerator.swift new file mode 100644 index 0000000..5e36a46 --- /dev/null +++ b/PadelClub/Views/Tournament/PlayerRotationTournamentGenerator.swift @@ -0,0 +1,358 @@ +import Foundation + +// MARK: - Player Rotation Tournament Generator + +class PlayerRotationTournamentGenerator { + + // MARK: - Public Function + + /// Generate a round-robin tournament where each player plays with every other player exactly once + /// - Parameters: + /// - playerRegistrations: List of player registrations to schedule + /// - courtsAvailable: Number of courts available + /// - Returns: Tournament data with rounds and matches + func generateTournament(playerRegistrations: [PlayerRegistration], courtsAvailable: Int) -> [Round] { + guard playerRegistrations.count >= 4, playerRegistrations.count % 2 == 0 else { + print("Error: Player count must be even and at least 4") + return [] + } + + let players = playerRegistrations + let playerCount = players.count + let numberOfRounds = playerCount - 1 + + var rounds: [Round] = [] + + // Track partnerships to ensure each player partners with every other player exactly once + var usedPartnerships: Set = [] + + // Track oppositions to balance who plays against whom + var oppositionCount: [String: Int] = [:] + + // Generate all rounds using circle method + for roundNumber in 0.. TeamRegistration in + createTemporaryTeam(player1: pair.0, player2: pair.1) + } + + // 3. Create matches by pairing teams + let matchPairs = createMatchPairsForRound( + teams: teams, + oppositionCount: &oppositionCount, + courtsAvailable: courtsAvailable + ) + + // 4. Create matches and team scores + let matches = matchPairs.map { pair -> Match in + let match = Match() + match.court = pair.court + + // Create team scores for this match + let teamScore1 = TeamScore() + teamScore1.team_registration = pair.team1 + teamScore1.match = match + + let teamScore2 = TeamScore() + teamScore2.team_registration = pair.team2 + teamScore2.match = match + + // Link team scores to match + match.team_scores = [teamScore1, teamScore2] + + return match + } + + // 5. Create round + let round = Round() + round.number = roundNumber + 1 + round.matches = matches + + rounds.append(round) + } + + return rounds + } + + // MARK: - Private Helper Functions + + /// Generate player partnerships for a specific round using the circle method + private func generatePartnershipsForRound( + players: [PlayerRegistration], + roundNumber: Int, + usedPartnerships: inout Set + ) -> [(PlayerRegistration, PlayerRegistration)] { + + var partnerships: [(PlayerRegistration, PlayerRegistration)] = [] + let playerCount = players.count + + // Create a circle of players + // Player 0 stays fixed in the center, all others rotate + + // 1. Player 0 partners with a different player each round + let centerPlayer = players[0] + let partnerIndex = (roundNumber + 1) % playerCount + let partnerForCenterPlayer = players[partnerIndex] + + // Check if partnership is already used (shouldn't happen with correct algorithm) + let partnershipKey = createPartnershipKey(player1: centerPlayer, player2: partnerForCenterPlayer) + if !usedPartnerships.contains(partnershipKey) { + usedPartnerships.insert(partnershipKey) + partnerships.append((centerPlayer, partnerForCenterPlayer)) + } + + // 2. Create a set of players already assigned + var assignedPlayers = Set([centerPlayer.id, partnerForCenterPlayer.id]) + + // 3. Get remaining players and rotate them based on round number + var remainingPlayers = players.filter { + !assignedPlayers.contains($0.id) + } + + // Apply rotation based on round number + if roundNumber > 0 { + // Rotate the players clockwise + let offset = roundNumber % remainingPlayers.count + remainingPlayers = Array(remainingPlayers.rotated(by: offset)) + } + + // 4. Pair players from opposite sides of the circle + let halfCount = remainingPlayers.count / 2 + for i in 0.. [MatchPair] { + + let teamCount = teams.count + let maxMatches = min(teamCount / 2, courtsAvailable) + var matchPairs: [MatchPair] = [] + + // Create a copy of teams we can modify + var remainingTeams = teams + + // For the first round, just create sequential matches + if oppositionCount.isEmpty { + for court in 0..= 2 else { break } + + let team1 = remainingTeams.removeFirst() + let team2 = remainingTeams.removeFirst() + + matchPairs.append(MatchPair(team1: team1, team2: team2, court: court + 1)) + + // Update opposition counts + updateOppositionCounts(team1: team1, team2: team2, oppositionCount: &oppositionCount) + } + return matchPairs + } + + // For subsequent rounds, optimize for balanced opposition + for court in 0..= 2 else { break } + + // Take the first team + let team1 = remainingTeams.removeFirst() + + // Find best opponent (team that these players have faced the least) + var bestOpponentIndex = 0 + var lowestOppositionScore = Int.max + + for (index, potentialOpponent) in remainingTeams.enumerated() { + let oppositionScore = calculateOppositionScore( + team1: team1, + team2: potentialOpponent, + oppositionCount: oppositionCount + ) + + if oppositionScore < lowestOppositionScore { + lowestOppositionScore = oppositionScore + bestOpponentIndex = index + } + } + + // Select the best opponent + let team2 = remainingTeams.remove(at: bestOpponentIndex) + + // Create the match pair + matchPairs.append(MatchPair(team1: team1, team2: team2, court: court + 1)) + + // Update opposition counts + updateOppositionCounts(team1: team1, team2: team2, oppositionCount: &oppositionCount) + } + + return matchPairs + } + + /// Calculates an opposition score between two teams (lower is better) + private func calculateOppositionScore( + team1: TeamRegistration, + team2: TeamRegistration, + oppositionCount: [String: Int] + ) -> Int { + let team1Players = team1.player_registrations + let team2Players = team2.player_registrations + + var total = 0 + for player1 in team1Players { + for player2 in team2Players { + let key = createOppositionKey(player1: player1, player2: player2) + total += oppositionCount[key, default: 0] + } + } + + return total + } + + /// Updates opposition counts after a match is scheduled + private func updateOppositionCounts( + team1: TeamRegistration, + team2: TeamRegistration, + oppositionCount: inout [String: Int] + ) { + let team1Players = team1.player_registrations + let team2Players = team2.player_registrations + + for player1 in team1Players { + for player2 in team2Players { + let key = createOppositionKey(player1: player1, player2: player2) + oppositionCount[key, default: 0] += 1 + } + } + } + + // MARK: - Helper Functions for Keys and Objects Creation + + /// Create a unique key for a partnership + private func createPartnershipKey(player1: PlayerRegistration, player2: PlayerRegistration) -> String { + let ids = [player1.id, player2.id].sorted() + return ids.joined(separator: "-") + } + + /// Create a unique key for opposition tracking + private func createOppositionKey(player1: PlayerRegistration, player2: PlayerRegistration) -> String { + let ids = [player1.id, player2.id].sorted() + return ids.joined(separator: "-") + } + + /// Create a temporary team from two players + private func createTemporaryTeam(player1: PlayerRegistration, player2: PlayerRegistration) -> TeamRegistration { + let team = TeamRegistration() + + // Set the team's player registrations + player1.team_registration = team + player2.team_registration = team + + // For convenience during algorithm + team.player_registrations = [player1, player2] + + return team + } +} + +// MARK: - Helper Models (Temporary) + +// Using these for convenience in the algorithm +extension TeamRegistration { + var player_registrations: [PlayerRegistration] { + get { + return objc_getAssociatedObject(self, &playerRegistrationsKey) as? [PlayerRegistration] ?? [] + } + set { + objc_setAssociatedObject(self, &playerRegistrationsKey, newValue, .OBJC_ASSOCIATION_RETAIN) + } + } +} + +private var playerRegistrationsKey: UInt8 = 0 + +extension Round { + var matches: [Match] { + get { + return objc_getAssociatedObject(self, &matchesKey) as? [Match] ?? [] + } + set { + objc_setAssociatedObject(self, &matchesKey, newValue, .OBJC_ASSOCIATION_RETAIN) + } + } +} + +private var matchesKey: UInt8 = 0 + +extension Match { + var team_scores: [TeamScore] { + get { + return objc_getAssociatedObject(self, &teamScoresKey) as? [TeamScore] ?? [] + } + set { + objc_setAssociatedObject(self, &teamScoresKey, newValue, .OBJC_ASSOCIATION_RETAIN) + } + } + + var court: Int { + get { + return objc_getAssociatedObject(self, &courtKey) as? Int ?? 0 + } + set { + objc_setAssociatedObject(self, &courtKey, newValue, .OBJC_ASSOCIATION_RETAIN) + } + } +} + +private var teamScoresKey: UInt8 = 0 +private var courtKey: UInt8 = 0 + +// MARK: - Array Extension +extension Array { + func rotated(by positions: Int) -> Array { + let normalizedPositions = positions % count + return Array(self[normalizedPositions.. [Round] { + let generator = PlayerRotationTournamentGenerator() + return generator.generateTournament(playerRegistrations: players, courtsAvailable: courtsAvailable) +} \ No newline at end of file