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.
394 lines
14 KiB
394 lines
14 KiB
//
|
|
// GroupStage.swift
|
|
// Padel Tournament
|
|
//
|
|
// Created by razmig on 10/03/2024.
|
|
//
|
|
|
|
import Foundation
|
|
import LeStorage
|
|
import Algorithms
|
|
import SwiftUI
|
|
|
|
@Observable
|
|
class GroupStage: ModelObject, Storable {
|
|
static func resourceName() -> String { "group-stages" }
|
|
static func tokenExemptedMethods() -> [HTTPMethod] { return [] }
|
|
|
|
var id: String = Store.randomId()
|
|
var tournament: String
|
|
var index: Int
|
|
var size: Int
|
|
private var format: MatchFormat?
|
|
var startDate: Date?
|
|
var name: String?
|
|
|
|
var matchFormat: MatchFormat {
|
|
get {
|
|
format ?? .defaultFormatForMatchType(.groupStage)
|
|
}
|
|
set {
|
|
format = newValue
|
|
}
|
|
}
|
|
|
|
internal init(tournament: String, index: Int, size: Int, matchFormat: MatchFormat? = nil, startDate: Date? = nil, name: String? = nil) {
|
|
self.tournament = tournament
|
|
self.index = index
|
|
self.size = size
|
|
self.format = matchFormat
|
|
self.startDate = startDate
|
|
self.name = name
|
|
}
|
|
|
|
// MARK: - Computed dependencies
|
|
|
|
func _matches() -> [Match] {
|
|
Store.main.filter { $0.groupStage == self.id }
|
|
}
|
|
|
|
func tournamentObject() -> Tournament? {
|
|
Store.main.findById(self.tournament)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
func teamAt(groupStagePosition: Int) -> TeamRegistration? {
|
|
teams().first(where: { $0.groupStagePosition == groupStagePosition })
|
|
}
|
|
|
|
func groupStageTitle(_ displayStyle: DisplayStyle = .wide) -> String {
|
|
if let name { return name }
|
|
switch displayStyle {
|
|
case .wide:
|
|
return "Poule \(index + 1)"
|
|
case .short:
|
|
return "#\(index + 1)"
|
|
}
|
|
}
|
|
|
|
func isRunning() -> Bool { // at least a match has started
|
|
_matches().anySatisfy({ $0.isRunning() })
|
|
}
|
|
|
|
func hasStarted() -> Bool { // meaning at least one match is over
|
|
_matches().filter { $0.hasEnded() }.isEmpty == false
|
|
}
|
|
|
|
func hasEnded() -> Bool {
|
|
guard teams().count == size else { return false }
|
|
let _matches = _matches()
|
|
if _matches.isEmpty { return false }
|
|
return _matches.allSatisfy { $0.hasEnded() }
|
|
}
|
|
|
|
func buildMatches() {
|
|
_removeMatches()
|
|
|
|
var _matches = [Match]()
|
|
var _teamScores = [TeamScore]()
|
|
|
|
for i in 0..<_numberOfMatchesToBuild() {
|
|
let newMatch = Match(groupStage: id, index: i, matchFormat: matchFormat, name: localizedMatchUpLabel(for: i))
|
|
_teamScores.append(contentsOf: newMatch.createTeamScores())
|
|
_matches.append(newMatch)
|
|
}
|
|
|
|
do {
|
|
try DataStore.shared.matches.addOrUpdate(contentOfs: _matches)
|
|
try DataStore.shared.teamScores.addOrUpdate(contentOfs: _teamScores)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
func playedMatches() -> [Match] {
|
|
let ordered = _matches()
|
|
if ordered.isEmpty == false && ordered.count == _matchOrder().count {
|
|
return _matchOrder().map {
|
|
ordered[$0]
|
|
}
|
|
} else {
|
|
return ordered
|
|
}
|
|
}
|
|
|
|
func updateGroupStageState() {
|
|
if hasEnded(), let tournament = tournamentObject() {
|
|
do {
|
|
let teams = teams(true)
|
|
for (index, team) in teams.enumerated() {
|
|
team.qualified = index < tournament.qualifiedPerGroupStage
|
|
if team.bracketPosition != nil && team.qualified == false {
|
|
tournamentObject()?.resetTeamScores(in: team.bracketPosition)
|
|
team.bracketPosition = nil
|
|
}
|
|
}
|
|
try DataStore.shared.teamRegistrations.addOrUpdate(contentOfs: teams)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func scoreLabel(forGroupStagePosition groupStagePosition: Int, score: TeamGroupStageScore? = nil) -> (wins: String, losses: String, setsDifference: String?, gamesDifference: String?)? {
|
|
if let scoreData = (score ?? _score(forGroupStagePosition: groupStagePosition, nilIfEmpty: true)) {
|
|
let hideSetDifference = matchFormat.setsToWin == 1
|
|
let setDifference = scoreData.setDifference.formatted(.number.sign(strategy: .always(includingZero: false)))
|
|
let gameDifference = scoreData.gameDifference.formatted(.number.sign(strategy: .always(includingZero: false)))
|
|
return (wins: scoreData.wins.formatted(), losses: scoreData.loses.formatted(), setsDifference: hideSetDifference ? nil : setDifference, gamesDifference: gameDifference)
|
|
// return "\(scoreData.wins)/\(scoreData.loses) " + differenceAsString
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func _score(forGroupStagePosition groupStagePosition: Int, nilIfEmpty: Bool = false) -> TeamGroupStageScore? {
|
|
guard let team = teamAt(groupStagePosition: groupStagePosition) else { return nil }
|
|
let matches = matches(forGroupStagePosition: groupStagePosition).filter({ $0.hasEnded() })
|
|
if matches.isEmpty && nilIfEmpty { return nil }
|
|
let wins = matches.filter { $0.winningTeamId == team.id }.count
|
|
let loses = matches.filter { $0.losingTeamId == team.id }.count
|
|
let differences = matches.compactMap { $0.scoreDifference(groupStagePosition) }
|
|
let setDifference = differences.map { $0.set }.reduce(0,+)
|
|
let gameDifference = differences.map { $0.game }.reduce(0,+)
|
|
return (team, wins, loses, setDifference, gameDifference)
|
|
}
|
|
|
|
func matches(forGroupStagePosition groupStagePosition: Int) -> [Match] {
|
|
let combos = Array((0..<size).combinations(ofCount: 2))
|
|
var matchIndexes = [Int]()
|
|
for (index, combo) in combos.enumerated() {
|
|
if combo.contains(groupStagePosition) { //team is playing
|
|
matchIndexes.append(index)
|
|
}
|
|
}
|
|
return _matches().filter { matchIndexes.contains($0.index) }
|
|
}
|
|
|
|
func matchPlayed(by groupStagePosition: Int, againstPosition: Int) -> Match? {
|
|
if groupStagePosition == againstPosition { return nil }
|
|
let combos = Array((0..<size).combinations(ofCount: 2))
|
|
var matchIndexes = [Int]()
|
|
for (index, combo) in combos.enumerated() {
|
|
if combo.contains(groupStagePosition) && combo.contains(againstPosition) { //teams are playing
|
|
matchIndexes.append(index)
|
|
}
|
|
}
|
|
return _matches().first(where: { matchIndexes.contains($0.index) })
|
|
}
|
|
|
|
func availableToStart(playedMatches: [Match], in runningMatches: [Match]) -> [Match] {
|
|
return playedMatches.filter({ $0.canBeStarted(inMatches: runningMatches) && $0.isRunning() == false })
|
|
}
|
|
|
|
func runningMatches(playedMatches: [Match]) -> [Match] {
|
|
playedMatches.filter({ $0.isRunning() }).sorted(by: \.computedStartDateForSorting)
|
|
}
|
|
|
|
func readyMatches(playedMatches: [Match]) -> [Match] {
|
|
playedMatches.filter({ $0.isReady() && $0.isRunning() == false && $0.hasEnded() == false })
|
|
}
|
|
|
|
func finishedMatches(playedMatches: [Match]) -> [Match] {
|
|
playedMatches.filter({ $0.hasEnded() }).sorted(by: \.computedEndDateForSorting).reversed()
|
|
}
|
|
|
|
private func _matchOrder() -> [Int] {
|
|
switch size {
|
|
case 3:
|
|
return [1, 2, 0]
|
|
case 4:
|
|
return [2, 3, 1, 4, 5, 0]
|
|
case 5:
|
|
return [5, 8, 0, 7, 3, 4, 2, 6, 1, 9]
|
|
// return [3, 5, 8, 2, 6, 7, 1, 9, 4, 0]
|
|
case 6:
|
|
return [1, 7, 13, 11, 3, 6, 10, 2, 8, 12, 5, 4, 9, 14, 0]
|
|
//return [4, 7, 9, 3, 6, 11, 2, 8, 10, 1, 13, 5, 12, 14, 0]
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
|
|
private func _matchUp(for matchIndex: Int) -> [Int] {
|
|
Array((0..<size).combinations(ofCount: 2))[matchIndex]
|
|
}
|
|
|
|
func localizedMatchUpLabel(for matchIndex: Int) -> String {
|
|
let matchUp = _matchUp(for: matchIndex)
|
|
if let index = matchUp.first, let index2 = matchUp.last {
|
|
return "#\(index + 1) contre #\(index2 + 1)"
|
|
} else {
|
|
return "--"
|
|
}
|
|
}
|
|
|
|
func team(teamPosition team: TeamPosition, inMatchIndex matchIndex: Int) -> TeamRegistration? {
|
|
let _teams = _teams(for: matchIndex)
|
|
switch team {
|
|
case .one:
|
|
return _teams.first!
|
|
case .two:
|
|
return _teams.last!
|
|
}
|
|
}
|
|
|
|
private func _teams(for matchIndex: Int) -> [TeamRegistration?] {
|
|
let combinations = Array(0..<size).combinations(ofCount: 2).map {$0}
|
|
return combinations[safe: matchIndex]?.map { teamAt(groupStagePosition: $0) } ?? []
|
|
}
|
|
|
|
private func _removeMatches() {
|
|
do {
|
|
try DataStore.shared.matches.delete(contentOfs: _matches())
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
private func _numberOfMatchesToBuild() -> Int {
|
|
(size * (size - 1)) / 2
|
|
}
|
|
|
|
func unsortedPlayers() -> [PlayerRegistration] {
|
|
unsortedTeams().flatMap({ $0.unsortedPlayers() })
|
|
}
|
|
|
|
fileprivate typealias TeamScoreAreInIncreasingOrder = (TeamGroupStageScore, TeamGroupStageScore) -> Bool
|
|
|
|
typealias TeamGroupStageScore = (team: TeamRegistration, wins: Int, loses: Int, setDifference: Int, gameDifference: Int)
|
|
|
|
fileprivate func _headToHead(_ teamPosition: TeamRegistration, _ otherTeam: TeamRegistration) -> Bool {
|
|
let indexes = [teamPosition, otherTeam].compactMap({ $0.groupStagePosition }).sorted()
|
|
let combos = Array((0..<size).combinations(ofCount: 2))
|
|
if let matchIndex = combos.firstIndex(of: indexes), let match = _matches().first(where: { $0.index == matchIndex }) {
|
|
return teamPosition.id == match.losingTeamId
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func unsortedTeams() -> [TeamRegistration] {
|
|
Store.main.filter { $0.groupStage == self.id && $0.groupStagePosition != nil }
|
|
}
|
|
|
|
func teams(_ sortedByScore: Bool = false, scores: [TeamGroupStageScore]? = nil) -> [TeamRegistration] {
|
|
if sortedByScore {
|
|
return unsortedTeams().compactMap({ team in
|
|
scores?.first(where: { $0.team.id == team.id }) ?? _score(forGroupStagePosition: team.groupStagePosition!)
|
|
}).sorted { (lhs, rhs) in
|
|
let predicates: [TeamScoreAreInIncreasingOrder] = [
|
|
{ $0.wins < $1.wins },
|
|
{ $0.setDifference < $1.setDifference },
|
|
{ $0.gameDifference < $1.gameDifference},
|
|
{ self._headToHead($0.team, $1.team) },
|
|
{ $0.team.groupStagePosition! > $1.team.groupStagePosition! }
|
|
]
|
|
|
|
for predicate in predicates {
|
|
if !predicate(lhs, rhs) && !predicate(rhs, lhs) {
|
|
continue
|
|
}
|
|
|
|
return predicate(lhs, rhs)
|
|
}
|
|
|
|
return false
|
|
}.map({ $0.team }).reversed()
|
|
} else {
|
|
return unsortedTeams().sorted(by: \TeamRegistration.groupStagePosition!)
|
|
}
|
|
}
|
|
|
|
func updateMatchFormat(_ updatedMatchFormat: MatchFormat) {
|
|
self.matchFormat = updatedMatchFormat
|
|
self.updateAllMatchesFormat()
|
|
}
|
|
|
|
func updateAllMatchesFormat() {
|
|
let playedMatches = playedMatches()
|
|
playedMatches.forEach { match in
|
|
match.matchFormat = matchFormat
|
|
}
|
|
do {
|
|
try DataStore.shared.matches.addOrUpdate(contentOfs: playedMatches)
|
|
} catch {
|
|
Logger.error(error)
|
|
}
|
|
}
|
|
|
|
override func deleteDependencies() throws {
|
|
try Store.main.deleteDependencies(items: self._matches())
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(id, forKey: ._id)
|
|
try container.encode(tournament, forKey: ._tournament)
|
|
try container.encode(index, forKey: ._index)
|
|
try container.encode(size, forKey: ._size)
|
|
|
|
if let format = format {
|
|
try container.encode(format, forKey: ._format)
|
|
} else {
|
|
try container.encodeNil(forKey: ._format)
|
|
}
|
|
|
|
if let startDate = startDate {
|
|
try container.encode(startDate, forKey: ._startDate)
|
|
} else {
|
|
try container.encodeNil(forKey: ._startDate)
|
|
}
|
|
|
|
if let name = name {
|
|
try container.encode(name, forKey: ._name)
|
|
} else {
|
|
try container.encodeNil(forKey: ._name)
|
|
}
|
|
}
|
|
|
|
func insertOnServer() throws {
|
|
try DataStore.shared.groupStages.writeChangeAndInsertOnServer(instance: self)
|
|
for match in self._matches() {
|
|
try match.insertOnServer()
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension GroupStage {
|
|
enum CodingKeys: String, CodingKey {
|
|
case _id = "id"
|
|
case _tournament = "tournament"
|
|
case _index = "index"
|
|
case _size = "size"
|
|
case _format = "format"
|
|
case _startDate = "startDate"
|
|
case _name = "name"
|
|
}
|
|
}
|
|
|
|
extension GroupStage: Selectable {
|
|
func selectionLabel() -> String {
|
|
groupStageTitle()
|
|
}
|
|
|
|
func badgeValue() -> Int? {
|
|
if teams().count < size { return nil }
|
|
return runningMatches(playedMatches: _matches()).count
|
|
}
|
|
|
|
func badgeValueColor() -> Color? {
|
|
return nil
|
|
}
|
|
|
|
func badgeImage() -> Badge? {
|
|
if teams().count < size {
|
|
return .xmark
|
|
} else {
|
|
return hasEnded() ? .checkmark : nil
|
|
}
|
|
}
|
|
}
|
|
|