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.
522 lines
18 KiB
522 lines
18 KiB
//
|
|
// Match_v2.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by razmig on 10/03/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
|
|
@Observable
|
|
class Match: ModelObject, Storable {
|
|
static func resourceName() -> String { "matches" }
|
|
|
|
var id: String = Store.randomId()
|
|
var round: String?
|
|
var groupStage: String?
|
|
var startDate: Date?
|
|
var endDate: Date?
|
|
var index: Int
|
|
var format: Int?
|
|
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
|
|
|
|
internal init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, court: String? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, broadcasted: Bool = false, name: String? = nil, order: Int = 0) {
|
|
self.round = round
|
|
self.groupStage = groupStage
|
|
self.startDate = startDate
|
|
self.endDate = endDate
|
|
self.index = index
|
|
self.format = matchFormat?.rawValue
|
|
self.court = court
|
|
self.servingTeamId = servingTeamId
|
|
self.winningTeamId = winningTeamId
|
|
self.losingTeamId = losingTeamId
|
|
self.broadcasted = broadcasted
|
|
self.name = name
|
|
self.order = order
|
|
}
|
|
|
|
func indexInRound() -> Int {
|
|
if groupStage != nil {
|
|
return index
|
|
} else if let index = roundObject?.playedMatches().firstIndex(where: { $0.id == id }) {
|
|
return index
|
|
}
|
|
return RoundRule.matchIndexWithinRound(fromMatchIndex: index)
|
|
}
|
|
|
|
func matchTitle(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
if let groupStageObject {
|
|
return groupStageObject.localizedMatchUpLabel(for: index)
|
|
}
|
|
|
|
switch displayStyle {
|
|
case .wide:
|
|
return "Match \(indexInRound() + 1)"
|
|
case .short:
|
|
return "#\(indexInRound() + 1)"
|
|
}
|
|
}
|
|
|
|
func isSeededBy(team: TeamRegistration, inTeamPosition teamPosition: TeamPosition) -> Bool {
|
|
guard let bracketPosition = team.bracketPosition else { return false }
|
|
return index * 2 + teamPosition.rawValue == bracketPosition
|
|
}
|
|
|
|
func resetMatch() {
|
|
losingTeamId = nil
|
|
winningTeamId = nil
|
|
endDate = nil
|
|
court = nil
|
|
servingTeamId = nil
|
|
}
|
|
|
|
func teamWillBeWalkOut(_ team: TeamRegistration) {
|
|
resetMatch()
|
|
let previousScores = teamScores.filter({ $0.luckyLoser != nil })
|
|
try? DataStore.shared.teamScores.delete(contentOfs: previousScores)
|
|
|
|
if let existingTeamScore = teamScore(ofTeam: team) {
|
|
try? DataStore.shared.teamScores.delete(instance: existingTeamScore)
|
|
}
|
|
|
|
let teamScoreWalkout = TeamScore(match: id, teamRegistration: team.id)
|
|
teamScoreWalkout.walkOut = 1
|
|
try? DataStore.shared.teamScores.addOrUpdate(instance: teamScoreWalkout)
|
|
}
|
|
|
|
func luckyLosers() -> [TeamRegistration] {
|
|
roundObject?.previousRound()?.losers() ?? []
|
|
}
|
|
|
|
func isWalkOutSpot(_ teamPosition: TeamPosition) -> Bool {
|
|
teamScore(teamPosition)?.walkOut == 1
|
|
}
|
|
|
|
func setLuckyLoser(team: TeamRegistration, teamPosition: TeamPosition) {
|
|
resetMatch()
|
|
let previousScores = teamScores.filter({ $0.luckyLoser != nil })
|
|
try? DataStore.shared.teamScores.delete(contentOfs: previousScores)
|
|
|
|
if let existingTeamScore = teamScore(ofTeam: team) {
|
|
try? DataStore.shared.teamScores.delete(instance: existingTeamScore)
|
|
}
|
|
|
|
let matchIndex = index
|
|
let position = matchIndex * 2 + teamPosition.rawValue
|
|
let teamScoreLuckyLoser = TeamScore(match: id, teamRegistration: team.id)
|
|
teamScoreLuckyLoser.luckyLoser = position
|
|
try? DataStore.shared.teamScores.addOrUpdate(instance: teamScoreLuckyLoser)
|
|
}
|
|
|
|
func disableMatch() {
|
|
_toggleMatchDisableState(true)
|
|
}
|
|
|
|
func enableMatch() {
|
|
_toggleMatchDisableState(false)
|
|
}
|
|
|
|
private func _toggleLoserMatchDisableState(_ state: Bool) {
|
|
if isLoserBracket == false {
|
|
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: index)
|
|
if let loserMatch = roundObject?.loserRounds().first?.getMatch(atMatchIndexInRound: indexInRound / 2) {
|
|
loserMatch.disabled = state
|
|
try? DataStore.shared.matches.addOrUpdate(instance: loserMatch)
|
|
loserMatch._toggleLoserMatchDisableState(state)
|
|
}
|
|
} else {
|
|
roundObject?.loserRounds().forEach({ round in
|
|
round.handleLoserRoundState()
|
|
})
|
|
}
|
|
}
|
|
|
|
fileprivate func _toggleMatchDisableState(_ state: Bool) {
|
|
disabled = state
|
|
_toggleLoserMatchDisableState(state)
|
|
topPreviousRoundMatch()?._toggleMatchDisableState(state)
|
|
bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
|
|
try? DataStore.shared.matches.addOrUpdate(instance: self)
|
|
}
|
|
|
|
func next() -> Match? {
|
|
Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first
|
|
}
|
|
|
|
func roundTitle() -> String? {
|
|
if groupStage != nil { return "Poule" }
|
|
else if let roundObject { return roundObject.roundTitle() }
|
|
else { return nil }
|
|
}
|
|
|
|
func topPreviousRoundMatchIndex() -> Int {
|
|
index * 2 + 1
|
|
}
|
|
|
|
func bottomPreviousRoundMatchIndex() -> Int {
|
|
(index + 1) * 2
|
|
}
|
|
|
|
func topPreviousRoundMatch() -> Match? {
|
|
guard let roundObject else { return nil }
|
|
return Store.main.filter { match in
|
|
match.index == topPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id
|
|
}.sorted(by: \.index).first
|
|
}
|
|
|
|
func bottomPreviousRoundMatch() -> Match? {
|
|
guard let roundObject else { return nil }
|
|
return Store.main.filter { match in
|
|
match.index == bottomPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id
|
|
}.sorted(by: \.index).first
|
|
}
|
|
|
|
func upperBracketMatch(_ teamPosition: TeamPosition) -> Match? {
|
|
if teamPosition == .one {
|
|
return roundObject?.upperBracketTopMatch(ofMatchIndex: index)
|
|
} else {
|
|
return roundObject?.upperBracketBottomMatch(ofMatchIndex: index)
|
|
}
|
|
}
|
|
|
|
func previousMatch(_ teamPosition: TeamPosition) -> Match? {
|
|
if teamPosition == .one {
|
|
return topPreviousRoundMatch()
|
|
} else {
|
|
return bottomPreviousRoundMatch()
|
|
}
|
|
}
|
|
|
|
func upperMatches() -> [Match] {
|
|
guard let roundObject else { return [] }
|
|
return [roundObject.upperBracketTopMatch(ofMatchIndex: index), roundObject.upperBracketBottomMatch(ofMatchIndex: index)].compactMap({ $0 })
|
|
}
|
|
|
|
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 [] }
|
|
return Store.main.filter { match in
|
|
(match.index == topPreviousRoundMatchIndex() || match.index == bottomPreviousRoundMatchIndex())
|
|
&& match.round == roundObject.previousRound()?.id
|
|
}.sorted(by: \.index)
|
|
}
|
|
|
|
var matchFormat: MatchFormat {
|
|
get {
|
|
MatchFormat(rawValue: format) ?? .defaultFormatForMatchType(.groupStage)
|
|
}
|
|
set {
|
|
format = newValue.rawValue
|
|
}
|
|
}
|
|
|
|
func setWalkOut(_ teamPosition: TeamPosition) {
|
|
let teamScoreWalkout = teamScore(teamPosition) ?? TeamScore(match: id, teamRegistration: team(teamPosition)?.id)
|
|
teamScoreWalkout.walkOut = 0
|
|
let teamScoreWinning = teamScore(teamPosition.otherTeam) ?? TeamScore(match: id, teamRegistration: team(teamPosition.otherTeam)?.id)
|
|
teamScoreWinning.walkOut = nil
|
|
try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreWalkout, teamScoreWinning])
|
|
|
|
if endDate == nil {
|
|
endDate = Date()
|
|
}
|
|
|
|
winningTeamId = teamScoreWinning.teamRegistration
|
|
losingTeamId = teamScoreWalkout.teamRegistration
|
|
|
|
// matchDescriptor.match?.tournament?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false)
|
|
// matchDescriptor.match?.loserBracket?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false)
|
|
// matchDescriptor.match?.currentTournament?.removeField(matchDescriptor.match?.fieldIndex)
|
|
}
|
|
|
|
func setScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
|
|
updateScore(fromMatchDescriptor: matchDescriptor)
|
|
if endDate == nil {
|
|
endDate = Date()
|
|
}
|
|
// matchDescriptor.match?.tournament?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false)
|
|
// matchDescriptor.match?.loserBracket?.generateLoserBracket(for: matchDescriptor.match!.round, viewContext: viewContext, reset: false)
|
|
// matchDescriptor.match?.currentTournament?.removeField(matchDescriptor.match?.fieldIndex)
|
|
winningTeamId = team(matchDescriptor.winner)?.id
|
|
losingTeamId = team(matchDescriptor.winner.otherTeam)?.id
|
|
}
|
|
|
|
func updateScore(fromMatchDescriptor matchDescriptor: MatchDescriptor) {
|
|
let teamScoreOne = teamScore(.one) ?? TeamScore(match: id, teamRegistration: team(.one)?.id)
|
|
teamScoreOne.score = matchDescriptor.teamOneScores.joined(separator: ",")
|
|
let teamScoreTwo = teamScore(.two) ?? TeamScore(match: id, teamRegistration: team(.two)?.id)
|
|
teamScoreTwo.score = matchDescriptor.teamTwoScores.joined(separator: ",")
|
|
try? DataStore.shared.teamScores.addOrUpdate(contentOfs: [teamScoreOne, teamScoreTwo])
|
|
matchFormat = matchDescriptor.matchFormat
|
|
}
|
|
|
|
func validateMatch(fromStartDate: Date, toEndDate: Date, fieldSetup: MatchFieldSetup) {
|
|
if hasEnded() == false {
|
|
startDate = fromStartDate
|
|
|
|
switch fieldSetup {
|
|
case .random:
|
|
let courtName = availableCourts().randomElement()
|
|
court = courtName
|
|
case .field(let courtName):
|
|
court = courtName
|
|
}
|
|
|
|
} else {
|
|
startDate = fromStartDate
|
|
endDate = toEndDate
|
|
}
|
|
}
|
|
|
|
func courtCount() -> Int {
|
|
currentTournament()?.courtCount ?? 1
|
|
}
|
|
|
|
func courtIsAvailable(_ courtIndex: Int) -> Bool {
|
|
let courtUsed = currentTournament()?.courtUsed() ?? []
|
|
return courtUsed.contains(String(courtIndex)) == false
|
|
// return Set(availableCourts().map { String($0) }).subtracting(Set(courtUsed))
|
|
}
|
|
|
|
func courtIsPreferred(_ courtIndex: Int) -> Bool {
|
|
false
|
|
}
|
|
|
|
func availableCourts() -> [String] {
|
|
let courtUsed = currentTournament()?.courtUsed() ?? []
|
|
let availableCourts = Array(1...courtCount())
|
|
return Array(Set(availableCourts.map { String($0) }).subtracting(Set(courtUsed)))
|
|
}
|
|
|
|
func removeCourt() {
|
|
court = nil
|
|
}
|
|
|
|
func setCourt(_ courtIndex: Int) {
|
|
court = String(courtIndex)
|
|
}
|
|
|
|
func canBeStarted() -> Bool {
|
|
let teams = teams()
|
|
guard teams.count == 2 else { return false }
|
|
guard hasEnded() == false else { return false }
|
|
guard hasStarted() == false else { return false }
|
|
return teams.allSatisfy({ $0.canPlay() && isTeamPlaying($0) == false })
|
|
}
|
|
|
|
func isTeamPlaying(_ team: TeamRegistration) -> Bool {
|
|
if isGroupStage() {
|
|
let isPlaying = groupStageObject?.runningMatches().filter({ $0.teams().contains(team) }).isEmpty == false
|
|
return isPlaying
|
|
} else {
|
|
//todo
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isReady() -> Bool {
|
|
teams().count == 2
|
|
}
|
|
|
|
func isEmpty() -> Bool {
|
|
teams().isEmpty
|
|
}
|
|
|
|
func hasEnded() -> Bool {
|
|
endDate != nil || hasWalkoutTeam() || winningTeamId != nil
|
|
}
|
|
|
|
func isGroupStage() -> Bool {
|
|
groupStage != nil
|
|
}
|
|
|
|
func isBracket() -> Bool {
|
|
round != nil
|
|
}
|
|
|
|
func walkoutTeam() -> [TeamRegistration] {
|
|
scores().filter({ $0.walkOut != nil }).compactMap { $0.team }
|
|
}
|
|
|
|
func hasWalkoutTeam() -> Bool {
|
|
walkoutTeam().isEmpty == false
|
|
}
|
|
|
|
func currentTournament() -> Tournament? {
|
|
groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject()
|
|
}
|
|
|
|
func tournamentId() -> String? {
|
|
groupStageObject?.tournament ?? roundObject?.tournament
|
|
}
|
|
|
|
func scores() -> [TeamScore] {
|
|
Store.main.filter(isIncluded: { $0.match == id })
|
|
}
|
|
|
|
func teams() -> [TeamRegistration] {
|
|
if groupStage != nil {
|
|
return [groupStageProjectedTeam(.one), groupStageProjectedTeam(.two)].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
|
|
if endedSetsOne.count == 1 {
|
|
setDifference = endedSetsOne[0] - endedSetsTwo[0]
|
|
} else {
|
|
setDifference = endedSetsOne.filter { $0 == matchFormat.setFormat.scoreToWin }.count - endedSetsTwo.filter { $0 == matchFormat.setFormat.scoreToWin }.count
|
|
}
|
|
let zip = zip(endedSetsOne, endedSetsTwo)
|
|
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 }
|
|
return roundObject.roundProjectedTeam(team, inMatch: self)
|
|
}
|
|
|
|
func teamWon(_ team: TeamRegistration?) -> Bool {
|
|
guard let winningTeamId else { return false }
|
|
return winningTeamId == team?.id
|
|
}
|
|
|
|
func team(_ team: TeamPosition) -> TeamRegistration? {
|
|
if groupStage != nil {
|
|
switch team {
|
|
case .one:
|
|
return groupStageProjectedTeam(.one)
|
|
case .two:
|
|
return groupStageProjectedTeam(.two)
|
|
}
|
|
} else {
|
|
switch team {
|
|
case .one:
|
|
return roundProjectedTeam(.one)
|
|
case .two:
|
|
return roundProjectedTeam(.two)
|
|
}
|
|
}
|
|
}
|
|
|
|
func teamNames(_ team: TeamRegistration?) -> [String]? {
|
|
team?.players().map { $0.playerLabel() }
|
|
}
|
|
|
|
func teamWalkOut(_ team: TeamRegistration?) -> Bool {
|
|
teamScore(ofTeam: team)?.isWalkOut() == true
|
|
}
|
|
|
|
func teamScore(_ team: TeamPosition) -> TeamScore? {
|
|
teamScore(ofTeam: self.team(team))
|
|
}
|
|
|
|
func teamScore(ofTeam team: TeamRegistration?) -> TeamScore? {
|
|
scores().first(where: { $0.teamRegistration == team?.id })
|
|
}
|
|
|
|
func isRunning() -> Bool { // at least a match has started
|
|
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 Store.main.findById(round)
|
|
}
|
|
|
|
var groupStageObject: GroupStage? {
|
|
guard let groupStage else { return nil }
|
|
return Store.main.findById(groupStage)
|
|
}
|
|
|
|
var isLoserBracket: Bool {
|
|
roundObject?.loser != nil
|
|
}
|
|
|
|
var teamScores: [TeamScore] {
|
|
Store.main.filter { $0.match == self.id }
|
|
}
|
|
|
|
override func deleteDependencies() throws {
|
|
try Store.main.deleteDependencies(items: self.teamScores)
|
|
}
|
|
|
|
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 _servingTeamId = "servingTeamId"
|
|
case _winningTeamId = "winningTeamId"
|
|
case _losingTeamId = "losingTeamId"
|
|
case _broadcasted = "broadcasted"
|
|
case _name = "name"
|
|
case _order = "order"
|
|
case _disabled = "disabled"
|
|
}
|
|
}
|
|
|
|
enum MatchDateSetup: Hashable, Identifiable {
|
|
case inMinutes(Int)
|
|
case now
|
|
case customDate
|
|
|
|
var id: Int { hashValue }
|
|
}
|
|
|
|
|
|
enum MatchFieldSetup: Hashable, Identifiable {
|
|
case random
|
|
// case firstAvailable
|
|
case field(String)
|
|
|
|
var id: Int { hashValue }
|
|
}
|
|
|