// // PadelRule.swift // Padel Tournament // // Created by razmig on 27/02/2023. // import Foundation import LeStorage enum RankSource: Hashable { case national case ligue case club(assimilation: Bool) func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .national: return "Classement National" case .ligue: return "Classement Ligue" case .club: return "Classement Club" } } } protocol TournamentBuildHolder: Identifiable { var id: String { get } var category: TournamentCategory { get } var level: TournamentLevel { get } var age: FederalTournamentAge { get } func buildHolderTitle(_ displayStyle: DisplayStyle) -> String } struct TournamentBuild: TournamentBuildHolder, Hashable, Codable, Identifiable { var uniqueId: String = Store.randomId() var id: String { uniqueId } let category: TournamentCategory let level: TournamentLevel let age: FederalTournamentAge // var japIdentifier: Int? = nil // var japFirstName: String? = nil // var japLastName: String? = nil func buildHolderTitle(_ displayStyle: DisplayStyle) -> String { computedLabel(displayStyle) } var identifier: String { level.localizedLevelLabel()+":"+category.localizedLabel()+":"+age.localizedFederalAgeLabel() } func computedLabel(_ displayStyle: DisplayStyle = .wide) -> String { if age == .senior { return localizedLabel(displayStyle) } return localizedLabel(displayStyle) + " " + localizedAge(displayStyle) } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle) } func localizedTitle(_ displayStyle: DisplayStyle = .wide) -> String { level.localizedLevelLabel(displayStyle) + " " + category.localizedLabel(displayStyle) } func localizedAge(_ displayStyle: DisplayStyle = .wide) -> String { age.localizedFederalAgeLabel(displayStyle) } } extension TournamentBuild { init?(category: String, level: String, age: FederalTournamentAge = .senior) { guard let levelFound = TournamentLevel.allCases.first(where: { $0.localizedLevelLabel() == level }) else { return nil } var c = category if c.hasPrefix("ME") { c = "H" } if c.hasPrefix("F") { c = "D" } guard let categoryFound = TournamentCategory.allCases.first(where: { c.canonicalVersion.hasPrefix($0.buildLabel.canonicalVersion) }) else { return nil } self.level = levelFound self.category = categoryFound self.age = age } } enum FederalTournamentType: String, Hashable, Codable, CaseIterable, Identifiable { case tournoi = "P" case championnatParEquipe = "S" case championnatParPaire = "L" var id: String { self.rawValue } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .tournoi: return "Tournois" case .championnatParEquipe: return "Championnats par équipes" case .championnatParPaire: return "Championnats par paires" } } } enum TournamentDifficulty { case rankS case rankA case rankB case rankC init?(gameDifference: Double) { switch gameDifference { case ..<3: self = .rankS case ..<5: self = .rankA case ..<7: self = .rankB case 7...: self = .rankC default: return nil } } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .rankS: return "S" case .rankA: return "A" case .rankB: return "B" case .rankC: return "C" } } var backgroundColor: String { switch self { case .rankS: return "#d4af37" case .rankA: return "#c0c0c0" case .rankB: return "#cd7f32" case .rankC: return "#DCC2E0" } } } enum FederalTournamentAge: Int, Hashable, Codable, CaseIterable, Identifiable { case unlisted = 0 case a11_12 = 120 case a13_14 = 140 case a15_16 = 160 case a17_18 = 180 case senior = 200 case a45 = 450 case a55 = 550 init?(rawValue: Int?) { guard let value = rawValue else { return nil } self.init(rawValue: value) } func computedBirthYear() -> (Int?, Int?) { let year = Calendar.current.getSportAge() switch self { case .unlisted: return (nil, nil) case .a11_12: return (year - 12, year - 11) case .a13_14: return (year - 14, year - 13) case .a15_16: return (year - 16, year - 15) case .a17_18: return (year - 18, year - 17) case .senior: return (nil, year - 19) case .a45: return (nil, year - 45) case .a55: return (nil, year - 55) } } var importingRawValue: String { switch self { case .unlisted: return "Animation" case .a11_12: return "11/12 ans" case .a13_14: return "13/14 ans" case .a15_16: return "15/16 ans" case .a17_18: return "17/18 ans" case .senior: return "Senior" case .a45: return "45 ans" case .a55: return "55 ans" } } static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { return tournaments.first?.federalTournamentAge ?? .senior } static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { let countedSet = NSCountedSet(array: tournaments.map { $0.federalTournamentAge }) let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } if mostFrequent != nil { return mostFrequent as! FederalTournamentAge } else { return mostRecent(inTournaments: tournaments) } } var id: Int { self.rawValue } var order: Int { switch self { case .unlisted: return 7 case .a11_12: return 6 case .a13_14: return 5 case .a15_16: return 4 case .a17_18: return 3 case .senior: return 0 case .a45: return 1 case .a55: return 2 } } func localizedFederalAgeLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .unlisted: return displayStyle == .title ? "Aucune" : "" case .a11_12: return "11/12 ans" case .a13_14: return "13/14 ans" case .a15_16: return "15/16 ans" case .a17_18: return "17/18 ans" case .senior: return displayStyle == .short ? "" : "Senior" case .a45: return "+45 ans" case .a55: return "+55 ans" } } var tournamentDescriptionLabel: String { return localizedFederalAgeLabel() } func isAgeValid(age: Int?) -> Bool { guard let age else { return true } switch self { case .unlisted: return true case .a11_12: return age < 13 case .a13_14: return age < 15 case .a15_16: return age < 17 case .a17_18: return age < 19 case .senior: return age >= 11 case .a45: return age >= 45 case .a55: return age >= 55 } } } enum TournamentLevel: Int, Hashable, Codable, CaseIterable, Identifiable { case unlisted = 0 case p25 = 25 case p100 = 100 case p250 = 250 case p500 = 500 case p1000 = 1000 case p1500 = 1500 case p2000 = 2000 case championship = 1 init?(rawValue: Int?) { guard let value = rawValue else { return nil } self.init(rawValue: value) } static var assimilationAllCases: [TournamentLevel] = { return [.p25, .p100, .p250, .p500, .p1000, .p1500, .p2000] }() var entryFee: Double? { switch self { case .unlisted, .championship: return nil case .p25: return 15 default: return 20 } } func isAnimation() -> Bool { switch self { case .unlisted: return true case .championship: return false default: return false } } func searchRawValue() -> String { String(describing: self) } func pointsRange(first: Int, last: Int, teamsCount: Int) -> String { let range = [points(for: first - 1, count: teamsCount), points(for: last - 1, count: teamsCount)] return range.map { $0.formatted(.number.sign(strategy: .always())) }.joined(separator: " / ") + " pts" } func hideWeight() -> Bool { switch self { case .unlisted: return true default: return false } } func shouldShareTeams() -> Bool { switch self { case .p500, .p1000, .p1500, .p2000: return true default: return false } } static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { return tournaments.first?.tournamentLevel ?? .p100 } static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentLevel }) let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } if mostFrequent != nil { return mostFrequent as! TournamentLevel } else { return mostRecent(inTournaments: tournaments) } } var id: Int { self.rawValue } func wildcardArePossible() -> Bool { switch self { case .p500, .p1000, .p1500, .p2000: return true default: return false } } func minimumPlayerRank(category: TournamentCategory, ageCategory: FederalTournamentAge) -> Int { switch self { case .p25: switch ageCategory { case .senior, .a45, .a55: return category == .men ? 20000 : 1000 default: return 0 } case .p100: switch ageCategory { case .senior, .a45, .a55: return category == .men ? 2000 : 300 default: return 0 } case .p250: switch ageCategory { case .senior, .a45, .a55: if category == .mix { return 0 } return category == .men ? 500 : 100 default: return 0 } default: return 0 } } func federalFormatForGroupStage() -> MatchFormat { federalFormatForBracketRound(5) } func federalFormatForBracketRound(_ roundIndex: Int) -> MatchFormat { switch self { case .p25: return .superTie case .p100: return .nineGamesDecisivePoint case .p250: if roundIndex == 0 { //finale return .twoSetsDecisivePointSuperTie } else { return .nineGamesDecisivePoint } case .p500: if roundIndex == 0 { //finale return .twoSetsDecisivePointSuperTie } else if roundIndex == 1 { //demi-finale return .twoSetsDecisivePointSuperTie } else { return .nineGamesDecisivePoint } case .p1000: if roundIndex <= 3 { //demi / finale / quart / 8eme return .twoSetsDecisivePoint } else { return .twoSetsDecisivePointSuperTie } case .p1500, .p2000: if roundIndex <= 3 { //demi / finale / quart / 8eme return .twoSetsDecisivePoint } else { return .twoSetsSuperTie } default: return .superTie } } func decisivePointRequired(ageCategory: FederalTournamentAge) -> Bool { switch ageCategory { case .a11_12, .a13_14, .a15_16, .a17_18: return true default: return false } } func federalFormatForLoserBracketRound(_ roundIndex: Int) -> MatchFormat { switch self { case .p25: return .superTie case .p100, .p250, .p500: return .nineGamesDecisivePoint case .p1000: return .nineGamesDecisivePoint case .p1500, .p2000: return .twoSetsSuperTie default: return .nineGamesDecisivePoint } } var defaultTeamSortingType: TeamSortingType { switch self { case .p25, .p100, .p250: return .inscriptionDate default: return .rank } } var order: Int { switch self { case .unlisted: return 7 case .p25: return 6 case .p100: return 5 case .p250: return 4 case .p500: return 3 case .p1000: return 2 case .p1500: return 1 case .p2000: return 0 case .championship: return 8 } } func localizedLevelLabel(_ displayStyle: DisplayStyle = .wide) -> String { if self == .unlisted { if DeviceHelper.isBigScreen() { return "Animation" } else { return displayStyle == .title ? "Animation" : "Anim." } } if self == .championship { if DeviceHelper.isBigScreen() { return "Championnat" } else { return displayStyle == .title ? "Championnat" : "CHPT" } } return String(describing: self).capitalized } var coachingIsAuthorized: Bool { switch self { case .p500, .p1000, .p1500, .p2000, .championship: return true default: return false } } func points(for rank: Int, count: Int) -> Int { if self == .unlisted { return 0 } if self == .championship { return 0 } let points = points(for: count) if rank >= points.count { return points.last! } else if rank == 0 { return Int(self.rawValue) } else { return points[rank-1] } } func allPoints(for count: Int) -> [Int] { [Int(self.rawValue)] + points(for: count) } func points(for count: Int) -> [Int] { switch self { case .unlisted, .championship: return [] case .p25: switch count { case 9...12: return [17, 15, 13, 11, 9, 7, 5, 4, 3, 2, 1] case 13...16: return [18,16,15,14,13,12,11,10,9,7,5,4,3,2, 1] case 17...20: return [20,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2, 1] case 21...24: return [20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2, 1] case 25...28: return [21,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2, 1] case _ where count > 28: return [23,21,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2, 1] default: return [15, 12, 9, 6, 4, 2, 1] } case .p100: switch count { case 9...12: return [65,55, 50, 35, 25, 20, 15, 10, 5, 3, 1] case 13...16: return [70, 60, 55, 45, 40, 35, 30, 25, 21, 18, 15, 10, 5, 3, 1] case 17...20: return [75,65,60,55,50,45,40,35,30,25,23,20,18,15,12,10,5,3, 1] case 21...24: return [75,70,65,60,55,50,47,43,40,37,33,30,28,25,23,20,18,15,12,10,5,3, 1] case 25...28: return [80,75,70,65,60,55,53,50,48,45,43,40,38,35,33,30,28,25,23,20,18,15,12,10,5,3, 1] case _ where count > 28: return [80,75,72,70,65,63,60,58,55,53,50,48,45,43,40,38,35,33,30,28,25,23,20,18,15,12,10,8,5,3, 1] default: return [60,50,40,25,10,5] } case .p250: switch count { case 9...12: return [163,138,125,88,63,50,38,25,13,8,3] case 13...16: return [175,150,138,113,100,88,75,63,53,45,38,25,13,8,3] case 17...20: return [188,163,150,138,123,113,100,88,75,63,58,50,45,38,30,25,13,8,3] case 21...24: return [188,175,163,150,138,125,118,108,100,93,83,75,70,63,58,50,45,38,30,25,13,8,3] case 25...28: return [200,188,175,163,150,138,133,125,120,113,108,100,95,88,83,75,70,63,58,50,45,38,30,25,13,8,3] case _ where count > 28: return [200,188,180,175,163,158,150,145,138,133,125,120,113,108,100,95,88,83,75,70,63,58,50,45,38,30,25,20,13,8,3] default: return [150,125,100,63,25,13,3] } case .p500: switch count { case 9...12: return [325,275,250,175,125,100,75,50,25,15,5] case 13...16: return [350,300,275,225,200,175,150,125,105,90,75,50,25,15,5] case 17...20: return [375,325,300,275,250,225,200,175,150,125,115,100,90,75,60,50,25,15,5] case 21...24: return [375,350,325,300,275,250,235,215,200,185,165,150,140,125,115,100,80,75,60,50,25,15,5] case 25...28: return [400,375,350,325,300,275,265,250,240,225,215,200,190,175,165,150,140,125,115,100,80,75,60,50,25,15,5] case _ where count > 28: return [400,375,360,350,325,315,300,290,275,265,250,240,225,215,200,190,175,165,150,140,125,115,100,80,75,60,50,40,25,15,5] default: return [300,250,200,125,50,25,5] } case .p1000: switch count { case 9...12: return [650,550, 500, 350, 250, 200, 150, 100, 50, 30, 10] case 13...16: return [700, 600, 550, 450, 400, 350, 300, 250, 210, 180, 150, 100, 50, 30, 10] case 17...20: return [750,650,600,550,500,450,400,350,300,250,230,200,180,150,120,100,50,30, 10] case 21...24: return [750,700,650,600,550,500,470,430,400,370,330,300,280,250,230,200,180,150,120,100,50,30, 10] case 25...28: return [800,750,700,650,600,550,530,500,480,450,430,400,380,350,330,300,280,250,230,200,180,150,120,100,50,30, 10] case _ where count > 28: return [800,750,720,700,650,630,600,580,550,530,500,480,450,430,400,380,350,330,300,280,250,230,200,180,150,120,100,80,50,30, 10] default: return [600,500,400,250,100,50] } case .p1500: switch count { case 21...24: return [1125,1050,975,900,825,750,705,645,600,555,495,450,420,375,345,300,270,225,180,150,75,45,15] case 25...28: return [1200,1125,1050,975,900,825,795,750,720,675,645,600,570,525,495,450,420,375,345,300,270,225,180,150,75,45,15] case _ where count > 28: return [1200,1125,1080,1050,975,945,900,870,825,795,750,720,675,645,600,570,525,495,450,420,375,345,300,270,225,180,150,120,75,45,15] default: return [1125,975,900,825,750,675,600,525,450,375,345,300,270,225,180,150,75,45,15] } case .p2000: return [1600,1440,1300,1180,1120,1060,1000,880,840,800,760,720,680,640,600,540,520,500,480,460,440,420,400,260,200,40] } } var ranges: [PlayersCountRange] { switch self { case .p1500: return [.N16, .N24, .N28, .N32] case .p2000: return [.N32] default: return PlayersCountRange.allCases } } // enum NewBallSystem { // case perField // case perMatch(fromRound: Int?) // // func localizedLabel(loserBracket: Bool = false) -> String { // switch self { // case .perField: // return "3 / piste" // case .perMatch(let fromRound): // if fromRound != nil { // if loserBracket { // return "3 / match pour les perdants des \(RoundLabel.shortLabels[fromRound!].lowercased())s" // } else { // return "3 / match à partir des \(RoundLabel.shortLabels[fromRound!].lowercased())s" // } // } else { // return "3 / match" // } // } // } // } // func minimumFormatFinalTableAndQualifier(roundIndex: Int) -> MatchFormat? { switch self { case .p25, .unlisted, .championship: return nil case .p100: return .nineGamesDecisivePoint case .p250: if roundIndex == 0 { //final return .twoSetsDecisivePointSuperTie } else { return .nineGamesDecisivePoint } case .p500: if roundIndex == 0 { //final return .twoSetsDecisivePoint } else if roundIndex == 1 { //demi return .twoSetsDecisivePointSuperTie } else { return .nineGamesDecisivePoint } case .p1000, .p1500, .p2000: if roundIndex == 3 { //16eme return .twoSetsDecisivePoint } else { return .twoSetsDecisivePointSuperTie } } } func minimumFormatLoserBracket(roundIndex: Int) -> MatchFormat? { switch self { case .p25, .unlisted, .championship: return nil case .p100, .p250, .p500: return .nineGamesDecisivePoint case .p1000, .p1500, .p2000: if roundIndex == 1 { //demi return .twoSetsDecisivePoint } else { return .nineGamesDecisivePoint } } } // func newBallsFinalTable() -> NewBallSystem? { // switch self { // case .p25, .p100: // return .perField // case .p250: // return .perMatch(fromRound: 1) //demi // case .p500: // return .perMatch(fromRound: 2) //quart // case .p1000, .p1500, .p2000: // return .perMatch(fromRound: nil) // } // } // // func newBallsLoserBracket() -> NewBallSystem? { // switch self { // case .p25, .p100: // return nil // case .p250: // return .perMatch(fromRound: 1) //demi // case .p500, .p1000, .p1500, .p2000: // return .perMatch(fromRound: 2) //quart // } // } // } enum TournamentCategory: Int, Hashable, Codable, CaseIterable, Identifiable { case men case women case mix case unlisted init?(rawValue: Int?) { guard let value = rawValue else { return nil } self.init(rawValue: value) } func mandatoryPlayerType() -> [Int] { switch self { case .unlisted: return [] case .mix: return [0, 1] case .women: return [0, 0] case .men: return [1, 1] } } var localizedPlayerLabel: String { switch self { case .women: return "joueuse" default: return "joueur" } } var showFemaleInMaleAssimilation: Bool { switch self { case .men: return true default: return false } } static func femaleInMaleAssimilationAddition(_ rank: Int) -> Int { switch rank { case 1...10: return 400 case 11...30: return 1000 case 31...60: return 2000 case 61...100: return 3500 case 101...200: return 10000 case 201...500: return 15000 case 501...1000: return 25000 case 1001...2000: return 35000 case 2001...3000: return 45000 default: return 50000 } } static func mostRecent(inTournaments tournaments: [Tournament]) -> Self { return tournaments.first?.tournamentCategory ?? .men } static func mostUsed(inTournaments tournaments: [Tournament]) -> Self { let countedSet = NSCountedSet(array: tournaments.map { $0.tournamentCategory }) let mostFrequent = countedSet.max { countedSet.count(for: $0) < countedSet.count(for: $1) } if mostFrequent != nil { return mostFrequent as! TournamentCategory } else { return mostRecent(inTournaments: tournaments) } } var id: Int { self.rawValue } var order: Int { switch self { case .unlisted: return 0 case .men: return 1 case .women: return 2 case .mix: return 3 } } var buildLabel: String { switch self { case .unlisted: return "" case .men: return "H" case .women: return "D" case .mix: return "M" } } var requestLabel: String { switch self { case .unlisted: return "" case .men: return "DM" case .women: return "DD" case .mix: return "DX" } } var importingRawValue: String { switch self { case .unlisted: return "messieurs" case .men: return "messieurs" case .women: return "dames" case .mix: return "mixte" } } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .unlisted: return displayStyle == .title ? "Aucune" : "" case .men: switch displayStyle { case .title: return "Hommes" case .wide: return "Hommes" case .short: return "H" } case .women: switch displayStyle { case .title: return "Dames" case .wide: return "Dames" case .short: return "D" } case .mix: switch displayStyle { case .title: return "Mixte" case .wide: return "Mixte" case .short: return "MX" } } } var playerFilterOption: PlayerFilterOption { switch self { case .men, .unlisted: return .all case .women: return .female case .mix: return .all } } } enum GroupStageOrderingMode: Int, Hashable, Codable, CaseIterable, Identifiable { case random case snake case swiss init?(rawValue: Int?) { guard let value = rawValue else { return nil } self.init(rawValue: value) } var id: Int { self.rawValue } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .random: return "Au hasard" case .snake: return "En serpentin" case .swiss: return "Suisse" } } var systemImage: String { switch self { case .random: return "dice.fill" case .snake: return "arrow.triangle.swap" case .swiss: return "cross.fill" } } } enum TournamentType: Int, Hashable, Codable, CaseIterable, Identifiable { case classic case doubleBrackets var id: Int { self.rawValue } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .classic: return "Classique" case .doubleBrackets: return "Double Poules" } } } enum TeamPosition: Int, Identifiable, Hashable, Codable, CaseIterable { case one case two var id: Int { self.rawValue } var otherTeam: TeamPosition { switch self { case .one: return .two case .two: return .one } } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { var shortName: String { switch self { case .one: return "#1" case .two: return "#2" } } switch displayStyle { case .wide, .title: return "Équipe " + shortName case .short: return shortName } } func localizedBranchLabel() -> String { switch self { case .one: return "Branche du haut" case .two: return "Branche du bas" } } } enum SetFormat: Int, Hashable, Codable { case nine case four case six case superTieBreak case megaTieBreak func shouldTiebreak(scoreTeamOne: Int, scoreTeamTwo: Int) -> Bool { if let tieBreak { return (scoreTeamOne + scoreTeamTwo) >= (2 * tieBreak) && (scoreTeamOne == tieBreak || scoreTeamTwo == tieBreak) } else { return false } } func winner(teamOne: Int, teamTwo: Int) -> TeamPosition { return teamOne > teamTwo ? .one : .two } func hasEnded(teamOne: Int, teamTwo: Int) -> Bool { switch self { case .nine: if teamOne == 9 || teamTwo == 9 { return true } case .four: if teamOne == 5 || teamTwo == 5 { return true } case .six: if teamOne == 7 || teamTwo == 7 { return true } case .superTieBreak, .megaTieBreak: if teamOne == 1 || teamTwo == 1 { return true } } return (teamOne >= scoreToWin && teamOne >= teamTwo + 2) || (teamTwo >= scoreToWin && teamTwo >= teamOne + 2) } var tieBreak: Int? { switch self { case .nine: return 8 case .four: return 4 case .six: return 6 case .superTieBreak, .megaTieBreak: return nil } } func disableValuesForTeamTwo(with teamOneScore: Int) -> [Int] { switch self { case .nine: if teamOneScore == 9 { return [9] } case .four: if teamOneScore == 4 { return [] } case .six: if teamOneScore == 6 { return [] } case .superTieBreak: if teamOneScore == 10 { return [] } case .megaTieBreak: if teamOneScore == 15 { return [] } } return [] } var possibleValues: [Int] { switch self { case .nine: return [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] case .four: return [5, 4, 3, 2, 1, 0] case .six: return [7, 6, 5, 4, 3, 2, 1, 0] case .superTieBreak: return [10,9, 8, 7, 6, 5, 4, 3, 2, 1, 0] case .megaTieBreak: return [15, 14, 13, 12, 11, 10,9, 8, 7, 6, 5, 4, 3, 2, 1, 0] } } var scoreToWin: Int { switch self { case .nine: return 9 case .four: return 4 case .six: return 6 case .superTieBreak: return 10 case .megaTieBreak: return 15 } } var firstGameFormat: Format { switch self { case .megaTieBreak: return .tiebreakFifteen case .superTieBreak: return .tiebreakTen default: return .normal } } } enum MatchType: String { case bracket = "bracket" case groupStage = "groupStage" case loserBracket = "loserBracket" } enum MatchFormat: Int, Hashable, Codable, CaseIterable, Identifiable { var id: Int { self.rawValue } case twoSets case twoSetsSuperTie case twoSetsOfFourGames case nineGames case superTie case megaTie case twoSetsDecisivePoint case twoSetsDecisivePointSuperTie case twoSetsOfFourGamesDecisivePoint case nineGamesDecisivePoint case twoSetsOfSuperTie case singleSet case singleSetDecisivePoint case singleSetOfFourGames case singleSetOfFourGamesDecisivePoint init?(rawValue: Int?) { guard let value = rawValue else { return nil } self.init(rawValue: value) } func defaultWalkOutScore(_ asWalkOutTeam: Bool) -> [Int] { Array(repeating: asWalkOutTeam ? 0 : setFormat.scoreToWin, count: setsToWin) } var weight: Int { switch self { case .twoSets, .twoSetsDecisivePoint: return 0 case .twoSetsSuperTie, .twoSetsDecisivePointSuperTie: return 1 case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint: return 2 case .nineGames, .nineGamesDecisivePoint: return 3 case .superTie: return 4 case .megaTie: return 5 case .twoSetsOfSuperTie: return 6 case .singleSet, .singleSetDecisivePoint: return 7 case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return 8 } } var rank: Int { switch self { case .twoSets: return 0 case .twoSetsDecisivePoint: return 0 case .twoSetsSuperTie: return 1 case .twoSetsDecisivePointSuperTie: return 1 case .twoSetsOfFourGames: return 2 case .twoSetsOfFourGamesDecisivePoint: return 2 case .nineGames: return 3 case .nineGamesDecisivePoint: return 3 case .superTie: return 4 case .megaTie: return 5 case .twoSetsOfSuperTie: return 6 case .singleSet, .singleSetDecisivePoint: return 7 case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return 8 } } static func defaultFormatForMatchType(_ matchType: MatchType) -> MatchFormat { switch matchType { case .bracket: DataStore.shared.user.bracketMatchFormatPreference ?? .nineGamesDecisivePoint case .groupStage: DataStore.shared.user.groupStageMatchFormatPreference ?? .nineGamesDecisivePoint case .loserBracket: DataStore.shared.user.loserBracketMatchFormatPreference ?? .nineGamesDecisivePoint } } static var allCases: [MatchFormat] = { [.twoSets, .twoSetsDecisivePoint, .twoSetsSuperTie, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint] }() func winner(scoreTeamOne: Int, scoreTeamTwo: Int) -> TeamPosition { scoreTeamOne >= scoreTeamTwo ? .one : .two } func hasEnded(scoreTeamOne: Int, scoreTeamTwo: Int) -> Bool { scoreTeamOne == setsToWin || scoreTeamTwo == setsToWin } var canSuperTie: Bool { switch self { case .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return true default: return false } } func getEstimatedDuration(_ additionalDuration: Int = 0) -> Int { estimatedDuration + additionalDuration } private var estimatedDuration: Int { DataStore.shared.user.matchFormatsDefaultDuration?[self] ?? defaultEstimatedDuration } func formattedEstimatedDuration(_ additionalDuration: Int = 0) -> String { Duration.seconds((estimatedDuration + additionalDuration) * 60).formatted(.units(allowed: [.minutes])) } func formattedEstimatedBreakDuration() -> String { var label = Duration.seconds(breakTime.breakTime * 60).formatted(.units(allowed: [.minutes])) if breakTime.matchCount > 1 { label += " de pause après \(breakTime.matchCount) match" label += breakTime.matchCount.pluralSuffix } else { label += " de pause" } return label } var defaultEstimatedDuration: Int { switch self { case .twoSets: return 105 case .twoSetsDecisivePoint: return 90 case .twoSetsSuperTie: return 80 case .twoSetsDecisivePointSuperTie: return 70 case .twoSetsOfFourGames: return 60 case .twoSetsOfFourGamesDecisivePoint: return 50 case .nineGames: return 45 case .nineGamesDecisivePoint: return 40 case .megaTie: return 20 case .superTie: return 15 case .twoSetsOfSuperTie: return 25 case .singleSet: return 30 case .singleSetDecisivePoint: return 25 case .singleSetOfFourGames: return 15 case .singleSetOfFourGamesDecisivePoint: return 10 } } var estimatedTimeWithBreak: Int { estimatedDuration + breakTime.breakTime } var breakTime: (breakTime: Int, matchCount: Int) { switch self { case .twoSets, .twoSetsDecisivePoint: return (90, 1) case .twoSetsSuperTie, .twoSetsDecisivePointSuperTie: return (60, 1) case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint: return (30, 1) case .superTie: return (15, 3) default: return (5, 1) } } func maximumMatchPerDay(for matchCount: Int) -> Int { switch self { case .twoSets, .twoSetsDecisivePoint: return matchCount < 5 ? 2 : 0 case .twoSetsSuperTie, .twoSetsDecisivePointSuperTie: return matchCount < 6 ? 3 : 0 case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .nineGames, .nineGamesDecisivePoint: return matchCount < 7 ? 6 : 2 case .superTie: return 7 default: return 10 } } var hasDecisivePoint: Bool { switch self { case .nineGamesDecisivePoint, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSetDecisivePoint, .singleSetOfFourGamesDecisivePoint: return true default: return false } } func newSetFormat(setCount: Int) -> SetFormat { if setCount == 2 && canSuperTie { return .superTieBreak } return setFormat } func formatTitle(_ displayStyle: DisplayStyle = .wide) -> String { switch displayStyle { case .short: return ["Format ", shortFormat].joined() default: return ["Format ", shortFormat, suffix].joined() } } var suffix: String { switch self { case .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfFourGamesDecisivePoint, .nineGamesDecisivePoint, .singleSetDecisivePoint: return " [Point Décisif]" default: return "" } } var longPrefix: String { return "Format \(format) : " } var shortPrefix: String { return "\(format) : " } var isFederal: Bool { switch self { case .megaTie, .twoSetsOfSuperTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return false default: return true } } var format: String { shortFormat + (isFederal ? "" : " (non officiel)") } var shortFormat: String { switch self { case .twoSets: return "A1" case .twoSetsSuperTie: return "B1" case .twoSetsOfFourGames: return "C1" case .nineGames: return "D1" case .superTie: return "E" case .twoSetsOfSuperTie: return "G" case .megaTie: return "F" case .singleSet: return "H1" case .singleSetDecisivePoint: return "H2" case .twoSetsDecisivePoint: return "A2" case .twoSetsDecisivePointSuperTie: return "B2" case .twoSetsOfFourGamesDecisivePoint: return "C2" case .nineGamesDecisivePoint: return "D2" case .singleSetOfFourGames: return "I1" case .singleSetOfFourGamesDecisivePoint: return "I2" } } var longLabel: String { switch self { case .singleSet, .singleSetDecisivePoint: return "1 set de 6" case .twoSets, .twoSetsDecisivePoint: return "2 sets de 6" case .twoSetsSuperTie, .twoSetsDecisivePointSuperTie: return "2 sets de 6, supertie au 3ème" case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint: return "2 sets de 4, tiebreak à 4/4, supertie au 3ème" case .nineGames, .nineGamesDecisivePoint: return "9 jeux, tiebreak à 8/8" case .twoSetsOfSuperTie: return "2 sets de supertie de 10 points" case .superTie: return "supertie de 10 points" case .megaTie: return "supertie de 15 points" case .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return "1 set de 4 jeux, tiebreak à 4/4" } } var computedShortLabelWithoutPrefix: String { longLabel + suffix } var computedShortLabel: String { shortPrefix + longLabel + suffix } var computedLongLabel: String { longPrefix + longLabel + suffix } var setsToWin: Int { switch self { case .twoSets, .twoSetsSuperTie, .twoSetsOfFourGames, .twoSetsDecisivePoint, .twoSetsOfFourGamesDecisivePoint, .twoSetsDecisivePointSuperTie, .twoSetsOfSuperTie: return 2 case .nineGames, .nineGamesDecisivePoint, .superTie, .megaTie, .singleSet, .singleSetDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return 1 } } var setFormat: SetFormat { switch self { case .twoSets, .twoSetsSuperTie, .twoSetsDecisivePoint, .twoSetsDecisivePointSuperTie, .singleSet, .singleSetDecisivePoint: return .six case .twoSetsOfFourGames, .twoSetsOfFourGamesDecisivePoint, .singleSetOfFourGames, .singleSetOfFourGamesDecisivePoint: return .four case .nineGames, .nineGamesDecisivePoint: return .nine case .superTie, .twoSetsOfSuperTie: return .superTieBreak case .megaTie: return .megaTieBreak } } } enum Format: Int, Hashable, Codable { case normal case tiebreakSeven case tiebreakTen case tiebreakFifteen func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .normal: return "normal" case .tiebreakSeven: return "tie-break en 7" case .tiebreakTen: return "tie-break en 10" case .tiebreakFifteen: return "tie-break en 15" } } var isTiebreak: Bool { switch self { case .normal: return false case .tiebreakSeven, .tiebreakTen, .tiebreakFifteen: return true } } var scoreToWin: Int? { switch self { case .normal: return nil case .tiebreakSeven: return 7 case .tiebreakTen: return 10 case .tiebreakFifteen: return 15 } } } enum ActionType: Int, Identifiable { case fault case winner var id: Self { self } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .fault: return "Faute" case .winner: return "Point" } } /* Break points won Break points Errors Smash winners Smashes Winners Volley winners Total points won % points won Total break points Break points won Gold points won Gold points won returning Gold points won with service Most consecutive points won Total set points Set points won Match points */ } enum EventType: Int, CaseIterable, Identifiable { case approvedTournament case friendlyTournament case simulation case animation var id: Self { self } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .approvedTournament: return "Tournoi homologué" case .friendlyTournament: return "Tournoi amical" case .simulation: return "Simulation" case .animation: return "Animation" } } } enum TeamSortingType: Int, Identifiable, CaseIterable, Hashable, Codable { case rank = 1 case inscriptionDate = 2 var id: Int { rawValue } init?(rawValue: Int?) { guard let value = rawValue else { return nil } self.init(rawValue: value) } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .rank: return "Rang" case .inscriptionDate: return "Date d'inscription" } } } enum PlayersCountRange: Int, CaseIterable { case N8 = 8 case N12 = 12 case N16 = 16 case N20 = 20 case N24 = 24 case N28 = 28 case N32 = 32 func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .N8: return "4 à 8" case .N12: return "9 à 12" case .N16: return "13 à 16" case .N20: return "17 à 20" case .N24: return "21 à 24" case .N28: return "25 à 28" case .N32: return "29 à 32" } } } enum RoundRule { static let colors = ["#99ff99", "#66ff66", "#33cc33", "#009900", "#006600", "#336633", "#DD6600", "#EE6633", "#EE6633", "#EE6633"] static func loserBrackets(index: Int) -> [String] { switch index { case 1: return ["#3/#4"] case 2: return ["#5/#6", "#7/#8"] default: return ["#9/#10", "#11/#12", "#13/#14", "#15/#16", "#17/#18", "#19/#20", "#21/#22", "#23/#24", "#25/#26", "#27/#28", "#29/#30", "#31/#32"] } } static func teamsInFirstRound(forTeams teams: Int) -> Int { Int(pow(2.0, ceil(log2(Double(teams))))) } static func numberOfMatches(forTeams teams: Int) -> Int { teamsInFirstRound(forTeams: teams) - 1 } static func numberOfRounds(forTeams teams: Int) -> Int { if teams == 0 { return 0 } return Int(log2(Double(teamsInFirstRound(forTeams: teams)))) } static func matchIndex(fromRoundIndex roundIndex: Int) -> Int { guard roundIndex >= 0 else { return -1 // Invalid round index } return (1 << roundIndex) - 1 } static func matchIndex(fromBracketPosition: Int) -> Int { roundIndex(fromMatchIndex: fromBracketPosition / 2) + fromBracketPosition%2 } static func roundIndex(fromMatchIndex matchIndex: Int) -> Int { Int(log2(Double(matchIndex + 1))) } static func numberOfMatches(forRoundIndex roundIndex: Int) -> Int { Int(pow(2.0, Double(roundIndex))) } static func matchIndexWithinRound(fromMatchIndex matchIndex: Int) -> Int { let roundIndex = roundIndex(fromMatchIndex: matchIndex) let matchIndexWithinRound = matchIndex - (Int(pow(2.0, Double(roundIndex))) - 1) return matchIndexWithinRound } static func roundName(fromMatchIndex matchIndex: Int) -> String { let roundIndex = roundIndex(fromMatchIndex: matchIndex) return roundName(fromRoundIndex: roundIndex) } static func roundName(fromRoundIndex roundIndex: Int, displayStyle: DisplayStyle = .wide) -> String { switch roundIndex { case 0: return "Finale" case 1: if displayStyle == .short { return "Demi" } return "Demi-finale" case 2: if displayStyle == .short { return "Quart" } return "Quart de finale" default: return "\(Int(pow(2.0, Double(roundIndex))))ème" } } } enum AnimationType: Int, CaseIterable, Hashable, Identifiable { case playerAnimation case upAndDown case brawl var id: Int { rawValue } func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String { switch self { case .playerAnimation: return "Par joueur" case .upAndDown: return "Montante / Descendante" case .brawl: return "Brawl" } } var descriptionLabel: String { switch self { case .playerAnimation: return "Chaque joueur joue avec quelqu'un de différent à chaque rotation (8 à 12 joueurs)" case .upAndDown: return "Les gagnants montent sur le terrain d'à côté, les perdants descendent" case .brawl: return "A chaque rotation, les gagnants de la rotation précédente se jouent entre eux" } } } enum PadelTournamentStructurePreset: Int, Identifiable, CaseIterable { var id: Int { self.rawValue } case manual case doubleGroupStage case federalStructure_8 case federalStructure_12 case federalStructure_16 case federalStructure_20 case federalStructure_24 case federalStructure_32 case federalStructure_48 case federalStructure_64 // Maximum qualified pairs based on the structure preset func tableDimension() -> Int { switch self { case .federalStructure_8: return 8 case .federalStructure_12: return 12 case .federalStructure_16: return 16 case .federalStructure_20: return 20 case .federalStructure_24: return 24 case .federalStructure_32: return 32 case .federalStructure_48: return 48 case .federalStructure_64: return 64 case .manual: return 24 case .doubleGroupStage: return 9 } } // Wildcards allowed in the Qualifiers func wildcardBrackets() -> Int { switch self { case .federalStructure_8: return 0 case .federalStructure_12: return 1 case .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32: return 2 case .federalStructure_48, .federalStructure_64: return 4 case .manual, .doubleGroupStage: return 0 } } // Wildcards allowed in the Qualifiers func wildcardQualifiers() -> Int { switch self { case .federalStructure_8: return 0 case .federalStructure_12, .federalStructure_16: return 1 case .federalStructure_20, .federalStructure_24: return 2 case .federalStructure_32: return 4 case .federalStructure_48: return 6 case .federalStructure_64: return 8 case .manual, .doubleGroupStage: return 0 } } // Number of teams admitted to the Qualifiers func teamsInQualifiers() -> Int { switch self { case .federalStructure_8: return 8 case .federalStructure_12: return 12 case .federalStructure_16: return 16 case .federalStructure_20: return 20 case .federalStructure_24: return 24 case .federalStructure_32: return 32 case .federalStructure_48: return 48 case .federalStructure_64: return 64 case .manual, .doubleGroupStage: return 0 } } // Maximum teams that can qualify from the Qualifiers to the Final Table func maxTeamsFromQualifiers() -> Int { switch self { case .federalStructure_8, .federalStructure_12: return 2 case .federalStructure_16, .federalStructure_20, .federalStructure_24: return 4 case .federalStructure_32, .federalStructure_48, .federalStructure_64: return 8 case .manual, .doubleGroupStage: return 0 } } func localizedStructurePresetTitle() -> String { switch self { case .manual: return "Défaut" case .doubleGroupStage: return "2 phases de poules" case .federalStructure_8: return "Structure fédérale 8" case .federalStructure_12: return "Structure fédérale 12" case .federalStructure_16: return "Structure fédérale 16" case .federalStructure_20: return "Structure fédérale 20" case .federalStructure_24: return "Structure fédérale 24" case .federalStructure_32: return "Structure fédérale 32" case .federalStructure_48: return "Structure fédérale 48" case .federalStructure_64: return "Structure fédérale 64" } } func localizedDescriptionStructurePresetTitle() -> String { switch self { case .manual: return "24 équipes, 4 poules de 4, 1 qualifié par poule" case .doubleGroupStage: return "Poules qui enchaînent sur une autre phase de poules: les premiers de chaque se retrouvent ensemble, puis les deuxièmes, etc." case .federalStructure_8: return "Tableau final à 8 paires, dont 2 qualifiées sortant de qualifications à 8 paires maximum. Aucune wildcard." case .federalStructure_12, .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32, .federalStructure_48, .federalStructure_64: return "Tableau final à \(tableDimension()) paires, dont \(maxTeamsFromQualifiers()) qualifiées sortant de qualifications à \(teamsInQualifiers()) paires maximum. \(wildcardBrackets()) wildcard\(wildcardBrackets().pluralSuffix) en tableau et \(wildcardQualifiers()) wildcard\(wildcardQualifiers().pluralSuffix) en qualifications." } } func groupStageCount() -> Int { switch self { case .manual: 4 case .doubleGroupStage: 3 case .federalStructure_8: 2 case .federalStructure_12: 2 case .federalStructure_16: 4 case .federalStructure_20: 4 case .federalStructure_24: 4 case .federalStructure_32: 8 case .federalStructure_48: 8 case .federalStructure_64: 8 } } func teamsPerGroupStage() -> Int { switch self { case .manual: 4 case .doubleGroupStage: 3 case .federalStructure_8: 4 case .federalStructure_12: 6 case .federalStructure_16: 4 case .federalStructure_20: 5 case .federalStructure_24: 6 case .federalStructure_32: 4 case .federalStructure_48: 6 case .federalStructure_64: 8 } } func qualifiedPerGroupStage() -> Int { switch self { case .doubleGroupStage: 0 default: 1 } } func hasWildcards() -> Bool { wildcardBrackets() > 0 || wildcardQualifiers() > 0 } func isFederalPreset() -> Bool { switch self { case .manual: return false case .doubleGroupStage: return false case .federalStructure_8, .federalStructure_12, .federalStructure_16, .federalStructure_20, .federalStructure_24, .federalStructure_32, .federalStructure_48, .federalStructure_64: return true } } } enum TournamentDeadlineType: String, CaseIterable { case inscription = "Inscription" case broadcastList = "Publication de la liste" case wildcardRequest = "Demande de WC" case wildcardLicensePurchase = "Prise de licence des WC" case definitiveBroadcastList = "Publication définitive" func daysOffset(level: TournamentLevel) -> Int { if level == .p500 { switch self { case .inscription: return -6 case .broadcastList: return -6 case .wildcardRequest: return -4 case .wildcardLicensePurchase, .definitiveBroadcastList: return -4 } } else { switch self { case .inscription: return -13 case .broadcastList: return -12 case .wildcardRequest: return -9 case .wildcardLicensePurchase, .definitiveBroadcastList: return -8 } } } var timeOffset: DateComponents { switch self { case .broadcastList, .definitiveBroadcastList: return DateComponents(hour: 12) case .inscription, .wildcardRequest, .wildcardLicensePurchase: return DateComponents(minute: -1) } } }