loser bracket system

multistore
Razmig Sarkissian 2 years ago
parent 82384e3049
commit c0475025ab
  1. 8
      PadelClub/Data/GroupStage.swift
  2. 146
      PadelClub/Data/Match.swift
  3. 60
      PadelClub/Data/Round.swift
  4. 10
      PadelClub/Data/TeamRegistration.swift
  5. 4
      PadelClub/Data/TeamScore.swift
  6. 12
      PadelClub/Data/Tournament.swift
  7. 8
      PadelClub/Manager/PadelRule.swift
  8. 2
      PadelClub/ViewModel/MatchDescriptor.swift
  9. 2
      PadelClub/ViewModel/SetDescriptor.swift
  10. 4
      PadelClub/Views/Match/MatchRowView.swift
  11. 110
      PadelClub/Views/Match/MatchSetupView.swift
  12. 4
      PadelClub/Views/Match/MatchSummaryView.swift
  13. 18
      PadelClub/Views/Match/PlayerBlockView.swift
  14. 41
      PadelClub/Views/Round/LoserRoundsView.swift
  15. 46
      PadelClub/Views/Round/RoundSettingsView.swift
  16. 29
      PadelClub/Views/Round/RoundView.swift
  17. 8
      PadelClub/Views/Round/RoundsView.swift
  18. 2
      PadelClub/Views/Score/EditScoreView.swift
  19. 24
      PadelClub/Views/Team/TeamPickerView.swift
  20. 6
      PadelClub/Views/Team/TeamRowView.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) let _teams = _teams(for: matchIndex)
