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.
1106 lines
41 KiB
1106 lines
41 KiB
//
|
|
// Match.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by razmig on 10/03/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
|
|
@Observable
|
|
final public class Match: BaseMatch, SideStorable {
|
|
|
|
static func == (lhs: Match, rhs: Match) -> Bool {
|
|
lhs.id == rhs.id && lhs.startDate == rhs.startDate
|
|
}
|
|
|
|
public static func setServerTitle(upperRound: Round, matchIndex: Int) -> String {
|
|
if upperRound.index == 0 { return upperRound.roundTitle() }
|
|
return upperRound.roundTitle() + " #" + (matchIndex + 1).formatted()
|
|
}
|
|
|
|
public var byeState: Bool = false
|
|
|
|
public 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)
|
|
}
|
|
|
|
required public init() {
|
|
super.init()
|
|
}
|
|
|
|
// MARK: - DidSet
|
|
|
|
public override func didSetStartDate() {
|
|
if hasStarted() {
|
|
return
|
|
}
|
|
if self.roundValue()?.tournamentObject()?.hasStarted() == false {
|
|
plannedStartDate = startDate
|
|
} else if self.groupStageValue()?.tournamentObject()?.hasStarted() == false {
|
|
plannedStartDate = startDate
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public func setMatchName(_ serverName: String?) {
|
|
self.name = serverName
|
|
}
|
|
|
|
public 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
|
|
}
|
|
}
|
|
|
|
public var tournamentStore: TournamentStore? {
|
|
if let id = self.store?.identifier {
|
|
return TournamentLibrary.shared.store(tournamentId: id)
|
|
}
|
|
fatalError("missing store for \(String(describing: type(of: self)))")
|
|
}
|
|
|
|
public var courtIndexForSorting: Int {
|
|
courtIndex ?? Int.max
|
|
}
|
|
|
|
// MARK: - Computed dependencies
|
|
|
|
public var teamScores: [TeamScore] {
|
|
guard let tournamentStore = self.tournamentStore else { return [] }
|
|
return tournamentStore.teamScores.filter { $0.match == self.id }
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public override func deleteDependencies(store: Store, shouldBeSynchronized: Bool) {
|
|
|
|
store.deleteDependencies(type: TeamScore.self, shouldBeSynchronized: shouldBeSynchronized) { $0.match == self.id }
|
|
|
|
// let teamScores = self.teamScores
|
|
// for teamScore in teamScores {
|
|
// teamScore.deleteDependencies(store: store, shouldBeSynchronized: shouldBeSynchronized)
|
|
// }
|
|
// self.tournamentStore?.teamScores.deleteDependencies(teamScores, shouldBeSynchronized: shouldBeSynchronized)
|
|
}
|
|
|
|
public 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 matches, let index = matches.firstIndex(where: { $0.id == id }) {
|
|
return index
|
|
} else if let roundObject, roundObject.isUpperBracket(), let index = roundObject.playedMatches().firstIndex(where: { $0.id == id }) {
|
|
return index
|
|
}
|
|
return RoundRule.matchIndexWithinRound(fromMatchIndex: index)
|
|
}
|
|
|
|
public func matchWarningSubject() -> String {
|
|
[roundTitle(), matchTitle(.short)].compacted().joined(separator: " ")
|
|
}
|
|
|
|
public func matchWarningMessage() -> String {
|
|
[roundAndMatchTitle(), startDate?.localizedDate(), courtName(), matchFormat.computedLongLabel].compacted().joined(separator: "\n")
|
|
}
|
|
|
|
public 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 "n˚\(indexInRound(in: matches) + 1)"
|
|
}
|
|
}
|
|
|
|
public func isSeedLocked(atTeamPosition teamPosition: TeamPosition) -> Bool {
|
|
return previousMatch(teamPosition)?.disabled == true
|
|
}
|
|
|
|
public func unlockSeedPosition(atTeamPosition teamPosition: TeamPosition) {
|
|
previousMatch(teamPosition)?.enableMatch()
|
|
}
|
|
|
|
@discardableResult
|
|
public func lockAndGetSeedPosition(atTeamPosition teamPosition: TeamPosition) -> Int {
|
|
let matchIndex = index
|
|
previousMatch(teamPosition)?.disableMatch()
|
|
return matchIndex * 2 + teamPosition.rawValue
|
|
}
|
|
|
|
public 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
|
|
}
|
|
|
|
public func estimatedEndDate(_ additionalEstimationDuration: Int) -> Date? {
|
|
let minutesToAdd = Double(matchFormat.getEstimatedDuration(additionalEstimationDuration))
|
|
return startDate?.addingTimeInterval(minutesToAdd * 60.0)
|
|
}
|
|
|
|
public func winner() -> TeamRegistration? {
|
|
guard let winningTeamId else { return nil }
|
|
return self.tournamentStore?.teamRegistrations.findById(winningTeamId)
|
|
}
|
|
|
|
public func loser() -> TeamRegistration? {
|
|
guard let losingTeamId else { return nil }
|
|
return self.tournamentStore?.teamRegistrations.findById(losingTeamId)
|
|
}
|
|
|
|
|
|
public func localizedStartDate() -> String {
|
|
if let startDate {
|
|
return startDate.formatted(date: .abbreviated, time: .shortened)
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
public 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
|
|
}
|
|
|
|
public func cleanScheduleAndSave(_ targetStartDate: Date? = nil) {
|
|
startDate = targetStartDate ?? startDate
|
|
confirmed = false
|
|
endDate = nil
|
|
followingMatch()?.cleanScheduleAndSave(nil)
|
|
_loserMatch()?.cleanScheduleAndSave(nil)
|
|
self.tournamentStore?.matches.addOrUpdate(instance: self)
|
|
}
|
|
|
|
public func resetMatch() {
|
|
losingTeamId = nil
|
|
winningTeamId = nil
|
|
endDate = nil
|
|
removeCourt()
|
|
servingTeamId = nil
|
|
groupStageObject?.updateGroupStageState()
|
|
roundObject?.updateTournamentState()
|
|
teams().forEach({ $0.resetRestingTime() })
|
|
}
|
|
|
|
public func resetScores() {
|
|
teamScores.forEach({ $0.score = nil })
|
|
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores)
|
|
}
|
|
|
|
public func teamWillBeWalkOut(_ team: TeamRegistration) {
|
|
resetMatch()
|
|
let existingTeamScore = teamScore(ofTeam: team) ?? TeamScore(match: id, team: team)
|
|
existingTeamScore.walkOut = 1
|
|
self.tournamentStore?.teamScores.addOrUpdate(instance: existingTeamScore)
|
|
}
|
|
|
|
public func luckyLosers() -> [TeamRegistration] {
|
|
return roundObject?.previousRound()?.losers() ?? []
|
|
}
|
|
|
|
public func isWalkOutSpot(_ teamPosition: TeamPosition) -> Bool {
|
|
return teamScore(teamPosition)?.walkOut == 1
|
|
}
|
|
|
|
public 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)
|
|
}
|
|
|
|
public func disableMatch() {
|
|
_toggleMatchDisableState(true)
|
|
}
|
|
|
|
public func enableMatch() {
|
|
_toggleMatchDisableState(false)
|
|
}
|
|
|
|
private func _loserMatch() -> Match? {
|
|
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index)
|
|
return roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2)
|
|
}
|
|
|
|
public 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 })
|
|
}
|
|
|
|
public 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.byeState = true
|
|
forwardMatch._toggleMatchDisableState(false, forward: true)
|
|
} else if forwardMatch.disabled != state {
|
|
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)
|
|
}
|
|
|
|
public func isSeededBy(team: TeamRegistration) -> Bool {
|
|
isSeededBy(team: team, inTeamPosition: .one) || isSeededBy(team: team, inTeamPosition: .two)
|
|
}
|
|
|
|
public func isSeeded() -> Bool {
|
|
return isSeededAt(.one) || isSeededAt(.two)
|
|
}
|
|
|
|
public func isSeededAt(_ teamPosition: TeamPosition) -> Bool {
|
|
if let team = team(teamPosition) {
|
|
return isSeededBy(team: team)
|
|
}
|
|
return false
|
|
}
|
|
|
|
public func _toggleMatchDisableState(_ state: Bool, forward: Bool = false, single: Bool = false) {
|
|
//if disabled == state { return }
|
|
let tournamentStore = self.tournamentStore
|
|
let currentState = disabled
|
|
disabled = state
|
|
|
|
|
|
if disabled != currentState {
|
|
tournamentStore?.teamScores.delete(contentOfs: teamScores)
|
|
}
|
|
if state == true, state != currentState {
|
|
let teams = teams()
|
|
for team in teams {
|
|
if isSeededBy(team: team) {
|
|
team.bracketPosition = nil
|
|
tournamentStore?.teamRegistrations.addOrUpdate(instance: team)
|
|
}
|
|
}
|
|
}
|
|
//byeState = false
|
|
if state != currentState {
|
|
roundObject?.invalidateCache()
|
|
name = nil
|
|
tournamentStore?.matches.addOrUpdate(instance: self)
|
|
}
|
|
|
|
if single == false {
|
|
_toggleLoserMatchDisableState(state)
|
|
if forward {
|
|
_toggleForwardMatchDisableState(state)
|
|
} else {
|
|
topPreviousRoundMatch()?._toggleMatchDisableState(state)
|
|
bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
|
|
}
|
|
}
|
|
}
|
|
|
|
public 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
|
|
}
|
|
|
|
public func followingMatch() -> Match? {
|
|
guard let nextRoundId = roundObject?.nextRound()?.id else { return nil }
|
|
return getFollowingMatch(fromNextRoundId: nextRoundId)
|
|
}
|
|
|
|
public func getFollowingMatch(fromNextRoundId nextRoundId: String) -> Match? {
|
|
return self.tournamentStore?.matches.first(where: { $0.round == nextRoundId && $0.index == (index - 1) / 2 })
|
|
}
|
|
|
|
public func getDuration() -> Int {
|
|
if let tournament = currentTournament() {
|
|
matchFormat.getEstimatedDuration(tournament.additionalEstimationDuration)
|
|
} else {
|
|
matchFormat.getEstimatedDuration()
|
|
}
|
|
}
|
|
|
|
public func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String? {
|
|
if groupStage != nil { return groupStageObject?.groupStageTitle() }
|
|
else if let roundObject { return roundObject.roundTitle() }
|
|
else { return nil }
|
|
}
|
|
|
|
public func roundAndMatchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
[roundTitle(displayStyle), matchTitle(displayStyle)].compactMap({ $0 }).joined(separator: " ")
|
|
}
|
|
|
|
public func topPreviousRoundMatchIndex() -> Int {
|
|
return index * 2 + 1
|
|
}
|
|
|
|
public func bottomPreviousRoundMatchIndex() -> Int {
|
|
return (index + 1) * 2
|
|
}
|
|
|
|
public 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
|
|
})
|
|
}
|
|
|
|
public 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
|
|
})
|
|
}
|
|
|
|
public func previousMatch(_ teamPosition: TeamPosition) -> Match? {
|
|
if teamPosition == .one {
|
|
return topPreviousRoundMatch()
|
|
} else {
|
|
return bottomPreviousRoundMatch()
|
|
}
|
|
}
|
|
|
|
public func loserMatches() -> [Match] {
|
|
guard let roundObject else { return [] }
|
|
return [roundObject.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil), roundObject.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)].compactMap({ $0 })
|
|
}
|
|
|
|
public func loserMatch(_ teamPosition: TeamPosition) -> Match? {
|
|
if teamPosition == .one {
|
|
return roundObject?.upperBracketTopMatch(ofMatchIndex: index, previousRound: nil)
|
|
} else {
|
|
return roundObject?.upperBracketBottomMatch(ofMatchIndex: index, previousRound: nil)
|
|
}
|
|
|
|
}
|
|
|
|
public var computedOrder: Int {
|
|
if let groupStageObject {
|
|
return (groupStageObject.index + 1) * 100 + groupStageObject.indexOf(index)
|
|
}
|
|
guard let roundObject else { return index }
|
|
return (300 - (roundObject.theoryCumulativeMatchCount * 10 + roundObject.index * 22)) * 10 + RoundRule.matchIndexWithinRound(fromMatchIndex: index)
|
|
}
|
|
|
|
public 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)
|
|
}
|
|
|
|
public var matchFormat: MatchFormat {
|
|
get {
|
|
format ?? .defaultFormatForMatchType(.groupStage)
|
|
}
|
|
set {
|
|
format = newValue
|
|
}
|
|
}
|
|
|
|
public func removeWalkOut() {
|
|
teamScores.forEach { teamScore in
|
|
teamScore.walkOut = nil
|
|
teamScore.score = nil
|
|
}
|
|
tournamentStore?.teamScores.addOrUpdate(contentOfs: teamScores)
|
|
endDate = nil
|
|
winningTeamId = nil
|
|
losingTeamId = nil
|
|
groupStageObject?.updateGroupStageState()
|
|
roundObject?.updateTournamentState()
|
|
updateFollowingMatchTeamScore()
|
|
}
|
|
|
|
public func setWalkOut(_ teamPosition: TeamPosition) {
|
|
let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, team: team(teamPosition))
|
|
teamScoreWalkout.walkOut = 0
|
|
teamScoreWalkout.score = matchFormat.defaultWalkOutScore(true).compactMap({ String($0) }).joined(separator: ",")
|
|
let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, team: team(teamPosition.otherTeam))
|
|
teamScoreWinning.walkOut = nil
|
|
teamScoreWinning.score = matchFormat.defaultWalkOutScore(false).compactMap({ String($0) }).joined(separator: ",")
|
|
do {
|
|
try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
|
|
if endDate == nil {
|
|
endDate = Date()
|
|
}
|
|
teams().forEach({ $0.resetRestingTime() })
|
|
winningTeamId = teamScoreWinning.teamRegistration
|
|
losingTeamId = teamScoreWalkout.teamRegistration
|
|
groupStageObject?.updateGroupStageState()
|
|
roundObject?.updateTournamentState()
|
|
updateFollowingMatchTeamScore()
|
|
}
|
|
|
|
public func setUnfinishedScore(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))
|
|
}
|
|
confirmed = true
|
|
}
|
|
|
|
public 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)
|
|
}
|
|
}
|
|
updateFollowingMatchTeamScore()
|
|
}
|
|
|
|
public 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: ",")
|
|
|
|
if matchDescriptor.teamOneScores.last?.contains("-") == true && matchDescriptor.teamTwoScores.last?.contains("-") == false {
|
|
teamScoreTwo.score = (matchDescriptor.teamTwoScores.dropLast() + [matchDescriptor.teamTwoScores.last! + "-0"]).joined(separator: ",")
|
|
} else if matchDescriptor.teamTwoScores.last?.contains("-") == true && matchDescriptor.teamOneScores.last?.contains("-") == false {
|
|
teamScoreOne.score = (matchDescriptor.teamOneScores.dropLast() + [matchDescriptor.teamOneScores.last! + "-0"]).joined(separator: ",")
|
|
}
|
|
|
|
do {
|
|
try self.tournamentStore?.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo])
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
matchFormat = matchDescriptor.matchFormat
|
|
}
|
|
|
|
public func updateFollowingMatchTeamScore() {
|
|
followingMatch()?.updateTeamScores()
|
|
_loserMatch()?.updateTeamScores()
|
|
}
|
|
|
|
public 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: [])
|
|
}
|
|
}
|
|
|
|
public 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
|
|
}
|
|
|
|
public 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
|
|
}
|
|
|
|
public func updateTeamScores() {
|
|
let teams = getOrCreateTeamScores()
|
|
|
|
self.tournamentStore?.teamScores.addOrUpdate(contentOfs: teams)
|
|
resetTeamScores(outsideOf: teams)
|
|
if teams.isEmpty == false {
|
|
updateFollowingMatchTeamScore()
|
|
}
|
|
}
|
|
|
|
public 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
|
|
}
|
|
}
|
|
|
|
public func courtName() -> String? {
|
|
guard let courtIndex else { return nil }
|
|
if let courtName = currentTournament()?.courtName(atIndex: courtIndex) {
|
|
return courtName
|
|
} else {
|
|
return Court.courtIndexedTitle(atIndex: courtIndex)
|
|
}
|
|
}
|
|
|
|
public func courtName(for selectedIndex: Int) -> String {
|
|
if let courtName = currentTournament()?.courtName(atIndex: selectedIndex) {
|
|
return courtName
|
|
} else {
|
|
return Court.courtIndexedTitle(atIndex: selectedIndex)
|
|
}
|
|
}
|
|
|
|
public func courtCount() -> Int {
|
|
return currentTournament()?.courtCount ?? 1
|
|
}
|
|
|
|
public func courtIsAvailable(_ courtIndex: Int, in runningMatches: [Match]) -> Bool {
|
|
let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
|
|
return courtUsed.contains(courtIndex) == false
|
|
}
|
|
|
|
public func courtIsPreferred(_ courtIndex: Int) -> Bool {
|
|
return false
|
|
}
|
|
|
|
public func allCourts() -> [Int] {
|
|
let availableCourts = Array(0..<courtCount())
|
|
return availableCourts
|
|
}
|
|
|
|
public func availableCourts(runningMatches: [Match]) -> [Int] {
|
|
let courtUsed = currentTournament()?.courtUsed(runningMatches: runningMatches) ?? []
|
|
return Set(allCourts().map { $0 }).subtracting(Set(courtUsed)).sorted()
|
|
}
|
|
|
|
public func removeCourt() {
|
|
courtIndex = nil
|
|
}
|
|
|
|
public func setCourt(_ courtIndex: Int) {
|
|
self.courtIndex = courtIndex
|
|
}
|
|
|
|
public 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
|
|
})
|
|
}
|
|
|
|
public func isTeamPlaying(_ team: TeamRegistration, inMatches matches: [Match]) -> Bool {
|
|
return matches.filter({ $0.teamScores.compactMap { $0.teamRegistration }.contains(team.id) }).isEmpty == false
|
|
}
|
|
|
|
public var computedStartDateForSorting: Date {
|
|
return startDate ?? .distantFuture
|
|
}
|
|
|
|
public var computedEndDateForSorting: Date {
|
|
return endDate ?? .distantFuture
|
|
}
|
|
|
|
public func hasSpaceLeft() -> Bool {
|
|
return teamScores.count < 2
|
|
}
|
|
|
|
public func isReady() -> Bool {
|
|
return teamScores.count >= 2
|
|
// teams().count == 2
|
|
}
|
|
|
|
public func isEmpty() -> Bool {
|
|
return teamScores.isEmpty
|
|
// teams().isEmpty
|
|
}
|
|
|
|
public func hasEnded() -> Bool {
|
|
return endDate != nil
|
|
}
|
|
|
|
public func isGroupStage() -> Bool {
|
|
return groupStage != nil
|
|
}
|
|
|
|
public func isBracket() -> Bool {
|
|
return round != nil
|
|
}
|
|
|
|
public func walkoutTeam() -> [TeamRegistration] {
|
|
//walkout 0 means real walkout, walkout 1 means lucky loser situation
|
|
return scores().filter({ $0.walkOut == 0 }).compactMap { $0.team }
|
|
}
|
|
|
|
public func hasWalkoutTeam() -> Bool {
|
|
return walkoutTeam().isEmpty == false
|
|
}
|
|
|
|
public func currentTournament() -> Tournament? {
|
|
return groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject()
|
|
}
|
|
|
|
public func tournamentId() -> String? {
|
|
return groupStageObject?.tournament ?? roundObject?.tournament
|
|
}
|
|
|
|
public func scores() -> [TeamScore] {
|
|
guard let tournamentStore = self.tournamentStore else { return [] }
|
|
return tournamentStore.teamScores.filter { $0.match == id }
|
|
}
|
|
|
|
public 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 }
|
|
}
|
|
|
|
public 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({ $0.components(separatedBy: "-").first }).compactMap({ Int($0) }) ?? matchFormat.defaultWalkOutScore(teamScoreTeam.isWalkOut())
|
|
let endedSetsTwo = teamScoreOtherTeam.score?.components(separatedBy: ",").compactMap({ $0.components(separatedBy: "-").first }).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)
|
|
}
|
|
}
|
|
|
|
public func groupStageProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
|
|
guard let groupStageObject else { return nil }
|
|
return groupStageObject.team(teamPosition: team, inMatchIndex: index)
|
|
}
|
|
|
|
public func roundProjectedTeam(_ team: TeamPosition) -> TeamRegistration? {
|
|
guard let roundObject else { return nil }
|
|
let previousRound = roundObject.previousRound()
|
|
return roundObject.roundProjectedTeam(team, inMatch: self, previousRound: previousRound)
|
|
}
|
|
|
|
public func teamWon(_ team: TeamRegistration?) -> Bool {
|
|
guard let winningTeamId else { return false }
|
|
return winningTeamId == team?.id
|
|
}
|
|
|
|
public func teamWon(atPosition teamPosition: TeamPosition) -> Bool {
|
|
guard let winningTeamId else { return false }
|
|
return winningTeamId == team(teamPosition)?.id
|
|
}
|
|
|
|
public 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)
|
|
}
|
|
}
|
|
|
|
public func teamNames(_ team: TeamRegistration?) -> [String]? {
|
|
return team?.players().map { $0.playerLabel() }
|
|
}
|
|
|
|
public func teamWalkOut(_ team: TeamRegistration?) -> Bool {
|
|
return teamScore(ofTeam: team)?.isWalkOut() == true
|
|
}
|
|
|
|
public func teamScore(_ team: TeamPosition) -> TeamScore? {
|
|
return teamScore(ofTeam: self.team(team))
|
|
}
|
|
|
|
public func teamScore(ofTeam team: TeamRegistration?) -> TeamScore? {
|
|
return scores().first(where: { $0.teamRegistration == team?.id })
|
|
}
|
|
|
|
public func isRunning() -> Bool { // at least a match has started
|
|
return confirmed && hasStarted() && hasEnded() == false
|
|
}
|
|
|
|
public 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
|
|
// }
|
|
}
|
|
|
|
public var roundObject: Round? {
|
|
guard let round else { return nil }
|
|
return self.tournamentStore?.rounds.findById(round)
|
|
}
|
|
|
|
public var groupStageObject: GroupStage? {
|
|
guard let groupStage else { return nil }
|
|
return self.tournamentStore?.groupStages.findById(groupStage)
|
|
}
|
|
|
|
public var isLoserBracket: Bool {
|
|
if let roundObject {
|
|
if roundObject.parent != nil || roundObject.groupStageLoserBracket {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
public var matchType: MatchType {
|
|
if isLoserBracket {
|
|
return .loserBracket
|
|
} else if isGroupStage() {
|
|
return .groupStage
|
|
} else {
|
|
return .bracket
|
|
}
|
|
}
|
|
|
|
public var restingTimeForSorting: TimeInterval {
|
|
(teams().compactMap({ $0.restingTime() }).max() ?? .distantFuture).timeIntervalSinceNow
|
|
}
|
|
|
|
public func isValidSpot() -> Bool {
|
|
previousMatches().allSatisfy({ $0.isSeeded() == false })
|
|
}
|
|
|
|
public func expectedToBeRunning() -> Bool {
|
|
guard let startDate else { return false }
|
|
return confirmed == false && startDate.timeIntervalSinceNow < 0
|
|
}
|
|
|
|
public 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())"
|
|
}
|
|
}
|
|
|
|
public func runningDuration() -> String {
|
|
guard let startDate else { return "" }
|
|
return " depuis " + startDate.timeElapsedString()
|
|
}
|
|
|
|
public func canBePlayedInSpecifiedCourt(runningMatches: [Match]) -> Bool {
|
|
guard let courtIndex else { return false }
|
|
if expectedToBeRunning() {
|
|
return courtIsAvailable(courtIndex, in: runningMatches)
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
public typealias CourtIndexAndDate = (courtIndex: Int, startDate: Date)
|
|
|
|
public 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
|
|
}
|
|
|
|
public 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
|
|
})
|
|
}
|
|
|
|
public 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
|
|
}
|
|
|
|
public func ancestors() -> [Match] {
|
|
previousMatches() + loserMatches()
|
|
}
|
|
|
|
public func matchSpots() -> [MatchSpot] {
|
|
[MatchSpot(match: self, teamPosition: .one), MatchSpot(match: self, teamPosition: .two)]
|
|
}
|
|
|
|
func insertOnServer() {
|
|
self.tournamentStore?.matches.writeChangeAndInsertOnServer(instance: self)
|
|
for teamScore in self.teamScores {
|
|
teamScore.insertOnServer()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public enum MatchDateSetup: Hashable, Identifiable {
|
|
case inMinutes(Int)
|
|
case now
|
|
case customDate
|
|
case previousRotation
|
|
case nextRotation
|
|
|
|
public var id: Int { hashValue }
|
|
}
|
|
|
|
public enum MatchFieldSetup: Hashable, Identifiable {
|
|
case random
|
|
case fullRandom
|
|
// case firstAvailable
|
|
case field(Int)
|
|
|
|
public var courtIndex: Int? {
|
|
switch self {
|
|
case .random:
|
|
return nil
|
|
case .fullRandom:
|
|
return nil
|
|
case .field(let int):
|
|
return int
|
|
}
|
|
}
|
|
|
|
public var id: Int { hashValue }
|
|
}
|
|
|