From 29cc0df4a283dddac47d036451c046a5b58b13cb Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 5 Apr 2024 18:19:37 +0200 Subject: [PATCH] in progress --- PadelClub/Data/Match.swift | 67 ++++++-- PadelClub/Data/Round.swift | 147 ++++++++++++++++-- PadelClub/Data/Tournament.swift | 8 +- PadelClub/ViewModel/MatchDescriptor.swift | 8 +- PadelClub/Views/Match/MatchSummaryView.swift | 4 +- PadelClub/Views/Match/PlayerBlockView.swift | 16 +- PadelClub/Views/Round/LoserBracketView.swift | 2 +- PadelClub/Views/Round/LoserRoundsView.swift | 21 ++- PadelClub/Views/Round/RoundSettingsView.swift | 2 +- PadelClub/Views/Round/RoundView.swift | 2 +- 10 files changed, 220 insertions(+), 57 deletions(-) diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index d1ced08..79bdd00 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -47,7 +47,7 @@ class Match: ModelObject, Storable { func indexInRound() -> Int { if groupStage != nil { return index - } else if let index = roundObject?.matches.firstIndex(where: { $0.id == id }) { + } else if let index = roundObject?.playedMatches().firstIndex(where: { $0.id == id }) { return index } return RoundRule.matchIndexWithinRound(fromMatchIndex: index) @@ -317,33 +317,66 @@ class Match: ModelObject, Storable { }) } + func isBye() -> Bool { + guard let roundObject else { return false } + return (roundObject.upperBracketMatches(ofMatch: self) + roundObject.previousRoundMatches(ofMatch: self)).anySatisfy({ $0.disabled }) + } + + 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? { guard let roundObject else { return nil } + return roundObject.roundProjectedTeam(team, inMatch: self) + if roundObject.isLoserBracket() == false, let seed = seed(team) { return seed } - let indexInRound = indexInRound() + switch team { case .one: - if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let loser = parentRound.matches.first(where: { parentRound.indexOfMatch($0) == indexInRound * 2 })?.losingTeamId { + if let loser = upperBracketTopMatch()?.losingTeamId { return Store.main.findById(loser) - } else if let teamId = topPreviousRoundMatch()?.winningTeamId { - return Store.main.findById(teamId) + } else if let match = topPreviousRoundMatch() { + if let teamId = match.winningTeamId { + return Store.main.findById(teamId) + } else if match.isBye() { + return match.teams().first + } } case .two: - if roundObject.isLoserBracket(), roundObject.previousRound() == nil, let parentRound = roundObject.parentRound, let loser = parentRound.matches.first(where: { parentRound.indexOfMatch($0) == indexInRound * 2 + 1 })?.losingTeamId { + if let loser = upperBracketBottomMatch()?.losingTeamId { return Store.main.findById(loser) - } else if let teamId = bottomPreviousRoundMatch()?.winningTeamId { - return Store.main.findById(teamId) + } else if let match = bottomPreviousRoundMatch() { + if let teamId = match.winningTeamId { + return Store.main.findById(teamId) + } else if match.isBye() { + return match.teams().first + } } } return nil } - func teamWon(_ team: TeamData) -> Bool { + func teamWon(_ team: TeamRegistration?) -> Bool { guard let winningTeamId else { return false } - return winningTeamId == self.team(team)?.id + return winningTeamId == team?.id } func team(_ team: TeamData) -> TeamRegistration? { @@ -364,18 +397,22 @@ class Match: ModelObject, Storable { } } - func teamNames(_ team: TeamData) -> [String]? { - self.team(team)?.players().map { $0.playerLabel() } + func teamNames(_ team: TeamRegistration?) -> [String]? { + team?.players().map { $0.playerLabel() } } - func teamWalkOut(_ team: TeamData) -> Bool { - teamScore(team)?.isWalkOut() == true + func teamWalkOut(_ team: TeamRegistration?) -> Bool { + teamScore(ofTeam: team)?.isWalkOut() == true } func teamScore(_ team: TeamData) -> TeamScore? { - scores().first(where: { $0.teamRegistration == self.team(team)?.id }) + teamScore(ofTeam: self.team(team)) } + func teamScore(ofTeam team: TeamRegistration?) -> TeamScore? { + scores().first(where: { $0.teamRegistration == team?.id }) + } + func isRunning() -> Bool { // at least a match has started hasStarted() && hasEnded() == false } diff --git a/PadelClub/Data/Round.swift b/PadelClub/Data/Round.swift index 05b2db6..0092d4f 100644 --- a/PadelClub/Data/Round.swift +++ b/PadelClub/Data/Round.swift @@ -36,18 +36,113 @@ class Round: ModelObject, Storable { func hasStarted() -> Bool { - matches.anySatisfy({ $0.hasStarted() }) + playedMatches().anySatisfy({ $0.hasStarted() }) } func hasEnded() -> Bool { - matches.allSatisfy({ $0.hasEnded() }) + playedMatches().allSatisfy({ $0.hasEnded() }) } func tournamentObject() -> Tournament? { Store.main.findById(tournament) } - var matches: [Match] { + private func _matches() -> [Match] { + Store.main.filter { $0.round == self.id } + } + + func team(_ team: TeamData, inMatch match: Match) -> TeamRegistration? { + switch team { + case .one: + return roundProjectedTeam(.one, inMatch: match) + case .two: + return roundProjectedTeam(.two, inMatch: match) + } + } + + func seed(_ team: TeamData, inMatchIndex matchIndex: Int) -> TeamRegistration? { + return Store.main.filter(isIncluded: { + $0.tournament == tournament && $0.bracketPosition != nil + }).first(where: { + ($0.bracketPosition! / 2) == matchIndex + && ($0.bracketPosition! % 2) == team.rawValue + }) + } + + func roundProjectedTeam(_ team: TeamData, 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 { + return Store.main.findById(loser) + } else if let previousMatch = topPreviousRoundMatch(ofMatch: match) { + if let teamId = previousMatch.winningTeamId { + return Store.main.findById(teamId) + } else if previousMatch.isBye() { + return previousMatch.teams().first + } + } + case .two: + 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 { + return Store.main.findById(teamId) + } else if previousMatch.isBye() { + return previousMatch.teams().first + } + } + } + + 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) + if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { + return upperBracketTopMatch + } + return nil + } + + func upperBracketBottomMatch(ofMatchIndex matchIndex: Int) -> Match? { + let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) + if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) { + return upperBracketBottomMatch + } + return nil + } + + func topPreviousRoundMatch(ofMatch match: Match) -> Match? { + guard let previousRound = previousRound() else { return nil } + return Store.main.filter { + $0.index == match.topPreviousRoundMatchIndex() && $0.round == previousRound.id + }.sorted(by: \.index).first + } + + func bottomPreviousRoundMatch(ofMatch match: Match) -> Match? { + guard let previousRound = previousRound() else { return nil } + return Store.main.filter { + $0.index == match.bottomPreviousRoundMatchIndex() && $0.round == previousRound.id + }.sorted(by: \.index).first + } + + + func getMatch(atMatchIndexInRound matchIndexInRound: Int) -> Match? { + _matches().first(where: { + let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index) + return index == matchIndexInRound + }) + } + + func playedMatches() -> [Match] { Store.main.filter { $0.round == self.id && $0.disabled == false } } @@ -63,13 +158,17 @@ class Round: ModelObject, Storable { return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.cumulativeMatchCount) } + func isDisabled() -> Bool { + playedMatches().allSatisfy({ $0.disabled || $0.isBye() }) + } + func getActiveLoserRound() -> Round? { let rounds = loserRounds() - return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false }).sorted(by: \.index).reversed().first ?? rounds.first + return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false }) } var cumulativeMatchCount: Int { - var totalMatches = matches.count + var totalMatches = playedMatches().count if let parent = parentRound { totalMatches += parent.cumulativeMatchCount } @@ -86,10 +185,10 @@ class Round: ModelObject, Storable { func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String { if let parentRound, let initialRound = parentRound.initialRound() { - let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.matches.count + let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count print("initialRound", initialRound.roundTitle()) - if let initialRoundNextRound = initialRound.nextRound()?.matches { - return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + matches.count * 2).localizedLabel(displayStyle) + if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() { + return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + playedMatches().count * 2).localizedLabel(displayStyle) } } return RoundRule.roundName(fromRoundIndex: index) @@ -102,11 +201,29 @@ class Round: ModelObject, Storable { return "à démarrer" } } +// +// func indexOfMatch(_ match: Match) -> Int? { +// playedMatches().firstIndex(where: { $0.id == match.id }) +// } - func indexOfMatch(_ match: Match) -> Int? { - matches.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() } @@ -122,7 +239,7 @@ class Round: ModelObject, Storable { func buildLoserBracket() { guard loserRounds().isEmpty else { return } - let currentRoundMatchCount = matches.count + let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index) guard currentRoundMatchCount > 1 else { return } let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount) let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat @@ -159,7 +276,7 @@ class Round: ModelObject, Storable { } override func deleteDependencies() throws { - try Store.main.deleteDependencies(items: self.matches) + try Store.main.deleteDependencies(items: _matches()) try Store.main.deleteDependencies(items: loserRoundsAndChildren()) } @@ -183,9 +300,9 @@ extension Round: Selectable { func badgeValue() -> Int? { if let parentRound { - return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.matches }.filter({ $0.isRunning() }).count + return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count } else { - return matches.filter({ $0.isRunning() }).count + return playedMatches().filter({ $0.isRunning() }).count } } } diff --git a/PadelClub/Data/Tournament.swift b/PadelClub/Data/Tournament.swift index 12fe20a..9211fdc 100644 --- a/PadelClub/Data/Tournament.swift +++ b/PadelClub/Data/Tournament.swift @@ -156,11 +156,11 @@ class Tournament : ModelObject, Storable { } func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] { - getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 0 } ?? [] + getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 0 } ?? [] } func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] { - getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 1 } ?? [] + getRound(atRoundIndex: roundIndex)?.playedMatches().filter { $0.teams().count == 1 } ?? [] } func availableSeedGroups() -> [SeedInterval] { @@ -613,7 +613,9 @@ class Tournament : ModelObject, Storable { try? DataStore.shared.matches.addOrUpdate(contentOfs: matches) - buildLoserBracket() + self.rounds().forEach { round in + round.buildLoserBracket() + } } func deleteStructure() { diff --git a/PadelClub/ViewModel/MatchDescriptor.swift b/PadelClub/ViewModel/MatchDescriptor.swift index b31024c..ba4c135 100644 --- a/PadelClub/ViewModel/MatchDescriptor.swift +++ b/PadelClub/ViewModel/MatchDescriptor.swift @@ -27,10 +27,12 @@ class MatchDescriptor: ObservableObject { self.matchFormat = format self.setDescriptors = [SetDescriptor(setFormat: format.setFormat)] } - self.teamLabelOne = match?.team(.one)?.teamLabel() ?? "" - self.teamLabelTwo = match?.team(.two)?.teamLabel() ?? "" + let teamOne = match?.team(.one) + let teamTwo = match?.team(.two) + self.teamLabelOne = teamOne?.teamLabel() ?? "" + self.teamLabelTwo = teamTwo?.teamLabel() ?? "" - if let match, let scoresTeamOne = match.teamScore(.one)?.score, let scoresTeamTwo = match.teamScore(.two)?.score { + if let match, let scoresTeamOne = match.teamScore(ofTeam: teamOne)?.score, let scoresTeamTwo = match.teamScore(ofTeam: teamTwo)?.score { self.setDescriptors = combineArraysIntoTuples(scoresTeamOne.components(separatedBy: ","), scoresTeamTwo.components(separatedBy: ",")).map({ (a:String?, b:String?) in SetDescriptor(valueTeamOne: a != nil ? Int(a!) : nil, valueTeamTwo: b != nil ? Int(b!) : nil, setFormat: match.matchFormat.setFormat) diff --git a/PadelClub/Views/Match/MatchSummaryView.swift b/PadelClub/Views/Match/MatchSummaryView.swift index e312219..5bd927c 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, team: .one, color: color, width: width) + PlayerBlockView(match: match, whichTeam: .one, color: color, width: width) .padding(matchViewStyle == .plainStyle ? 0 : 8) if width == 1 { Divider() } else { Divider().frame(height: width).overlay(color) } - PlayerBlockView(match: match, team: .two, color: color, width: width) + PlayerBlockView(match: match, whichTeam: .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 c8802bf..405b30c 100644 --- a/PadelClub/Views/Match/PlayerBlockView.swift +++ b/PadelClub/Views/Match/PlayerBlockView.swift @@ -9,11 +9,19 @@ import SwiftUI struct PlayerBlockView: View { var match: Match - - let team: TeamData + let whichTeam: TeamData + let team: TeamRegistration? let color: Color let width: CGFloat + init(match: Match, whichTeam: TeamData, color: Color, width: CGFloat) { + self.match = match + self.whichTeam = whichTeam + self.team = match.team(whichTeam) + self.color = color + self.width = width + } + var names: [String]? { match.teamNames(team) } @@ -31,11 +39,11 @@ struct PlayerBlockView: View { } var scores: [String] { - match.teamScore(team)?.score?.components(separatedBy: ",") ?? [] + match.teamScore(ofTeam: team)?.score?.components(separatedBy: ",") ?? [] } private func _defaultLabel() -> String { - team.localizedLabel() + whichTeam.localizedLabel() } var body: some View { diff --git a/PadelClub/Views/Round/LoserBracketView.swift b/PadelClub/Views/Round/LoserBracketView.swift index ae25942..470c033 100644 --- a/PadelClub/Views/Round/LoserBracketView.swift +++ b/PadelClub/Views/Round/LoserBracketView.swift @@ -38,7 +38,7 @@ struct LoserBracketView: View { private func _loserRoundView(_ loserRound: Round) -> some View { Section { - ForEach(loserRound.matches) { match in + ForEach(loserRound.playedMatches()) { match in MatchRowView(match: match, matchViewStyle: .standardStyle) } } header: { diff --git a/PadelClub/Views/Round/LoserRoundsView.swift b/PadelClub/Views/Round/LoserRoundsView.swift index 46e5609..0e9b0ca 100644 --- a/PadelClub/Views/Round/LoserRoundsView.swift +++ b/PadelClub/Views/Round/LoserRoundsView.swift @@ -7,17 +7,6 @@ import SwiftUI -extension Int: Selectable, Identifiable { - public var id: Int { self } - func selectionLabel() -> String { - "Tour #\(self + 1)" - } - - func badgeValue() -> Int? { - nil - } -} - struct LoserRoundsView: View { var upperBracketRound: Round @State private var selectedRound: Round? @@ -52,8 +41,16 @@ struct LoserRoundView: View { List { ForEach(loserRounds) { loserRound in Section { - ForEach(loserRound.matches) { match in + ForEach(loserRound.playedMatches()) { match in MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) + .overlay { + if match.isBye() { + Image(systemName: "pencil.slash") + .resizable() + .scaledToFit() + .opacity(0.3) + } + } } } header: { Text(loserRound.roundTitle(.wide)) diff --git a/PadelClub/Views/Round/RoundSettingsView.swift b/PadelClub/Views/Round/RoundSettingsView.swift index d492403..ed45818 100644 --- a/PadelClub/Views/Round/RoundSettingsView.swift +++ b/PadelClub/Views/Round/RoundSettingsView.swift @@ -88,7 +88,7 @@ struct RoundSettingsView: View { // seeds.prefix(1).first?.bracketPosition = lastIndex * 2 + 1 //TS 1 branche du bas du dernier match // seeds.prefix(2).dropFirst().first?.bracketPosition = startIndex * 2 //TS 2 branche du haut du premier match - if let matches = tournament.getRound(atRoundIndex: roundIndex)?.matches { + if let matches = tournament.getRound(atRoundIndex: roundIndex)?.playedMatches() { if let lastMatch = matches.last { seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, upperBranch: 1, opposingSeeding: false) } diff --git a/PadelClub/Views/Round/RoundView.swift b/PadelClub/Views/Round/RoundView.swift index bd01ac7..8ab1b88 100644 --- a/PadelClub/Views/Round/RoundView.swift +++ b/PadelClub/Views/Round/RoundView.swift @@ -24,7 +24,7 @@ struct RoundView: View { } } - ForEach(round.matches) { match in + ForEach(round.playedMatches()) { match in Section { MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle) } header: {