add seeding system

multistore
Razmig Sarkissian 2 years ago
parent af97c40270
commit c5e1f4b356
  1. 4
      PadelClub.xcodeproj/project.pbxproj
  2. 2
      PadelClub/Data/GroupStage.swift
  3. 130
      PadelClub/Data/Match.swift
  4. 4
      PadelClub/Data/PlayerRegistration.swift
  5. 20
      PadelClub/Data/Round.swift
  6. 46
      PadelClub/Data/TeamRegistration.swift
  7. 3
      PadelClub/Data/TeamScore.swift
  8. 131
      PadelClub/Data/Tournament.swift
  9. 17
      PadelClub/Manager/PadelRule.swift
  10. 29
      PadelClub/ViewModel/TournamentSeedEditing.swift
  11. 2
      PadelClub/Views/GroupStage/GroupStageView.swift
  12. 6
      PadelClub/Views/Match/MatchRowView.swift
  13. 54
      PadelClub/Views/Match/MatchSetupView.swift
  14. 111
      PadelClub/Views/Round/RoundSettingsView.swift
  15. 4
      PadelClub/Views/Round/RoundView.swift
  16. 7
      PadelClub/Views/Round/RoundsView.swift
  17. 60
      PadelClub/Views/Team/TeamPickerView.swift
  18. 24
      PadelClub/Views/Team/TeamRowView.swift
  19. 6
      PadelClub/Views/Tournament/Screen/Components/InscriptionInfoView.swift
  20. 2
      PadelClub/Views/Tournament/Screen/InscriptionManagerView.swift

@ -116,6 +116,7 @@
FF5DA18F2BB9268800A33061 /* GroupStageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */; };
FF5DA1932BB9279B00A33061 /* RoundSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */; };
FF5DA1952BB927E800A33061 /* GenericDestinationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5DA1942BB927E800A33061 /* GenericDestinationPickerView.swift */; };
FF5DA19B2BB9662200A33061 /* TournamentSeedEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */; };
FF6EC8F72B94773200EA7F5A /* RowButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */; };
FF6EC8FB2B94788600EA7F5A /* TournamentButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */; };
FF6EC8FE2B94792300EA7F5A /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6EC8FD2B94792300EA7F5A /* Screen.swift */; };
@ -343,6 +344,7 @@
FF5DA18E2BB9268800A33061 /* GroupStageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupStageSettingsView.swift; sourceTree = "<group>"; };
FF5DA1922BB9279B00A33061 /* RoundSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundSettingsView.swift; sourceTree = "<group>"; };
FF5DA1942BB927E800A33061 /* GenericDestinationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericDestinationPickerView.swift; sourceTree = "<group>"; };
FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentSeedEditing.swift; sourceTree = "<group>"; };
FF6EC8F62B94773100EA7F5A /* RowButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButtonView.swift; sourceTree = "<group>"; };
FF6EC8FA2B94788600EA7F5A /* TournamentButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentButtonView.swift; sourceTree = "<group>"; };
FF6EC8FD2B94792300EA7F5A /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
@ -775,6 +777,7 @@
FF3F74FE2B91A2D4004CFE0E /* AgendaDestination.swift */,
FF4AB6BA2B9256D50002987F /* SearchViewModel.swift */,
FF1CBC1E2BB53E0C0036DAAB /* FederalTournamentSearchScope.swift */,
FF5DA19A2BB9662200A33061 /* TournamentSeedEditing.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -1168,6 +1171,7 @@
FF8F263B2BAD528600650388 /* EventCreationView.swift in Sources */,
FFC1E1082BAC29FC008D6F59 /* LocationManager.swift in Sources */,
FFA1B1292BB71773006CE248 /* PadelClubButtonView.swift in Sources */,
FF5DA19B2BB9662200A33061 /* TournamentSeedEditing.swift in Sources */,
FF70916C2B91005400AB08DA /* TournamentView.swift in Sources */,
FF1DC5552BAB36DD00FD8220 /* CreateClubView.swift in Sources */,
FFC1E10A2BAC2A77008D6F59 /* NetworkFederalService.swift in Sources */,

