@ -6,19 +6,40 @@
//
import Foundation
import LeStorage
struct TimeMatch {
struct GroupStage TimeMatch {
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 // i n m i n u t e s
var minimumBreakTime : Int // i n m i n u t e s
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
let groupLastRotation : [ Int : Int ]
}
extension Match {
@ -31,12 +52,44 @@ extension Match {
}
}
enum MatchSchedulerOption : Hashable {
case accountUpperBracketBreakTime
case accountLoserBracketBreakTime
case randomizeCourts
case rotationDifferenceIsImportant
case shouldHandleUpperRoundSlice
}
class MatchScheduler {
static let shared = MatchScheduler ( )
var options : Set < MatchSchedulerOption > = Set ( arrayLiteral : . accountUpperBracketBreakTime )
var timeDifferenceLimit : Double = 300.0
var loserBracketRotationDifference : Int = 0
var upperBracketRotationDifference : Int = 1
func groupStageDispatcher ( numberOfCourtsAvailablePerRotation : Int , groupStages : [ GroupStage ] , startingDate : Date ? , randomizeCourts : Bool ) -> MatchDispatcher {
func shouldHandleUpperRoundSlice ( ) -> Bool {
options . contains ( . shouldHandleUpperRoundSlice )
}
func accountLoserBracketBreakTime ( ) -> Bool {
options . contains ( . accountLoserBracketBreakTime )
}
func accountUpperBracketBreakTime ( ) -> Bool {
options . contains ( . accountUpperBracketBreakTime )
}
func randomizeCourts ( ) -> Bool {
options . contains ( . randomizeCourts )
}
func rotationDifferenceIsImportant ( ) -> Bool {
options . contains ( . rotationDifferenceIsImportant )
}
func groupStageDispatcher ( numberOfCourtsAvailablePerRotation : Int , groupStages : [ GroupStage ] , startingDate : Date ? ) -> GroupStageMatchDispatcher {
let _groupStages = groupStages . filter { startingDate = = nil || $0 . startDate = = startingDate }
let _groupStages = groupStages
// G e t t h e m a x i m u m c o u n t o f m a t c h e s i n a n y g r o u p
let maxMatchesCount = _groupStages . map { $0 . _matches ( ) . count } . max ( ) ? ? 0
@ -50,7 +103,7 @@ class MatchScheduler {
}
}
var slots = [ TimeMatch ] ( )
var slots = [ GroupStage TimeMatch] ( )
var availableMatchs = flattenedMatches
var rotationIndex = 0
var teamsPerRotation = [ Int : [ String ] ] ( )
@ -81,7 +134,8 @@ class MatchScheduler {
if let first = rotationMatches . first ( where : { match in
teamsPerRotation [ rotationIndex ] ! . allSatisfy ( { match . containsTeamId ( $0 ) = = false } ) = = true
} ) {
slots . append ( TimeMatch ( matchID : first . id , rotationIndex : rotationIndex , courtIndex : courtIndex , groupIndex : first . groupStageObject ! . index ) )
let timeMatch = GroupStageTimeMatch ( matchID : first . id , rotationIndex : rotationIndex , courtIndex : courtIndex , groupIndex : first . groupStageObject ! . index )
slots . append ( timeMatch )
teamsPerRotation [ rotationIndex ] ! . append ( contentsOf : first . teamIds ( ) )
rotationMatches . removeAll ( where : { $0 . id = = first . id } )
availableMatchs . removeAll ( where : { $0 . id = = first . id } )
@ -96,10 +150,10 @@ class MatchScheduler {
rotationIndex += 1
}
var organizedSlots = [ TimeMatch ] ( )
var organizedSlots = [ GroupStage TimeMatch] ( )
for i in 0. . < rotationIndex {
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 ) )
for j in 0. . < matches . count {
@ -109,11 +163,19 @@ class MatchScheduler {
}
return MatchDispatcher ( timedMatches : organizedSlots , freeCourtPerRotation : freeCourtPerRotation , rotationCount : rotationIndex , groupLastRotation : groupLastRotation )
return GroupStageMatchDispatcher ( timedMatches : organizedSlots , freeCourtPerRotation : freeCourtPerRotation , rotationCount : rotationIndex , groupLastRotation : groupLastRotation )
}
func rotationDifference ( loserBracket : Bool ) -> Int {
if loserBracket {
return loserBracketRotationDifference
} else {
return upperBracketRotationDifference
}
}
func roundMatchCanBePlayed ( _ match : Match , roundObject : Round , slots : [ TimeMatch ] , rotationIndex : Int ) -> Bool {
print ( roundObject . roundTitle ( ) , match . matchTitle ( ) )
func roundMatchCanBePlayed ( _ match : Match , roundObject : Round , slots : [ TimeMatch ] , rotationIndex : Int , targetedStartDate : Date , minimumTargetedEndDate : inout Date ) -> Bool {
// p r i n t ( r o u n d O b j e c t . r o u n d T i t l e ( ) , m a t c h . m a t c h T i t l e ( ) )
let previousMatches = roundObject . precedentMatches ( ofMatch : match )
if previousMatches . isEmpty { return true }
@ -135,47 +197,199 @@ class MatchScheduler {
return false
}
let previousMatchIsInPreviousRotation = previousMatchSlots . allSatisfy ( { $0 . rotationIndex + ( roundObject . loser = = nil ? 1 : 0 ) < rotationIndex } )
var includeBreakTime = false
if accountLoserBracketBreakTime ( ) && roundObject . isLoserBracket ( ) {
includeBreakTime = true
}
if accountUpperBracketBreakTime ( ) && roundObject . isLoserBracket ( ) = = false {
includeBreakTime = true
}
let previousMatchIsInPreviousRotation = previousMatchSlots . allSatisfy ( { $0 . rotationIndex + rotationDifference ( loserBracket : roundObject . isLoserBracket ( ) ) < rotationIndex } )
guard let minimumPossibleEndDate = previousMatchSlots . map ( { $0 . estimatedEndDate ( includeBreakTime : includeBreakTime ) } ) . max ( ) else {
return previousMatchIsInPreviousRotation
}
if targetedStartDate >= minimumPossibleEndDate {
if rotationDifferenceIsImportant ( ) {
return previousMatchIsInPreviousRotation
} else {
return true
}
} else {
if targetedStartDate = = minimumTargetedEndDate {
minimumTargetedEndDate = minimumPossibleEndDate
} else {
minimumTargetedEndDate = min ( minimumPossibleEndDate , minimumTargetedEndDate )
}
return false
}
}
func getNextStartDate ( fromPreviousRotationSlots slots : [ TimeMatch ] , includeBreakTime : Bool ) -> Date ? {
slots . map { $0 . estimatedEndDate ( includeBreakTime : includeBreakTime ) } . min ( )
}
func getNextEarliestAvailableDate ( from slots : [ TimeMatch ] ) -> [ ( Int , Date ) ] {
let byCourt = Dictionary ( grouping : slots , by : { $0 . courtIndex } )
return ( byCourt . keys . flatMap { courtIndex in
let matchesByCourt = byCourt [ courtIndex ] ? . sorted ( by : \ . startDate )
let lastMatch = matchesByCourt ? . last
var results = [ ( Int , Date ) ] ( )
if let courtFreeDate = lastMatch ? . estimatedEndDate ( includeBreakTime : false ) {
results . append ( ( courtIndex , courtFreeDate ) )
}
return results
}
)
}
func getAvailableCourts ( from matches : [ Match ] ) -> [ ( String , Date ) ] {
let validMatches = matches . filter ( { $0 . court != nil && $0 . startDate != nil } )
let byCourt = Dictionary ( grouping : validMatches , by : { $0 . court ! } )
return ( byCourt . keys . flatMap { court in
let matchesByCourt = byCourt [ court ] ? . sorted ( by : \ . startDate ! )
let lastMatch = matchesByCourt ? . last
var results = [ ( String , Date ) ] ( )
if let courtFreeDate = lastMatch ? . estimatedEndDate ( ) {
results . append ( ( court , courtFreeDate ) )
}
return results
}
)
}
func roundDispatcher ( numberOfCourtsAvailablePerRotation : Int , flattenedMatches : [ Match ] , randomizeCourts : Bool , initialOccupiedCourt : Int = 0 ) -> MatchDispatcher {
func roundDispatcher ( numberOfCourtsAvailablePerRotation : Int , flattenedMatches : [ Match ] , dispatcherStartDate : Date , initialCourts : [ Int ] ? ) -> MatchDispatcher {
var slots = [ TimeMatch ] ( )
var availableMatchs = flattenedMatches
var _startDate : Date ?
var rotationIndex = 0
var availableMatchs = flattenedMatches . filter ( { $0 . startDate = = nil } )
flattenedMatches . filter { $0 . startDate != nil } . sorted ( by : \ . startDate ! ) . forEach { match in
if _startDate = = nil {
_startDate = match . startDate
} else if match . startDate ! > _startDate ! {
_startDate = match . startDate
rotationIndex += 1
}
let timeMatch = TimeMatch ( matchID : match . id , rotationIndex : rotationIndex , courtIndex : match . courtIndex ( ) ? ? 0 , startDate : match . startDate ! , durationLeft : match . matchFormat . estimatedDuration , minimumBreakTime : match . matchFormat . breakTime . breakTime )
slots . append ( timeMatch )
}
if slots . isEmpty = = false {
rotationIndex += 1
}
var freeCourtPerRotation = [ Int : [ Int ] ] ( )
var groupLastRotation = [ Int : Int ] ( )
while slots . count < flattenedMatches . count {
let availableCourt = numberOfCourtsAvailablePerRotation
var courts = initialCourts ? ? ( 0. . < availableCourt ) . map { $0 }
var shouldStartAtDispatcherDate = rotationIndex > 0
while availableMatchs . count > 0 {
freeCourtPerRotation [ rotationIndex ] = [ ]
var matchPerRound = [ Int : Int ] ( )
var availableCourt = numberOfCourtsAvailablePerRotation
if rotationIndex = = 0 {
availableCourt = availableCourt - initialOccupiedCourt
let previousRotationSlots = slots . filter ( { $0 . rotationIndex = = rotationIndex - 1 } )
var rotationStartDate : Date = getNextStartDate ( fromPreviousRotationSlots : previousRotationSlots , includeBreakTime : false ) ? ? dispatcherStartDate
if shouldStartAtDispatcherDate {
rotationStartDate = dispatcherStartDate
shouldStartAtDispatcherDate = false
} else {
courts = rotationIndex = = 0 ? courts : ( 0. . < availableCourt ) . map { $0 }
}
courts . sort ( )
print ( " courts available at rotation \( rotationIndex ) " , courts )
print ( " rotationStartDate " , rotationStartDate )
if rotationIndex > 0 , let freeCourtPreviousRotation = freeCourtPerRotation [ rotationIndex - 1 ] , freeCourtPreviousRotation . count > 0 {
print ( " scenario where we are waiting for a breaktime to be over without any match to play in between or a free court was available and we need to recheck breaktime left on it " )
let previousPreviousRotationSlots = slots . filter ( { $0 . rotationIndex = = rotationIndex - 2 && freeCourtPreviousRotation . contains ( $0 . courtIndex ) } )
let previousEndDate = getNextStartDate ( fromPreviousRotationSlots : previousPreviousRotationSlots , includeBreakTime : true )
let previousEndDateNoBreak = getNextStartDate ( fromPreviousRotationSlots : previousPreviousRotationSlots , includeBreakTime : false )
let noBreakAlreadyTested = previousRotationSlots . anySatisfy ( { $0 . startDate = = previousEndDateNoBreak } )
if let previousEndDate , let previousEndDateNoBreak {
let differenceWithBreak = rotationStartDate . timeIntervalSince ( previousEndDate )
let differenceWithoutBreak = rotationStartDate . timeIntervalSince ( previousEndDateNoBreak )
print ( " difference w break " , differenceWithBreak )
print ( " difference w/o break " , differenceWithoutBreak )
var difference = differenceWithBreak
if differenceWithBreak <= 0 {
difference = differenceWithoutBreak
} else if differenceWithBreak > timeDifferenceLimit && differenceWithoutBreak > timeDifferenceLimit {
difference = noBreakAlreadyTested ? differenceWithBreak : max ( differenceWithBreak , differenceWithoutBreak )
}
if difference > timeDifferenceLimit {
courts . removeAll ( where : { index in freeCourtPreviousRotation . contains ( index )
} )
freeCourtPerRotation [ rotationIndex ] = courts
courts = freeCourtPreviousRotation
rotationStartDate = rotationStartDate . addingTimeInterval ( - difference )
}
}
}
dispatchCourts ( availableCourts : numberOfCourtsAvailablePerRotation , courts : courts , availableMatchs : & availableMatchs , slots : & slots , rotationIndex : rotationIndex , rotationStartDate : rotationStartDate , freeCourtPerRotation : & freeCourtPerRotation )
rotationIndex += 1
}
var organizedSlots = [ TimeMatch ] ( )
for i in 0. . < rotationIndex {
let courtsSorted = slots . filter ( { $0 . rotationIndex = = i } ) . map { $0 . courtIndex } . sorted ( )
let courts = randomizeCourts ( ) ? courtsSorted . shuffled ( ) : courtsSorted
var matches = slots . filter ( { $0 . rotationIndex = = i } ) . sorted ( using : . keyPath ( \ . courtIndex ) )
for j in 0. . < matches . count {
matches [ j ] . courtIndex = courts [ j ]
organizedSlots . append ( matches [ j ] )
}
}
return MatchDispatcher ( timedMatches : slots , freeCourtPerRotation : freeCourtPerRotation , rotationCount : rotationIndex )
}
( 0. . < availableCourt ) . forEach { courtIndex in
func dispatchCourts ( availableCourts : Int , courts : [ Int ] , availableMatchs : inout [ Match ] , slots : inout [ TimeMatch ] , rotationIndex : Int , rotationStartDate : Date , freeCourtPerRotation : inout [ Int : [ Int ] ] ) {
var matchPerRound = [ Int : Int ] ( )
var minimumTargetedEndDate : Date = rotationStartDate
courts . forEach { courtIndex in
// p r i n t ( m t . m a p { ( $ 0 . b r a c k e t ! . i n d e x . i n t V a l u e , c o u n t s [ $ 0 . b r a c k e t ! . i n d e x . i n t V a l u e ] ) } )
if let first = availableMatchs . first ( where : { match in
let roundObject = match . roundObject !
let canBePlayed = roundMatchCanBePlayed ( match , roundObject : roundObject , slots : slots , rotationIndex : rotationIndex )
if roundObject . loser = = nil && roundObject . index > 0 , match . indexInRound ( ) = = 0 , numberOfCourtsAvailablePerRotation > 1 , let nextMatch = match . next ( ) {
if canBePlayed && roundMatchCanBePlayed ( nextMatch , roundObject : roundObject , slots : slots , rotationIndex : rotationIndex ) {
let canBePlayed = roundMatchCanBePlayed ( match , roundObject : roundObject , slots : slots , rotationIndex : rotationIndex , targetedStartDate : rotationStartDate , minimumTargetedEndDate : & minimumTargetedEndDate )
let currentRotationSameRoundMatches = matchPerRound [ roundObject . index ] ? ? 0
if shouldHandleUpperRoundSlice ( ) {
let roundMatchesCount = roundObject . playedMatches ( ) . count
if roundObject . loser = = nil && roundMatchesCount > courts . count {
if currentRotationSameRoundMatches >= min ( roundMatchesCount / 2 , courts . count ) { return false }
}
}
if roundObject . loser = = nil && roundObject . index > 0 , match . indexInRound ( ) = = 0 , courts . count > 1 , let nextMatch = match . next ( ) {
if canBePlayed && roundMatchCanBePlayed ( nextMatch , roundObject : roundObject , slots : slots , rotationIndex : rotationIndex , targetedStartDate : rotationStartDate , minimumTargetedEndDate : & minimumTargetedEndDate ) {
return true
} else {
return false
}
}
if ( matchPerRound [ roundObject . index ] ? ? 0 ) % 2 = = 0 && roundObject . index != 0 && roundObject . loser = = nil && courtIndex = = numberOfCourtsAvailablePerRotation - 1 {
if currentRotationSameRoundMatches % 2 = = 0 && roundObject . index != 0 && roundObject . loser = = nil && courtIndex = = courts . count - 1 {
return false
}
return canBePlayed
} ) {
// p r i n t ( f i r s t . r o u n d O b j e c t ! . r o u n d T i t l e ( ) , f i r s t . m a t c h T i t l e ( ) )
print ( first . roundObject ! . roundTitle ( ) , first . matchTitle ( ) , courtIndex , rotationStartDate )
if first . roundObject ! . loser = = nil {
if let roundIndex = matchPerRound [ first . roundObject ! . index ] {
@ -184,68 +398,82 @@ class MatchScheduler {
matchPerRound [ first . roundObject ! . index ] = 1
}
}
slots . append ( TimeMatch ( matchID : first . id , rotationIndex : rotationIndex , courtIndex : courtIndex , groupIndex : first . roundObject ! . index ) )
let timeMatch = TimeMatch ( matchID : first . id , rotationIndex : rotationIndex , courtIndex : courtIndex , startDate : rotationStartDate , durationLeft : first . matchFormat . estimatedDuration , minimumBreakTime : first . matchFormat . breakTime . breakTime )
slots . append ( timeMatch )
availableMatchs . removeAll ( where : { $0 . id = = first . id } )
if let index = first . roundObject ? . index {
groupLastRotation [ index ] = rotationIndex
}
} else {
freeCourtPerRotation [ rotationIndex ] ! . append ( courtIndex )
}
}
rotationIndex += 1
}
var organizedSlots = [ TimeMatch ] ( )
for i in 0. . < rotationIndex {
let courtsSorted = slots . filter ( { $0 . rotationIndex = = i } ) . map { $0 . courtIndex } . sorted ( )
let courts = randomizeCourts ? courtsSorted . shuffled ( ) : courtsSorted
var matches = slots . filter ( { $0 . rotationIndex = = i } ) . sorted ( using : . keyPath ( \ . groupIndex ) , . keyPath ( \ . courtIndex ) )
if freeCourtPerRotation [ rotationIndex ] ! . count = = availableCourts {
freeCourtPerRotation [ rotationIndex ] = [ ]
let courtsUsed = getNextEarliestAvailableDate ( from : slots )
let freeCourts = courtsUsed . filter { ( courtIndex , availableDate ) in
availableDate <= minimumTargetedEndDate
} . sorted ( by : \ . 1 ) . map { $0 . 0 }
for j in 0. . < matches . count {
matches [ j ] . courtIndex = courts [ j ]
organizedSlots . append ( matches [ j ] )
dispatchCourts ( availableCourts : availableCourts , courts : freeCourts , availableMatchs : & availableMatchs , slots : & slots , rotationIndex : rotationIndex , rotationStartDate : minimumTargetedEndDate , freeCourtPerRotation : & freeCourtPerRotation )
}
}
func updateSchedule ( tournament : Tournament , fromRoundId roundId : String ? , fromMatchId matchId : String ? , startDate : Date ) {
return MatchDispatcher ( timedMatches : organizedSlots , freeCourtPerRotation : freeCourtPerRotation , rotationCount : rotationIndex , groupLastRotation : groupLastRotation )
}
let upperRounds = tournament . rounds ( )
let allMatches = tournament . allMatches ( )
func updateSchedule ( tournament : Tournament , fromRoundId roundId : String ? , fromMatchId matchId : String ? , randomizeCourts : Bool , startDate : Date ) {
let upperRounds = tournament . rounds ( )
var roundIndex = 0
if let roundId {
roundIndex = upperRounds . firstIndex ( where : { $0 . id = = roundId } ) ? ? 0
}
let rounds = upperRounds . flatMap {
[ $0 ] + $0 . loserRoundsAndChildren ( )
let rounds = upperRounds . map {
$0
} + upperRounds . flatMap {
$0 . loserRoundsAndChildren ( )
}
var flattenedMatches = rounds [ roundIndex . . . ] . flatMap { round in
var flattenedMatches = rounds . flatMap { round in
round . _matches ( ) . filter ( { $0 . disabled = = false } ) . sorted ( by : \ . index )
}
if let matchId , let matchIndex = flattenedMatches . firstIndex ( where : { $0 . id = = matchId } ) {
flattenedMatches = Array ( flattenedMatches [ matchIndex . . . ] )
flattenedMatches . forEach ( {
if ( roundId = = nil && matchId = = nil ) || $0 . startDate ? . isEarlierThan ( startDate ) = = false {
$0 . startDate = nil
}
} )
if let roundId {
if let round : Round = Store . main . findById ( roundId ) {
let matches = round . _matches ( )
round . resetRound ( )
flattenedMatches = matches + flattenedMatches
}
} else if let matchId {
if let match : Match = Store . main . findById ( matchId ) {
if let round = match . roundObject {
round . resetRound ( from : match )
}
flattenedMatches = [ match ] + flattenedMatches
}
}
let usedCourts = getAvailableCourts ( from : allMatches . filter ( { $0 . startDate ? . isEarlierThan ( startDate ) = = true } ) )
let initialCourts = usedCourts . filter { ( court , availableDate ) in
availableDate <= startDate
} . sorted ( by : \ . 1 ) . compactMap { tournament . getCourtIndex ( $0 . 0 ) }
flattenedMatches . forEach ( { $0 . startDate = nil } )
let courts : [ Int ] ? = initialCourts . isEmpty ? nil : initialCourts
let roundDispatch = self . roundDispatcher ( numberOfCourtsAvailablePerRotation : tournament . courtCount , flattenedMatches : flattenedMatches , randomizeCourts : randomizeCourts , initialOccupiedCourt : 0 )
let roundDispatch = self . roundDispatcher ( numberOfCourtsAvailablePerRotation : tournament . courtCount , flattenedMatches : flattenedMatches , dispatcherStartDate : startDate , initialCourts : courts )
roundDispatch . timedMatches . forEach { matchSchedule in
if let match = flattenedMatches . first ( where : { $0 . id = = matchSchedule . matchID } ) {
let timeIntervalToAdd = ( Double ( matchSchedule . rotationIndex ) ) * Double ( match . matchFormat . estimatedDuration ) * 60
match . startDate = startDate . addingTimeInterval ( timeIntervalToAdd )
match . startDate = matchSchedule . startDate
match . setCourt ( matchSchedule . courtIndex + 1 )
}
}
try ? DataStore . shared . matches . addOrUpdate ( contentOfs : flattenedMatches )
try ? DataStore . shared . matches . addOrUpdate ( contentOfs : all Matches)
}
}