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

515 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 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 }
}