diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index 15eee50..da476cb 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -115,9 +115,38 @@ final class GroupStage: ModelObject, Storable { return match } + func removeReturnMatches() { + let returnMatches = _matches().filter({ $0.index >= matchCount }) + do { + try self.tournamentStore.matches.delete(contentOfs: returnMatches) + } catch { + Logger.error(error) + } + } + + func addReturnMatches() { + var teamScores = [TeamScore]() + var matches = [Match]() + + for i in 0..<_numberOfMatchesToBuild() { + let newMatch = self._createMatch(index: i + matchCount) +// let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) + teamScores.append(contentsOf: newMatch.createTeamScores()) + matches.append(newMatch) + } + + do { + try self.tournamentStore.matches.addOrUpdate(contentOfs: matches) + try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) + } catch { + Logger.error(error) + } + } + func buildMatches(keepExistingMatches: Bool = false) { var teamScores = [TeamScore]() var matches = [Match]() + clearScoreCache() if keepExistingMatches == false { _removeMatches() @@ -227,7 +256,7 @@ final class GroupStage: ModelObject, Storable { matchIndexes.append(index) } } - return _matches().filter { matchIndexes.contains($0.index) } + return _matches().filter { matchIndexes.contains($0.index%matchCount) } } func initialStartDate(forTeam team: TeamRegistration) -> Date? { @@ -291,40 +320,58 @@ final class GroupStage: ModelObject, Storable { return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed() } + func isReturnMatchEnabled() -> Bool { + _matches().count > matchCount + } + private func _matchOrder() -> [Int] { + var order: [Int] + switch size { case 3: - return [1, 2, 0] + order = [1, 2, 0] case 4: - return [2, 3, 1, 4, 5, 0] + order = [2, 3, 1, 4, 5, 0] case 5: -// return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] - return [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] + order = [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] case 6: - //return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] - return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] + order = [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] default: - return [] + order = [] } + + if isReturnMatchEnabled() { + let arraySize = order.count + // Duplicate and increment each value by the array size + order += order.map { $0 + arraySize } + } + + return order } - + + func indexOf(_ matchIndex: Int) -> Int { _matchOrder().firstIndex(of: matchIndex) ?? matchIndex } private func _matchUp(for matchIndex: Int) -> [Int] { - Array((0.. String { let matchUp = _matchUp(for: matchIndex) if let index = matchUp.first, let index2 = matchUp.last { - return "#\(index + 1) vs #\(index2 + 1)" + return "#\(index + 1) vs #\(index2 + 1)" + (matchIndex >= matchCount ? " - retour" : "") } else { return "--" } } + var matchCount: Int { + (size * size - 1) / 2 + } + func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { let _teams = _teams(for: matchIndex) switch team { @@ -337,7 +384,7 @@ final class GroupStage: ModelObject, Storable { private func _teams(for matchIndex: Int) -> [TeamRegistration?] { let combinations = Array(0.. Bool { let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let combos = Array((0.. 1 { + let scoreA = calculateScore(for: teamPosition, matches: matches, groupStagePosition: teamPosition.groupStagePosition!) + let scoreB = calculateScore(for: otherTeam, matches: matches, groupStagePosition: otherTeam.groupStagePosition!) + + let teamsSorted = [scoreA, scoreB].sorted { (lhs, rhs) in + let predicates: [TeamScoreAreInIncreasingOrder] = [ + { $0.wins < $1.wins }, + { $0.setDifference < $1.setDifference }, + { $0.gameDifference < $1.gameDifference}, + { [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! } + ] + + for predicate in predicates { + if !predicate(lhs, rhs) && !predicate(rhs, lhs) { + continue + } + + return predicate(lhs, rhs) + } + + return false + }.map({ $0.team }) + + return teamsSorted.first == teamPosition } else { - return false + + if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) { + return teamPosition.id == match.losingTeamId + } else { + return false + } + } } @@ -428,16 +505,19 @@ final class GroupStage: ModelObject, Storable { guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) if matches.isEmpty && nilIfEmpty { return nil } + let score = calculateScore(for: team, matches: matches, groupStagePosition: groupStagePosition) + scoreCache[groupStagePosition] = score + return score + } + + private func calculateScore(for team: TeamRegistration, matches: [Match], groupStagePosition: Int) -> TeamGroupStageScore { let wins = matches.filter { $0.winningTeamId == team.id }.count let loses = matches.filter { $0.losingTeamId == team.id }.count let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } let setDifference = differences.map { $0.set }.reduce(0,+) let gameDifference = differences.map { $0.game }.reduce(0,+) - // Calculate the score and store it in the cache - let score = (team, wins, loses, setDifference, gameDifference) - scoreCache[groupStagePosition] = score - return score + return (team, wins, loses, setDifference, gameDifference) } // Clear the cache if necessary, for example when starting a new step or when matches update diff --git a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift index ad0d943..5bfa715 100644 --- a/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift +++ b/PadelClub/Views/GroupStage/Components/GroupStageSettingsView.swift @@ -143,12 +143,28 @@ struct GroupStageSettingsView: View { Section { RowButtonView("Recommencer tous les matchs", role: .destructive) { + let isReturnMatchesEnabled = groupStage.isReturnMatchEnabled() groupStage.buildMatches() + if isReturnMatchesEnabled { + groupStage.addReturnMatches() + } } } footer: { Text("Tous les matchs seront recronstruits, les données des matchs seront perdus.") } + Section { + if groupStage.isReturnMatchEnabled() { + RowButtonView("Effacer les matchs retours", role: .destructive) { + groupStage.removeReturnMatches() + } + } else { + RowButtonView("Rajouter les matchs retours", role: .destructive) { + groupStage.addReturnMatches() + } + } + } + Section { RowButtonView("Rafraichir", role: .destructive) { let playedMatches = groupStage.playedMatches() diff --git a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift index 8f31945..bafec28 100644 --- a/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift +++ b/PadelClub/Views/GroupStage/GroupStagesSettingsView.swift @@ -12,7 +12,7 @@ struct GroupStagesSettingsView: View { @EnvironmentObject var dataStore: DataStore @Environment(\.dismiss) private var dismiss @Environment(Tournament.self) var tournament: Tournament - @State private var generationDone: Bool = false + @State private var generationDoneMessage: String? let step: Int var tournamentStore: TournamentStore { @@ -167,6 +167,32 @@ struct GroupStagesSettingsView: View { } footer: { Text("Redistribue les équipes par la méthode du serpentin") } + + let groupStages = tournament.groupStages() + + Section { + if groupStages.anySatisfy({ $0.isReturnMatchEnabled() }) { + RowButtonView("Effacer les matchs retours", role: .destructive) { + groupStages.filter({ $0.isReturnMatchEnabled() }).forEach { groupStage in + groupStage.removeReturnMatches() + } + generationDoneMessage = "Matchs retours effacés" + } + } + + } + + Section { + if groupStages.anySatisfy({ $0.isReturnMatchEnabled() == false }) { + RowButtonView("Rajouter les matchs retours", role: .destructive) { + groupStages.filter({ $0.isReturnMatchEnabled() == false }).forEach { groupStage in + groupStage.addReturnMatches() + } + + generationDoneMessage = "Matchs retours créés" + } + } + } Section { RowButtonView("Nommer les poules alphabétiquement", role: .destructive) { @@ -220,8 +246,8 @@ struct GroupStagesSettingsView: View { } .overlay(alignment: .bottom) { - if generationDone { - Label("Poules mises à jour", systemImage: "checkmark.circle.fill") + if let generationDoneMessage { + Label(generationDoneMessage, systemImage: "checkmark.circle.fill") .toastFormatted() .deferredRendering(for: .seconds(2)) } @@ -238,7 +264,7 @@ struct GroupStagesSettingsView: View { RowButtonView("Refaire les poules", role: .destructive) { tournament.deleteGroupStages() tournament.buildGroupStages() - generationDone = true + generationDoneMessage = "Poules mises à jour" tournament.shouldVerifyGroupStage = false _save() } @@ -249,7 +275,7 @@ struct GroupStagesSettingsView: View { RowButtonView("Poule \(mode.localizedLabel().lowercased())", role: .destructive, systemImage: mode.systemImage) { tournament.groupStageOrderingMode = mode tournament.refreshGroupStages(keepExistingMatches: true) - generationDone = true + generationDoneMessage = "Poules mises à jour" tournament.shouldVerifyGroupStage = false _save() }