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/Round.swift

338 lines
12 KiB

//
// Round_v2.swift
// Padel Tournament
//
// Created by razmig on 10/03/2024.
//
import Foundation
import LeStorage
@Observable
class Round: ModelObject, Storable {
static func resourceName() -> String { "rounds" }
var id: String = Store.randomId()
var tournament: String
var index: Int
var loser: String?
var format: Int?
internal init(tournament: String, index: Int, loser: String? = nil, matchFormat: MatchFormat? = nil) {
self.tournament = tournament
self.index = index
self.loser = loser
self.format = matchFormat?.rawValue
}
var matchFormat: MatchFormat {
get {
MatchFormat(rawValue: format) ?? .defaultFormatForMatchType(.bracket)
}
set {
format = newValue.rawValue
}
}
func hasStarted() -> Bool {
playedMatches().anySatisfy({ $0.hasStarted() })
}
func hasEnded() -> Bool {
playedMatches().allSatisfy({ $0.hasEnded() })
}
func tournamentObject() -> Tournament? {
Store.main.findById(tournament)
}
private func _matches() -> [Match] {
Store.main.filter { $0.round == self.id }
}
func team(_ team: TeamData, inMatch match: Match) -> TeamRegistration? {
switch team {
case .one:
return roundProjectedTeam(.one, inMatch: match)
case .two:
return roundProjectedTeam(.two, inMatch: match)
}
}
func seed(_ team: TeamData, inMatchIndex matchIndex: Int) -> TeamRegistration? {
return Store.main.filter(isIncluded: {
$0.tournament == tournament && $0.bracketPosition != nil
}).first(where: {
($0.bracketPosition! / 2) == matchIndex
&& ($0.bracketPosition! % 2) == team.rawValue
})
}
func roundProjectedTeam(_ team: TeamData, inMatch match: Match) -> TeamRegistration? {
if isLoserBracket() == false, let seed = seed(team, inMatchIndex: match.index) {
return seed
}
switch team {
case .one:
if let loser = upperBracketTopMatch(ofMatchIndex: match.index)?.losingTeamId {
return Store.main.findById(loser)
} else if let previousMatch = topPreviousRoundMatch(ofMatch: match) {
if let teamId = previousMatch.winningTeamId {
return Store.main.findById(teamId)
} else if previousMatch.disabled {
return previousMatch.teams().first
}
}
case .two:
if let loser = upperBracketBottomMatch(ofMatchIndex: match.index)?.losingTeamId {
return Store.main.findById(loser)
} else if let previousMatch = bottomPreviousRoundMatch(ofMatch: match) {
if let teamId = previousMatch.winningTeamId {
return Store.main.findById(teamId)
} else if previousMatch.disabled {
return previousMatch.teams().first
}
}
}
return nil
}
// func isMatchBye(_ match: Match) -> Bool {
// return (upperBracketMatches(ofMatch: match) + previousRoundMatches(ofMatch: match)).anySatisfy({ $0.disabled })
// }
func upperBracketTopMatch(ofMatchIndex matchIndex: Int) -> Match? {
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketTopMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2) {
return upperBracketTopMatch
}
return nil
}
func upperBracketBottomMatch(ofMatchIndex matchIndex: Int) -> Match? {
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex)
if isLoserBracket(), previousRound() == nil, let parentRound = parentRound, let upperBracketBottomMatch = parentRound.getMatch(atMatchIndexInRound: indexInRound * 2 + 1) {
return upperBracketBottomMatch
}
return nil
}
func topPreviousRoundMatch(ofMatch match: Match) -> Match? {
guard let previousRound = previousRound() else { return nil }
return Store.main.filter {
$0.index == match.topPreviousRoundMatchIndex() && $0.round == previousRound.id
}.sorted(by: \.index).first
}
func bottomPreviousRoundMatch(ofMatch match: Match) -> Match? {
guard let previousRound = previousRound() else { return nil }
return Store.main.filter {
$0.index == match.bottomPreviousRoundMatchIndex() && $0.round == previousRound.id
}.sorted(by: \.index).first
}
func getMatch(atMatchIndexInRound matchIndexInRound: Int) -> Match? {
_matches().first(where: {
let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index)
return index == matchIndexInRound
})
}
func playedMatches() -> [Match] {
if loser == nil {
Store.main.filter { $0.round == self.id && $0.disabled == false }
} else {
Store.main.filter { $0.round == self.id }
}
}
func previousRound() -> Round? {
Store.main.filter(isIncluded: { $0.tournament == tournament && $0.loser == loser && $0.index == index + 1 }).first
}
func nextRound() -> Round? {
Store.main.filter(isIncluded: { $0.tournament == tournament && $0.loser == loser && $0.index == index - 1 }).first
}
func loserRounds(forRoundIndex roundIndex: Int) -> [Round] {
return loserRoundsAndChildren().filter({ $0.index == roundIndex }).sorted(by: \.cumulativeMatchCount)
}
func isDisabled() -> Bool {
_matches().allSatisfy({ $0.disabled })
}
func getActiveLoserRound() -> Round? {
let rounds = loserRounds()
return rounds.filter({ $0.hasStarted() && $0.hasEnded() == false && $0.isDisabled() == false }).sorted(by: \.index).reversed().first ?? rounds.first(where: { $0.isDisabled() == false })
}
func enableRound() {
let _matches = _matches()
_matches.forEach { match in
match.disabled = false
match.losingTeamId = nil
match.winningTeamId = nil
match.endDate = nil
match.court = nil
match.servingTeamId = nil
try? DataStore.shared.teamScores.delete(contentOfs: match.teamScores)
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
}
func disableLoserRound(_ disable: Bool) {
let _matches = _matches()
_matches.forEach { match in
match.disabled = match.topPreviousRoundMatch()?.disabled == disable || match.bottomPreviousRoundMatch()?.disabled == disable
}
try? DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
loserRounds().forEach { round in
round.disableLoserRound(disable)
}
}
var cumulativeMatchCount: Int {
var totalMatches = playedMatches().count
if let parent = parentRound {
totalMatches += parent.cumulativeMatchCount
}
return totalMatches
}
func initialRound() -> Round? {
if let parentRound {
return parentRound.initialRound()
} else {
return self
}
}
func roundTitle(_ displayStyle: DisplayStyle = .wide) -> String {
if let parentRound, let initialRound = parentRound.initialRound() {
let parentMatchCount = parentRound.cumulativeMatchCount - initialRound.playedMatches().count
print("initialRound", initialRound.roundTitle())
if let initialRoundNextRound = initialRound.nextRound()?.playedMatches() {
return SeedInterval(first: parentMatchCount + initialRoundNextRound.count * 2 + 1, last: parentMatchCount + initialRoundNextRound.count * 2 + (previousRound() ?? parentRound).playedMatches().count).localizedLabel(displayStyle)
}
}
return RoundRule.roundName(fromRoundIndex: index)
}
func roundStatus() -> String {
if hasStarted() && hasEnded() == false {
return "en cours"
} else {
return "à démarrer"
}
}
//
// func indexOfMatch(_ match: Match) -> Int? {
// playedMatches().firstIndex(where: { $0.id == match.id })
// }
func previousRoundMatches(ofMatch match: Match) -> [Match] {
return Store.main.filter {
$0.round == previousRound()?.id && ($0.index == match.topPreviousRoundMatchIndex() || $0.index == match.bottomPreviousRoundMatchIndex())
}
}
func upperBracketMatches(ofMatch match: Match) -> [Match] {
let indexInRound = RoundRule.matchIndexWithinRound(fromMatchIndex: match.index)
if isLoserBracket(), previousRound() == nil, let parentRound {
let upperBracketMatches = parentRound._matches().filter({
let index = RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index)
return index == indexInRound * 2 || index == indexInRound * 2 + 1
})
return upperBracketMatches
}
return []
}
func loserRounds() -> [Round] {
return Store.main.filter(isIncluded: { $0.loser == id }).sorted(by: \.index).reversed()
}
func loserRoundsAndChildren() -> [Round] {
let loserRounds = loserRounds()
return loserRounds + loserRounds.flatMap({ $0.loserRoundsAndChildren() })
}
func isLoserBracket() -> Bool {
loser != nil
}
func buildLoserBracket() {
guard loserRounds().isEmpty else { return }
let currentRoundMatchCount = RoundRule.numberOfMatches(forRoundIndex: index)
guard currentRoundMatchCount > 1 else { return }
let roundCount = RoundRule.numberOfRounds(forTeams: currentRoundMatchCount)
let loserBracketMatchFormat = tournamentObject()?.loserBracketMatchFormat
let rounds = (0..<roundCount).map { //index 0 is the final
let round = Round(tournament: tournament, index: $0, matchFormat: loserBracketMatchFormat)
round.loser = id //parent
return round
}
try? DataStore.shared.rounds.addOrUpdate(contentOfs: rounds)
let matchCount = RoundRule.numberOfMatches(forTeams: currentRoundMatchCount)
let matches = (0..<matchCount).map { //0 is final match
let roundIndex = RoundRule.roundIndex(fromMatchIndex: $0)
let round = rounds[roundIndex]
return Match(round: round.id, index: $0, matchFormat: loserBracketMatchFormat)
}
print(matches.map {
(RoundRule.roundName(fromMatchIndex: $0.index), RoundRule.matchIndexWithinRound(fromMatchIndex: $0.index))
})
try? DataStore.shared.matches.addOrUpdate(contentOfs: matches)
loserRounds().forEach { round in
round.buildLoserBracket()
}
}
var parentRound: Round? {
guard let parentRound = loser else { return nil }
return Store.main.findById(parentRound)
}
override func deleteDependencies() throws {
try Store.main.deleteDependencies(items: _matches())
try Store.main.deleteDependencies(items: loserRoundsAndChildren())
}
enum CodingKeys: String, CodingKey {
case _id = "id"
case _tournament = "tournament"
case _index = "index"
case _loser = "loser"
case _format = "format"
}
}
extension Round: Selectable {
func selectionLabel() -> String {
if let parentRound {
return "Tour #\(parentRound.loserRounds().count - index)"
} else {
return roundTitle()
}
}
func badgeValue() -> Int? {
if let parentRound {
return parentRound.loserRounds(forRoundIndex: index).flatMap { $0.playedMatches() }.filter({ $0.isRunning() }).count
} else {
return playedMatches().filter({ $0.isRunning() }).count
}
}
}