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)
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..<size).combinations(ofCount: 2))
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 {
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() {
_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))
}

@ -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()

@ -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 {

@ -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

@ -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
}

@ -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
}

@ -61,7 +61,7 @@ class MatchDescriptor: ObservableObject {
}
}
var winner: TeamData {
var winner: TeamPosition {
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 {
return setFormat.winner(teamOne: valueTeamOne, teamTwo: valueTeamTwo)
} else {

@ -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 {

@ -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)
}
}
}

@ -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)
}
}

@ -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)
}

@ -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
}
}
}
}
}
}

@ -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)
}

@ -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())
}

@ -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)
}

@ -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()

@ -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: {

@ -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)

Loading…
Cancel
Save