@ -27,7 +27,9 @@ struct TableStructureView: View {
@ State private var selectedTournament : Tournament ?
@ State private var initialSeedCount : Int = 0
@ State private var initialSeedRound : Int = 0
@ State private var showSeedRepartition : Bool = false
@ State private var seedRepartition : [ Int ] = [ ]
func displayWarning ( ) -> Bool {
let unsortedTeamsCount = tournament . unsortedTeamsCount ( )
return tournament . shouldWarnOnlineRegistrationUpdates ( ) && teamCount != tournament . teamCount && ( tournament . teamCount <= unsortedTeamsCount || teamCount <= unsortedTeamsCount )
@ -64,6 +66,10 @@ struct TableStructureView: View {
max ( teamCount - groupStageCount * teamsPerGroupStage , 0 )
}
var tf : Int {
max ( teamCount - teamsFromGroupStages + qualifiedFromGroupStage + ( groupStageCount > 0 ? groupStageAdditionalQualified : 0 ) , 0 )
}
init ( tournament : Tournament ) {
self . tournament = tournament
_teamCount = . init ( wrappedValue : tournament . teamCount )
@ -92,7 +98,14 @@ struct TableStructureView: View {
Text ( " Préréglage " )
}
. disabled ( selectedTournament != nil )
} footer : {
Text ( structurePreset . localizedDescriptionStructurePresetTitle ( ) )
}
. onChange ( of : structurePreset ) {
_updatePreset ( )
}
Section {
NavigationLink {
TournamentSelectorView ( selectedTournament : $ selectedTournament )
. environment ( tournament )
@ -103,24 +116,10 @@ struct TableStructureView: View {
Text ( " À partir d'un tournoi existant " )
}
}
NavigationLink {
HeadManagerView ( initialSeedRound : $ initialSeedRound , initialSeedCount : $ initialSeedCount )
. environment ( tournament )
} label : {
Text ( " Configuration des têtes de série " )
}
. disabled ( selectedTournament != nil )
} footer : {
Text ( structurePreset . localizedDescriptionStructurePresetTitle ( ) )
}
. onChange ( of : selectedTournament ) {
_updatePreset ( )
}
. onChange ( of : structurePreset ) {
_updatePreset ( )
}
}
Section {
@ -240,7 +239,6 @@ struct TableStructureView: View {
}
Section {
let tf = max ( teamCount - teamsFromGroupStages + qualifiedFromGroupStage + ( groupStageCount > 0 ? groupStageAdditionalQualified : 0 ) , 0 )
if groupStageCount > 0 {
if structurePreset != . doubleGroupStage {
LabeledContent {
@ -268,15 +266,13 @@ struct TableStructureView: View {
Text ( " Attention ! " ) . foregroundStyle ( . red )
}
}
LabeledContent {
Text ( tf . formatted ( ) )
} label : {
Text ( " Effectif " )
}
LabeledContent {
Text ( RoundRule . teamsInFirstRound ( forTeams : tf ) . formatted ( ) )
} label : {
Text ( " Dimension " )
if groupStageCount > 0 {
LabeledContent {
Text ( tf . formatted ( ) )
} label : {
Text ( " Effectif " )
}
}
} else {
LabeledContent {
@ -307,6 +303,27 @@ struct TableStructureView: View {
}
}
if tournament . state ( ) != . build {
Section {
LabeledContent {
Image ( systemName : seedRepartition . isEmpty ? " xmark " : " checkmark " )
} label : {
FooterButtonView ( " Configuration du tableau " ) {
showSeedRepartition = true
}
. disabled ( selectedTournament != nil )
}
} footer : {
if seedRepartition . isEmpty {
Text ( " Aucune répartition n'a été indiqué, vous devrez réserver ou placer les têtes de séries dans le tableau manuellement. " )
} else {
FooterButtonView ( " Supprimer la configuration " , role : . destructive ) {
seedRepartition = [ ]
}
}
}
}
if tournament . rounds ( ) . isEmpty && tournament . state ( ) = = . build {
Section {
RowButtonView ( " Ajouter un tableau " , role : . destructive ) {
@ -327,13 +344,13 @@ struct TableStructureView: View {
Section {
RowButtonView ( " Reconstruire les poules " , role : . destructive ) {
_save ( rebuildEverything : false )
await _save ( rebuildEverything : false )
}
}
Section {
RowButtonView ( " Tout refaire " , role : . destructive ) {
_save ( rebuildEverything : true )
await _save ( rebuildEverything : true )
}
}
@ -360,6 +377,13 @@ struct TableStructureView: View {
updatedElements . remove ( . teamCount )
}
}
. sheet ( isPresented : $ showSeedRepartition , content : {
NavigationStack {
HeadManagerView ( teamsInBracket : tf , heads : tsPure , initialSeedRepartition : seedRepartition ) { seedRepartition in
self . seedRepartition = seedRepartition
}
}
} )
. onChange ( of : groupStageCount ) {
if groupStageCount != tournament . groupStageCount {
updatedElements . insert ( . groupStageCount )
@ -412,13 +436,17 @@ struct TableStructureView: View {
ToolbarItem ( placement : . confirmationAction ) {
if tournament . state ( ) = = . initial {
ButtonValidateView {
_save ( rebuildEverything : true )
Task {
await _save ( rebuildEverything : true )
}
}
} else {
let requirements = Set ( updatedElements . compactMap { $0 . requiresRebuilding } )
ButtonValidateView ( role : . destructive ) {
if requirements . isEmpty {
_save ( rebuildEverything : false )
Task {
await _save ( rebuildEverything : false )
}
} else {
presentRefreshStructureWarning = true
}
@ -429,11 +457,15 @@ struct TableStructureView: View {
}
Button ( " Reconstruire les poules " ) {
_save ( rebuildEverything : false )
Task {
await _save ( rebuildEverything : false )
}
}
Button ( " Tout refaire " , role : . destructive ) {
_save ( rebuildEverything : true )
Task {
await _save ( rebuildEverything : true )
}
}
} , message : {
ForEach ( Array ( requirements ) ) { requirement in
@ -511,7 +543,7 @@ struct TableStructureView: View {
}
}
private func _save ( rebuildEverything : Bool = false ) {
private func _save ( rebuildEverything : Bool = false ) async {
_verifyValueIntegrity ( )
do {
@ -531,8 +563,8 @@ struct TableStructureView: View {
tournament . initialSeedRound = selectedTournament . initialSeedRound
tournament . initialSeedCount = selectedTournament . initialSeedCount
} else {
tournament . initialSeedRound = initialSeedRound
tournament . initialSeedCount = initialSeedCount
tournament . initialSeedRound = seedRepartition . firstIndex ( where : { $0 > 0 } ) ? ? 0
tournament . initialSeedCount = seedRepartition . first ( where : { $0 > 0 } ) ? ? 0
}
tournament . removeWildCards ( )
if structurePreset . hasWildcards ( ) , buildWildcards {
@ -541,6 +573,12 @@ struct TableStructureView: View {
}
tournament . deleteAndBuildEverything ( preset : structurePreset )
if seedRepartition . count > 0 {
while tournament . rounds ( ) . count < seedRepartition . count {
await tournament . addNewRound ( tournament . rounds ( ) . count )
}
}
if let selectedTournament {
let oldTournamentStart = selectedTournament . startDate
let newTournamentStart = tournament . startDate
@ -577,25 +615,65 @@ struct TableStructureView: View {
tournament . tournamentStore ? . matches . addOrUpdate ( contentOfs : tournament . _allMatchesIncludingDisabled ( ) )
} else {
if initialSeedRound > 0 {
if let round = tournament . rounds ( ) . first ( where : { $0 . index = = initialSeedRound } ) {
let seedSorted = frenchUmpireOrder ( for : RoundRule . numberOfMatches ( forRoundIndex : round . index ) )
print ( seedSorted )
seedSorted . prefix ( initialSeedCount ) . forEach { index in
if let match = round . _matches ( ) [ safe : index ] {
if match . indexInRound ( ) < RoundRule . numberOfMatches ( forRoundIndex : round . index ) / 2 {
match . previousMatch ( . one ) ? . disableMatch ( )
for ( index , seedCount ) in seedRepartition . enumerated ( ) {
if let round = tournament . rounds ( ) . first ( where : { $0 . index = = index } ) {
let baseIndex = RoundRule . baseIndex ( forRoundIndex : round . index )
let numberOfMatches = RoundRule . numberOfMatches ( forRoundIndex : round . index )
let playedMatches = round . playedMatches ( ) . map { $0 . index - baseIndex }
let allMatches = round . _matches ( )
let seedSorted = frenchUmpireOrder ( for : numberOfMatches ) . filter ( { index in
playedMatches . contains ( index )
} ) . prefix ( seedCount )
for ( index , value ) in seedSorted . enumerated ( ) {
let isOpponentTurn = index >= playedMatches . count
if let match = allMatches [ safe : value ] {
if match . index - baseIndex < numberOfMatches / 2 {
if isOpponentTurn {
match . previousMatch ( . two ) ? . disableMatch ( )
} else {
match . previousMatch ( . one ) ? . disableMatch ( )
}
} else {
match . previousMatch ( . two ) ? . disableMatch ( )
if isOpponentTurn {
match . previousMatch ( . one ) ? . disableMatch ( )
} else {
match . previousMatch ( . two ) ? . disableMatch ( )
}
}
}
}
if initialSeedCount > 0 {
tournament . tournamentStore ? . matches . addOrUpdate ( contentOfs : tournament . _allMatchesIncludingDisabled ( ) )
if seedCount > 0 {
tournament . tournamentStore ? . matches . addOrUpdate ( contentOfs : round . _matches ( ) )
tournament . tournamentStore ? . matches . addOrUpdate ( contentOfs : round . allLoserRoundMatches ( ) )
round . deleteLoserBracket ( )
round . buildLoserBracket ( )
round . loserRounds ( ) . forEach { loserRound in
loserRound . disableUnplayedLoserBracketMatches ( )
}
}
}
}
// i f i n i t i a l S e e d R o u n d > 0 {
// i f l e t r o u n d = t o u r n a m e n t . r o u n d s ( ) . f i r s t ( w h e r e : { $ 0 . i n d e x = = i n i t i a l S e e d R o u n d } ) {
// l e t s e e d S o r t e d = f r e n c h U m p i r e O r d e r ( f o r : R o u n d R u l e . n u m b e r O f M a t c h e s ( f o r R o u n d I n d e x : r o u n d . i n d e x ) )
// p r i n t ( s e e d S o r t e d )
// s e e d S o r t e d . p r e f i x ( i n i t i a l S e e d C o u n t ) . f o r E a c h { i n d e x i n
// i f l e t m a t c h = r o u n d . _ m a t c h e s ( ) [ s a f e : i n d e x ] {
// i f m a t c h . i n d e x I n R o u n d ( ) < R o u n d R u l e . n u m b e r O f M a t c h e s ( f o r R o u n d I n d e x : r o u n d . i n d e x ) / 2 {
// m a t c h . p r e v i o u s M a t c h ( . o n e ) ? . d i s a b l e M a t c h ( )
// } e l s e {
// m a t c h . p r e v i o u s M a t c h ( . t w o ) ? . d i s a b l e M a t c h ( )
// }
// }
// }
//
// i f i n i t i a l S e e d C o u n t > 0 {
// t o u r n a m e n t . t o u r n a m e n t S t o r e ? . m a t c h e s . a d d O r U p d a t e ( c o n t e n t O f s : t o u r n a m e n t . _ a l l M a t c h e s I n c l u d i n g D i s a b l e d ( ) )
// }
// }
// }
}
} else if ( rebuildEverything = = false && requirements . contains ( . groupStage ) ) {
tournament . deleteGroupStages ( )
@ -730,41 +808,44 @@ extension TableStructureView {
// . e n v i r o n m e n t O b j e c t ( D a t a S t o r e . s h a r e d )
// }
// }
func frenchUmpireOrder ( for matches : [ Int ] ) -> [ Int ] {
guard matches . count > 1 else { return matches }
// B a s e c a s e
if matches . count = = 2 {
return [ matches [ 1 ] , matches [ 0 ] ] // b o t t o m , t o p
if matches . count <= 1 { return matches }
if matches . count = = 2 { return [ matches [ 1 ] , matches [ 0 ] ] }
var result : [ Int ] = [ ]
// S t e p 1 : T a k e l a s t , t h e n f i r s t
result . append ( matches . last ! )
result . append ( matches . first ! )
// S t e p 2 : G e t r e m a i n d e r ( e v e r y t h i n g e x c e p t f i r s t a n d l a s t )
let remainder = Array ( matches [ 1. . < matches . count - 1 ] )
if remainder . isEmpty { return result }
// S t e p 3 : S p l i t r e m a i n d e r i n h a l f
let mid = remainder . count / 2
let firstHalf = Array ( remainder [ 0. . < mid ] )
let secondHalf = Array ( remainder [ mid . . < remainder . count ] )
// S t e p 4 : T a k e f i r s t o f 2 n d h a l f , t h e n l a s t o f 1 s t h a l f
if ! secondHalf . isEmpty {
result . append ( secondHalf . first ! )
}
let n = matches . count
let mid = n / 2
let topHalf = Array ( matches [ 0. . < mid ] )
let bottomHalf = Array ( matches [ mid . . < n ] )
var order : [ Int ] = [ ]
// S t e p 1 : l a s t m a t c h o f r o u n d ( b o t t o m )
order . append ( bottomHalf . last ! )
// S t e p 2 : f i r s t m a t c h o f r o u n d ( t o p )
order . append ( topHalf . first ! )
// S t e p 3 & 4 : r e c u r s i v e l y a p p l y o n h a l v e s m i n u s t h e o n e s w e j u s t u s e d
let topInner = Array ( topHalf . dropFirst ( ) )
let bottomInner = Array ( bottomHalf . dropLast ( ) )
let innerOrder = frenchUmpireOrder ( for : bottomInner ) + frenchUmpireOrder ( for : topInner )
order . append ( contentsOf : innerOrder )
return order
if ! firstHalf . isEmpty {
result . append ( firstHalf . last ! )
}
// S t e p 5 : B u i l d n e w r e m a i n d e r f r o m w h a t ' s l e f t a n d r e c u r s e
let newRemainder = Array ( firstHalf . dropLast ( ) ) + Array ( secondHalf . dropFirst ( ) )
result . append ( contentsOf : frenchUmpireOrder ( for : newRemainder ) )
return result
}
// / C o n v e n i e n c e w r a p p e r t o c a l l b y m a t c h C o u n t
// / C o n v e n i e n c e w r a p p e r
func frenchUmpireOrder ( for matchCount : Int ) -> [ Int ] {
return frenchUmpireOrder ( for : Array ( 0. . < matchCount ) )
let m = frenchUmpireOrder ( for : Array ( 0. . < matchCount ) )
return m + m . reversed ( )
}