@ -6,6 +6,7 @@
//
//
import Foundation
import Foundation
import LeStorage
struct GroupStageTimeMatch {
struct GroupStageTimeMatch {
let matchID : String
let matchID : String
@ -51,10 +52,42 @@ extension Match {
}
}
}
}
enum MatchSchedulerOption : Hashable {
case accountUpperBracketBreakTime
case accountLoserBracketBreakTime
case randomizeCourts
case rotationDifferenceIsImportant
case shouldHandleUpperRoundSlice
}
class MatchScheduler {
class MatchScheduler {
static let shared = 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 ) -> GroupStageMatchDispatcher {
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
let _groupStages = groupStages
@ -120,7 +153,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 {
@ -133,6 +166,14 @@ class MatchScheduler {
return GroupStageMatchDispatcher ( 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 , targetedStartDate : Date , minimumTargetedEndDate : inout Date ) -> Bool {
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 ( ) )
// 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 )
let previousMatches = roundObject . precedentMatches ( ofMatch : match )
@ -156,13 +197,28 @@ class MatchScheduler {
return false
return false
}
}
let previousMatchIsInPreviousRotation = previousMatchSlots . allSatisfy ( { $0 . rotationIndex < rotationIndex } )
var includeBreakTime = false
guard let minimumPossibleEndDate = previousMatchSlots . map ( { $0 . estimatedEndDate ( includeBreakTime : roundObject . isLoserBracket ( ) = = false ) } ) . max ( ) else {
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
return previousMatchIsInPreviousRotation
}
}
if targetedStartDate >= minimumPossibleEndDate {
if targetedStartDate >= minimumPossibleEndDate {
if rotationDifferenceIsImportant ( ) {
return previousMatchIsInPreviousRotation
} else {
return true
return true
}
} else {
} else {
if targetedStartDate = = minimumTargetedEndDate {
if targetedStartDate = = minimumTargetedEndDate {
minimumTargetedEndDate = minimumPossibleEndDate
minimumTargetedEndDate = minimumPossibleEndDate
@ -191,24 +247,66 @@ class MatchScheduler {
)
)
}
}
func roundDispatcher ( numberOfCourtsAvailablePerRotation : Int , flattenedMatches : [ Match ] , randomizeCourts : Bool , dispatcherStartDate : Date ) -> MatchDispatcher {
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 ] , dispatcherStartDate : Date , initialCourts : [ Int ] ? ) -> MatchDispatcher {
var slots = [ TimeMatch ] ( )
var slots = [ TimeMatch ] ( )
var availableMatchs = flattenedMatches
var _startDate : Date ?
var rotationIndex = 0
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 freeCourtPerRotation = [ Int : [ Int ] ] ( )
var courts = [ Int ] ( )
let availableCourt = numberOfCourtsAvailablePerRotation
var courts = initialCourts ? ? ( 0. . < availableCourt ) . map { $0 }
var shouldStartAtDispatcherDate = rotationIndex > 0
while availableMatchs . count > 0 {
while availableMatchs . count > 0 {
freeCourtPerRotation [ rotationIndex ] = [ ]
freeCourtPerRotation [ rotationIndex ] = [ ]
var matchPerRound = [ Int : Int ] ( )
var availableCourt = numberOfCourtsAvailablePerRotation
courts = ( 0. . < availableCourt ) . map { $0 }
let previousRotationSlots = slots . filter ( { $0 . rotationIndex = = rotationIndex - 1 } )
let previousRotationSlots = slots . filter ( { $0 . rotationIndex = = rotationIndex - 1 } )
var rotationStartDate : Date = getNextStartDate ( fromPreviousRotationSlots : previousRotationSlots , includeBreakTime : false ) ? ? dispatcherStartDate
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 ( )
courts . sort ( )
print ( " courts available at rotation \( rotationIndex ) " , courts )
print ( " courts available at rotation \( rotationIndex ) " , courts )
print ( " rotationStartDate " , rotationStartDate )
print ( " rotationStartDate " , rotationStartDate )
if rotationIndex > 0 , let freeCourtPreviousRotation = freeCourtPerRotation [ rotationIndex - 1 ] , freeCourtPreviousRotation . count > 0 {
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 " )
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 previousPreviousRotationSlots = slots . filter ( { $0 . rotationIndex = = rotationIndex - 2 && freeCourtPreviousRotation . contains ( $0 . courtIndex ) } )
@ -224,11 +322,11 @@ class MatchScheduler {
var difference = differenceWithBreak
var difference = differenceWithBreak
if differenceWithBreak <= 0 {
if differenceWithBreak <= 0 {
difference = differenceWithoutBreak
difference = differenceWithoutBreak
} else if differenceWithBreak > 0 && differenceWithoutBreak > 0 {
} else if differenceWithBreak > timeDifferenceLimit && differenceWithoutBreak > timeDifferenceLimit {
difference = noBreakAlreadyTested ? differenceWithBreak : max ( differenceWithBreak , differenceWithoutBreak )
difference = noBreakAlreadyTested ? differenceWithBreak : max ( differenceWithBreak , differenceWithoutBreak )
}
}
if difference > 0 {
if difference > timeDifferenceLimit {
courts . removeAll ( where : { index in freeCourtPreviousRotation . contains ( index )
courts . removeAll ( where : { index in freeCourtPreviousRotation . contains ( index )
} )
} )
freeCourtPerRotation [ rotationIndex ] = courts
freeCourtPerRotation [ rotationIndex ] = courts
@ -245,7 +343,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 {
@ -267,6 +365,16 @@ class MatchScheduler {
if let first = availableMatchs . first ( where : { match in
if let first = availableMatchs . first ( where : { match in
let roundObject = match . roundObject !
let roundObject = match . roundObject !
let canBePlayed = roundMatchCanBePlayed ( match , roundObject : roundObject , slots : slots , rotationIndex : rotationIndex , targetedStartDate : rotationStartDate , minimumTargetedEndDate : & minimumTargetedEndDate )
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 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 ) {
if canBePlayed && roundMatchCanBePlayed ( nextMatch , roundObject : roundObject , slots : slots , rotationIndex : rotationIndex , targetedStartDate : rotationStartDate , minimumTargetedEndDate : & minimumTargetedEndDate ) {
return true
return true
@ -275,7 +383,7 @@ class MatchScheduler {
}
}
}
}
if ( matchPerRound [ roundObject . index ] ? ? 0 ) % 2 = = 0 && roundObject . index != 0 && roundObject . loser = = nil && courtIndex = = courts . count - 1 {
if currentRotationSameRoundMatches % 2 = = 0 && roundObject . index != 0 && roundObject . loser = = nil && courtIndex = = courts . count - 1 {
return false
return false
}
}
@ -309,9 +417,12 @@ class MatchScheduler {
}
}
}
}
func updateSchedule ( tournament : Tournament , fromRoundId roundId : String ? , fromMatchId matchId : String ? , randomizeCourts : Bool , startDate : Date ) {
func updateSchedule ( tournament : Tournament , fromRoundId roundId : String ? , fromMatchId matchId : String ? , startDate : Date ) {
let upperRounds = tournament . rounds ( )
let upperRounds = tournament . rounds ( )
let allMatches = tournament . allMatches ( )
var roundIndex = 0
var roundIndex = 0
let rounds = upperRounds . map {
let rounds = upperRounds . map {
@ -320,21 +431,40 @@ class MatchScheduler {
$0 . loserRoundsAndChildren ( )
$0 . loserRoundsAndChildren ( )
}
}
if let roundId {
var flattenedMatches = rounds . flatMap { round in
roundIndex = rounds . firstIndex ( where : { $0 . id = = roundId } ) ? ? 0
round . _matches ( ) . filter ( { $0 . d isable d = = false } ) . sorted ( by : \ . index )
}
}
var flattenedMatches = rounds [ roundIndex . . . ] . flatMap { round in
flattenedMatches . forEach ( {
round . _matches ( ) . filter ( { $0 . disabled = = false } ) . sorted ( by : \ . index )
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
}
}
if let matchId , let matchIndex = flattenedMatches . firstIndex ( where : { $0 . id = = matchId } ) {
} else if let matchId {
flattenedMatches = Array ( flattenedMatches [ matchIndex . . . ] )
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 , dispatcherStartDate : startDate )
let roundDispatch = self . roundDispatcher ( numberOfCourtsAvailablePerRotation : tournament . courtCount , flattenedMatches : flattenedMatches , dispatcherStartDate : startDate , initialCourts : courts )
roundDispatch . timedMatches . forEach { matchSchedule in
roundDispatch . timedMatches . forEach { matchSchedule in
if let match = flattenedMatches . first ( where : { $0 . id = = matchSchedule . matchID } ) {
if let match = flattenedMatches . first ( where : { $0 . id = = matchSchedule . matchID } ) {
@ -343,7 +473,7 @@ class MatchScheduler {
}
}
}
}
try ? DataStore . shared . matches . addOrUpdate ( contentOfs : flattened Matches)
try ? DataStore . shared . matches . addOrUpdate ( contentOfs : all Matches)
}
}
}
}