@ -21,7 +21,7 @@ class GroupStage: ModelObject, Storable {
var matchFormat: MatchFormat {
get {
MatchFormat(rawValue: format ?? 0) ?? .defaultFormatForMatchType(.groupStage)
MatchFormat(rawValue: format) ?? .defaultFormatForMatchType(.groupStage)
}
set {
format = newValue.rawValue

@ -26,6 +26,7 @@ class Match: ModelObject, Storable {
var broadcasted: Bool
var name: String?
var order: Int
private(set) var disabled: Bool = false
internal init(round: String? = nil, groupStage: String? = nil, startDate: Date? = nil, endDate: Date? = nil, index: Int, matchFormat: MatchFormat? = nil, court: String? = nil, servingTeamId: String? = nil, winningTeamId: String? = nil, losingTeamId: String? = nil, broadcasted: Bool = false, name: String? = nil, order: Int = 0) {
self.round = round
@ -46,6 +47,8 @@ class Match: ModelObject, Storable {
func indexInRound() -> Int {
if groupStage != nil {
return index
} else if let index = roundObject?.matches.firstIndex(where: { $0.id == id }) {
return index
}
return RoundRule.matchIndexWithinRound(fromMatchIndex: index)
}
@ -58,26 +61,63 @@ class Match: ModelObject, Storable {
return "#\(indexInRound() + 1)"
}
}
func topPreviousRoundMatches() -> Int {
func disableMatch() {
_toggleMatchDisableState(true)
}
func enableMatch() {
_toggleMatchDisableState(false)
}
fileprivate func _toggleMatchDisableState(_ state: Bool) {
disabled = state
topPreviousRoundMatch()?._toggleMatchDisableState(state)
bottomPreviousRoundMatch()?._toggleMatchDisableState(state)
try? DataStore.shared.matches.addOrUpdate(instance: self)
}
func topPreviousRoundMatchIndex() -> Int {
index * 2 + 1
}
func bottomPreviousRoundMatches() -> Int {
func bottomPreviousRoundMatchIndex() -> Int {
(index + 1) * 2
}
func topPreviousRoundMatch() -> Match? {
guard let roundObject else { return nil }
return Store.main.filter { match in
match.index == topPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id
}.sorted(by: \.index).first
}
func bottomPreviousRoundMatch() -> Match? {
guard let roundObject else { return nil }
return Store.main.filter { match in
match.index == bottomPreviousRoundMatchIndex() && match.round == roundObject.previousRound()?.id
}.sorted(by: \.index).first
}
func previousMatch(_ teamPosition: Int) -> Match? {
if teamPosition == 0 {
return topPreviousRoundMatch()
} else {
return bottomPreviousRoundMatch()
}
}
func previousMatches() -> [Match] {
guard let roundObject else { return [] }
return Store.main.filter { match in
(match.index == topPreviousRoundMatches() || match.index == bottomPreviousRoundMatches())
(match.index == topPreviousRoundMatchIndex() || match.index == bottomPreviousRoundMatchIndex())
&& match.round == roundObject.previousRound()?.id
}.sorted(by: \.index)
}
var matchFormat: MatchFormat {
get {
MatchFormat(rawValue: format ?? 0) ?? .defaultFormatForMatchType(.groupStage)
MatchFormat(rawValue: format) ?? .defaultFormatForMatchType(.groupStage)
}
set {
format = newValue.rawValue
@ -100,6 +140,10 @@ class Match: ModelObject, Storable {
groupStage != nil
}
func isBracket() -> Bool {
round != nil
}
func isTournamentMatch() -> Bool {
groupStageObject?.tournament != nil
}
@ -113,7 +157,7 @@ class Match: ModelObject, Storable {
}
func currentTournament() -> Tournament? {
groupStageObject?.tournamentObject()
groupStageObject?.tournamentObject() ?? roundObject?.tournamentObject()
}
func scores() -> [TeamScore] {
@ -121,7 +165,57 @@ class Match: ModelObject, Storable {
}
func teams() -> [TeamRegistration] {
scores().compactMap({ $0.team }).sorted(by: \.computedPosition)
if groupStage != nil {
return scores().compactMap({ $0.team }).sorted(by: \.groupStagePosition!)
}
return [roundProjectedTeam(.one), roundProjectedTeam(.two)].compactMap { $0 }
}
func groupStageProjectedTeam(_ team: TeamData) -> TeamRegistration? {
guard groupStage != nil else { return nil }
switch team {
case .one:
if let teamId = topPreviousRoundMatch()?.winningTeamId {
return Store.main.findById(teamId)
}
case .two:
if let teamId = bottomPreviousRoundMatch()?.winningTeamId {
return Store.main.findById(teamId)
}
}
return nil
}
func seed(_ team: TeamData) -> TeamRegistration? {
guard let roundObject else { return nil }
return Store.main.filter(isIncluded: {
$0.tournament == roundObject.tournament && $0.bracketPosition != nil
}).first(where: {
($0.bracketPosition! / 2) == self.index
&& ($0.bracketPosition! % 2) == team.rawValue
})
}
func roundProjectedTeam(_ team: TeamData) -> TeamRegistration? {
guard round != nil else { return nil }
if let seed = seed(team) {
return seed
}
switch team {
case .one:
if let teamId = topPreviousRoundMatch()?.winningTeamId {
return Store.main.findById(teamId)
}
case .two:
if let teamId = bottomPreviousRoundMatch()?.winningTeamId {
return Store.main.findById(teamId)
}
}
return nil
}
func teamWon(_ team: TeamData) -> Bool {
@ -129,16 +223,25 @@ class Match: ModelObject, Storable {
}
func team(_ team: TeamData) -> TeamRegistration? {
switch team {
case .one:
teams().first
case .two:
teams().last
if groupStage != nil {
switch team {
case .one:
return teams().first
case .two:
return teams().last
}
} else {
switch team {
case .one:
return roundProjectedTeam(.one)
case .two:
return roundProjectedTeam(.two)
}
}
}
func teamNames(_ team: TeamData) -> [String]? {
self.team(team)?.players().map { $0.lastName }
self.team(team)?.players().map { $0.playerLabel() }
}
func teamWalkOut(_ team: TeamData) -> Bool {
@ -207,5 +310,6 @@ class Match: ModelObject, Storable {
case _broadcasted = "broadcasted"
case _name = "name"
case _order = "order"
case _disabled = "disabled"
}
}

@ -89,6 +89,10 @@ class PlayerRegistration: ModelObject, Storable {
[firstName.capitalized, lastName.capitalized, licenceId].compactMap({ $0 }).joined(separator: " ")
}
func isPlaying() -> Bool {
team()?.isPlaying() == true
}
func contains(_ searchField: String) -> Bool {
firstName.localizedCaseInsensitiveContains(searchField) || lastName.localizedCaseInsensitiveContains(searchField)
}

@ -18,13 +18,23 @@ class Round: ModelObject, Storable {
var loser: String?
var format: Int?
internal init(tournament: String, index: Int, loser: String? = nil, format: Int? = nil) {
internal init(tournament: String, index: Int, loser: String? = nil, matchFormat: MatchFormat? = nil) {
self.tournament = tournament
self.index = index
self.loser = loser
self.format = format
self.format = matchFormat?.rawValue
}
var matchFormat: MatchFormat {
get {
MatchFormat(rawValue: format) ?? .defaultFormatForMatchType(.bracket)
}
set {
format = newValue.rawValue
}
}
func hasStarted() -> Bool {
matches.anySatisfy({ $0.hasStarted() })
}
@ -32,9 +42,13 @@ class Round: ModelObject, Storable {
func hasEnded() -> Bool {
matches.allSatisfy({ $0.hasEnded() })
}
func tournamentObject() -> Tournament? {
Store.main.findById(tournament)
}
var matches: [Match] {
Store.main.filter { $0.round == self.id }
Store.main.filter { $0.round == self.id && $0.disabled == false }
}
func previousRound() -> Round? {

@ -48,6 +48,23 @@ class TeamRegistration: ModelObject, Storable {
self.category = category
}
func isSeedable() -> Bool {
bracketPosition == nil && groupStage == nil
}
func setSeedPosition(inSpot match: Match, upperBranch: Int?, opposingSeeding: Bool) {
let matchIndex = match.index
let seedRound = RoundRule.roundIndex(fromMatchIndex: matchIndex)
let numberOfMatches = RoundRule.numberOfMatches(forRoundIndex: seedRound)
let isUpper = RoundRule.matchIndexWithinRound(fromMatchIndex: matchIndex) < (numberOfMatches / 2)
var teamPosition = upperBranch ?? (isUpper ? 0 : 1)
if opposingSeeding {
teamPosition = upperBranch ?? (isUpper ? 1 : 0)
}
match.previousMatch(teamPosition)?.disableMatch()
bracketPosition = matchIndex * 2 + teamPosition
}
var initialWeight: Int {
lockWeight ?? weight
}
@ -68,6 +85,18 @@ class TeamRegistration: ModelObject, Storable {
wildCardBracket || wildCardGroupStage
}
func isPlaying() -> Bool {
currentMatch() != nil
}
func currentMatch() -> Match? {
teamScores().compactMap { $0.matchObject() }.first(where: { $0.isRunning() })
}
func teamScores() -> [TeamScore] {
Store.main.filter(isIncluded: { $0.teamRegistration == id })
}
var tournamentCategory: TournamentCategory {
get {
TournamentCategory(rawValue: category ?? 0) ?? .men
@ -134,6 +163,18 @@ class TeamRegistration: ModelObject, Storable {
groupStagePosition ?? -1
}
func available() -> Bool {
groupStage == nil && bracketPosition == nil
}
func inGroupStage() -> Bool {
groupStagePosition != nil
}
func inRound() -> Bool {
bracketPosition != nil
}
func resetPositions() {
groupStage = nil
groupStagePosition = nil
@ -234,6 +275,11 @@ class TeamRegistration: ModelObject, Storable {
tournamentObject()?.unrankValue(for: malePlayer) ?? 100_000
}
func groupStageObject() -> GroupStage? {
guard let groupStage else { return nil }
return Store.main.findById(groupStage)
}
func tournamentObject() -> Tournament? {
Store.main.findById(tournament)
}

@ -30,6 +30,9 @@ class TeamScore: ModelObject, Storable {
self.luckyLoser = luckyLoser
}
func matchObject() -> Match? {
Store.main.findById(match)
}
var team: TeamRegistration? {
guard let teamRegistration else {

@ -46,10 +46,7 @@ class Tournament : ModelObject, Storable {
@ObservationIgnored
var navigationPath: [Screen] = []
@ObservationIgnored
var undoManager: Int = 0
internal init(event: String? = nil, creator: String? = nil, name: String? = nil, startDate: Date = Date(), endDate: Date? = nil, creationDate: Date = Date(), isPrivate: Bool = true, groupStageFormat: Int? = nil, roundFormat: Int? = nil, loserRoundFormat: Int? = nil, groupStageSortMode: GroupStageOrderingMode, groupStageCount: Int = 4, rankSourceDate: Date? = nil, dayDuration: Int = 1, teamCount: Int = 24, teamSorting: TeamSortingType? = nil, federalCategory: TournamentCategory, federalLevelCategory: TournamentLevel, federalAgeCategory: FederalTournamentAge, groupStageCourtCount: Int? = nil, seedCount: Int = 8, closedRegistrationDate: Date? = nil, groupStageAdditionalQualified: Int = 0, courtCount: Int = 2, prioritizeClubMembers: Bool = false, qualifiedPerGroupStage: Int = 1, teamsPerGroupStage: Int = 4, entryFee: Double? = nil, maleUnrankedValue: Int? = nil, femaleUnrankedValue: Int? = nil) {
self.event = event
self.creator = creator
@ -131,6 +128,115 @@ class Tournament : ModelObject, Storable {
return .initial
}
func seeds() -> [TeamRegistration] {
let seeds = max(teamCount - groupStageCount * teamsPerGroupStage, 0)
return Array(selectedSortedTeams().prefix(seeds))
}
func availableSeeds() -> [TeamRegistration] {
return seeds().filter { $0.isSeedable() }
}
func lastSeedRound() -> Int? {
if let last = seeds().filter({ $0.bracketPosition != nil }).last {
return RoundRule.roundIndex(fromMatchIndex: last.bracketPosition! / 2)
} else {
return nil
}
}
func getRound(atRoundIndex roundIndex: Int) -> Round? {
Store.main.filter(isIncluded: { $0.tournament == id && $0.index == roundIndex }).first
}
func availableSeedSpot(inRoundIndex roundIndex: Int) -> [Match] {
getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 0 } ?? []
}
func availableSeedOpponentSpot(inRoundIndex roundIndex: Int) -> [Match] {
getRound(atRoundIndex: roundIndex)?.matches.filter { $0.teams().count == 1 } ?? []
}
func availableSeedGroups() -> [SeedInterval] {
let seeds = seeds()
var availableSeedGroup = Set<SeedInterval>()
for (index, seed) in seeds.enumerated() {
if seed.isSeedable(), let seedGroup = seedGroup(for: index) {
availableSeedGroup.insert(seedGroup)
}
}
return availableSeedGroup.sorted(by: <)
}
func seedGroup(for alreadySetupSeeds: Int) -> SeedInterval? {
switch alreadySetupSeeds {
case 0...1:
return SeedInterval(first: 1, last: 2)
case 2...3:
return SeedInterval(first: 3, last: 4)
case 4...7:
return SeedInterval(first: 5, last: 8)
case 8...15:
// if 16 - 9 > availableSeeds().count {
// switch alreadySetupSeeds {
// case 8...15:
// return SeedInterval(first: 5, last: 8)
// case 8...15:
// return SeedInterval(first: 5, last: 8)
// }
return SeedInterval(first: 9, last: 16)
case 16...23:
return SeedInterval(first: 17, last: 24)
case 24...31:
return SeedInterval(first: 25, last: 32)
default:
return nil
}
}
func availableSeedGroup() -> SeedInterval? {
let seeds = seeds()
if let firstIndex = seeds.firstIndex(where: { $0.isSeedable() }) {
guard let seedGroup = seedGroup(for: firstIndex) else { return nil }
return seedGroup
}
return nil
}
func randomSeed(fromSeedGroup seedGroup: SeedInterval) -> TeamRegistration? {
let availableSeeds = seeds(inSeedGroup: seedGroup)
return availableSeeds.randomElement()
}
func seeds(inSeedGroup seedGroup: SeedInterval) -> [TeamRegistration] {
let availableSeedInSeedGroup = (seedGroup.last - seedGroup.first) + 1
let availableSeeds = seeds().dropFirst(seedGroup.first - 1).prefix(availableSeedInSeedGroup).filter({ $0.isSeedable() })
return availableSeeds
}
func setSeeds(inRoundIndex roundIndex: Int, inSeedGroup seedGroup: SeedInterval) {
let availableSeedSpot = availableSeedSpot(inRoundIndex: roundIndex)
let availableSeedOpponentSpot = availableSeedOpponentSpot(inRoundIndex: roundIndex)
let availableSeeds = seeds(inSeedGroup: seedGroup)
if availableSeeds.count <= availableSeedSpot.count {
let spots = availableSeedSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: false)
}
} else if (availableSeeds.count <= availableSeedOpponentSpot.count && availableSeeds.count == self.availableSeeds().count) {
let spots = availableSeedOpponentSpot.shuffled()
for (index, seed) in availableSeeds.enumerated() {
seed.setSeedPosition(inSpot: spots[index], upperBranch: nil, opposingSeeding: true)
}
} else if let chunk = seedGroup.chunk() {
setSeeds(inRoundIndex: roundIndex, inSeedGroup: chunk)
}
}
func inscriptionClosed() -> Bool {
closedRegistrationDate != nil
}
@ -260,11 +366,6 @@ class Tournament : ModelObject, Storable {
var clubName: String? {
nil
}
//todo
func roundCount() -> Int {
4
}
//todo
func significantPlayerCount() -> Int {
@ -590,7 +691,7 @@ class Tournament : ModelObject, Storable {
var matchFormat: MatchFormat {
get {
MatchFormat(rawValue: roundFormat ?? 0) ?? .defaultFormatForMatchType(.bracket)
MatchFormat(rawValue: roundFormat) ?? .defaultFormatForMatchType(.bracket)
}
set {
roundFormat = newValue.rawValue
@ -599,7 +700,7 @@ class Tournament : ModelObject, Storable {
var groupStageMatchFormat: MatchFormat {
get {
MatchFormat(rawValue: groupStageFormat ?? 0) ?? .defaultFormatForMatchType(.groupStage)
MatchFormat(rawValue: groupStageFormat) ?? .defaultFormatForMatchType(.groupStage)
}
set {
groupStageFormat = newValue.rawValue
@ -608,7 +709,7 @@ class Tournament : ModelObject, Storable {
var loserBracketMatchFormat: MatchFormat {
get {
MatchFormat(rawValue: loserRoundFormat ?? 0) ?? .defaultFormatForMatchType(.loserBracket)
MatchFormat(rawValue: loserRoundFormat) ?? .defaultFormatForMatchType(.loserBracket)
}
set {
loserRoundFormat = newValue.rawValue
@ -662,8 +763,7 @@ class Tournament : ModelObject, Storable {
}
func loserBracketSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let idx = roundCount() - roundIndex
let format = tournamentLevel.federalFormatForLoserBracketRound(idx)
let format = tournamentLevel.federalFormatForLoserBracketRound(roundIndex)
if loserBracketMatchFormat.rank > format.rank {
return format
} else {
@ -681,8 +781,7 @@ class Tournament : ModelObject, Storable {
}
func roundSmartMatchFormat(_ roundIndex: Int) -> MatchFormat {
let idx = roundCount() - roundIndex
let format = tournamentLevel.federalFormatForBracketRound(idx)
let format = tournamentLevel.federalFormatForBracketRound(roundIndex)
if matchFormat.rank > format.rank {
return format
} else {

@ -967,6 +967,11 @@ enum MatchFormat: Int, Hashable, Codable, CaseIterable {
case twoSetsOfFourGamesDecisivePoint
case nineGamesDecisivePoint
init?(rawValue: Int?) {
guard let value = rawValue else { return nil }
self.init(rawValue: value)
}
var weight: Int {
switch self {
case .twoSets, .twoSetsDecisivePoint:
@ -1402,10 +1407,22 @@ enum RoundRule {
Int(log2(Double(teamsInFirstRound(forTeams: teams))))
}
static func matchIndex(fromRoundIndex roundIndex: Int) -> Int {
guard roundIndex >= 0 else {
return -1 // Invalid round index
}
return (1 << roundIndex) - 1
}
static func roundIndex(fromMatchIndex matchIndex: Int) -> Int {
Int(log2(Double(matchIndex + 1)))
}
static func numberOfMatches(forRoundIndex roundIndex: Int) -> Int {
Int(pow(2.0, Double(roundIndex)))
}
static func matchIndexWithinRound(fromMatchIndex matchIndex: Int) -> Int {
let roundIndex = roundIndex(fromMatchIndex: matchIndex)
let matchIndexWithinRound = matchIndex - (Int(pow(2.0, Double(roundIndex))) - 1)

@ -0,0 +1,29 @@
//
// tournamentSeedEditing.swift
// PadelClub
//
// Created by Razmig Sarkissian on 31/03/2024.
//
import Foundation
import SwiftUI
// Create an environment key
private struct TournamentSeedEditing: EnvironmentKey {
static let defaultValue: Bool = false
}
// ## Introduce new value to EnvironmentValues
extension EnvironmentValues {
var isEditingTournamentSeed: Bool {
get { self[TournamentSeedEditing.self] }
set { self[TournamentSeedEditing.self] = newValue }
}
}
// Add a dedicated modifier (Optional)
extension View {
func editTournamentSeed(_ value: Bool) -> some View {
environment(\.isEditingTournamentSeed, value)
}
}

@ -196,7 +196,7 @@ struct GroupStageView: View {
if groupStage.matches.isEmpty == false {
Section {
ForEach(groupStage.matches) { match in
MatchRowView(match: match, setupSeedContext: false, matchViewStyle: .sectionedStandardStyle)
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
}
} header: {
Text("Matchs de la " + groupStage.groupStageTitle())

@ -9,12 +9,12 @@ import SwiftUI
struct MatchRowView: View {
var match: Match
let setupSeedContext: Bool
let matchViewStyle: MatchViewStyle
@Environment(\.isEditingTournamentSeed) private var isEditingTournamentSeed
@ViewBuilder
var body: some View {
if setupSeedContext {
if isEditingTournamentSeed && match.isGroupStage() == false {
MatchSetupView(match: match)
} else {
NavigationLink {
@ -29,5 +29,5 @@ struct MatchRowView: View {
#Preview {
MatchRowView(match: Match.mock(), setupSeedContext: false, matchViewStyle: .standardStyle)
MatchRowView(match: Match.mock(), matchViewStyle: .standardStyle)
}

@ -8,28 +8,62 @@
import SwiftUI
struct MatchSetupView: View {
@EnvironmentObject var dataStore: DataStore
var match: Match
@State private var seedGroup: SeedInterval?
@ViewBuilder
var body: some View {
HStack {
VStack(alignment: .leading) {
_teamView(match.team(.one), index: 0)
_teamView(match.team(.two), index: 1)
}
}
_teamView(match.team(.one), teamPosition: 0)
_teamView(match.team(.two), teamPosition: 1)
}
@ViewBuilder
func _teamView(_ team: TeamRegistration?, index: Int) -> some View {
func _teamView(_ team: TeamRegistration?, teamPosition: Int) -> some View {
if let team {
TeamDetailView(team: team)
TeamRowView(team: team, teamPosition: teamPosition)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .cancel) {
team.bracketPosition = nil
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
} label: {
Label("retirer", systemImage: "xmark")
}
}
} else {
TeamPickerView(match: match, index: match.index*2 + 1 + index)
.disabled(match.groupStage != nil)
HStack {
TeamPickerView(teamPicked: { team in
print(team.pasteData())
team.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false)
try? dataStore.matches.addOrUpdate(instance: match)
try? dataStore.teamRegistrations.addOrUpdate(instance: team)
})
if let tournament = match.currentTournament() {
Menu {
ForEach(tournament.availableSeedGroups(), id: \.self) { seedGroup in
Button {
if let randomTeam = tournament.randomSeed(fromSeedGroup: seedGroup) {
randomTeam.setSeedPosition(inSpot: match, upperBranch: teamPosition, opposingSeeding: false)
try? dataStore.matches.addOrUpdate(instance: match)
try? dataStore.teamRegistrations.addOrUpdate(instance: randomTeam)
}
} label: {
Label(seedGroup.localizedLabel(), systemImage: "dice")
}
}
} label: {
Text("Tirage").tag(nil as SeedInterval?)
}
}
}
.fixedSize(horizontal: false, vertical: true)
.buttonBorderShape(.capsule)
.buttonStyle(.borderedProminent)
}
}
}
#Preview {
MatchSetupView(match: Match.mock())
.environmentObject(DataStore.shared)
}

@ -8,15 +8,124 @@
import SwiftUI
struct RoundSettingsView: View {
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@Binding var isEditingTournamentSeed: Bool
@State private var roundIndex: Int?
var round: Round? {
guard let roundIndex else { return nil }
return tournament.rounds()[roundIndex]
}
var body: some View {
List {
Toggle("Éditer les têtes de série", isOn: $isEditingTournamentSeed)
Section {
RowButtonView(title: "Retirer toutes les têtes de séries") {
tournament.unsortedTeams().forEach({ $0.bracketPosition = nil })
}
}
Section {
if let lastRound = tournament.rounds().first { // first is final, last round
RowButtonView(title: "Supprimer " + lastRound.roundTitle()) {
try? dataStore.rounds.delete(instance: lastRound)
}
}
}
Section {
let roundIndex = tournament.rounds().count
RowButtonView(title: "Ajouter " + RoundRule.roundName(fromRoundIndex: roundIndex)) {
let round = Round(tournament: tournament.id, index: roundIndex, matchFormat: tournament.matchFormat)
let matchCount = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
let matchStartIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
let matches = (0..<matchCount).map { //0 is final match
return Match(round: round.id, index: $0 + matchStartIndex, matchFormat: round.matchFormat)
}
try? dataStore.rounds.addOrUpdate(instance: round)
try? dataStore.matches.addOrUpdate(contentOfs: matches)
}
}
if let availableSeedGroup = tournament.availableSeedGroup() {
Section {
Picker(selection: $roundIndex) {
Text("choisir de la manche").tag(nil as Int?)
ForEach(tournament.rounds()) { round in
Text(round.roundTitle()).tag(round.index as Int?)
}
} label: {
Text(availableSeedGroup.localizedLabel())
}
if let roundIndex {
RowButtonView(title: "Valider") {
if availableSeedGroup == SeedInterval(first: 1, last: 2) {
let seeds = tournament.seeds()
// let startIndex = RoundRule.matchIndex(fromRoundIndex: roundIndex)
// let numberOfMatchInRound = RoundRule.numberOfMatches(forRoundIndex: roundIndex)
// let lastIndex = startIndex + numberOfMatchInRound - 1
// seeds.prefix(1).first?.bracketPosition = lastIndex * 2 + 1 //TS 1 branche du bas du dernier match
// seeds.prefix(2).dropFirst().first?.bracketPosition = startIndex * 2 //TS 2 branche du haut du premier match
if let matches = tournament.getRound(atRoundIndex: roundIndex)?.matches {
if let lastMatch = matches.last {
seeds.prefix(1).first?.setSeedPosition(inSpot: lastMatch, upperBranch: 1, opposingSeeding: false)
}
if let firstMatch = matches.first {
seeds.prefix(2).dropFirst().first?.setSeedPosition(inSpot: firstMatch, upperBranch: 0, opposingSeeding: false)
}
}
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: seeds)
} else {
tournament.setSeeds(inRoundIndex: roundIndex, inSeedGroup: availableSeedGroup)
try? dataStore.teamRegistrations.addOrUpdate(contentOfs: tournament.seeds())
}
}
}
} header: {
Text("Placement des têtes de série")
}
}
}
}
}
#Preview {
RoundSettingsView()
RoundSettingsView(isEditingTournamentSeed: .constant(true))
.environment(Tournament.mock())
.environmentObject(DataStore.shared)
}
struct SeedInterval: Hashable, Comparable {
let first: Int
let last: Int
static func <(lhs: SeedInterval, rhs: SeedInterval) -> Bool {
return lhs.first < rhs.first
}
func chunk() -> SeedInterval? {
if last - (last - first) / 2 > first {
return SeedInterval(first: first, last: last - (last - first) / 2)
} else {
return nil
}
}
}
extension SeedInterval {
func localizedLabel(_ displayStyle: DisplayStyle = .wide) -> String {
if last - first < 2 {
return "#\(first) / #\(last)"
} else {
return "#\(first) à #\(last)"
}
}
}

@ -14,9 +14,9 @@ struct RoundView: View {
List {
ForEach(round.matches) { match in
Section {
MatchRowView(match: match, setupSeedContext: false, matchViewStyle: .sectionedStandardStyle)
MatchRowView(match: match, matchViewStyle: .sectionedStandardStyle)
} header: {
Text(match.matchTitle())
Text(round.roundTitle(.wide) + " " + match.matchTitle(.short))
}
}
}

@ -10,10 +10,14 @@ import SwiftUI
struct RoundsView: View {
var tournament: Tournament
@State private var selectedRound: Round?
@State private var isEditingTournamentSeed = false
init(tournament: Tournament) {
self.tournament = tournament
_selectedRound = State(wrappedValue: tournament.getActiveRound())
if tournament.availableSeeds().isEmpty == false {
_isEditingTournamentSeed = State(wrappedValue: true)
}
}
var body: some View {
@ -21,11 +25,12 @@ struct RoundsView: View {
GenericDestinationPickerView(selectedDestination: $selectedRound, destinations: tournament.rounds(), nilDestinationIsValid: true)
switch selectedRound {
case .none:
RoundSettingsView()
RoundSettingsView(isEditingTournamentSeed: $isEditingTournamentSeed)
.navigationTitle("Réglages")
case .some(let selectedRound):
RoundView(round: selectedRound)
.navigationTitle(selectedRound.roundTitle())
.editTournamentSeed(isEditingTournamentSeed)
}
}
.navigationBarTitleDisplayMode(.inline)

@ -8,15 +8,67 @@
import SwiftUI
struct TeamPickerView: View {
var match: Match
var index: Int
@EnvironmentObject var dataStore: DataStore
@Environment(Tournament.self) var tournament: Tournament
@Environment(\.dismiss) private var dismiss
@State private var presentTeamPickerView: Bool = false
@State private var searchField: String = ""
let teamPicked: ((TeamRegistration) -> (Void))
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
Button("Choisir") {
presentTeamPickerView = true
}
.sheet(isPresented: $presentTeamPickerView) {
NavigationStack {
List {
let teams = tournament.sortedTeams()
Section {
_teamListView(teams.filter({ $0.available() }).sorted(by: \.weight).reversed())
} header: {
Text("Disponible")
}
Section {
_teamListView(teams.filter({ $0.inGroupStage() }).sorted(by: \.groupStagePosition!).reversed())
} header: {
Text("Déjà placée en poule")
}
Section {
_teamListView(teams.filter({ $0.inRound() }).sorted(by: \.bracketPosition!).reversed())
} header: {
Text("Déjà placée dans le tableau")
}
}
.searchable(text: $searchField, placement: .navigationBarDrawer(displayMode: .always))
.keyboardType(.alphabet)
.autocorrectionDisabled()
.navigationTitle("Choisir une équipe")
.toolbarBackground(.visible, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline)
}
}
}
private func _teamListView(_ teams: [TeamRegistration]) -> some View {
ForEach(teams) { team in
if searchField.isEmpty || team.contains(searchField) {
Button {
teamPicked(team)
presentTeamPickerView = false
} label: {
TeamRowView(team: team)
}
.buttonStyle(.plain)
}
}
}
}
#Preview {
TeamPickerView(match: Match.mock(), index: 0)
TeamPickerView(teamPicked: { team in
})
.environment(Tournament.mock())
.environmentObject(DataStore.shared)
}

@ -8,11 +8,29 @@
import SwiftUI
struct TeamRowView: View {
@EnvironmentObject var dataStore: DataStore
var team: TeamRegistration
var teamPosition: Int? = nil
var body: some View {
TeamDetailView(team: team)
LabeledContent {
VStack(alignment: .trailing, spacing: 0) {
if teamPosition == 0 || teamPosition == nil {
Text(team.weight.formatted())
.font(.caption)
}
if let teams = team.tournamentObject()?.selectedSortedTeams(), let index = team.index(in: teams) {
Text("#" + (index + 1).formatted())
.font(.title)
}
if teamPosition == 1 {
Text(team.weight.formatted())
.font(.caption)
}
}
} label: {
Text(team.teamLabel(.short))
}
}
}

@ -26,7 +26,7 @@ struct InscriptionInfoView: View {
Section {
DisclosureGroup {
ForEach(waitingListInBracket) { team in
TeamRowView(team: team)
TeamDetailView(team: team)
}
} label: {
LabeledContent {
@ -39,7 +39,7 @@ struct InscriptionInfoView: View {
DisclosureGroup {
ForEach(waitingListInGroupStage) { team in
TeamRowView(team: team)
TeamDetailView(team: team)
}
} label: {
LabeledContent {
@ -126,7 +126,7 @@ struct InscriptionInfoView: View {
Section {
DisclosureGroup {
ForEach(playersMissing) {
TeamRowView(team: $0)
TeamDetailView(team: $0)
}
} label: {
LabeledContent {

@ -232,7 +232,7 @@ struct InscriptionManagerView: View {
ForEach(teams) { team in
let teamIndex = team.index(in: unfilteredTeams)
Section {
TeamRowView(team: team)
TeamDetailView(team: team)
} header: {
_teamHeaderView(team, teamIndex: teamIndex)
} footer: {

Loading…
Cancel
Save