|
|
|
@ -7,91 +7,71 @@ |
|
|
|
|
|
|
|
|
|
|
|
import Foundation |
|
|
|
import Foundation |
|
|
|
import LeStorage |
|
|
|
import LeStorage |
|
|
|
|
|
|
|
import SwiftUI |
|
|
|
struct GroupStageTimeMatch { |
|
|
|
|
|
|
|
let matchID: String |
|
|
|
@Observable |
|
|
|
let rotationIndex: Int |
|
|
|
class MatchScheduler : ModelObject, Storable { |
|
|
|
var courtIndex: Int |
|
|
|
static func resourceName() -> String { return "match-scheduler" } |
|
|
|
let groupIndex: Int |
|
|
|
static func requestsRequiresToken() -> Bool { true } |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private(set) var id: String = Store.randomId() |
|
|
|
struct TimeMatch { |
|
|
|
var tournament: String |
|
|
|
let matchID: String |
|
|
|
var timeDifferenceLimit: Int |
|
|
|
let rotationIndex: Int |
|
|
|
var loserBracketRotationDifference: Int |
|
|
|
var courtIndex: Int |
|
|
|
var upperBracketRotationDifference: Int |
|
|
|
var startDate: Date |
|
|
|
var accountUpperBracketBreakTime: Bool |
|
|
|
var durationLeft: Int //in minutes |
|
|
|
var accountLoserBracketBreakTime: Bool |
|
|
|
var minimumBreakTime: Int //in minutes |
|
|
|
var randomizeCourts: Bool |
|
|
|
|
|
|
|
var rotationDifferenceIsImportant: Bool |
|
|
|
func estimatedEndDate(includeBreakTime: Bool) -> Date { |
|
|
|
var shouldHandleUpperRoundSlice: Bool |
|
|
|
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) |
|
|
|
var shouldEndRoundBeforeStartingNext: Bool |
|
|
|
return startDate.addingTimeInterval(minutesToAdd * 60.0) |
|
|
|
|
|
|
|
} |
|
|
|
init(tournament: String, |
|
|
|
} |
|
|
|
timeDifferenceLimit: Int = 5, |
|
|
|
|
|
|
|
loserBracketRotationDifference: Int = 0, |
|
|
|
struct GroupStageMatchDispatcher { |
|
|
|
upperBracketRotationDifference: Int = 1, |
|
|
|
let timedMatches: [GroupStageTimeMatch] |
|
|
|
accountUpperBracketBreakTime: Bool = true, |
|
|
|
let freeCourtPerRotation: [Int: [Int]] |
|
|
|
accountLoserBracketBreakTime: Bool = false, |
|
|
|
let rotationCount: Int |
|
|
|
randomizeCourts: Bool = true, |
|
|
|
let groupLastRotation: [Int: Int] |
|
|
|
rotationDifferenceIsImportant: Bool = false, |
|
|
|
} |
|
|
|
shouldHandleUpperRoundSlice: Bool = true, |
|
|
|
|
|
|
|
shouldEndRoundBeforeStartingNext: Bool = true) { |
|
|
|
struct MatchDispatcher { |
|
|
|
self.tournament = tournament |
|
|
|
let timedMatches: [TimeMatch] |
|
|
|
self.timeDifferenceLimit = timeDifferenceLimit |
|
|
|
let freeCourtPerRotation: [Int: [Int]] |
|
|
|
self.loserBracketRotationDifference = loserBracketRotationDifference |
|
|
|
let rotationCount: Int |
|
|
|
self.upperBracketRotationDifference = upperBracketRotationDifference |
|
|
|
} |
|
|
|
self.accountUpperBracketBreakTime = accountUpperBracketBreakTime |
|
|
|
|
|
|
|
self.accountLoserBracketBreakTime = accountLoserBracketBreakTime |
|
|
|
extension Match { |
|
|
|
self.randomizeCourts = randomizeCourts |
|
|
|
func teamIds() -> [String] { |
|
|
|
self.rotationDifferenceIsImportant = rotationDifferenceIsImportant |
|
|
|
return teams().map { $0.id } |
|
|
|
self.shouldHandleUpperRoundSlice = shouldHandleUpperRoundSlice |
|
|
|
|
|
|
|
self.shouldEndRoundBeforeStartingNext = shouldEndRoundBeforeStartingNext |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func containsTeamId(_ id: String) -> Bool { |
|
|
|
enum CodingKeys: String, CodingKey { |
|
|
|
teamIds().contains(id) |
|
|
|
case _id = "id" |
|
|
|
|
|
|
|
case _tournament = "tournament" |
|
|
|
|
|
|
|
case _timeDifferenceLimit = "timeDifferenceLimit" |
|
|
|
|
|
|
|
case _loserBracketRotationDifference = "loserBracketRotationDifference" |
|
|
|
|
|
|
|
case _upperBracketRotationDifference = "upperBracketRotationDifference" |
|
|
|
|
|
|
|
case _accountUpperBracketBreakTime = "accountUpperBracketBreakTime" |
|
|
|
|
|
|
|
case _accountLoserBracketBreakTime = "accountLoserBracketBreakTime" |
|
|
|
|
|
|
|
case _randomizeCourts = "randomizeCourts" |
|
|
|
|
|
|
|
case _rotationDifferenceIsImportant = "rotationDifferenceIsImportant" |
|
|
|
|
|
|
|
case _shouldHandleUpperRoundSlice = "shouldHandleUpperRoundSlice" |
|
|
|
|
|
|
|
case _shouldEndRoundBeforeStartingNext = "shouldEndRoundBeforeStartingNext" |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
enum MatchSchedulerOption: Hashable { |
|
|
|
var courtsUnavailability: [DateInterval]? { |
|
|
|
case accountUpperBracketBreakTime |
|
|
|
tournamentObject()?.eventObject()?.courtsUnavailability |
|
|
|
case accountLoserBracketBreakTime |
|
|
|
|
|
|
|
case randomizeCourts |
|
|
|
|
|
|
|
case rotationDifferenceIsImportant |
|
|
|
|
|
|
|
case shouldHandleUpperRoundSlice |
|
|
|
|
|
|
|
case shouldEndRoundBeforeStartingNext |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MatchScheduler { |
|
|
|
|
|
|
|
static let shared = MatchScheduler() |
|
|
|
|
|
|
|
var additionalEstimationDuration : Int = 0 |
|
|
|
|
|
|
|
var options: Set<MatchSchedulerOption> = Set(arrayLiteral: .accountUpperBracketBreakTime) |
|
|
|
|
|
|
|
var timeDifferenceLimit: Double = 300.0 |
|
|
|
|
|
|
|
var loserBracketRotationDifference: Int = 0 |
|
|
|
|
|
|
|
var upperBracketRotationDifference: Int = 1 |
|
|
|
|
|
|
|
var courtsUnavailability: [DateInterval]? = nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func shouldEndRoundBeforeStartingNext() -> Bool { |
|
|
|
|
|
|
|
options.contains(.shouldEndRoundBeforeStartingNext) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func shouldHandleUpperRoundSlice() -> Bool { |
|
|
|
var additionalEstimationDuration : Int { |
|
|
|
options.contains(.shouldHandleUpperRoundSlice) |
|
|
|
tournamentObject()?.additionalEstimationDuration ?? 0 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func accountLoserBracketBreakTime() -> Bool { |
|
|
|
func tournamentObject() -> Tournament? { |
|
|
|
options.contains(.accountLoserBracketBreakTime) |
|
|
|
Store.main.findById(tournament) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func accountUpperBracketBreakTime() -> Bool { |
|
|
|
|
|
|
|
options.contains(.accountUpperBracketBreakTime) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func randomizeCourts() -> Bool { |
|
|
|
|
|
|
|
options.contains(.randomizeCourts) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func rotationDifferenceIsImportant() -> Bool { |
|
|
|
|
|
|
|
options.contains(.rotationDifferenceIsImportant) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@discardableResult |
|
|
|
@discardableResult |
|
|
|
@ -99,7 +79,6 @@ class MatchScheduler { |
|
|
|
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 |
|
|
|
let groupStageCourtCount = tournament.groupStageCourtCount ?? 1 |
|
|
|
let groupStages = tournament.groupStages() |
|
|
|
let groupStages = tournament.groupStages() |
|
|
|
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount |
|
|
|
let numberOfCourtsAvailablePerRotation: Int = tournament.courtCount |
|
|
|
courtsUnavailability = tournament.eventObject()?.courtsUnavailability |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let matches = groupStages.flatMap({ $0._matches() }) |
|
|
|
let matches = groupStages.flatMap({ $0._matches() }) |
|
|
|
matches.forEach({ |
|
|
|
matches.forEach({ |
|
|
|
@ -197,7 +176,7 @@ class MatchScheduler { |
|
|
|
var organizedSlots = [GroupStageTimeMatch]() |
|
|
|
var organizedSlots = [GroupStageTimeMatch]() |
|
|
|
for i in 0..<rotationIndex { |
|
|
|
for i in 0..<rotationIndex { |
|
|
|
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() |
|
|
|
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() |
|
|
|
let courts = randomizeCourts() ? courtsSorted.shuffled() : courtsSorted |
|
|
|
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted |
|
|
|
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex)) |
|
|
|
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.groupIndex), .keyPath(\.courtIndex)) |
|
|
|
|
|
|
|
|
|
|
|
for j in 0..<matches.count { |
|
|
|
for j in 0..<matches.count { |
|
|
|
@ -254,11 +233,11 @@ class MatchScheduler { |
|
|
|
|
|
|
|
|
|
|
|
var includeBreakTime = false |
|
|
|
var includeBreakTime = false |
|
|
|
|
|
|
|
|
|
|
|
if accountLoserBracketBreakTime() && roundObject.isLoserBracket() { |
|
|
|
if accountLoserBracketBreakTime && roundObject.isLoserBracket() { |
|
|
|
includeBreakTime = true |
|
|
|
includeBreakTime = true |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if accountUpperBracketBreakTime() && roundObject.isLoserBracket() == false { |
|
|
|
if accountUpperBracketBreakTime && roundObject.isLoserBracket() == false { |
|
|
|
includeBreakTime = true |
|
|
|
includeBreakTime = true |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -269,7 +248,7 @@ class MatchScheduler { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if targetedStartDate >= minimumPossibleEndDate { |
|
|
|
if targetedStartDate >= minimumPossibleEndDate { |
|
|
|
if rotationDifferenceIsImportant() { |
|
|
|
if rotationDifferenceIsImportant { |
|
|
|
return previousMatchIsInPreviousRotation |
|
|
|
return previousMatchIsInPreviousRotation |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
return true |
|
|
|
return true |
|
|
|
@ -375,14 +354,15 @@ class MatchScheduler { |
|
|
|
let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak) |
|
|
|
let differenceWithoutBreak = rotationStartDate.timeIntervalSince(previousEndDateNoBreak) |
|
|
|
print("difference w break", differenceWithBreak) |
|
|
|
print("difference w break", differenceWithBreak) |
|
|
|
print("difference w/o break", differenceWithoutBreak) |
|
|
|
print("difference w/o break", differenceWithoutBreak) |
|
|
|
|
|
|
|
let timeDifferenceLimitInSeconds = Double(timeDifferenceLimit * 60) |
|
|
|
var difference = differenceWithBreak |
|
|
|
var difference = differenceWithBreak |
|
|
|
if differenceWithBreak <= 0 { |
|
|
|
if differenceWithBreak <= 0 { |
|
|
|
difference = differenceWithoutBreak |
|
|
|
difference = differenceWithoutBreak |
|
|
|
} else if differenceWithBreak > timeDifferenceLimit && differenceWithoutBreak > timeDifferenceLimit { |
|
|
|
} else if differenceWithBreak > timeDifferenceLimitInSeconds && differenceWithoutBreak > timeDifferenceLimitInSeconds { |
|
|
|
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak) |
|
|
|
difference = noBreakAlreadyTested ? differenceWithBreak : max(differenceWithBreak, differenceWithoutBreak) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if difference > timeDifferenceLimit { |
|
|
|
if difference > timeDifferenceLimitInSeconds { |
|
|
|
courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index) |
|
|
|
courts.removeAll(where: { index in freeCourtPreviousRotation.contains(index) |
|
|
|
}) |
|
|
|
}) |
|
|
|
freeCourtPerRotation[rotationIndex] = courts |
|
|
|
freeCourtPerRotation[rotationIndex] = courts |
|
|
|
@ -399,7 +379,7 @@ class MatchScheduler { |
|
|
|
var organizedSlots = [TimeMatch]() |
|
|
|
var organizedSlots = [TimeMatch]() |
|
|
|
for i in 0..<rotationIndex { |
|
|
|
for i in 0..<rotationIndex { |
|
|
|
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() |
|
|
|
let courtsSorted = slots.filter({ $0.rotationIndex == i }).map { $0.courtIndex }.sorted() |
|
|
|
let courts = randomizeCourts() ? courtsSorted.shuffled() : courtsSorted |
|
|
|
let courts = randomizeCourts ? courtsSorted.shuffled() : courtsSorted |
|
|
|
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.courtIndex)) |
|
|
|
var matches = slots.filter({ $0.rotationIndex == i }).sorted(using: .keyPath(\.courtIndex)) |
|
|
|
|
|
|
|
|
|
|
|
for j in 0..<matches.count { |
|
|
|
for j in 0..<matches.count { |
|
|
|
@ -430,7 +410,7 @@ class MatchScheduler { |
|
|
|
|
|
|
|
|
|
|
|
let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0 |
|
|
|
let currentRotationSameRoundMatches = matchPerRound[roundObject.index] ?? 0 |
|
|
|
|
|
|
|
|
|
|
|
if shouldHandleUpperRoundSlice() { |
|
|
|
if shouldHandleUpperRoundSlice { |
|
|
|
let roundMatchesCount = roundObject.playedMatches().count |
|
|
|
let roundMatchesCount = roundObject.playedMatches().count |
|
|
|
print("shouldHandleUpperRoundSlice \(roundMatchesCount)") |
|
|
|
print("shouldHandleUpperRoundSlice \(roundMatchesCount)") |
|
|
|
if roundObject.parent == nil && roundMatchesCount > courts.count { |
|
|
|
if roundObject.parent == nil && roundMatchesCount > courts.count { |
|
|
|
@ -502,14 +482,13 @@ class MatchScheduler { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) { |
|
|
|
func updateBracketSchedule(tournament: Tournament, fromRoundId roundId: String?, fromMatchId matchId: String?, startDate: Date) { |
|
|
|
courtsUnavailability = tournament.eventObject()?.courtsUnavailability |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let upperRounds = tournament.rounds() |
|
|
|
let upperRounds = tournament.rounds() |
|
|
|
let allMatches = tournament.allMatches() |
|
|
|
let allMatches = tournament.allMatches() |
|
|
|
|
|
|
|
|
|
|
|
var rounds = [Round]() |
|
|
|
var rounds = [Round]() |
|
|
|
|
|
|
|
|
|
|
|
if shouldEndRoundBeforeStartingNext() { |
|
|
|
if shouldEndRoundBeforeStartingNext { |
|
|
|
rounds = upperRounds.flatMap { |
|
|
|
rounds = upperRounds.flatMap { |
|
|
|
[$0] + $0.loserRoundsAndChildren() |
|
|
|
[$0] + $0.loserRoundsAndChildren() |
|
|
|
} |
|
|
|
} |
|
|
|
@ -607,8 +586,51 @@ class MatchScheduler { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func updateSchedule(tournament: Tournament) { |
|
|
|
func updateSchedule(tournament: Tournament) { |
|
|
|
courtsUnavailability = tournament.eventObject()?.courtsUnavailability |
|
|
|
|
|
|
|
let lastDate = updateGroupStageSchedule(tournament: tournament) |
|
|
|
let lastDate = updateGroupStageSchedule(tournament: tournament) |
|
|
|
updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) |
|
|
|
updateBracketSchedule(tournament: tournament, fromRoundId: nil, fromMatchId: nil, startDate: lastDate) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct GroupStageTimeMatch { |
|
|
|
|
|
|
|
let matchID: String |
|
|
|
|
|
|
|
let rotationIndex: Int |
|
|
|
|
|
|
|
var courtIndex: Int |
|
|
|
|
|
|
|
let groupIndex: Int |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct TimeMatch { |
|
|
|
|
|
|
|
let matchID: String |
|
|
|
|
|
|
|
let rotationIndex: Int |
|
|
|
|
|
|
|
var courtIndex: Int |
|
|
|
|
|
|
|
var startDate: Date |
|
|
|
|
|
|
|
var durationLeft: Int //in minutes |
|
|
|
|
|
|
|
var minimumBreakTime: Int //in minutes |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func estimatedEndDate(includeBreakTime: Bool) -> Date { |
|
|
|
|
|
|
|
let minutesToAdd = Double(durationLeft + (includeBreakTime ? minimumBreakTime : 0)) |
|
|
|
|
|
|
|
return startDate.addingTimeInterval(minutesToAdd * 60.0) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct GroupStageMatchDispatcher { |
|
|
|
|
|
|
|
let timedMatches: [GroupStageTimeMatch] |
|
|
|
|
|
|
|
let freeCourtPerRotation: [Int: [Int]] |
|
|
|
|
|
|
|
let rotationCount: Int |
|
|
|
|
|
|
|
let groupLastRotation: [Int: Int] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
struct MatchDispatcher { |
|
|
|
|
|
|
|
let timedMatches: [TimeMatch] |
|
|
|
|
|
|
|
let freeCourtPerRotation: [Int: [Int]] |
|
|
|
|
|
|
|
let rotationCount: Int |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extension Match { |
|
|
|
|
|
|
|
func teamIds() -> [String] { |
|
|
|
|
|
|
|
return teams().map { $0.id } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func containsTeamId(_ id: String) -> Bool { |
|
|
|
|
|
|
|
teamIds().contains(id) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |