@ -12,8 +12,10 @@ import SwiftUI
@ Observable
final class Round : BaseRound , SideStorable {
var _cachedSeedInterval : SeedInterval ?
private var _cachedSeedInterval : SeedInterval ?
private var _cachedLoserRounds : [ Round ] ?
private var _cachedLoserRoundsAndChildren : [ Round ] ?
internal init ( tournament : String , index : Int , parent : String ? = nil , matchFormat : MatchFormat ? = nil , startDate : Date ? = nil , groupStageLoserBracket : Bool = false , loserBracketMode : LoserBracketMode = . automatic ) {
super . init ( tournament : tournament , index : index , parent : parent , format : matchFormat , startDate : startDate , groupStageLoserBracket : groupStageLoserBracket , loserBracketMode : loserBracketMode )
@ -45,7 +47,16 @@ final class Round: BaseRound, SideStorable {
func tournamentObject ( ) -> Tournament ? {
return Store . main . findById ( tournament )
}
func _unsortedMatches ( includeDisabled : Bool ) -> [ Match ] {
guard let tournamentStore = self . tournamentStore else { return [ ] }
if includeDisabled {
return tournamentStore . matches . filter { $0 . round = = self . id }
} else {
return tournamentStore . matches . filter { $0 . round = = self . id && $0 . disabled = = false }
}
}
func _matches ( ) -> [ Match ] {
guard let tournamentStore = self . tournamentStore else { return [ ] }
return tournamentStore . matches . filter { $0 . round = = self . id } . sorted ( by : \ . index )
@ -78,7 +89,20 @@ final class Round: BaseRound, SideStorable {
return enabledMatches ( ) . anySatisfy ( { $0 . hasEnded ( ) = = false } ) = = false
}
}
func upperMatches ( upperRound : Round , match : Match ) -> [ Match ] {
let matchIndex = match . index
let indexInRound = RoundRule . matchIndexWithinRound ( fromMatchIndex : matchIndex )
return [ upperRound . getMatch ( atMatchIndexInRound : indexInRound * 2 ) , upperRound . getMatch ( atMatchIndexInRound : indexInRound * 2 + 1 ) ] . compactMap ( { $0 } )
}
func previousMatches ( previousRound : Round , match : Match ) -> [ Match ] {
guard let tournamentStore = self . tournamentStore else { return [ ] }
return tournamentStore . matches . filter {
$0 . round = = previousRound . id && ( $0 . index = = match . topPreviousRoundMatchIndex ( ) || $0 . index = = match . bottomPreviousRoundMatchIndex ( ) )
}
}
func upperMatches ( ofMatch match : Match ) -> [ Match ] {
if parent != nil , previousRound ( ) = = nil , let parentRound {
let matchIndex = match . index
@ -164,12 +188,12 @@ final class Round: BaseRound, SideStorable {
func losers ( ) -> [ TeamRegistration ] {
let teamIds : [ String ] = self . _matches ( ) . compactMap { $0 . losingTeamId }
let teamIds : [ String ] = self . _unsortedMatches ( includeDisabled : false ) . compactMap { $0 . losingTeamId }
return teamIds . compactMap { self . tournamentStore ? . teamRegistrations . findById ( $0 ) }
}
func winners ( ) -> [ TeamRegistration ] {
let teamIds : [ String ] = self . _matches ( ) . compactMap { $0 . winningTeamId }
let teamIds : [ String ] = self . _unsortedMatches ( includeDisabled : false ) . compactMap { $0 . winningTeamId }
return teamIds . compactMap { self . tournamentStore ? . teamRegistrations . findById ( $0 ) }
}
@ -302,7 +326,7 @@ defer {
func enabledMatches ( ) -> [ Match ] {
guard let tournamentStore = self . tournamentStore else { return [ ] }
return tournamentStore . matches . filter { $0 . round = = self . id && $0 . disabled = = false } . sorted ( by : \ . index )
return tournamentStore . matches . filter { $0 . disabled = = false && $0 . round = = self . id } . sorted ( by : \ . index )
}
// f u n c d i s p l a y a b l e M a t c h e s ( ) - > [ M a t c h ] {
@ -353,17 +377,21 @@ defer {
func loserRounds ( forRoundIndex roundIndex : Int , loserRoundsAndChildren : [ Round ] ) -> [ Round ] {
return loserRoundsAndChildren . filter ( { $0 . index = = roundIndex } ) . sorted ( by : \ . theoryCumulativeMatchCount )
}
func isEnabled ( ) -> Bool {
return _unsortedMatches ( includeDisabled : false ) . isEmpty = = false
}
func isDisabled ( ) -> Bool {
return _matches ( ) . allSatisfy ( { $0 . disabled } )
return _unsortedMatches ( includeDisabled : true ) . allSatisfy ( { $0 . disabled } )
}
func isRankDisabled ( ) -> Bool {
return _matches ( ) . allSatisfy ( { $0 . disabled && $0 . teamScores . isEmpty } )
return _unsortedMatches ( includeDisabled : true ) . allSatisfy ( { $0 . disabled && $0 . teamScores . isEmpty } )
}
func resetFromRoundAllMatchesStartDate ( ) {
_matches ( ) . forEach ( {
_unsortedMatches ( includeDisabled : false ) . forEach ( {
$0 . startDate = nil
} )
loserRoundsAndChildren ( ) . forEach { round in
@ -373,7 +401,7 @@ defer {
}
func resetFromRoundAllMatchesStartDate ( from match : Match ) {
let matches = _matches ( )
let matches = _unsortedMatches ( includeDisabled : false )
if let index = matches . firstIndex ( where : { $0 . id = = match . id } ) {
matches [ index . . . ] . forEach { match in
match . startDate = nil
@ -386,10 +414,36 @@ defer {
}
func getActiveLoserRound ( ) -> Round ? {
let rounds = loserRounds ( ) . filter ( { $0 . isDisabled ( ) = = false } ) . sorted ( by : \ . index ) . reversed ( )
return rounds . first ( where : { $0 . hasStarted ( ) && $0 . hasEnded ( ) = = false } ) ? ? rounds . first
// G e t a l l l o s e r r o u n d s o n c e
let allLoserRounds = loserRounds ( )
var lowestIndexRound : Round ? = nil
let currentRoundMatchCount = RoundRule . numberOfMatches ( forRoundIndex : index )
let roundCount = RoundRule . numberOfRounds ( forTeams : currentRoundMatchCount )
for currentIndex in 0. . < roundCount { // A s s u m i n g n o t o u r n a m e n t h a s > 1 0 0 r o u n d s
// F i n d n o n - d i s a b l e d r o u n d w i t h c u r r e n t i n d e x
let roundAtIndex = allLoserRounds . first ( where : { $0 . index = = currentIndex && $0 . isEnabled ( ) } )
// N o r o u n d a t t h i s i n d e x , w e ' v e c h e c k e d a l l a v a i l a b l e r o u n d s
if roundAtIndex = = nil {
break
}
// S a v e t h e f i r s t n o n - d i s a b l e d r o u n d w e f i n d ( s h o u l d b e i n d e x 0 )
if lowestIndexRound = = nil {
lowestIndexRound = roundAtIndex
}
// I f t h i s r o u n d i s a c t i v e , r e t u r n i t i m m e d i a t e l y
if roundAtIndex ! . hasStarted ( ) && ! roundAtIndex ! . hasEnded ( ) {
return roundAtIndex
}
}
// I f n o a c t i v e r o u n d f o u n d , r e t u r n t h e o n e w i t h l o w e s t i n d e x
return lowestIndexRound
}
func enableRound ( ) {
_toggleRound ( disable : false )
}
@ -399,7 +453,7 @@ defer {
}
private func _toggleRound ( disable : Bool ) {
let _matches = _matches ( )
let _matches = _unsortedMatches ( includeDisabled : true )
_matches . forEach { match in
match . disabled = disable
match . resetMatch ( )
@ -414,7 +468,7 @@ defer {
}
var cumulativeMatchCount : Int {
var totalMatches = playedMatches ( ) . count
var totalMatches = _unsortedMatches ( includeDisabled : false ) . count
if let parentRound {
totalMatches += parentRound . cumulativeMatchCount
}
@ -443,11 +497,12 @@ defer {
}
func disabledMatches ( ) -> [ Match ] {
return _matches ( ) . filter ( { $0 . disabled } )
guard let tournamentStore = self . tournamentStore else { return [ ] }
return tournamentStore . matches . filter { $0 . round = = self . id && $0 . disabled = = true }
}
func allLoserRoundMatches ( ) -> [ Match ] {
loserRoundsAndChildren ( ) . flatMap ( { $0 . _matches ( ) } )
loserRoundsAndChildren ( ) . flatMap ( { $0 . _unsortedMatches ( includeDisabled : false ) } )
}
var theoryCumulativeMatchCount : Int {
@ -520,7 +575,7 @@ defer {
let start = Date ( )
defer {
let duration = Duration . milliseconds ( Date ( ) . timeIntervalSince ( start ) * 1_000 )
print ( " func seedInterval(initialMode) " , initialMode , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
print ( " func seedInterval(initialMode) " , id , index , i nitialMode , _cachedSeedInterval , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
}
#endif
@ -560,15 +615,19 @@ defer {
if let previousRound = previousRound ( ) {
if ( previousRound . enabledMatches ( ) . isEmpty = = false || initialMode ) {
return previousRound . seedInterval ( initialMode : initialMode ) ? . chunks ( ) ? . first
_cachedSeedInterval = previousRound . seedInterval ( initialMode : initialMode ) ? . chunks ( ) ? . first
return _cachedSeedInterval
} else {
return previousRound . seedInterval ( initialMode : initialMode )
_cachedSeedInterval = previousRound . seedInterval ( initialMode : initialMode )
return _cachedSeedInterval
}
} else if let parentRound {
if parentRound . isUpperBracket ( ) {
return parentRound . seedInterval ( initialMode : initialMode )
_cachedSeedInterval = parentRound . seedInterval ( initialMode : initialMode )
return _cachedSeedInterval
}
return parentRound . seedInterval ( initialMode : initialMode ) ? . chunks ( ) ? . last
_cachedSeedInterval = parentRound . seedInterval ( initialMode : initialMode ) ? . chunks ( ) ? . last
return _cachedSeedInterval
}
return nil
@ -599,9 +658,10 @@ defer {
tournamentObject ? . updateTournamentState ( )
}
func roundStatus ( ) -> String {
let hasEnded = hasEnded ( )
if hasStarted ( ) && hasEnded = = false {
func roundStatus ( playedMatches : [ Match ] ) -> String {
let hasEnded = playedMatches . anySatisfy ( { $0 . hasEnded ( ) = = false } ) = = false
let hasStarted = playedMatches . anySatisfy ( { $0 . hasStarted ( ) } )
if hasStarted && hasEnded = = false {
return " en cours "
} else if hasEnded {
return " terminée "
@ -613,6 +673,9 @@ defer {
}
func loserRounds ( ) -> [ Round ] {
if let _cachedLoserRounds {
return _cachedLoserRounds
}
guard let tournamentStore = self . tournamentStore else { return [ ] }
#if _DEBUG_TIME // D E B U G I N G T I M E
let start = Date ( )
@ -622,12 +685,57 @@ defer {
}
#endif
return tournamentStore . rounds . filter ( { $0 . parent = = id } ) . sorted ( by : \ . index ) . reversed ( )
// F i l t e r f i r s t t o r e d u c e s o r t i n g w o r k
let filteredRounds = tournamentStore . rounds . filter { $0 . parent = = id }
// R e t u r n e m p t y a r r a y e a r l y i f n o r o u n d s m a t c h
if filteredRounds . isEmpty {
return [ ]
}
// S o r t d i r e c t l y i n d e s c e n d i n g o r d e r t o a v o i d t h e s e p a r a t e r e v e r s e d ( ) c a l l
_cachedLoserRounds = filteredRounds . sorted { $0 . index > $1 . index }
return _cachedLoserRounds !
}
func loserRoundsAndChildren ( ) -> [ Round ] {
let loserRounds = loserRounds ( )
return loserRounds + loserRounds . flatMap ( { $0 . loserRoundsAndChildren ( ) } )
#if _DEBUG_TIME // D E B U G I N G T I M E
let start = Date ( )
defer {
let duration = Duration . milliseconds ( Date ( ) . timeIntervalSince ( start ) * 1_000 )
print ( " func loserRoundsAndChildren: " , id , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
}
#endif
// R e t u r n c a c h e d r e s u l t i f a v a i l a b l e
if let cached = _cachedLoserRoundsAndChildren {
return cached
}
// C a l c u l a t e r e s u l t i f c a c h e i s i n v a l i d o r u n a v a i l a b l e
let direct = loserRounds ( )
// R e t u r n q u i c k l y i f t h e r e a r e n o d i r e c t l o s e r r o u n d s
if direct . isEmpty {
// U p d a t e c a c h e w i t h e m p t y r e s u l t
_cachedLoserRoundsAndChildren = [ ]
return [ ]
}
// P r e - a l l o c a t e c a p a c i t y t o a v o i d r e a l l o c a t i o n s ( e s t i m a t e b a s e d o n t y p i c a l t o u r n a m e n t s t r u c t u r e )
var allRounds = direct
let estimatedChildrenCount = direct . count * 2 // R o u g h e s t i m a t e
allRounds . reserveCapacity ( estimatedChildrenCount )
// C o l l e c t a l l c h i l d r e n r o u n d s i n o n e p a s s
for round in direct {
allRounds . append ( contentsOf : round . loserRoundsAndChildren ( ) )
}
// S t o r e r e s u l t i n c a c h e
_cachedLoserRoundsAndChildren = allRounds
return allRounds
}
func isUpperBracket ( ) -> Bool {
@ -639,43 +747,106 @@ defer {
}
func deleteLoserBracket ( ) {
let loserRounds = loserRounds ( )
self . tournamentStore ? . rounds . delete ( contentOfs : loserRounds )
#if DEBUG // D E B U G I N G T I M E
let start = Date ( )
defer {
let duration = Duration . milliseconds ( Date ( ) . timeIntervalSince ( start ) * 1_000 )
print ( " func deleteLoserBracket: " , id , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
}
#endif
self . tournamentStore ? . rounds . delete ( contentOfs : self . loserRounds ( ) )
self . invalidateCache ( )
}
func buildLoserBracket ( ) {
#if DEBUG // D E B U G I N G T I M E
let start = Date ( )
defer {
let duration = Duration . milliseconds ( Date ( ) . timeIntervalSince ( start ) * 1_000 )
print ( " func buildLoserBracket: " , id , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
}
#endif
guard loserRounds ( ) . isEmpty else { return }
self . invalidateCache ( )
let currentRoundMatchCount = RoundRule . numberOfMatches ( forRoundIndex : index )
guard currentRoundMatchCount > 1 else { return }
guard let tournamentStore else { return }
let roundCount = RoundRule . numberOfRounds ( forTeams : currentRoundMatchCount )
var loserBracketMatchFormat = tournamentObject ( ) ? . loserBracketMatchFormat
let loserBracketMatchFormat = tournamentObject ( ) ? . loserBracketMatchFormat
// i f l e t p a r e n t R o u n d {
// l o s e r B r a c k e t M a t c h F o r m a t = t o u r n a m e n t O b j e c t ( ) ? . l o s e r B r a c k e t S m a r t M a t c h F o r m a t ( p a r e n t R o u n d . i n d e x )
// }
var titles = [ String : String ] ( )
let rounds = ( 0. . < roundCount ) . map { // i n d e x 0 i s t h e f i n a l
let round = Round ( tournament : tournament , index : $0 , matchFormat : loserBracketMatchFormat )
round . parent = id // p a r e n t
// t i t l e s [ r o u n d . i d ] = r o u n d . r o u n d T i t l e ( i n i t i a l M o d e : t r u e )
return round
}
self . tournamentStore ? . rounds . addOrUpdate ( contentOfs : rounds )
tournamentStore . rounds . addOrUpdate ( contentOfs : rounds )
let matchCount = RoundRule . numberOfMatches ( forTeams : currentRoundMatchCount )
let matches = ( 0. . < matchCount ) . map { // 0 i s f i n a l m a t c h
let roundIndex = RoundRule . roundIndex ( fromMatchIndex : $0 )
let round = rounds [ roundIndex ]
return Match ( round : round . id , index : $0 , format : loserBracketMatchFormat , name : round . roundTitle ( initialMode : true ) )
// l e t t i t l e = t i t l e s [ r o u n d . i d ]
return Match ( round : round . id , index : $0 , format : loserBracketMatchFormat )
// i n i t i a l m o d e l e t t h e r o u n d T i t l e g i v e a n a m e w i t h o u t c o n s i d e r i n g t h e p l a y a b l e m a t c h
}
self . tournamentStore ? . matches . addOrUpdate ( contentOfs : matches )
tournamentStore . matches . addOrUpdate ( contentOfs : matches )
lose rR ounds( ) . forEach { round in
rounds . forEach { round in
round . buildLoserBracket ( )
}
}
func disableUnplayedLoserBracketMatches ( ) {
let m = self . _matches ( )
if let previousRound = self . previousRound ( ) {
m . forEach { match in
let prmc = previousRound . previousMatches ( previousRound : previousRound , match : match ) . filter ( {
$0 . disabled
} ) . count
let byeprmc = previousRound . previousMatches ( previousRound : previousRound , match : match ) . filter ( { $0 . byeState } ) . count
if byeprmc = = 2 {
match . disabled = false
} else {
if prmc = = 2 {
match . disabled = true
if byeprmc = = 1 {
match . byeState = true
}
} else if prmc = = 1 {
match . byeState = true
match . disabled = true
} else {
match . disabled = false
}
}
}
} else if let upperRound = self . parentRound {
m . forEach { match in
let prmc = self . upperMatches ( upperRound : upperRound , match : match ) . filter ( { $0 . disabled } ) . count
if prmc > 0 {
match . disabled = true
if upperRound . isUpperBracket ( ) , prmc = = 1 {
match . byeState = true
}
} else {
match . disabled = false
}
}
}
tournamentStore ? . matches . addOrUpdate ( contentOfs : m )
loserRounds ( ) . forEach { loserRound in
loserRound . disableUnplayedLoserBracketMatches ( )
}
}
var parentRound : Round ? {
guard let parent = parent else { return nil }
return self . tournamentStore ? . rounds . findById ( parent )
@ -706,15 +877,41 @@ defer {
}
func updateMatchFormatOfAllMatches ( _ updatedMatchFormat : MatchFormat ) {
let playedMatches = _matches ( )
let playedMatches = _unsortedMatches ( includeDisabled : true )
playedMatches . forEach { match in
match . matchFormat = updatedMatchFormat
}
self . tournamentStore ? . matches . addOrUpdate ( contentOfs : playedMatches )
}
func loserBracketTurns ( ) -> [ LoserRound ] {
#if _DEBUG_TIME // D E B U G I N G T I M E
let start = Date ( )
defer {
let duration = Duration . milliseconds ( Date ( ) . timeIntervalSince ( start ) * 1_000 )
print ( " func loserBracketTurns() " , id , duration . formatted ( . units ( allowed : [ . seconds , . milliseconds ] ) ) )
}
#endif
var rounds = [ LoserRound ] ( )
let currentRoundMatchCount = RoundRule . numberOfMatches ( forRoundIndex : index )
let roundCount = RoundRule . numberOfRounds ( forTeams : currentRoundMatchCount )
for index in 0. . < roundCount {
let lr = LoserRound ( roundIndex : roundCount - index - 1 , turnIndex : index , upperBracketRound : self )
rounds . append ( lr )
}
return rounds
}
func invalidateCache ( ) {
_cachedLoserRounds = nil
_cachedSeedInterval = nil
_cachedLoserRoundsAndChildren = nil
}
override func deleteDependencies ( ) {
let matches = self . _matches ( )
let matches = self . _unsortedMatches ( includeDisabled : true )
for match in matches {
match . deleteDependencies ( )
}
@ -775,7 +972,7 @@ defer {
func insertOnServer ( ) {
self . tournamentStore ? . rounds . writeChangeAndInsertOnServer ( instance : self )
for match in self . _matches ( ) {
for match in self . _unsortedMatches ( includeDisabled : true ) {
match . insertOnServer ( )
}
}