From c0475025abe441e5c6878dcc02d51c438332174c Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sat, 6 Apr 2024 11:38:08 +0200 Subject: [PATCH] loser bracket system --- PadelClub/Data/GroupStage.swift | 8 +- PadelClub/Data/Match.swift | 146 +++++++++--------- PadelClub/Data/Round.swift | 60 +++---- PadelClub/Data/TeamRegistration.swift | 10 +- PadelClub/Data/TeamScore.swift | 4 +- PadelClub/Data/Tournament.swift | 12 +- PadelClub/Manager/PadelRule.swift | 8 +- PadelClub/ViewModel/MatchDescriptor.swift | 2 +- PadelClub/ViewModel/SetDescriptor.swift | 2 +- PadelClub/Views/Match/MatchRowView.swift | 4 +- PadelClub/Views/Match/MatchSetupView.swift | 110 ++++++++----- PadelClub/Views/Match/MatchSummaryView.swift | 4 +- PadelClub/Views/Match/PlayerBlockView.swift | 18 ++- PadelClub/Views/Round/LoserRoundsView.swift | 41 +++++ PadelClub/Views/Round/RoundSettingsView.swift | 46 +++--- PadelClub/Views/Round/RoundView.swift | 29 +++- PadelClub/Views/Round/RoundsView.swift | 8 +- PadelClub/Views/Score/EditScoreView.swift | 2 +- PadelClub/Views/Team/TeamPickerView.swift | 24 ++- PadelClub/Views/Team/TeamRowView.swift | 6 +- 20 files changed, 326 insertions(+), 218 deletions(-) diff --git a/PadelClub/Data/GroupStage.swift b/PadelClub/Data/GroupStage.swift index ed50a94..04e6388 100644 --- a/PadelClub/Data/GroupStage.swift +++ b/PadelClub/Data/GroupStage.swift @@ -172,7 +172,7 @@ class GroupStage: ModelObject, Storable { } } - func team(whichTeam team: TeamData, inMatchIndex matchIndex: Int) -> TeamRegistration? { + func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { let _teams = _teams(for: matchIndex) switch team { case .one: @@ -202,11 +202,11 @@ class GroupStage: ModelObject, Storable { fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) - fileprivate func _headToHead(_ whichTeam: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool { - let indexes = [whichTeam, otherTeam].compactMap({ $0.groupStagePosition }).sorted() + fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool { + let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let combos = Array((0.. Bool { + guard let bracketPosition = team.bracketPosition else { return false } + return index * 2 + teamPosition.rawValue == bracketPosition + } + + func resetMatch() { + losingTeamId = nil + winningTeamId = nil + endDate = nil + court = nil + servingTeamId = nil + } + + func teamWillBeWalkOut(_ team: TeamRegistration) { + resetMatch() + let previousScores = teamScores.filter({ $0.luckyLoser != nil }) + try? DataStore.shared.teamScores.delete(contentOfs: previousScores) + + if let existingTeamScore = teamScore(ofTeam: team) { + try? DataStore.shared.teamScores.delete(instance: existingTeamScore) + } + + let teamScoreWalkout = TeamScore(match: id, teamRegistration: team.id) + teamScoreWalkout.walkOut = 1 + try? DataStore.shared.teamScores.addOrUpdate(instance: teamScoreWalkout) + } + + func luckyLosers() -> [TeamRegistration] { + roundObject?.previousRound()?.losers() ?? [] + } + + func isWalkOutSpot(_ teamPosition: TeamPosition) -> Bool { + teamScore(teamPosition)?.walkOut == 1 + } + + func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) { + resetMatch() + let previousScores = teamScores.filter({ $0.luckyLoser != nil }) + try? DataStore.shared.teamScores.delete(contentOfs: previousScores) + + if let existingTeamScore = teamScore(ofTeam: team) { + try? DataStore.shared.teamScores.delete(instance: existingTeamScore) + } + + let matchIndex = index + let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) + let position = matchIndex * 2 + teamPosition.rawValue + let teamScoreLuckyLoser = TeamScore(match: id, teamRegistration: team.id) + teamScoreLuckyLoser.luckyLoser = position + try? DataStore.shared.teamScores.addOrUpdate(instance: teamScoreLuckyLoser) + } + func disableMatch() { _toggleMatchDisableState(true) } @@ -121,8 +173,16 @@ class Match: ModelObject, Storable { }.sorted(by: \.index).first } - func previousMatch(_ teamPosition: Int) -> Match? { - if teamPosition == 0 { + func upperBracketMatch(_ teamPosition: TeamPosition) -> Match? { + if teamPosition == .one { + return roundObject?.upperBracketTopMatch(ofMatchIndex: index) + } else { + return roundObject?.upperBracketBottomMatch(ofMatchIndex: index) + } + } + + func previousMatch(_ teamPosition: TeamPosition) -> Match? { + if teamPosition == .one { return topPreviousRoundMatch() } else { return bottomPreviousRoundMatch() @@ -146,10 +206,10 @@ class Match: ModelObject, Storable { } } - func setWalkOut(_ whichTeam: TeamData) { - let teamScoreWalkout = teamScore(whichTeam) ?? TeamScore(match: id, teamRegistration: team(whichTeam)?.id) + func setWalkOut(_ teamPosition: TeamPosition) { + let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, teamRegistration: team(teamPosition)?.id) teamScoreWalkout.walkOut = 0 - let teamScoreWinning = teamScore(whichTeam.otherTeam) ?? TeamScore(match: id, teamRegistration: team(whichTeam.otherTeam)?.id) + let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, teamRegistration: team(teamPosition.otherTeam)?.id) teamScoreWinning.walkOut = nil try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) @@ -301,10 +361,10 @@ class Match: ModelObject, Storable { return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } } - func scoreDifference(_ whichTeam: Int) -> (set: Int, game: Int)? { + func scoreDifference(_ teamPosition: Int) -> (set: Int, game: Int)? { guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil } var reverseValue = 1 - if whichTeam == team(.two)?.groupStagePosition { + if teamPosition == team(.two)?.groupStagePosition { reverseValue = -1 } let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) @@ -320,76 +380,14 @@ class Match: ModelObject, Storable { return (setDifference * reverseValue, gameDifference * reverseValue) } - func groupStageProjectedTeam(_ team: TeamData) -> TeamRegistration? { + func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let groupStageObject else { return nil } - return groupStageObject.team(whichTeam: team, inMatchIndex: index) - } - - func seed(_ team: TeamData) -> TeamRegistration? { - guard let roundObject else { return nil } - return Store.main.filter(isIncluded: { - $0.tournament == roundObject.tournament && $0.bracketPosition != nil - }).first(where: { - ($0.bracketPosition! / 2) == self.index - && ($0.bracketPosition! % 2) == team.rawValue - }) - } - - func isBye() -> Bool { - guard let roundObject else { return false } - return topPreviousRoundMatch()?.disabled == true || bottomPreviousRoundMatch()?.disabled == true + return groupStageObject.team(teamPosition: team, inMatchIndex: index) } - func upperBracketTopMatch() -> Match? { - guard let roundObject else { return nil } - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) - if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { - return upperBracketTopMatch - } - return nil - } - - func upperBracketBottomMatch() -> Match? { - guard let roundObject else { return nil } - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) - if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { - return upperBracketBottomMatch - } - return nil - } - - func roundProjectedTeam(_ team: TeamData) -> TeamRegistration? { + func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let roundObject else { return nil } return roundObject.roundProjectedTeam(team, inMatch: self) - - if roundObject.isLoserBracket() == false, let seed = seed(team) { - return seed - } - - switch team { - case .one: - if let loser = upperBracketTopMatch()?.losingTeamId { - return Store.main.findById(loser) - } else if let match = topPreviousRoundMatch() { - if let teamId = match.winningTeamId { - return Store.main.findById(teamId) - } else if match.disabled { - return match.teams().first - } - } - case .two: - if let loser = upperBracketBottomMatch()?.losingTeamId { - return Store.main.findById(loser) - } else if let match = bottomPreviousRoundMatch() { - if let teamId = match.winningTeamId { - return Store.main.findById(teamId) - } else if match.disabled { - return match.teams().first - } - } - } - - return nil } func teamWon(_ team: TeamRegistration?) -> Bool { @@ -397,7 +395,7 @@ class Match: ModelObject, Storable { return winningTeamId == team?.id } - func team(_ team: TeamData) -> TeamRegistration? { + func team(_ team: TeamPosition) -> TeamRegistration? { if groupStage != nil { switch team { case .one: @@ -423,7 +421,7 @@ class Match: ModelObject, Storable { teamScore(ofTeam: team)?.isWalkOut() == true } - func teamScore(_ team: TeamData) -> TeamScore? { + func teamScore(_ team: TeamPosition) -> TeamScore? { teamScore(ofTeam: self.team(team)) } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index b7d5f47..68a4378 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -51,7 +51,7 @@ class Round: ModelObject, Storable { Store.main.filter { $0.round == self.id } } - func team(_ team: TeamData, inMatch match: Match) -> TeamRegistration? { + func team(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? { switch team { case .one: return roundProjectedTeam(.one, inMatch: match) @@ -60,7 +60,7 @@ class Round: ModelObject, Storable { } } - func seed(_ team: TeamData, inMatchIndex matchIndex: Int) -> TeamRegistration? { + func seed(_ team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { return Store.main.filter(isIncluded: { $0.tournament == tournament && $0.bracketPosition != nil }).first(where: { @@ -69,14 +69,20 @@ class Round: ModelObject, Storable { }) } - func roundProjectedTeam(_ team: TeamData, inMatch match: Match) -> TeamRegistration? { + func losers() -> [TeamRegistration] { + _matches().compactMap { $0.losingTeamId }.compactMap { Store.main.findById($0) } + } + + func roundProjectedTeam(_ team: TeamPosition, inMatch match: Match) -> TeamRegistration? { if isLoserBracket() == false, let seed = seed(team, inMatchIndex: match.index) { return seed } switch team { case .one: - if let loser = upperBracketTopMatch(ofMatchIndex: match.index)?.losingTeamId { + if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 }) { + return luckyLoser.team + } else if let loser = upperBracketTopMatch(ofMatchIndex: match.index)?.losingTeamId { return Store.main.findById(loser) } else if let previousMatch = topPreviousRoundMatch(ofMatch: match) { if let teamId = previousMatch.winningTeamId { @@ -86,7 +92,9 @@ class Round: ModelObject, Storable { } } case .two: - if let loser = upperBracketBottomMatch(ofMatchIndex: match.index)?.losingTeamId { + if let luckyLoser = match.teamScores.first(where: { $0.luckyLoser == match.index * 2 + 1 }) { + return luckyLoser.team + } else if let loser = upperBracketBottomMatch(ofMatchIndex: match.index)?.losingTeamId { return Store.main.findById(loser) } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match) { if let teamId = previousMatch.winningTeamId { @@ -99,10 +107,6 @@ class Round: ModelObject, Storable { return nil } - -// func isMatchBye(_ match: Match) -> Bool { -// return (upperBracketMatches(ofMatch: match) + previousRoundMatches(ofMatch: match)).anySatisfy({ $0.disabled }) -// } func upperBracketTopMatch(ofMatchIndex matchIndex: Int) -> Match? { let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) @@ -175,11 +179,7 @@ class Round: ModelObject, Storable { let _matches = _matches() _matches.forEach { match in match.disabled = false - match.losingTeamId = nil - match.winningTeamId = nil - match.endDate = nil - match.court = nil - match.servingTeamId = nil + match.resetMatch() try? DataStore.shared.teamScores.delete(contentOfs: match.teamScores) } try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) @@ -188,7 +188,15 @@ class Round: ModelObject, Storable { func disableLoserRound(_ disable: Bool) { let _matches = _matches() _matches.forEach { match in - match.disabled = match.topPreviousRoundMatch()?.disabled == disable || match.bottomPreviousRoundMatch()?.disabled == disable + if disable { + if upperBracketTopMatch(ofMatchIndex: match.index)?.disabled == true || upperBracketBottomMatch(ofMatchIndex: match.index)?.disabled == true { + match.disabled = true + } + } else { + if upperBracketTopMatch(ofMatchIndex: match.index)?.disabled == false && upperBracketBottomMatch(ofMatchIndex: match.index)?.disabled == false { + match.disabled = false + } + } } try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) @@ -231,28 +239,6 @@ class Round: ModelObject, Storable { return "à démarrer" } } -// -// func indexOfMatch(_ match: Match) -> Int? { -// playedMatches().firstIndex(where: { $0.id == match.id }) -// } - - func previousRoundMatches(ofMatch match: Match) -> [Match] { - return Store.main.filter { - $0.round == previousRound()?.id && ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex()) - } - } - - func upperBracketMatches(ofMatch match: Match) -> [Match] { - let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: match.index) - if isLoserBracket(), previousRound() == nil, let parentRound { - let upperBracketMatches = parentRound._matches().filter({ - let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index) - return index == indexInRound * 2 || index == indexInRound * 2 + 1 - }) - return upperBracketMatches - } - return [] - } func loserRounds() -> [Round] { return Store.main.filter(isIncluded: { $0.loser == id }).sorted(by: \.index).reversed() diff --git a/PadelClub/Data/TeamRegistration.swift b/PadelClub/Data/TeamRegistration.swift index 37484e1..1891628 100644 --- a/PadelClub/Data/TeamRegistration.swift +++ b/PadelClub/Data/TeamRegistration.swift @@ -53,18 +53,18 @@ class TeamRegistration: ModelObject, Storable { bracketPosition == nil && groupStage == nil } - func setSeedPosition(inSpot match: Match, upperBranch: Int?, opposingSeeding: Bool) { + func setSeedPosition(inSpot match: Match, slot: TeamPosition?, opposingSeeding: Bool) { let matchIndex = match.index let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) - var teamPosition = upperBranch ?? (isUpper ? 0 : 1) + var teamPosition = slot ?? (isUpper ? .one : .two) if opposingSeeding { - teamPosition = upperBranch ?? (isUpper ? 1 : 0) + teamPosition = slot ?? (isUpper ? .two : .one) } + match.enableMatch() match.previousMatch(teamPosition)?.disableMatch() - match._toggleLoserMatchDisableState(false) - bracketPosition = matchIndex * 2 + teamPosition + bracketPosition = matchIndex * 2 + teamPosition.rawValue } var initialWeight: Int { diff --git a/PadelClub/Data/TeamScore.swift b/PadelClub/Data/TeamScore.swift index 7bc103a..46654aa 100644 --- a/PadelClub/Data/TeamScore.swift +++ b/PadelClub/Data/TeamScore.swift @@ -19,9 +19,9 @@ class TeamScore: ModelObject, Storable { var playerRegistrations: [String]? var score: String? var walkOut: Int? - var luckyLoser: Bool + var luckyLoser: Int? - internal init(match: String, teamRegistration: String? = nil, playerRegistrations: [String]? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Bool = false) { + internal init(match: String, teamRegistration: String? = nil, playerRegistrations: [String]? = nil, score: String? = nil, walkOut: Int? = nil, luckyLoser: Int? = nil) { self.match = match self.teamRegistration = teamRegistration self.playerRegistrations = playerRegistrations diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 50200bd..8c65c16 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -221,13 +221,13 @@ class Tournament : ModelObject, Storable { if availableSeeds.count <= availableSeedSpot.count { let spots = availableSeedSpot.shuffled() for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: false) + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: false) } } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) { let spots = availableSeedOpponentSpot.shuffled() for (index, seed) in availableSeeds.enumerated() { - seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: true) + seed.setSeedPosition(inSpot: spots[index], slot: nil, opposingSeeding: true) } } else if let chunk = seedGroup.chunk() { setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk) @@ -506,6 +506,10 @@ class Tournament : ModelObject, Storable { } + func availableQualifiedTeams() -> [TeamRegistration] { + unsortedTeams().filter({ $0.qualified && $0.bracketPosition == nil }) + } + func qualifiedTeams() -> [TeamRegistration] { unsortedTeams().filter({ $0.qualifiedFromGroupStage() }) } @@ -526,9 +530,11 @@ class Tournament : ModelObject, Storable { } func groupStagesAreOver() -> Bool { - guard groupStages().isEmpty == false else { + let groupStages = groupStages() + guard groupStages.isEmpty == false else { return true } + return groupStages.allSatisfy({ $0.hasEnded() }) return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified } diff --git a/PadelClub/Manager/PadelRule.swift b/PadelClub/Manager/PadelRule.swift index 8cbf94f..cd48f06 100644 --- a/PadelClub/Manager/PadelRule.swift +++ b/PadelClub/Manager/PadelRule.swift @@ -794,11 +794,11 @@ enum TournamentType: Int, Hashable, Codable, CaseIterable, Identifiable { } } -enum TeamData: Int, Hashable, Codable, CaseIterable { +enum TeamPosition: Int, Hashable, Codable, CaseIterable { case one case two - var otherTeam: TeamData { + var otherTeam: TeamPosition { switch self { case .one: return .two @@ -841,7 +841,7 @@ enum SetFormat: Int, Hashable, Codable { } } - func winner(teamOne: Int, teamTwo: Int) -> TeamData { + func winner(teamOne: Int, teamTwo: Int) -> TeamPosition { return teamOne >= teamTwo ? .one : .two } @@ -1029,7 +1029,7 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable { [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie] } - func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamData { + func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition { scoreTeamOne >= scoreTeamTwo ? .one : .two } diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index ba4c135..1d2d86a 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -61,7 +61,7 @@ class MatchDescriptor: ObservableObject { } } - var winner: TeamData { + var winner: TeamPosition { matchFormat.winner(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo) } diff --git a/PadelClub/ViewModel/SetDescriptor.swift b/PadelClub/ViewModel/SetDescriptor.swift index 42c65e1..2811e4e 100644 --- a/PadelClub/ViewModel/SetDescriptor.swift +++ b/PadelClub/ViewModel/SetDescriptor.swift @@ -23,7 +23,7 @@ struct SetDescriptor: Identifiable, Equatable { } } - var winner: TeamData? { + var winner: TeamPosition? { if let valueTeamTwo, let valueTeamOne { return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo) } else { diff --git a/PadelClub/Views/Match/MatchRowView.swift b/PadelClub/Views/Match/MatchRowView.swift index 32c244c..79e1e3b 100644 --- a/PadelClub/Views/Match/MatchRowView.swift +++ b/PadelClub/Views/Match/MatchRowView.swift @@ -10,11 +10,11 @@ import SwiftUI struct MatchRowView: View { var match: Match let matchViewStyle: MatchViewStyle - @Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed + @Environment(\.editMode) private var editMode @ViewBuilder var body: some View { - if isEditingTournamentSeed && match.isGroupStage() == false { + if editMode?.wrappedValue.isEditing == true && match.isGroupStage() == false && match.isLoserBracket == false { MatchSetupView(match: match) } else { NavigationLink { diff --git a/PadelClub/Views/Match/MatchSetupView.swift b/PadelClub/Views/Match/MatchSetupView.swift index 8e35ea3..9b30547 100644 --- a/PadelClub/Views/Match/MatchSetupView.swift +++ b/PadelClub/Views/Match/MatchSetupView.swift @@ -14,54 +14,90 @@ struct MatchSetupView: View { @ViewBuilder var body: some View { - _teamView(match.team(.one), teamPosition: 0) - _teamView(match.team(.two), teamPosition: 1) + _teamView(inTeamPosition: .one) + _teamView(inTeamPosition: .two) } @ViewBuilder - func _teamView(_ team: TeamRegistration?, teamPosition: Int) -> some View { - if let team { - TeamRowView(team: team, teamPosition: teamPosition) - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button(role: .cancel) { - team.bracketPosition = nil - match._toggleLoserMatchDisableState(false) - try? dataStore.teamRegistrations.addOrUpdate(instance: team) - } label: { - Label("retirer", systemImage: "xmark") - } + func _teamView(inTeamPosition teamPosition: TeamPosition) -> some View { + let team = match.team(teamPosition) + let teamScore = match.teamScore(ofTeam: team) + if let team, teamScore?.walkOut == nil { + VStack(alignment: .leading, spacing: 0) { + if let teamScore, teamScore.luckyLoser != nil { + Text("Repêchée").italic().font(.caption) } + TeamRowView(team: team, teamPosition: teamPosition) + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .cancel) { + if match.isSeededBy(team: team, inTeamPosition: teamPosition) { + team.bracketPosition = nil + match.enableMatch() + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + } else { + match.teamWillBeWalkOut(team) + try? dataStore.matches.addOrUpdate(instance: match) + } + } label: { + Label("retirer", systemImage: "xmark") + } + } + } } else { - HStack { - TeamPickerView(teamPicked: { team in - print(team.pasteData()) - team.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false) - try? dataStore.matches.addOrUpdate(instance: match) - try? dataStore.teamRegistrations.addOrUpdate(instance: team) - }) - if let tournament = match.currentTournament() { - let availableSeedGroups = tournament.availableSeedGroups() - Menu { - ForEach(availableSeedGroups, id: \.self) { seedGroup in - Button { - if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) { - randomTeam.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false) - try? dataStore.matches.addOrUpdate(instance: match) - try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam) + VStack(alignment: .leading) { + if let team { + TeamRowView(team: team, teamPosition: teamPosition) + .strikethrough() + } + HStack { + let walkOutSpot = match.isWalkOutSpot(teamPosition) + let luckyLosers = walkOutSpot ? match.luckyLosers() : [] + TeamPickerView(luckyLosers: luckyLosers, teamPicked: { team in + print(team.pasteData()) + if walkOutSpot { + match.setLuckyLoser(team: team, teamPosition: teamPosition) + try? dataStore.matches.addOrUpdate(instance: match) + } else { + team.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false) + try? dataStore.matches.addOrUpdate(instance: match) + try? dataStore.teamRegistrations.addOrUpdate(instance: team) + } + }) + if let tournament = match.currentTournament() { + let availableSeedGroups = tournament.availableSeedGroups() + Menu { + if walkOutSpot, luckyLosers.isEmpty == false { + Button { + if let randomTeam = luckyLosers.randomElement() { + match.setLuckyLoser(team: randomTeam, teamPosition: teamPosition) + try? dataStore.matches.addOrUpdate(instance: match) + } + } label: { + Label("Repêchage", systemImage: "dice") } - } label: { - Label(seedGroup.localizedLabel(), systemImage: "dice") } + ForEach(availableSeedGroups, id: \.self) { seedGroup in + Button { + if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) { + randomTeam.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false) + try? dataStore.matches.addOrUpdate(instance: match) + try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam) + } + } label: { + Label(seedGroup.localizedLabel(), systemImage: "dice") + } + } + } label: { + Text("Tirage").tag(nil as SeedInterval?) } - } label: { - Text("Tirage").tag(nil as SeedInterval?) + .disabled(availableSeedGroups.isEmpty && walkOutSpot == false) } - .disabled(availableSeedGroups.isEmpty) } + .fixedSize(horizontal: false, vertical: true) + .buttonBorderShape(.capsule) + .buttonStyle(.borderedProminent) + } - .fixedSize(horizontal: false, vertical: true) - .buttonBorderShape(.capsule) - .buttonStyle(.borderedProminent) } } } diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index 5bd927c..c7cefca 100644 --- a/PadelClub/Views/Match/MatchSummaryView.swift +++ b/PadelClub/Views/Match/MatchSummaryView.swift @@ -84,14 +84,14 @@ struct MatchSummaryView: View { if matchViewStyle != .feedStyle { HStack(spacing: 0) { VStack(alignment: .leading, spacing: matchViewStyle == .plainStyle ? 8 : 0) { - PlayerBlockView(match: match, whichTeam: .one, color: color, width: width) + PlayerBlockView(match: match, teamPosition: .one, color: color, width: width) .padding(matchViewStyle == .plainStyle ? 0 : 8) if width == 1 { Divider() } else { Divider().frame(height: width).overlay(color) } - PlayerBlockView(match: match, whichTeam: .two, color: color, width: width) + PlayerBlockView(match: match, teamPosition: .two, color: color, width: width) .padding(matchViewStyle == .plainStyle ? 0 : 8) } } diff --git a/PadelClub/Views/Match/PlayerBlockView.swift b/PadelClub/Views/Match/PlayerBlockView.swift index 405b30c..3b9aa15 100644 --- a/PadelClub/Views/Match/PlayerBlockView.swift +++ b/PadelClub/Views/Match/PlayerBlockView.swift @@ -9,15 +9,15 @@ import SwiftUI struct PlayerBlockView: View { var match: Match - let whichTeam: TeamData + let teamPosition: TeamPosition let team: TeamRegistration? let color: Color let width: CGFloat - init(match: Match, whichTeam: TeamData, color: Color, width: CGFloat) { + init(match: Match, teamPosition: TeamPosition, color: Color, width: CGFloat) { self.match = match - self.whichTeam = whichTeam - self.team = match.team(whichTeam) + self.teamPosition = teamPosition + self.team = match.team(teamPosition) self.color = color self.width = width } @@ -43,13 +43,21 @@ struct PlayerBlockView: View { } private func _defaultLabel() -> String { - whichTeam.localizedLabel() + if match.upperBracketMatch(teamPosition)?.disabled == true { + return "Bye" + } + + return teamPosition.localizedLabel() } var body: some View { HStack { VStack(alignment: .leading) { if let names { + if let teamScore = match.teamScore(ofTeam: team), teamScore.luckyLoser != nil { + Text("Repêchée").italic().font(.caption) + } + ForEach(names, id: \.self) { name in Text(name).lineLimit(1) } diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 60c647d..f4e6063 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -24,6 +24,9 @@ struct LoserRoundsView: View { switch selectedRound { case .none: List { + RowButtonView("Effacer", role: .destructive) { + + } } case .some(let selectedRound): LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index)) @@ -35,10 +38,20 @@ struct LoserRoundsView: View { } struct LoserRoundView: View { + @EnvironmentObject var dataStore: DataStore let loserRounds: [Round] + @Environment(\.editMode) private var editMode + + private func _roundDisabled() -> Bool { + loserRounds.allSatisfy({ $0.isDisabled() }) + } var body: some View { List { + if editMode?.wrappedValue.isEditing == true { + _editingView() + } + ForEach(loserRounds) { loserRound in Section { ForEach(loserRound.playedMatches()) { match in @@ -59,5 +72,33 @@ struct LoserRoundView: View { } } .headerProminence(.increased) + .toolbar { + EditButton() + } + } + + private func _editingView() -> some View { + if _roundDisabled() { + RowButtonView("Jouer ce tour", role: .destructive) { + loserRounds.forEach { round in + let matches = round.playedMatches() + matches.forEach { match in + if round.upperBracketTopMatch(ofMatchIndex: match.index)?.disabled == false && round.upperBracketBottomMatch(ofMatchIndex: match.index)?.disabled == false { + match.disabled = false + } + } + + try? dataStore.matches.addOrUpdate(contentOfs: matches) + } + } + } else { + RowButtonView("Ne pas jouer ce tour", role: .destructive) { + loserRounds.forEach { round in + round.playedMatches().forEach { match in + match.disabled = true + } + } + } + } } } diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index 8004418..daabeae 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -9,8 +9,8 @@ import SwiftUI struct RoundSettingsView: View { @EnvironmentObject var dataStore: DataStore + @Environment(\.editMode) private var editMode @Environment(Tournament.self) var tournament: Tournament - @Binding var isEditingTournamentSeed: Bool @State private var roundIndex: Int? var round: Round? { @@ -20,8 +20,6 @@ struct RoundSettingsView: View { var body: some View { List { - Toggle("Éditer les têtes de série", isOn: $isEditingTournamentSeed) - if let availableSeedGroup = tournament.availableSeedGroup() { Section { @@ -47,10 +45,10 @@ struct RoundSettingsView: View { if let matches = tournament.getRound(atRoundIndex: roundIndex)?.playedMatches() { if let lastMatch = matches.last { - seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, upperBranch: 1, opposingSeeding: false) + seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, slot: .two, opposingSeeding: false) } if let firstMatch = matches.first { - seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, upperBranch: 0, opposingSeeding: false) + seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, slot: .one, opposingSeeding: false) } } try? dataStore.teamRegistrations.addOrUpdate(contentOfs: seeds) @@ -58,6 +56,10 @@ struct RoundSettingsView: View { tournament.setSeeds(inRoundIndex: roundIndex, inSeedGroup: availableSeedGroup) try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) } + + if tournament.availableSeeds().isEmpty { + editMode?.wrappedValue = .inactive + } } } @@ -65,22 +67,22 @@ struct RoundSettingsView: View { Text("Placement des têtes de série") } } - - Section { - RowButtonView("Effacer classement", role: .destructive) { - tournament.rounds().forEach { round in - try? dataStore.rounds.delete(contentOfs: round.loserRounds()) - } - } - } - - Section { - RowButtonView("Match de classement") { - tournament.rounds().forEach { round in - round.buildLoserBracket() - } - } - } +// +// Section { +// RowButtonView("Effacer classement", role: .destructive) { +// tournament.rounds().forEach { round in +// try? dataStore.rounds.delete(contentOfs: round.loserRounds()) +// } +// } +// } +// +// Section { +// RowButtonView("Match de classement") { +// tournament.rounds().forEach { round in +// round.buildLoserBracket() +// } +// } +// } Section { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) @@ -119,7 +121,7 @@ struct RoundSettingsView: View { } #Preview { - RoundSettingsView(isEditingTournamentSeed: .constant(true)) + RoundSettingsView() .environment(Tournament.mock()) .environmentObject(DataStore.shared) } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index 03e2f2c..58f7d62 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -8,18 +8,25 @@ import SwiftUI struct RoundView: View { + @Environment(\.editMode) private var editMode + @Environment(Tournament.self) var tournament: Tournament + var round: Round var body: some View { List { - let loserRounds = round.loserRounds() - if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) { - Section { - NavigationLink { - LoserRoundsView(upperBracketRound: round) - .navigationTitle(first.roundTitle()) - } label: { - Text(first.roundTitle()) + + if editMode?.wrappedValue.isEditing == false { + let loserRounds = round.loserRounds() + if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) { + Section { + NavigationLink { + LoserRoundsView(upperBracketRound: round) + .environment(tournament) + .navigationTitle(first.roundTitle()) + } label: { + Text(first.roundTitle()) + } } } } @@ -33,9 +40,15 @@ struct RoundView: View { } } .headerProminence(.increased) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + EditButton() + } + } } } #Preview { RoundView(round: Round.mock()) + .environment(Tournament.mock()) } diff --git a/PadelClub/Views/Round/RoundsView.swift b/PadelClub/Views/Round/RoundsView.swift index 7d028b3..1134fed 100644 --- a/PadelClub/Views/Round/RoundsView.swift +++ b/PadelClub/Views/Round/RoundsView.swift @@ -10,13 +10,13 @@ import SwiftUI struct RoundsView: View { var tournament: Tournament @State private var selectedRound: Round? - @State private var isEditingTournamentSeed = false + @State var editMode: EditMode = .inactive init(tournament: Tournament) { self.tournament = tournament _selectedRound = State(wrappedValue: tournament.getActiveRound()) if tournament.availableSeeds().isEmpty == false { - _isEditingTournamentSeed = State(wrappedValue: true) + _editMode = .init(wrappedValue: .active) } } @@ -25,14 +25,14 @@ struct RoundsView: View { GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: tournament.rounds(), nilDestinationIsValid: true) switch selectedRound { case .none: - RoundSettingsView(isEditingTournamentSeed: $isEditingTournamentSeed) + RoundSettingsView() .navigationTitle("Réglages") case .some(let selectedRound): RoundView(round: selectedRound) .navigationTitle(selectedRound.roundTitle()) - .editTournamentSeed(isEditingTournamentSeed) } } + .environment(\.editMode, $editMode) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(.visible, for: .navigationBar) } diff --git a/PadelClub/Views/Score/EditScoreView.swift b/PadelClub/Views/Score/EditScoreView.swift index 4e5bd6c..3c0cd33 100644 --- a/PadelClub/Views/Score/EditScoreView.swift +++ b/PadelClub/Views/Score/EditScoreView.swift @@ -12,7 +12,7 @@ struct EditScoreView: View { @ObservedObject var matchDescriptor: MatchDescriptor @Environment(\.dismiss) private var dismiss - func walkout(_ team: TeamData) { + func walkout(_ team: TeamPosition) { matchDescriptor.match?.setWalkOut(team) save() dismiss() diff --git a/PadelClub/Views/Team/TeamPickerView.swift b/PadelClub/Views/Team/TeamPickerView.swift index efc9aba..7747d5f 100644 --- a/PadelClub/Views/Team/TeamPickerView.swift +++ b/PadelClub/Views/Team/TeamPickerView.swift @@ -13,16 +13,34 @@ struct TeamPickerView: View { @Environment(\.dismiss) private var dismiss @State private var presentTeamPickerView: Bool = false @State private var searchField: String = "" + var luckyLosers: [TeamRegistration] = [] let teamPicked: ((TeamRegistration) -> (Void)) var body: some View { - Button("Choisir") { - presentTeamPickerView = true - } + Button("Choisir") { + presentTeamPickerView = true + } .sheet(isPresented: $presentTeamPickerView) { NavigationStack { List { let teams = tournament.sortedTeams() + if luckyLosers.isEmpty == false { + Section { + _teamListView(luckyLosers.sorted(by: \.weight)) + } header: { + Text("Repêchage") + } + } + + let qualified = tournament.availableQualifiedTeams() + if qualified.isEmpty == false { + Section { + _teamListView(qualified.sorted(by: \.weight)) + } header: { + Text("Qualifiées entrants") + } + } + Section { _teamListView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight).reversed()) } header: { diff --git a/PadelClub/Views/Team/TeamRowView.swift b/PadelClub/Views/Team/TeamRowView.swift index e24a93c..4ae5b05 100644 --- a/PadelClub/Views/Team/TeamRowView.swift +++ b/PadelClub/Views/Team/TeamRowView.swift @@ -9,12 +9,12 @@ import SwiftUI struct TeamRowView: View { var team: TeamRegistration - var teamPosition: Int? = nil + var teamPosition: TeamPosition? = nil var body: some View { LabeledContent { VStack(alignment: .trailing, spacing: 0) { - if teamPosition == 0 || teamPosition == nil { + if teamPosition == .one || teamPosition == nil { Text(team.weight.formatted()) .font(.caption) } @@ -22,7 +22,7 @@ struct TeamRowView: View { Text("#" + (index + 1).formatted()) .font(.title) } - if teamPosition == 1 { + if teamPosition == .two { Text(team.weight.formatted()) .font(.caption)