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.
881 lines
30 KiB
881 lines
30 KiB
//
|
|
// Match.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by razmig on 10/03/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
|
|
final class Match: ModelObject, Storable {
|
|
static func resourceName() -> String { "matches" }
|
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
|
|
static func filterByStoreIdentifier() -> Bool { return true }
|
|
static var relationshipNames: [String] = ["round", "groupStage"]
|
|
|
|
|
|
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 {
|
|
let matches = (matches ?? roundObject?.playedMatches().sorted(by: \.index))
|
|
if let index = matches?.firstIndex(where: { $0.id == id }) {
|
|
return index
|
|
}
|
|
}
|
|
return RoundRule.matchIndexWithinRound(fromMatchIndex: index)
|
|
}
|
|
|
|
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 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
|
|
}
|
|
|
|
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) {
|
|
//if disabled == state { return }
|
|
disabled = state
|
|
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)
|
|
}
|
|
|
|
_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 }
|
|
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 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 {
|
|
guard let roundObject else { return index }
|
|
return roundObject.isLoserBracket() ? roundObject.index * 100 + indexInRound() : roundObject.index * 1000 + 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()
|
|
updateFollowingMatchTeamScore()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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..<courtCount())
|
|
return availableCourts
|
|
}
|
|
|
|
func availableCourts() -> [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]) -> Bool {
|
|
let teams = teamScores
|
|
guard teams.count == 2 else { return false }
|
|
guard hasEnded() == false else { return false }
|
|
guard hasStarted() == false else { return false }
|
|
return teams.compactMap({ $0.team }).allSatisfy({ $0.canPlay() && 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) -> (set: Int, game: Int)? {
|
|
guard let teamScoreTeam = teamScore(.one), let teamScoreOtherTeam = teamScore(.two) else { return nil }
|
|
var reverseValue = 1
|
|
if teamPosition == team(.two)?.groupStagePosition {
|
|
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 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
|
|
}
|
|
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 {
|
|
return roundObject?.parent != nil
|
|
}
|
|
|
|
// 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)
|
|
//
|
|
// if let round = round {
|
|
// try container.encode(round, forKey: ._round)
|
|
// } else {
|
|
// try container.encodeNil(forKey: ._round)
|
|
// }
|
|
//
|
|
// if let groupStage = groupStage {
|
|
// try container.encode(groupStage, forKey: ._groupStage)
|
|
// } else {
|
|
// try container.encodeNil(forKey: ._groupStage)
|
|
// }
|
|
//
|
|
// if let startDate = startDate {
|
|
// try container.encode(startDate, forKey: ._startDate)
|
|
// } else {
|
|
// try container.encodeNil(forKey: ._startDate)
|
|
// }
|
|
//
|
|
// if let endDate = endDate {
|
|
// try container.encode(endDate, forKey: ._endDate)
|
|
// } else {
|
|
// try container.encodeNil(forKey: ._endDate)
|
|
// }
|
|
//
|
|
// try container.encode(index, forKey: ._index)
|
|
//
|
|
// if let format = format {
|
|
// try container.encode(format, forKey: ._format)
|
|
// } else {
|
|
// try container.encodeNil(forKey: ._format)
|
|
// }
|
|
//
|
|
// if let servingTeamId = servingTeamId {
|
|
// try container.encode(servingTeamId, forKey: ._servingTeamId)
|
|
// } else {
|
|
// try container.encodeNil(forKey: ._servingTeamId)
|
|
// }
|
|
//
|
|
// if let winningTeamId = winningTeamId {
|
|
// try container.encode(winningTeamId, forKey: ._winningTeamId)
|
|
// } else {
|
|
// try container.encodeNil(forKey: ._winningTeamId)
|
|
// }
|
|
//
|
|
// if let losingTeamId = losingTeamId {
|
|
// try container.encode(losingTeamId, forKey: ._losingTeamId)
|
|
// } else {
|
|
// try container.encodeNil(forKey: ._losingTeamId)
|
|
// }
|
|
//
|
|
// if let name = name {
|
|
// try container.encode(name, forKey: ._name)
|
|
// } else {
|
|
// try container.encodeNil(forKey: ._name)
|
|
// }
|
|
//
|
|
// try container.encode(disabled, forKey: ._disabled)
|
|
//
|
|
// if let courtIndex = courtIndex {
|
|
// try container.encode(courtIndex, forKey: ._courtIndex)
|
|
// } else {
|
|
// try container.encodeNil(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 id: Int { hashValue }
|
|
}
|
|
|