// // GroupStage.swift // Padel Tournament // // Created by razmig on 10/03/2024. // import Foundation import LeStorage import Algorithms import SwiftUI @Observable final class GroupStage: ModelObject, Storable { static func resourceName() -> String { "group-stages" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } static var relationshipNames: [String] = [] var id: String = Store.randomId() var tournament: String var index: Int var size: Int private var format: MatchFormat? var startDate: Date? var name: String? var step: Int = 0 var matchFormat: MatchFormat { get { format ?? .defaultFormatForMatchType(.groupStage) } set { format = newValue } } internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil, step: Int = 0) { self.tournament = tournament self.index = index self.size = size self.format = matchFormat self.startDate = startDate self.name = name self.step = step } var tournamentStore: TournamentStore { return TournamentStore.instance(tournamentId: self.tournament) } // MARK: - Computed dependencies func _matches() -> [Match] { return self.tournamentStore.matches.filter { $0.groupStage == self.id }.sorted(by: \.index) // Store.main.filter { $0.groupStage == self.id } } func tournamentObject() -> Tournament? { Store.main.findById(self.tournament) } // MARK: - func teamAt(groupStagePosition: Int) -> TeamRegistration? { if step > 0 { return teams().first(where: { $0.groupStagePositionAtStep(step) == groupStagePosition }) } return teams().first(where: { $0.groupStagePosition == groupStagePosition }) } func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String { if let name { return name } var stepLabel = "" if step > 0 { stepLabel = " (" + (step + 1).ordinalFormatted(feminine: true) + " phase)" } switch displayStyle { case .title: return "Poule \(index + 1)" + stepLabel case .wide: return "Poule \(index + 1)" case .short: return "#\(index + 1)" } } var computedOrder: Int { index + step * 100 } func isRunning() -> Bool { // at least a match has started _matches().anySatisfy({ $0.isRunning() }) } func hasStarted() -> Bool { // meaning at least one match is over _matches().filter { $0.hasEnded() }.isEmpty == false } func hasEnded() -> Bool { let _matches = _matches() if _matches.isEmpty { return false } //guard teams().count == size else { return false } return _matches.anySatisfy { $0.hasEnded() == false } == false } fileprivate func _createMatch(index: Int) -> Match { let match: Match = Match(groupStage: self.id, index: index, matchFormat: self.matchFormat, name: self.localizedMatchUpLabel(for: index)) match.store = self.store return match } func buildMatches() { _removeMatches() var matches = [Match]() var teamScores = [TeamScore]() for i in 0..<_numberOfMatchesToBuild() { let newMatch = self._createMatch(index: i) // let newMatch = Match(groupStage: self.id, index: i, matchFormat: self.matchFormat, name: localizedMatchUpLabel(for: i)) teamScores.append(contentsOf: newMatch.createTeamScores()) matches.append(newMatch) } do { try self.tournamentStore.matches.addOrUpdate(contentOfs: matches) try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) } catch { Logger.error(error) } } func playedMatches() -> [Match] { let ordered = _matches() if ordered.isEmpty == false && ordered.count == _matchOrder().count { return _matchOrder().map { ordered[$0] } } else { return ordered } } func updateGroupStageState() { clearScoreCache() if hasEnded(), let tournament = tournamentObject() { do { let teams = teams(true) for (index, team) in teams.enumerated() { team.qualified = index < tournament.qualifiedPerGroupStage if team.bracketPosition != nil && team.qualified == false { tournamentObject()?.resetTeamScores(in: team.bracketPosition) team.bracketPosition = nil } } try self.tournamentStore.teamRegistrations.addOrUpdate(contentOfs: teams) } catch { Logger.error(error) } let groupStagesAreOverAtFirstStep = tournament.groupStagesAreOver(atStep: 0) let nextStepGroupStages = tournament.groupStages(atStep: 1) let groupStagesAreOverAtSecondStep = tournament.groupStagesAreOver(atStep: 1) if groupStagesAreOverAtFirstStep, nextStepGroupStages.isEmpty || groupStagesAreOverAtSecondStep == true, tournament.groupStageLoserBracketAreOver(), tournament.rounds().isEmpty { tournament.endDate = Date() do { try DataStore.shared.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) } } } } func scoreLabel(forGroupStagePosition groupStagePosition: Int, score: TeamGroupStageScore? = nil) -> (wins: String, losses: String, setsDifference: String?, gamesDifference: String?)? { if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) { let hideSetDifference = matchFormat.setsToWin == 1 let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false))) let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: false))) return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference) // return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString } else { return nil } } // func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? { // guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } // let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) // if matches.isEmpty && nilIfEmpty { return nil } // let wins = matches.filter { $0.winningTeamId == team.id }.count // let loses = matches.filter { $0.losingTeamId == team.id }.count // let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } // let setDifference = differences.map { $0.set }.reduce(0,+) // let gameDifference = differences.map { $0.game }.reduce(0,+) // return (team, wins, loses, setDifference, gameDifference) // /* // • 2 points par rencontre gagnée // • 1 point par rencontre perdue // • -1 point en cas de rencontre perdue par disqualification (scores de 6/0 6/0 attribués aux trois matchs) // • -2 points en cas de rencontre perdu par WO (scores de 6/0 6/0 attribués aux trois matchs) // */ // } // func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] { let combos = Array((0.. Date? { guard let groupStagePosition = team.groupStagePositionAtStep(step) else { return nil } return matches(forGroupStagePosition: groupStagePosition).compactMap({ $0.startDate }).sorted().first ?? startDate } func matchPlayed(by groupStagePosition: Int, againstPosition: Int) -> Match? { if groupStagePosition == againstPosition { return nil } let combos = Array((0.. [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func group stage availableToStart", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return playedMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false }) } func runningMatches(playedMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func group stage runningMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting) } func readyMatches(playedMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func group stage readyMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false }) } func finishedMatches(playedMatches: [Match]) -> [Match] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func group stage finishedMatches", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif return playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed() } private func _matchOrder() -> [Int] { switch size { case 3: return [1, 2, 0] case 4: return [2, 3, 1, 4, 5, 0] case 5: // return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9] return [3, 5, 8, 2, 6, 1, 9, 4, 7, 0] case 6: //return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0] return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0] default: return [] } } func indexOf(_ matchIndex: Int) -> Int { _matchOrder().firstIndex(of: matchIndex) ?? matchIndex } private func _matchUp(for matchIndex: Int) -> [Int] { Array((0.. String { let matchUp = _matchUp(for: matchIndex) if let index = matchUp.first, let index2 = matchUp.last { return "#\(index + 1) vs #\(index2 + 1)" } else { return "--" } } func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? { let _teams = _teams(for: matchIndex) switch team { case .one: return _teams.first ?? nil case .two: return _teams.last ?? nil } } private func _teams(for matchIndex: Int) -> [TeamRegistration?] { let combinations = Array(0.. Int { (size * (size - 1)) / 2 } func unsortedPlayers() -> [PlayerRegistration] { unsortedTeams().flatMap({ $0.unsortedPlayers() }) } fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int) fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool { let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted() let combos = Array((0.. [TeamRegistration] { if step > 0 { return self.tournamentStore.groupStages.filter({ $0.step == step - 1 }).compactMap({ $0.teams(true)[safe: index] }) } return self.tournamentStore.teamRegistrations.filter { $0.groupStage == self.id && $0.groupStagePosition != nil } } var scoreCache: [Int: TeamGroupStageScore] = [:] func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { if sortedByScore { return unsortedTeams().compactMap({ team in // Check cache or use provided scores, otherwise calculate and store in cache scores?.first(where: { $0.team.id == team.id }) ?? { if let cachedScore = scoreCache[team.groupStagePositionAtStep(step)!] { return cachedScore } else { let score = _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!) if let score = score { scoreCache[team.groupStagePositionAtStep(step)!] = score } return score } }() }).sorted { (lhs, rhs) in let predicates: [TeamScoreAreInIncreasingOrder] = [ { $0.wins < $1.wins }, { $0.setDifference < $1.setDifference }, { $0.gameDifference < $1.gameDifference}, { self._headToHead($0.team, $1.team) }, { [self] in $0.team.groupStagePositionAtStep(self.step)! > $1.team.groupStagePositionAtStep(self.step)! } ] for predicate in predicates { if !predicate(lhs, rhs) && !predicate(rhs, lhs) { continue } return predicate(lhs, rhs) } return false }.map({ $0.team }).reversed() } else { return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!) } } func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? { // Check if the score for this position is already cached if let cachedScore = scoreCache[groupStagePosition] { return cachedScore } guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil } let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() }) if matches.isEmpty && nilIfEmpty { return nil } let wins = matches.filter { $0.winningTeamId == team.id }.count let loses = matches.filter { $0.losingTeamId == team.id }.count let differences = matches.compactMap { $0.scoreDifference(groupStagePosition, atStep: step) } let setDifference = differences.map { $0.set }.reduce(0,+) let gameDifference = differences.map { $0.game }.reduce(0,+) // Calculate the score and store it in the cache let score = (team, wins, loses, setDifference, gameDifference) scoreCache[groupStagePosition] = score return score } // Clear the cache if necessary, for example when starting a new step or when matches update func clearScoreCache() { scoreCache.removeAll() } // func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] { // if sortedByScore { // return unsortedTeams().compactMap({ team in // scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePositionAtStep(step)!) // }).sorted { (lhs, rhs) in // // Calculate intermediate values once and reuse them // let lhsWins = lhs.wins // let rhsWins = rhs.wins // let lhsSetDifference = lhs.setDifference // let rhsSetDifference = rhs.setDifference // let lhsGameDifference = lhs.gameDifference // let rhsGameDifference = rhs.gameDifference // let lhsHeadToHead = self._headToHead(lhs.team, rhs.team) // let rhsHeadToHead = self._headToHead(rhs.team, lhs.team) // let lhsGroupStagePosition = lhs.team.groupStagePositionAtStep(self.step)! // let rhsGroupStagePosition = rhs.team.groupStagePositionAtStep(self.step)! // // // Define comparison predicates in the same order // let predicates: [(Bool, Bool)] = [ // (lhsWins < rhsWins, lhsWins > rhsWins), // (lhsSetDifference < rhsSetDifference, lhsSetDifference > rhsSetDifference), // (lhsGameDifference < rhsGameDifference, lhsGameDifference > rhsGameDifference), // (lhsHeadToHead, rhsHeadToHead), // (lhsGroupStagePosition > rhsGroupStagePosition, lhsGroupStagePosition < rhsGroupStagePosition) // ] // // // Iterate over predicates and return as soon as a valid comparison is found // for (lhsPredicate, rhsPredicate) in predicates { // if lhsPredicate { return true } // if rhsPredicate { return false } // } // // return false // }.map({ $0.team }).reversed() // } else { // return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!) // } // } func updateMatchFormat(_ updatedMatchFormat: MatchFormat) { self.matchFormat = updatedMatchFormat self.updateAllMatchesFormat() } func updateAllMatchesFormat() { let playedMatches = playedMatches() playedMatches.forEach { match in match.matchFormat = matchFormat } do { try self.tournamentStore.matches.addOrUpdate(contentOfs: playedMatches) } catch { Logger.error(error) } } func pasteData() -> String { var data: [String] = [] data.append(self.groupStageTitle()) teams().forEach { team in data.append(team.teamLabelRanked(displayRank: true, displayTeamName: true)) } return data.joined(separator: "\n") } func finalPosition(ofTeam team: TeamRegistration) -> Int? { guard hasEnded() else { return nil } return teams(true).firstIndex(of: team) } override func deleteDependencies() throws { let matches = self._matches() for match in matches { try match.deleteDependencies() } self.tournamentStore.matches.deleteDependencies(matches) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: ._id) tournament = try container.decode(String.self, forKey: ._tournament) index = try container.decode(Int.self, forKey: ._index) size = try container.decode(Int.self, forKey: ._size) format = try container.decodeIfPresent(MatchFormat.self, forKey: ._format) startDate = try container.decodeIfPresent(Date.self, forKey: ._startDate) name = try container.decodeIfPresent(String.self, forKey: ._name) step = try container.decodeIfPresent(Int.self, forKey: ._step) ?? 0 } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) try container.encode(tournament, forKey: ._tournament) try container.encode(index, forKey: ._index) try container.encode(size, forKey: ._size) try container.encode(format, forKey: ._format) try container.encode(startDate, forKey: ._startDate) try container.encode(name, forKey: ._name) try container.encode(step, forKey: ._step) } func insertOnServer() { self.tournamentStore.groupStages.writeChangeAndInsertOnServer(instance: self) for match in self._matches() { match.insertOnServer() } } } extension GroupStage { enum CodingKeys: String, CodingKey { case _id = "id" case _tournament = "tournament" case _index = "index" case _size = "size" case _format = "format" case _startDate = "startDate" case _name = "name" case _step = "step" } } extension GroupStage: Selectable { func selectionLabel(index: Int) -> String { groupStageTitle() } func badgeValue() -> Int? { return runningMatches(playedMatches: _matches()).count } func badgeValueColor() -> Color? { return nil } func badgeImage() -> Badge? { if teams().count < size { return .xmark } else { return hasEnded() ? .checkmark : nil } } }