switch team { switch team {
case .one: case .one:
@ -202,11 +202,11 @@ class GroupStage: ModelObject, Storable {
fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) fileprivate typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int)
fileprivate func _headToHead(_ whichTeam: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool { fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool {
let indexes = [whichTeam, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted()
let combos = Array((0..<size).combinations(ofCount: 2)) let combos = Array((0..<size).combinations(ofCount: 2))
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) { if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
return whichTeam.id == match.losingTeamId return teamPosition.id == match.losingTeamId
} else { } else {
return false return false
} }

@ -66,6 +66,58 @@ class Match: ModelObject, Storable {
} }
} }
func isSeededBy(team: TeamRegistration, inTeamPosition teamPosition: TeamPosition) -> 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() { func disableMatch() {
_toggleMatchDisableState(true) _toggleMatchDisableState(true)
} }
@ -121,8 +173,16 @@ class Match: ModelObject, Storable {
}.sorted(by: \.index).first }.sorted(by: \.index).first
} }
func previousMatch(_ teamPosition: Int) -> Match? { func upperBracketMatch(_ teamPosition: TeamPosition) -> Match? {
if teamPosition == 0 { if teamPosition == .one {
return roundObject?.upperBracketTopMatch(ofMatchIndex: index)
} else {
return roundObject?.upperBracketBottomMatch(ofMatchIndex: index)
}
}
func previousMatch(_ teamPosition: TeamPosition) -> Match? {
if teamPosition == .one {
return topPreviousRoundMatch() return topPreviousRoundMatch()
} else { } else {
return bottomPreviousRoundMatch() return bottomPreviousRoundMatch()
@ -146,10 +206,10 @@ class Match: ModelObject, Storable {
} }
} }
func setWalkOut(_ whichTeam: TeamData) { func setWalkOut(_ teamPosition: TeamPosition) {
let teamScoreWalkout = teamScore(whichTeam) ?? TeamScore(match: id, teamRegistration: team(whichTeam)?.id) let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, teamRegistration: team(teamPosition)?.id)
teamScoreWalkout.walkOut = 0 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 teamScoreWinning.walkOut = nil
try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
@ -301,10 +361,10 @@ class Match: ModelObject, Storable {
return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } 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 } guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil }
var reverseValue = 1 var reverseValue = 1
if whichTeam == team(.two)?.groupStagePosition { if teamPosition == team(.two)?.groupStagePosition {
reverseValue = -1 reverseValue = -1
} }
let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) 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) return (setDifference * reverseValue, gameDifference * reverseValue)
} }
func groupStageProjectedTeam(_ team: TeamData) -> TeamRegistration? { func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
guard let groupStageObject else { return nil } guard let groupStageObject else { return nil }
return groupStageObject.team(whichTeam: team, inMatchIndex: index) return groupStageObject.team(teamPosition: 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
} }
func upperBracketTopMatch() -> Match? { func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
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 } guard let roundObject else { return nil }
return roundObject.roundProjectedTeam(team, inMatch: self) 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 { func teamWon(_ team: TeamRegistration?) -> Bool {
@ -397,7 +395,7 @@ class Match: ModelObject, Storable {
return winningTeamId == team?.id return winningTeamId == team?.id
} }
func team(_ team: TeamData) -> TeamRegistration? { func team(_ team: TeamPosition) -> TeamRegistration? {
if groupStage != nil { if groupStage != nil {
switch team { switch team {
case .one: case .one:
@ -423,7 +421,7 @@ class Match: ModelObject, Storable {
teamScore(ofTeam: team)?.isWalkOut() == true teamScore(ofTeam: team)?.isWalkOut() == true
} }
func teamScore(_ team: TeamData) -> TeamScore? { func teamScore(_ team: TeamPosition) -> TeamScore? {
teamScore(ofTeam: self.team(team)) teamScore(ofTeam: self.team(team))
} }

@ -51,7 +51,7 @@ class Round: ModelObject, Storable {
Store.main.filter { $0.round == self.id } 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 { switch team {
case .one: case .one:
return roundProjectedTeam(.one, inMatch: match) 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: { return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil $0.tournament == tournament && $0.bracketPosition != nil
}).first(where: { }).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) { if isLoserBracket() == false, let seed = seed(team, inMatchIndex: match.index) {
return seed return seed
} }
switch team { switch team {
case .one: 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) return Store.main.findById(loser)
} else if let previousMatch = topPreviousRoundMatch(ofMatch: match) { } else if let previousMatch = topPreviousRoundMatch(ofMatch: match) {
if let teamId = previousMatch.winningTeamId { if let teamId = previousMatch.winningTeamId {
@ -86,7 +92,9 @@ class Round: ModelObject, Storable {
} }
} }
case .two: 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) return Store.main.findById(loser)
} else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match) { } else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match) {
if let teamId = previousMatch.winningTeamId { if let teamId = previousMatch.winningTeamId {
@ -100,10 +108,6 @@ class Round: ModelObject, Storable {
return nil return nil
} }
// func isMatchBye(_ match: Match) -> Bool {
// return (upperBracketMatches(ofMatch: match) + previousRoundMatches(ofMatch: match)).anySatisfy({ $0.disabled })
// }
func upperBracketTopMatch(ofMatchIndex matchIndex: Int) -> Match? { func upperBracketTopMatch(ofMatchIndex matchIndex: Int) -> Match? {
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) { if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) {
@ -175,11 +179,7 @@ class Round: ModelObject, Storable {
let _matches = _matches() let _matches = _matches()
_matches.forEach { match in _matches.forEach { match in
match.disabled = false match.disabled = false
match.losingTeamId = nil match.resetMatch()
match.winningTeamId = nil
match.endDate = nil
match.court = nil
match.servingTeamId = nil
try? DataStore.shared.teamScores.delete(contentOfs: match.teamScores) try? DataStore.shared.teamScores.delete(contentOfs: match.teamScores)
} }
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches) try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
@ -188,7 +188,15 @@ class Round: ModelObject, Storable {
func disableLoserRound(_ disable: Bool) { func disableLoserRound(_ disable: Bool) {
let _matches = _matches() let _matches = _matches()
_matches.forEach { match in _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) try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
@ -231,28 +239,6 @@ class Round: ModelObject, Storable {
return "à démarrer" 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] { func loserRounds() -> [Round] {
return Store.main.filter(isIncluded: { $0.loser == id }).sorted(by: \.index).reversed() return Store.main.filter(isIncluded: { $0.loser == id }).sorted(by: \.index).reversed()

@ -53,18 +53,18 @@ class TeamRegistration: ModelObject, Storable {
bracketPosition == nil && groupStage == nil 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 matchIndex = match.index
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = upperBranch ?? (isUpper ? 0 : 1) var teamPosition = slot ?? (isUpper ? .one : .two)
if opposingSeeding { if opposingSeeding {
teamPosition = upperBranch ?? (isUpper ? 1 : 0) teamPosition = slot ?? (isUpper ? .two : .one)
} }
match.enableMatch()
match.previousMatch(teamPosition)?.disableMatch() match.previousMatch(teamPosition)?.disableMatch()
match._toggleLoserMatchDisableState(false) bracketPosition = matchIndex * 2 + teamPosition.rawValue
bracketPosition = matchIndex * 2 + teamPosition
} }
var initialWeight: Int { var initialWeight: Int {

@ -19,9 +19,9 @@ class TeamScore: ModelObject, Storable {
var playerRegistrations: [String]? var playerRegistrations: [String]?
var score: String? var score: String?
var walkOut: Int? 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.match = match
self.teamRegistration = teamRegistration self.teamRegistration = teamRegistration
self.playerRegistrations = playerRegistrations self.playerRegistrations = playerRegistrations

@ -221,13 +221,13 @@ class Tournament : ModelObject, Storable {
if availableSeeds.count <= availableSeedSpot.count { if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled() let spots = availableSeedSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() { 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) { } else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count <= self.availableSeeds().count) {
let spots = availableSeedOpponentSpot.shuffled() let spots = availableSeedOpponentSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() { 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() { } else if let chunk = seedGroup.chunk() {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: 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] { func qualifiedTeams() -> [TeamRegistration] {
unsortedTeams().filter({ $0.qualifiedFromGroupStage() }) unsortedTeams().filter({ $0.qualifiedFromGroupStage() })
} }
@ -526,9 +530,11 @@ class Tournament : ModelObject, Storable {
} }
func groupStagesAreOver() -> Bool { func groupStagesAreOver() -> Bool {
guard groupStages().isEmpty == false else { let groupStages = groupStages()
guard groupStages.isEmpty == false else {
return true return true
} }
return groupStages.allSatisfy({ $0.hasEnded() })
return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified return qualifiedTeams().count == qualifiedFromGroupStage() + groupStageAdditionalQualified
} }

@ -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 one
case two case two
var otherTeam: TeamData { var otherTeam: TeamPosition {
switch self { switch self {
case .one: case .one:
return .two 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 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] [.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 scoreTeamOne >= scoreTeamTwo ? .one : .two
} }

@ -61,7 +61,7 @@ class MatchDescriptor: ObservableObject {
} }
} }
var winner: TeamData { var winner: TeamPosition {
matchFormat.winner(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo) matchFormat.winner(scoreTeamOne: scoreTeamOne, scoreTeamTwo: scoreTeamTwo)
} }

@ -23,7 +23,7 @@ struct SetDescriptor: Identifiable, Equatable {
} }
} }
var winner: TeamData? { var winner: TeamPosition? {
if let valueTeamTwo, let valueTeamOne { if let valueTeamTwo, let valueTeamOne {
return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo) return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo)
} else { } else {

@ -10,11 +10,11 @@ import SwiftUI
struct MatchRowView: View { struct MatchRowView: View {
var match: Match var match: Match
let matchViewStyle: MatchViewStyle let matchViewStyle: MatchViewStyle
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed @Environment(\.editMode) private var editMode
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
if isEditingTournamentSeed && match.isGroupStage() == false { if editMode?.wrappedValue.isEditing == true && match.isGroupStage() == false && match.isLoserBracket == false {
MatchSetupView(match: match) MatchSetupView(match: match)
} else { } else {
NavigationLink { NavigationLink {

@ -14,54 +14,90 @@ struct MatchSetupView: View {
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
_teamView(match.team(.one), teamPosition: 0) _teamView(inTeamPosition: .one)
_teamView(match.team(.two), teamPosition: 1) _teamView(inTeamPosition: .two)
} }
@ViewBuilder @ViewBuilder
func _teamView(_ team: TeamRegistration?, teamPosition: Int) -> some View { func _teamView(inTeamPosition teamPosition: TeamPosition) -> some View {
if let team { let team = match.team(teamPosition)
TeamRowView(team: team, teamPosition: teamPosition) let teamScore = match.teamScore(ofTeam: team)
.swipeActions(edge: .trailing, allowsFullSwipe: false) { if let team, teamScore?.walkOut == nil {
Button(role: .cancel) { VStack(alignment: .leading, spacing: 0) {
team.bracketPosition = nil if let teamScore, teamScore.luckyLoser != nil {
match._toggleLoserMatchDisableState(false) Text("Repêchée").italic().font(.caption)
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
} label: {
Label("retirer", systemImage: "xmark")
}
} }
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 { } else {
HStack { VStack(alignment: .leading) {
TeamPickerView(teamPicked: { team in if let team {
print(team.pasteData()) TeamRowView(team: team, teamPosition: teamPosition)
team.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false) .strikethrough()
try? dataStore.matches.addOrUpdate(instance: match) }
try? dataStore.teamRegistrations.addOrUpdate(instance: team) HStack {
}) let walkOutSpot = match.isWalkOutSpot(teamPosition)
if let tournament = match.currentTournament() { let luckyLosers = walkOutSpot ? match.luckyLosers() : []
let availableSeedGroups = tournament.availableSeedGroups() TeamPickerView(luckyLosers: luckyLosers, teamPicked: { team in
Menu { print(team.pasteData())
ForEach(availableSeedGroups, id: \.self) { seedGroup in if walkOutSpot {
Button { match.setLuckyLoser(team: team, teamPosition: teamPosition)
if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) { try? dataStore.matches.addOrUpdate(instance: match)
randomTeam.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false) } else {
try? dataStore.matches.addOrUpdate(instance: match) team.setSeedPosition(inSpot: match, slot: teamPosition, opposingSeeding: false)
try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam) 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: { .disabled(availableSeedGroups.isEmpty && walkOutSpot == false)
Text("Tirage").tag(nil as SeedInterval?)
} }
.disabled(availableSeedGroups.isEmpty)
} }
.fixedSize(horizontal: false, vertical: true)
.buttonBorderShape(.capsule)
.buttonStyle(.borderedProminent)
} }
.fixedSize(horizontal: false, vertical: true)
.buttonBorderShape(.capsule)
.buttonStyle(.borderedProminent)
} }
} }
} }

@ -84,14 +84,14 @@ struct MatchSummaryView: View {
if matchViewStyle != .feedStyle { if matchViewStyle != .feedStyle {
HStack(spacing: 0) { HStack(spacing: 0) {
VStack(alignment: .leading, spacing: matchViewStyle == .plainStyle ? 8 : 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) .padding(matchViewStyle == .plainStyle ? 0 : 8)
if width == 1 { if width == 1 {
Divider() Divider()
} else { } else {
Divider().frame(height: width).overlay(color) 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) .padding(matchViewStyle == .plainStyle ? 0 : 8)
} }
} }

@ -9,15 +9,15 @@ import SwiftUI
struct PlayerBlockView: View { struct PlayerBlockView: View {
var match: Match var match: Match
let whichTeam: TeamData let teamPosition: TeamPosition
let team: TeamRegistration? let team: TeamRegistration?
let color: Color let color: Color
let width: CGFloat 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.match = match
self.whichTeam = whichTeam self.teamPosition = teamPosition
self.team = match.team(whichTeam) self.team = match.team(teamPosition)
self.color = color self.color = color
self.width = width self.width = width
} }
@ -43,13 +43,21 @@ struct PlayerBlockView: View {
} }
private func _defaultLabel() -> String { private func _defaultLabel() -> String {
whichTeam.localizedLabel() if match.upperBracketMatch(teamPosition)?.disabled == true {
return "Bye"
}
return teamPosition.localizedLabel()
} }
var body: some View { var body: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let names { 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 ForEach(names, id: \.self) { name in
Text(name).lineLimit(1) Text(name).lineLimit(1)
} }

@ -24,6 +24,9 @@ struct LoserRoundsView: View {
switch selectedRound { switch selectedRound {
case .none: case .none:
List { List {
RowButtonView("Effacer", role: .destructive) {
}
} }
case .some(let selectedRound): case .some(let selectedRound):
LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index)) LoserRoundView(loserRounds: upperBracketRound.loserRounds(forRoundIndex: selectedRound.index))
@ -35,10 +38,20 @@ struct LoserRoundsView: View {
} }
struct LoserRoundView: View { struct LoserRoundView: View {
@EnvironmentObject var dataStore: DataStore
let loserRounds: [Round] let loserRounds: [Round]
@Environment(\.editMode) private var editMode
private func _roundDisabled() -> Bool {
loserRounds.allSatisfy({ $0.isDisabled() })
}
var body: some View { var body: some View {
List { List {
if editMode?.wrappedValue.isEditing == true {
_editingView()
}
ForEach(loserRounds) { loserRound in ForEach(loserRounds) { loserRound in
Section { Section {
ForEach(loserRound.playedMatches()) { match in ForEach(loserRound.playedMatches()) { match in
@ -59,5 +72,33 @@ struct LoserRoundView: View {
} }
} }
.headerProminence(.increased) .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
}
}
}
}
} }
} }

@ -9,8 +9,8 @@ import SwiftUI
struct RoundSettingsView: View { struct RoundSettingsView: View {
@EnvironmentObject var dataStore: DataStore @EnvironmentObject var dataStore: DataStore
@Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament @Environment(Tournament.self) var tournament: Tournament
@Binding var isEditingTournamentSeed: Bool
@State private var roundIndex: Int? @State private var roundIndex: Int?
var round: Round? { var round: Round? {
@ -20,8 +20,6 @@ struct RoundSettingsView: View {
var body: some View { var body: some View {
List { List {
Toggle("Éditer les têtes de série", isOn: $isEditingTournamentSeed)
if let availableSeedGroup = tournament.availableSeedGroup() { if let availableSeedGroup = tournament.availableSeedGroup() {
Section { Section {
@ -47,10 +45,10 @@ struct RoundSettingsView: View {
if let matches = tournament.getRound(atRoundIndex: roundIndex)?.playedMatches() { if let matches = tournament.getRound(atRoundIndex: roundIndex)?.playedMatches() {
if let lastMatch = matches.last { 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 { 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) try? dataStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
@ -58,6 +56,10 @@ struct RoundSettingsView: View {
tournament.setSeeds(inRoundIndex: roundIndex, inSeedGroup: availableSeedGroup) tournament.setSeeds(inRoundIndex: roundIndex, inSeedGroup: availableSeedGroup)
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds()) 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") Text("Placement des têtes de série")
} }
} }
//
Section { // Section {
RowButtonView("Effacer classement", role: .destructive) { // RowButtonView("Effacer classement", role: .destructive) {
tournament.rounds().forEach { round in // tournament.rounds().forEach { round in
try? dataStore.rounds.delete(contentOfs: round.loserRounds()) // try? dataStore.rounds.delete(contentOfs: round.loserRounds())
} // }
} // }
} // }
//
Section { // Section {
RowButtonView("Match de classement") { // RowButtonView("Match de classement") {
tournament.rounds().forEach { round in // tournament.rounds().forEach { round in
round.buildLoserBracket() // round.buildLoserBracket()
} // }
} // }
} // }
Section { Section {
RowButtonView("Retirer toutes les têtes de séries", role: .destructive) { RowButtonView("Retirer toutes les têtes de séries", role: .destructive) {
tournament.unsortedTeams().forEach({ $0.bracketPosition = nil }) tournament.unsortedTeams().forEach({ $0.bracketPosition = nil })
@ -119,7 +121,7 @@ struct RoundSettingsView: View {
} }
#Preview { #Preview {
RoundSettingsView(isEditingTournamentSeed: .constant(true)) RoundSettingsView()
.environment(Tournament.mock()) .environment(Tournament.mock())
.environmentObject(DataStore.shared) .environmentObject(DataStore.shared)
} }

@ -8,18 +8,25 @@
import SwiftUI import SwiftUI
struct RoundView: View { struct RoundView: View {
@Environment(\.editMode) private var editMode
@Environment(Tournament.self) var tournament: Tournament
var round: Round var round: Round
var body: some View { var body: some View {
List { List {
let loserRounds = round.loserRounds()
if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) { if editMode?.wrappedValue.isEditing == false {
Section { let loserRounds = round.loserRounds()
NavigationLink { if loserRounds.isEmpty == false, let first = loserRounds.first(where: { $0.isDisabled() == false }) {
LoserRoundsView(upperBracketRound: round) Section {
.navigationTitle(first.roundTitle()) NavigationLink {
} label: { LoserRoundsView(upperBracketRound: round)
Text(first.roundTitle()) .environment(tournament)
.navigationTitle(first.roundTitle())
} label: {
Text(first.roundTitle())
}
} }
} }
} }
@ -33,9 +40,15 @@ struct RoundView: View {
} }
} }
.headerProminence(.increased) .headerProminence(.increased)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
EditButton()
}
}
} }
} }
#Preview { #Preview {
RoundView(round: Round.mock()) RoundView(round: Round.mock())
.environment(Tournament.mock())
} }

@ -10,13 +10,13 @@ import SwiftUI
struct RoundsView: View { struct RoundsView: View {
var tournament: Tournament var tournament: Tournament
@State private var selectedRound: Round? @State private var selectedRound: Round?
@State private var isEditingTournamentSeed = false @State var editMode: EditMode = .inactive
init(tournament: Tournament) { init(tournament: Tournament) {
self.tournament = tournament self.tournament = tournament
_selectedRound = State(wrappedValue: tournament.getActiveRound()) _selectedRound = State(wrappedValue: tournament.getActiveRound())
if tournament.availableSeeds().isEmpty == false { 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) GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: tournament.rounds(), nilDestinationIsValid: true)
switch selectedRound { switch selectedRound {
case .none: case .none:
RoundSettingsView(isEditingTournamentSeed: $isEditingTournamentSeed) RoundSettingsView()
.navigationTitle("Réglages") .navigationTitle("Réglages")
case .some(let selectedRound): case .some(let selectedRound):
RoundView(round: selectedRound) RoundView(round: selectedRound)
.navigationTitle(selectedRound.roundTitle()) .navigationTitle(selectedRound.roundTitle())
.editTournamentSeed(isEditingTournamentSeed)
} }
} }
.environment(\.editMode, $editMode)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.visible, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar)
} }

@ -12,7 +12,7 @@ struct EditScoreView: View {
@ObservedObject var matchDescriptor: MatchDescriptor @ObservedObject var matchDescriptor: MatchDescriptor
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
func walkout(_ team: TeamData) { func walkout(_ team: TeamPosition) {
matchDescriptor.match?.setWalkOut(team) matchDescriptor.match?.setWalkOut(team)
save() save()
dismiss() dismiss()

@ -13,16 +13,34 @@ struct TeamPickerView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var presentTeamPickerView: Bool = false @State private var presentTeamPickerView: Bool = false
@State private var searchField: String = "" @State private var searchField: String = ""
var luckyLosers: [TeamRegistration] = []
let teamPicked: ((TeamRegistration) -> (Void)) let teamPicked: ((TeamRegistration) -> (Void))
var body: some View { var body: some View {
Button("Choisir") { Button("Choisir") {
presentTeamPickerView = true presentTeamPickerView = true
} }
.sheet(isPresented: $presentTeamPickerView) { .sheet(isPresented: $presentTeamPickerView) {
NavigationStack { NavigationStack {
List { List {
let teams = tournament.sortedTeams() 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 { Section {
_teamListView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight).reversed()) _teamListView(teams.filter({ $0.availableForSeedPick() }).sorted(by: \.weight).reversed())
} header: { } header: {

@ -9,12 +9,12 @@ import SwiftUI
struct TeamRowView: View { struct TeamRowView: View {
var team: TeamRegistration var team: TeamRegistration
var teamPosition: Int? = nil var teamPosition: TeamPosition? = nil
var body: some View { var body: some View {
LabeledContent { LabeledContent {
VStack(alignment: .trailing, spacing: 0) { VStack(alignment: .trailing, spacing: 0) {
if teamPosition == 0 || teamPosition == nil { if teamPosition == .one || teamPosition == nil {
Text(team.weight.formatted()) Text(team.weight.formatted())
.font(.caption) .font(.caption)
} }
@ -22,7 +22,7 @@ struct TeamRowView: View {
Text("#" + (index + 1).formatted()) Text("#" + (index + 1).formatted())
.font(.title) .font(.title)
} }
if teamPosition == 1 { if teamPosition == .two {
Text(team.weight.formatted()) Text(team.weight.formatted())
.font(.caption) .font(.caption)

Loading…
Cancel
Save