You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
PadelClub/PadelClub/Data/Match.swift

1042 lines
37 KiB

//
// Match.swift
// Padel Tournament
//
// Created by razmig on 10/03/2024.
//
import Foundation
import LeStorage
@Observable
final class Match: BaseMatch, SideStorable {
static func == (lhs: Match, rhs: Match) -> Bool {
lhs.id == rhs.id && lhs.startDate == rhs.startDate
}
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
init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, format: MatchFormat? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, name: String? = nil, disabled: Bool = false, courtIndex: Int? = nil, confirmed: Bool = false) {
super.init(round: round, groupStage: groupStage, startDate: startDate, endDate: endDate, index: index, format: format, servingTeamId: servingTeamId, winningTeamId: winningTeamId, losingTeamId: losingTeamId, name: name, disabled: disabled, courtIndex: courtIndex, confirmed: confirmed)
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
}
func setMatchName(_ serverName: String?) {
self.name = serverName
}
func isFromLastRound() -> Bool {
guard let roundObject, roundObject.parent == nil else { return false }
guard let currentTournament = currentTournament() else { return false }
if currentTournament.rounds().count - 1 == roundObject.index {
return true
} else {
return false
}
}
var tournamentStore: TournamentStore? {
if let id = self.store?.identifier {
return TournamentLibrary.shared.store(tournamentId: id)
}
fatalError("missing store for \(String(describing: type(of: self)))")
}
var courtIndexForSorting: Int {
courtIndex ?? Int.max
}
// MARK: - Computed dependencies
var teamScores: [TeamScore] {
guard let tournamentStore = self.tournamentStore else { return [] }
return tournamentStore.teamScores.filter { $0.match == self.id }
}
// MARK: -
override func deleteDependencies() {
let teamScores = self.teamScores
for teamScore in teamScores {
teamScore.deleteDependencies()
}
self.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 {
[roundAndMatchTitle(), startDate?.localizedDate(), courtName(), matchFormat.computedLongLabel].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 teamPosition: TeamPosition) -> Int {
let matchIndex = index
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 ?? startDate
confirmed = false
endDate = nil
followingMatch()?.cleanScheduleAndSave(nil)
_loserMatch()?.cleanScheduleAndSave(nil)
self.tournamentStore?.matches.addOrUpdate(instance: self)
}
func resetMatch() {
losingTeamId = nil
winningTeamId = nil
endDate = nil
removeCourt()
servingTeamId = nil
groupStageObject?.updateGroupStageState()
roundObject?.updateTournamentState()
currentTournament()?.updateTournamentState()
teams().forEach({ $0.resetRestingTime() })
}
func resetScores() {
teamScores.forEach({ $0.score = nil })
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores)
}
func teamWillBeWalkOut(_ team: TeamRegistration) {
resetMatch()
let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
existingTeamScore.walkOut = 1
self.tournamentStore?.teamScores.addOrUpdate(instance: existingTeamScore)
}
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 })
self.tournamentStore?.teamScores.delete(contentOfs: previousScores)
let teamScoreLuckyLoser = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
teamScoreLuckyLoser.luckyLoser = position
self.tournamentStore?.teamScores.addOrUpdate(instance: teamScoreLuckyLoser)
}
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 {
if forwardMatch.disabled != state || forwardMatch.byeState {
forwardMatch.byeState = false
forwardMatch._toggleMatchDisableState(state, forward: true)
}
} else if byeState && next.byeState {
print("don't disable forward match")
if forwardMatch.byeState || forwardMatch.disabled {
forwardMatch.byeState = false
forwardMatch._toggleMatchDisableState(false, forward: true)
}
} else {
if forwardMatch.byeState == false || forwardMatch.disabled != state {
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 }
let currentState = disabled
disabled = state
if disabled != currentState {
do {
try self.tournamentStore?.teamScores.delete(contentOfs: teamScores)
} catch {
Logger.error(error)
}
}
if state == true, state != currentState {
let teams = teams()
for team in teams {
if isSeededBy(team: team) {
team.bracketPosition = nil
self.tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
}
}
}
//byeState = false
roundObject?._cachedSeedInterval = nil
name = nil
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? {
guard let tournamentStore = self.tournamentStore else { return nil }
let matches: [Match] = 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(_ displayStyle: DisplayStyle = .wide) -> String? {
if groupStage != nil { return groupStageObject?.groupStageTitle() }
else if let roundObject { return roundObject.roundTitle() }
else { return nil }
}
func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
[roundTitle(displayStyle), matchTitle(displayStyle)].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()
}
}
func loserMatches() -> [Match] {
guard let roundObject else { return [] }
return [roundObject.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil), roundObject.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)].compactMap({ $0 })
}
func loserMatch(_ teamPosition: TeamPosition) -> Match? {
if teamPosition == .one {
return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil)
} else {
return roundObject?.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)
}
}
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) * 10000 + indexInRound() : (roundObject.index + 1) * 1000 + indexInRound()
}
func previousMatches() -> [Match] {
guard let roundObject else { return [] }
guard let tournamentStore = self.tournamentStore else { return [] }
let roundObjectPreviousRoundId = roundObject.previousRound()?.id
return 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
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
if endDate == nil {
endDate = Date()
}
teams().forEach({ $0.resetRestingTime() })
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))
} else if let startDate, let endDate, startDate >= endDate {
self.startDate = endDate.addingTimeInterval(Double(-getDuration()*60))
}
let teamOne = team(matchDescriptor.winner)
let teamTwo = team(matchDescriptor.winner.otherTeam)
teamOne?.hasArrived()
teamTwo?.hasArrived()
teamOne?.resetRestingTime()
teamTwo?.resetRestingTime()
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: ",")
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo])
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 {
self.tournamentStore?.teamScores.delete(contentOfs: teamScores)
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()
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teams)
resetTeamScores(outsideOf: teams)
if teams.isEmpty == false {
updateFollowingMatchTeamScore()
}
}
func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup, forced: Bool = false) {
if hasEnded() == false {
startDate = fromStartDate
switch fieldSetup {
case .fullRandom:
if let _courtIndex = allCourts().randomElement() {
setCourt(_courtIndex)
}
case .random:
let runningMatches: [Match] = DataStore.shared.runningMatches()
if let _courtIndex = availableCourts(runningMatches: runningMatches).randomElement() {
setCourt(_courtIndex)
}
case .field(let _courtIndex):
setCourt(_courtIndex)
}
} else {
startDate = fromStartDate
endDate = toEndDate
}
if let startDate, startDate.timeIntervalSinceNow <= 300 {
confirmed = true
} else {
confirmed = false
}
}
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 courtName(for selectedIndex: Int) -> String {
if let courtName = currentTournament()?.courtName(atIndex: selectedIndex) {
return courtName
} else {
return Court.courtIndexedTitle(atIndex: selectedIndex)
}
}
func courtCount() -> Int {
return currentTournament()?.courtCount ?? 1
}
func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool {
let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
return courtUsed.contains(courtIndex) == false
}
func courtIsPreferred(_ courtIndex: Int) -> Bool {
return false
}
func allCourts() -> [Int] {
let availableCourts = Array(0..<courtCount())
return availableCourts
}
func availableCourts(runningMatches: [Match]) -> [Int] {
let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted()
}
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] {
guard let tournamentStore = self.tournamentStore else { return [] }
return 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
}
// si 3 sets et 3eme set super tie break, different des 2 premiers sets, alors super tie points ne sont pas des jeux et doivent etre compté comme un jeu
if matchFormat.canSuperTie, endedSetsOne.count == 3 {
let games = zip.map { ($0, $1) }
let gameDifference = games.enumerated().map({ index, pair in
if index < 2 {
return pair.0 - pair.1
} else {
return pair.0 < pair.1 ? -1 : 1
}
})
.reduce(0,+)
return (setDifference * reverseValue, gameDifference * reverseValue)
} else {
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
}
func isValidSpot() -> Bool {
previousMatches().allSatisfy({ $0.isSeeded() == false })
}
func expectedToBeRunning() -> Bool {
guard let startDate else { return false }
return confirmed == false && startDate.timeIntervalSinceNow < 0
}
func expectedFormattedStartDate(canBePlayedInSpecifiedCourt: Bool, availableCourts: [Int], estimatedStartDate: CourtIndexAndDate?, updatedField: Int?) -> String {
guard let startDate else { return "" }
guard hasEnded() == false, isRunning() == false else { return "" }
let depthReadiness = depthReadiness()
if depthReadiness == 0 {
if canBePlayedInSpecifiedCourt {
return "possible tout de suite"
} else if let updatedField, availableCourts.contains(updatedField) {
return "possible tout de suite \(courtName(for: updatedField))"
} else if let first = availableCourts.first {
return "possible tout de suite \(courtName(for: first))"
} else if let estimatedStartDate {
return "dans ~" + estimatedStartDate.1.timeElapsedString() + " " + courtName(for: estimatedStartDate.0)
}
return "était prévu à " + startDate.formattedAsHourMinute()
} else if depthReadiness == 1 {
return "possible prochaine rotation"
} else {
return "dans \(depthReadiness) rotation\(depthReadiness.pluralSuffix), ~\((getDuration() * depthReadiness).durationInHourMinutes())"
}
}
func runningDuration() -> String {
guard let startDate else { return "" }
return " depuis " + startDate.timeElapsedString()
}
func canBePlayedInSpecifiedCourt(runningMatches: [Match]) -> Bool {
guard let courtIndex else { return false }
if expectedToBeRunning() {
return courtIsAvailable(courtIndex, in: runningMatches)
} else {
return true
}
}
typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date)
func nextCourtsAvailable(availableCourts: [Int], runningMatches: [Match]) -> [CourtIndexAndDate] {
guard let tournament = currentTournament() else { return [] }
let startDate = Date().withoutSeconds()
if runningMatches.isEmpty {
return availableCourts.map {
($0, startDate)
}
}
let optionalDates : [CourtIndexAndDate?] = runningMatches.map({ match in
guard let endDate = match.estimatedEndDate(tournament.additionalEstimationDuration) else { return nil }
guard let courtIndex = match.courtIndex else { return nil }
if endDate <= startDate {
return (courtIndex, startDate.addingTimeInterval(600))
} else {
return (courtIndex, endDate)
}
})
let dates : [CourtIndexAndDate] = optionalDates.compacted().sorted { a, b in
a.1 < b.1
}
return dates
}
func estimatedStartDate(availableCourts: [Int], runningMatches: [Match]) -> CourtIndexAndDate? {
guard isReady() else { return nil }
guard let tournament = currentTournament() else { return nil }
let availableCourts = nextCourtsAvailable(availableCourts: availableCourts, runningMatches: runningMatches)
return availableCourts.first(where: { (courtIndex, startDate) in
let endDate = startDate.addingTimeInterval(TimeInterval(matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)) * 60)
if tournament.courtUnavailable(courtIndex: courtIndex, from: startDate, to: endDate) == false {
return true
}
return false
})
}
func depthReadiness() -> Int {
// Base case: If this match is ready, the depth is 0
if isReady() {
return 0
}
// Recursive case: If not ready, check the maximum depth of readiness among previous matches
// If previousMatches() is empty, return a default depth of -1
let previousDepth = ancestors().map { $0.depthReadiness() }.max() ?? -1
return previousDepth + 1
}
func ancestors() -> [Match] {
previousMatches() + loserMatches()
}
func insertOnServer() {
self.tournamentStore?.matches.writeChangeAndInsertOnServer(instance: self)
for teamScore in self.teamScores {
teamScore.insertOnServer()
}
}
}
enum MatchDateSetup: Hashable, Identifiable {
case inMinutes(Int)
case now
case customDate
case previousRotation
case nextRotation
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 }
}