// // Match.swift // Padel Tournament // // Created by razmig on 10/03/2024. // import Foundation import LeStorage @Observable final class Match: ModelObject, Storable, Equatable { static func == (lhs: Match, rhs: Match) -> Bool { lhs.id == rhs.id && lhs.startDate == rhs.startDate } static func resourceName() -> String { "matches" } static func tokenExemptedMethods() -> [HTTPMethod] { return [] } static func filterByStoreIdentifier() -> Bool { return true } static var relationshipNames: [String] = ["round", "groupStage"] static func setServerTitle(upperRound: Round, matchIndex: Int) -> String { if upperRound.index == 0 { return upperRound.roundTitle() } return upperRound.roundTitle() + " #" + (matchIndex + 1).formatted() } var byeState: Bool = false var id: String = Store.randomId() var round: String? var groupStage: String? var startDate: Date? var endDate: Date? var index: Int private var format: MatchFormat? //var court: String? var servingTeamId: String? var winningTeamId: String? var losingTeamId: String? //var broadcasted: Bool var name: String? //var order: Int var disabled: Bool = false private(set) var courtIndex: Int? var confirmed: Bool = false init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) { self.round = round self.groupStage = groupStage self.startDate = startDate self.endDate = endDate self.index = index self.format = matchFormat //self.court = court self.servingTeamId = servingTeamId self.winningTeamId = winningTeamId self.losingTeamId = losingTeamId self.disabled = disabled self.name = name self.courtIndex = courtIndex self.confirmed = confirmed // self.broadcasted = broadcasted // self.order = order } var tournamentStore: TournamentStore { if let store = self.store as? TournamentStore { return store } fatalError("missing store for \(String(describing: type(of: self)))") } var courtIndexForSorting: Int { courtIndex ?? Int.max } // MARK: - Computed dependencies var teamScores: [TeamScore] { return self.tournamentStore.teamScores.filter { $0.match == self.id } } // MARK: - override func deleteDependencies() throws { guard let tournament = self.currentTournament() else { return } let teamScores = self.teamScores for teamScore in teamScores { try teamScore.deleteDependencies() } tournament.tournamentStore.teamScores.deleteDependencies(teamScores) } func indexInRound(in matches: [Match]? = nil) -> Int { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func indexInRound(in", matches?.count, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if groupStage != nil { return index } else if let index = (matches ?? roundObject?.playedMatches().sorted(by: \.index))?.firstIndex(where: { $0.id == id }) { return index } return RoundRule.matchIndexWithinRound(fromMatchIndex: index) } func matchWarningSubject() -> String { [roundTitle(), matchTitle(.short)].compacted().joined(separator: " ") } func matchWarningMessage() -> String { [roundTitle(), matchTitle(.short), startDate?.localizedDate(), courtName()].compacted().joined(separator: "\n") } func matchTitle(_ displayStyle: DisplayStyle = .wide, inMatches matches: [Match]? = nil) -> String { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func matchTitle", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if roundObject?.groupStageLoserBracket == true { return "\(index)\(index.ordinalFormattedSuffix()) place" } if let groupStageObject { return groupStageObject.localizedMatchUpLabel(for: index) } switch displayStyle { case .wide, .title: return "Match \(indexInRound(in: matches) + 1)" case .short: return "#\(indexInRound(in: matches) + 1)" } } func isSeedLocked(atTeamPosition teamPosition: TeamPosition) -> Bool { return previousMatch(teamPosition)?.disabled == true } func unlockSeedPosition(atTeamPosition teamPosition: TeamPosition) { previousMatch(teamPosition)?.enableMatch() } @discardableResult func lockAndGetSeedPosition(atTeamPosition slot: TeamPosition?, opposingSeeding: Bool = false) -> Int { let matchIndex = index var teamPosition : TeamPosition { if let slot { return slot } else { let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex) let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound) let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2) var teamPosition = slot ?? (isUpper ? .one : .two) if opposingSeeding { teamPosition = slot ?? (isUpper ? .two : .one) } return teamPosition } } previousMatch(teamPosition)?.disableMatch() return matchIndex * 2 + teamPosition.rawValue } func isSeededBy(team: TeamRegistration, inTeamPosition teamPosition: TeamPosition) -> Bool { guard let roundObject, roundObject.isUpperBracket() else { return false } guard let bracketPosition = team.bracketPosition else { return false } return index * 2 + teamPosition.rawValue == bracketPosition } func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? { let minutesToAdd = Double(matchFormat.getEstimatedDuration(additionalEstimationDuration)) return startDate?.addingTimeInterval(minutesToAdd * 60.0) } func winner() -> TeamRegistration? { guard let winningTeamId else { return nil } return self.tournamentStore.teamRegistrations.findById(winningTeamId) } func loser() -> TeamRegistration? { guard let losingTeamId else { return nil } return self.tournamentStore.teamRegistrations.findById(losingTeamId) } func localizedStartDate() -> String { if let startDate { return startDate.formatted(date: .abbreviated, time: .shortened) } else { return "" } } func scoreLabel() -> String { if hasWalkoutTeam() == true { return "WO" } let scoreOne = teamScore(.one)?.score?.components(separatedBy: ",") ?? [] let scoreTwo = teamScore(.two)?.score?.components(separatedBy: ",") ?? [] let tuples = zip(scoreOne, scoreTwo).map { ($0, $1) } let scores = tuples.map { $0 + "/" + $1 }.joined(separator: " ") return scores } func cleanScheduleAndSave(_ targetStartDate: Date? = nil) { startDate = targetStartDate confirmed = targetStartDate == nil ? false : true endDate = nil followingMatch()?.cleanScheduleAndSave(nil) _loserMatch()?.cleanScheduleAndSave(nil) do { try self.tournamentStore.matches.addOrUpdate(instance: self) } catch { Logger.error(error) } } func resetMatch() { losingTeamId = nil winningTeamId = nil endDate = nil removeCourt() servingTeamId = nil groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() currentTournament()?.updateTournamentState() } func resetScores() { teamScores.forEach({ $0.score = nil }) do { try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teamScores) } catch { Logger.error(error) } } func teamWillBeWalkOut(_ team: TeamRegistration) { resetMatch() let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team) existingTeamScore.walkOut = 1 do { try self.tournamentStore.teamScores.addOrUpdate(instance: existingTeamScore) } catch { Logger.error(error) } } func luckyLosers() -> [TeamRegistration] { return roundObject?.previousRound()?.losers() ?? [] } func isWalkOutSpot(_ teamPosition: TeamPosition) -> Bool { return teamScore(teamPosition)?.walkOut == 1 } func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) { resetMatch() let matchIndex = index let position = matchIndex * 2 + teamPosition.rawValue let previousScores = teamScores.filter({ $0.luckyLoser == position }) do { try self.tournamentStore.teamScores.delete(contentOfs: previousScores) } catch { Logger.error(error) } let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team) teamScoreLuckyLoser.luckyLoser = position do { try self.tournamentStore.teamScores.addOrUpdate(instance: teamScoreLuckyLoser) } catch { Logger.error(error) } } func disableMatch() { _toggleMatchDisableState(true) } func enableMatch() { _toggleMatchDisableState(false) } private func _loserMatch() -> Match? { let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index) return roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) } func _toggleLoserMatchDisableState(_ state: Bool) { guard let loserMatch = _loserMatch() else { return } guard let next = _otherMatch() else { return } loserMatch.byeState = true if next.disabled { loserMatch.byeState = false } loserMatch._toggleMatchDisableState(state, forward: true) } fileprivate func _otherMatch() -> Match? { guard let round else { return nil } guard index > 0 else { return nil } let nextIndex = (index - 1) / 2 let topMatchIndex = (nextIndex * 2) + 1 let bottomMatchIndex = (nextIndex + 1) * 2 let isTopMatch = topMatchIndex + 1 == index let lookingForIndex = isTopMatch ? topMatchIndex : bottomMatchIndex return self.tournamentStore.matches.first(where: { $0.round == round && $0.index == lookingForIndex }) } private func _forwardMatch(inRound round: Round) -> Match? { guard let roundObjectNextRound = round.nextRound() else { return nil } let nextIndex = (index - 1) / 2 return self.tournamentStore.matches.first(where: { $0.round == roundObjectNextRound.id && $0.index == nextIndex }) } func _toggleForwardMatchDisableState(_ state: Bool) { guard let roundObject else { return } guard roundObject.parent != nil else { return } guard let forwardMatch = _forwardMatch(inRound: roundObject) else { return } guard let next = _otherMatch() else { return } if next.disabled && byeState == false && next.byeState == false { forwardMatch.byeState = false forwardMatch._toggleMatchDisableState(state, forward: true) } else if byeState && next.byeState { print("don't disable forward match") forwardMatch.byeState = false forwardMatch._toggleMatchDisableState(false, forward: true) } else { forwardMatch.byeState = true forwardMatch._toggleMatchDisableState(state, forward: true) } // if next.disabled == false { // forwardMatch.byeState = state // } // // if next.disabled == state { // if next.byeState != byeState { // //forwardMatch.byeState = state // forwardMatch._toggleMatchDisableState(state) // } else { // forwardMatch._toggleByeState(state) // } // } else { // } // forwardMatch._toggleByeState(state) } func isSeededBy(team: TeamRegistration) -> Bool { isSeededBy(team: team, inTeamPosition: .one) || isSeededBy(team: team, inTeamPosition: .two) } func isSeeded() -> Bool { return isSeededAt(.one) || isSeededAt(.two) } func isSeededAt(_ teamPosition: TeamPosition) -> Bool { if let team = team(teamPosition) { return isSeededBy(team: team) } return false } func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) { //if disabled == state { return } disabled = state if disabled { do { try self.tournamentStore.teamScores.delete(contentOfs: teamScores) } catch { Logger.error(error) } } if state == true { let teams = teams() for team in teams { if isSeededBy(team: team) { team.bracketPosition = nil do { try self.tournamentStore.teamRegistrations.addOrUpdate(instance: team) } catch { Logger.error(error) } } } } //byeState = false do { try self.tournamentStore.matches.addOrUpdate(instance: self) } catch { Logger.error(error) } if single == false { _toggleLoserMatchDisableState(state) if forward { _toggleForwardMatchDisableState(state) } else { topPreviousRoundMatch()?._toggleMatchDisableState(state) bottomPreviousRoundMatch()?._toggleMatchDisableState(state) } } } func next() -> Match? { let matches: [Match] = self.tournamentStore.matches.filter { $0.round == round && $0.index > index && $0.disabled == false } return matches.sorted(by: \.index).first } func followingMatch() -> Match? { guard let nextRoundId = roundObject?.nextRound()?.id else { return nil } return getFollowingMatch(fromNextRoundId: nextRoundId) } func getFollowingMatch(fromNextRoundId nextRoundId: String) -> Match? { return self.tournamentStore.matches.first(where: { $0.round == nextRoundId && $0.index == (index - 1) / 2 }) } func getDuration() -> Int { if let tournament = currentTournament() { matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration) } else { matchFormat.getEstimatedDuration() } } func roundTitle() -> String? { if groupStage != nil { return groupStageObject?.groupStageTitle() } else if let roundObject { return roundObject.roundTitle() } else { return nil } } func roundAndMatchTitle() -> String { [roundTitle(), matchTitle()].compactMap({ $0 }).joined(separator: " ") } func topPreviousRoundMatchIndex() -> Int { return index * 2 + 1 } func bottomPreviousRoundMatchIndex() -> Int { return (index + 1) * 2 } func topPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } let topPreviousRoundMatchIndex = topPreviousRoundMatchIndex() let roundObjectPreviousRoundId = roundObject.previousRound()?.id return self.tournamentStore.matches.first(where: { match in match.round != nil && match.round == roundObjectPreviousRoundId && match.index == topPreviousRoundMatchIndex }) } func bottomPreviousRoundMatch() -> Match? { guard let roundObject else { return nil } let bottomPreviousRoundMatchIndex = bottomPreviousRoundMatchIndex() let roundObjectPreviousRoundId = roundObject.previousRound()?.id return self.tournamentStore.matches.first(where: { match in match.round != nil && match.round == roundObjectPreviousRoundId && match.index == bottomPreviousRoundMatchIndex }) } func previousMatch(_ teamPosition: TeamPosition) -> Match? { if teamPosition == .one { return topPreviousRoundMatch() } else { return bottomPreviousRoundMatch() } } var computedOrder: Int { if let groupStageObject { return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index) } guard let roundObject else { return index } return roundObject.isLoserBracket() ? (roundObject.index + 1) * 1000 + indexInRound() : (roundObject.index + 1) * 10000 + indexInRound() } func previousMatches() -> [Match] { guard let roundObject else { return [] } let roundObjectPreviousRoundId = roundObject.previousRound()?.id return self.tournamentStore.matches.filter { match in match.round == roundObjectPreviousRoundId && (match.index == topPreviousRoundMatchIndex() || match.index == bottomPreviousRoundMatchIndex()) }.sorted(by: \.index) } var matchFormat: MatchFormat { get { format ?? .defaultFormatForMatchType(.groupStage) } set { format = newValue } } func setWalkOut(_ teamPosition: TeamPosition) { let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, team: team(teamPosition)) teamScoreWalkout.walkOut = 0 let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, team: team(teamPosition.otherTeam)) teamScoreWinning.walkOut = nil do { try self.tournamentStore.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning]) } catch { Logger.error(error) } if endDate == nil { endDate = Date() } winningTeamId = teamScoreWinning.teamRegistration losingTeamId = teamScoreWalkout.teamRegistration groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() currentTournament()?.updateTournamentState() updateFollowingMatchTeamScore() } func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { updateScore(fromMatchDescriptor: matchDescriptor) if endDate == nil { endDate = Date() } if startDate == nil { startDate = endDate?.addingTimeInterval(Double(-getDuration()*60)) } let teamOne = team(matchDescriptor.winner) let teamTwo = team(matchDescriptor.winner.otherTeam) teamOne?.hasArrived() teamTwo?.hasArrived() winningTeamId = teamOne?.id losingTeamId = teamTwo?.id confirmed = true groupStageObject?.updateGroupStageState() roundObject?.updateTournamentState() if let tournament = currentTournament(), let endDate, let startDate { if endDate.isEarlierThan(tournament.startDate) { tournament.startDate = startDate } do { try DataStore.shared.tournaments.addOrUpdate(instance: tournament) } catch { Logger.error(error) } tournament.updateTournamentState() } updateFollowingMatchTeamScore() } func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) { let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, team: team(.one)) teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",") let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, team: team(.two)) teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",") do { try self.tournamentStore.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo]) } catch { Logger.error(error) } matchFormat = matchDescriptor.matchFormat } func updateFollowingMatchTeamScore() { followingMatch()?.updateTeamScores() _loserMatch()?.updateTeamScores() } func resetTeamScores(outsideOf newTeamScores: [TeamScore]) { let ids = newTeamScores.map { $0.id } let teamScores = teamScores.filter({ ids.contains($0.id) == false }) if teamScores.isEmpty == false { do { try self.tournamentStore.teamScores.delete(contentOfs: teamScores) } catch { Logger.error(error) } followingMatch()?.resetTeamScores(outsideOf: []) _loserMatch()?.resetTeamScores(outsideOf: []) } } func createTeamScores() -> [TeamScore] { let teamOne = team(.one) let teamTwo = team(.two) let teams = [teamOne, teamTwo].compactMap({ $0 }).map { TeamScore(match: id, team: $0) } return teams } func getOrCreateTeamScores() -> [TeamScore] { let teamOne = team(.one) let teamTwo = team(.two) let teams = [teamOne, teamTwo].compactMap({ $0 }).map { teamScore(ofTeam: $0) ?? TeamScore(match: id, team: $0) } return teams } func updateTeamScores() { let teams = getOrCreateTeamScores() do { try self.tournamentStore.teamScores.addOrUpdate(contentOfs: teams) } catch { Logger.error(error) } resetTeamScores(outsideOf: teams) if teams.isEmpty == false { updateFollowingMatchTeamScore() } } func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup) { if hasEnded() == false { startDate = fromStartDate switch fieldSetup { case .fullRandom: if let _courtIndex = allCourts().randomElement() { setCourt(_courtIndex) } case .random: if let _courtIndex = availableCourts().randomElement() { setCourt(_courtIndex) } case .field(let _courtIndex): setCourt(_courtIndex) } } else { startDate = fromStartDate endDate = toEndDate } if hasStarted() { confirmed = true } } func courtName() -> String? { guard let courtIndex else { return nil } if let courtName = currentTournament()?.courtName(atIndex: courtIndex) { return courtName } else { return Court.courtIndexedTitle(atIndex: courtIndex) } } func courtCount() -> Int { return currentTournament()?.courtCount ?? 1 } func courtIsAvailable(_ courtIndex: Int) -> Bool { let courtUsed = currentTournament()?.courtUsed() ?? [] return courtUsed.contains(courtIndex) == false } func courtIsPreferred(_ courtIndex: Int) -> Bool { return false } func allCourts() -> [Int] { let availableCourts = Array(0.. [Int] { let courtUsed = currentTournament()?.courtUsed() ?? [] return Array(Set(allCourts().map { $0 }).subtracting(Set(courtUsed))) } func removeCourt() { courtIndex = nil } func setCourt(_ courtIndex: Int) { self.courtIndex = courtIndex } func canBeStarted(inMatches matches: [Match], checkCanPlay: Bool) -> Bool { let teams = teamScores guard teams.count == 2 else { print("teams.count != 2") return false } guard hasEnded() == false else { return false } guard hasStarted() == false else { return false } return teams.compactMap({ $0.team }).allSatisfy({ ((checkCanPlay && $0.canPlay()) || checkCanPlay == false) && isTeamPlaying($0, inMatches: matches) == false }) } func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool { return matches.filter({ $0.teamScores.compactMap { $0.teamRegistration }.contains(team.id) }).isEmpty == false } var computedStartDateForSorting: Date { return startDate ?? .distantFuture } var computedEndDateForSorting: Date { return endDate ?? .distantFuture } func hasSpaceLeft() -> Bool { return teamScores.count < 2 } func isReady() -> Bool { return teamScores.count >= 2 // teams().count == 2 } func isEmpty() -> Bool { return teamScores.isEmpty // teams().isEmpty } func hasEnded() -> Bool { return endDate != nil } func isGroupStage() -> Bool { return groupStage != nil } func isBracket() -> Bool { return round != nil } func walkoutTeam() -> [TeamRegistration] { //walkout 0 means real walkout, walkout 1 means lucky loser situation return scores().filter({ $0.walkOut == 0 }).compactMap { $0.team } } func hasWalkoutTeam() -> Bool { return walkoutTeam().isEmpty == false } func currentTournament() -> Tournament? { return groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject() } func tournamentId() -> String? { return groupStageObject?.tournament ?? roundObject?.tournament } func scores() -> [TeamScore] { return self.tournamentStore.teamScores.filter { $0.match == id } } func teams() -> [TeamRegistration] { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func teams()", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if groupStage != nil { return [groupStageProjectedTeam(.one), groupStageProjectedTeam(.two)].compactMap { $0 } } guard let roundObject else { return [] } let previousRound = roundObject.previousRound() return [roundObject.roundProjectedTeam(.one, inMatch: self, previousRound: previousRound), roundObject.roundProjectedTeam(.two, inMatch: self, previousRound: previousRound)].compactMap { $0 } // return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 } } func scoreDifference(_ teamPosition: Int, atStep step: Int) -> (set: Int, game: Int)? { guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil } var reverseValue = 1 if teamPosition == team(.two)?.groupStagePositionAtStep(step) { reverseValue = -1 } let endedSetsOne = teamScoreTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut()) let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreOtherTeam.isWalkOut()) var setDifference : Int = 0 let zip = zip(endedSetsOne, endedSetsTwo) if matchFormat.setsToWin == 1 { setDifference = endedSetsOne[0] - endedSetsTwo[0] } else { setDifference = zip.filter { $0 > $1 }.count - zip.filter { $1 > $0 }.count } let gameDifference = zip.map { ($0, $1) }.map { $0.0 - $0.1 }.reduce(0,+) return (setDifference * reverseValue, gameDifference * reverseValue) } func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let groupStageObject else { return nil } return groupStageObject.team(teamPosition: team, inMatchIndex: index) } func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? { guard let roundObject else { return nil } let previousRound = roundObject.previousRound() return roundObject.roundProjectedTeam(team, inMatch: self, previousRound: previousRound) } func teamWon(_ team: TeamRegistration?) -> Bool { guard let winningTeamId else { return false } return winningTeamId == team?.id } func teamWon(atPosition teamPosition: TeamPosition) -> Bool { guard let winningTeamId else { return false } return winningTeamId == team(teamPosition)?.id } func team(_ team: TeamPosition) -> TeamRegistration? { #if _DEBUG_TIME //DEBUGING TIME let start = Date() defer { let duration = Duration.milliseconds(Date().timeIntervalSince(start) * 1_000) print("func match get team", id, duration.formatted(.units(allowed: [.seconds, .milliseconds]))) } #endif if groupStage != nil { return groupStageProjectedTeam(team) } else { return roundProjectedTeam(team) } } func teamNames(_ team: TeamRegistration?) -> [String]? { return team?.players().map { $0.playerLabel() } } func teamWalkOut(_ team: TeamRegistration?) -> Bool { return teamScore(ofTeam: team)?.isWalkOut() == true } func teamScore(_ team: TeamPosition) -> TeamScore? { return teamScore(ofTeam: self.team(team)) } func teamScore(ofTeam team: TeamRegistration?) -> TeamScore? { return scores().first(where: { $0.teamRegistration == team?.id }) } func isRunning() -> Bool { // at least a match has started return confirmed && hasStarted() && hasEnded() == false } func hasStarted() -> Bool { // meaning at least one match is over if let startDate { return startDate.timeIntervalSinceNow < 0 && confirmed } if hasEnded() { return true } return false //todo scores // if let score { // return score.hasEnded == false && score.sets.isEmpty == false // } else { // return false // } } var roundObject: Round? { guard let round else { return nil } return self.tournamentStore.rounds.findById(round) } var groupStageObject: GroupStage? { guard let groupStage else { return nil } return self.tournamentStore.groupStages.findById(groupStage) } var isLoserBracket: Bool { if let roundObject { if roundObject.parent != nil || roundObject.groupStageLoserBracket { return true } } return false } var matchType: MatchType { if isLoserBracket { return .loserBracket } else if isGroupStage() { return .groupStage } else { return .bracket } } var restingTimeForSorting: TimeInterval { (teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow } enum CodingKeys: String, CodingKey { case _id = "id" case _round = "round" case _groupStage = "groupStage" case _startDate = "startDate" case _endDate = "endDate" case _index = "index" case _format = "format" // case _court = "court" case _courtIndex = "courtIndex" case _servingTeamId = "servingTeamId" case _winningTeamId = "winningTeamId" case _losingTeamId = "losingTeamId" // case _broadcasted = "broadcasted" case _name = "name" // case _order = "order" case _disabled = "disabled" case _confirmed = "confirmed" } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: ._id) try container.encode(round, forKey: ._round) try container.encode(groupStage, forKey: ._groupStage) try container.encode(startDate, forKey: ._startDate) try container.encode(endDate, forKey: ._endDate) try container.encode(format, forKey: ._format) try container.encode(servingTeamId, forKey: ._servingTeamId) try container.encode(index, forKey: ._index) try container.encode(winningTeamId, forKey: ._winningTeamId) try container.encode(losingTeamId, forKey: ._losingTeamId) try container.encode(name, forKey: ._name) try container.encode(disabled, forKey: ._disabled) try container.encode(courtIndex, forKey: ._courtIndex) try container.encode(confirmed, forKey: ._confirmed) } func insertOnServer() { self.tournamentStore.matches.writeChangeAndInsertOnServer(instance: self) for teamScore in self.teamScores { try teamScore.insertOnServer() } } } enum MatchDateSetup: Hashable, Identifiable { case inMinutes(Int) case now case customDate var id: Int { hashValue } } enum MatchFieldSetup: Hashable, Identifiable { case random case fullRandom // case firstAvailable case field(Int) var courtIndex: Int? { switch self { case .random: return nil case .fullRandom: return nil case .field(let int): return int } } var id: Int { hashValue } }