diff --git a/PadelClub/Data/Match.swift b/PadelClub/Data/Match.swift index ee4cd30..69baa1e 100644 --- a/PadelClub/Data/Match.swift +++ b/PadelClub/Data/Match.swift @@ -151,6 +151,13 @@ class Match: ModelObject, Storable { func next() -> Match? { Store.main.filter(isIncluded: { $0.round == round && $0.index == index + 1 }).first } + + func roundTitle() -> String? { + if groupStage != nil { return "Poule" } + else if let roundObject { return roundObject.roundTitle() } + else { return nil } + } + func topPreviousRoundMatchIndex() -> Int { index * 2 + 1 } diff --git a/PadelClub/ViewModel/MatchScheduler.swift b/PadelClub/ViewModel/MatchScheduler.swift index 5778eca..7919d25 100644 --- a/PadelClub/ViewModel/MatchScheduler.swift +++ b/PadelClub/ViewModel/MatchScheduler.swift @@ -7,11 +7,34 @@ import Foundation +struct GroupStageTimeMatch { + let matchID: String + let rotationIndex: Int + var courtIndex: Int + let groupIndex: Int +} + struct TimeMatch { let matchID: String let rotationIndex: Int var courtIndex: Int let groupIndex: Int + var startDate: Date + var durationLeft: Int //in minutes + var minimumBreakTime: Int //in minutes + var courtLocked: Bool = false + + 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 { @@ -34,7 +57,7 @@ extension Match { class MatchScheduler { static let shared = MatchScheduler() - func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> MatchDispatcher { + func groupStageDispatcher(numberOfCourtsAvailablePerRotation: Int, groupStages: [GroupStage], startingDate: Date?, randomizeCourts: Bool) -> GroupStageMatchDispatcher { let _groupStages = groupStages.filter { startingDate == nil || $0.startDate == startingDate } @@ -50,7 +73,7 @@ class MatchScheduler { } } - var slots = [TimeMatch]() + var slots = [GroupStageTimeMatch]() var availableMatchs = flattenedMatches var rotationIndex = 0 var teamsPerRotation = [Int: [String]]() @@ -81,7 +104,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,7 +120,7 @@ class MatchScheduler { rotationIndex += 1 } - var organizedSlots = [TimeMatch]() + var organizedSlots = [GroupStageTimeMatch]() for i in 0.. Bool { + func roundMatchCanBePlayed(_ match: Match, roundObject: Round, slots: [TimeMatch], rotationIndex: Int, targetedStartDate: Date) -> Bool { print(roundObject.roundTitle(), match.matchTitle()) let previousMatches = roundObject.precedentMatches(ofMatch: match) if previousMatches.isEmpty { return true } @@ -135,34 +159,91 @@ class MatchScheduler { return false } - let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex + (roundObject.loser == nil ? 1 : 0) < rotationIndex }) - return previousMatchIsInPreviousRotation +// if roundObject.isLoserBracket() { +// let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex < rotationIndex }) +// return previousMatchIsInPreviousRotation +// } + + let previousMatchIsInPreviousRotation = previousMatchSlots.allSatisfy({ $0.rotationIndex < rotationIndex }) + guard let minimumPossibleEndDate = previousMatchSlots.map({ $0.estimatedEndDate(includeBreakTime: true) }).max() else { + return previousMatchIsInPreviousRotation + } + + return targetedStartDate >= minimumPossibleEndDate + } + + func getAvailableCourt(inSlots slots: [TimeMatch], nextStartDate: Date) -> [TimeMatch] { + guard let minimumDuration = slots.compactMap({ $0.durationLeft }).min() else { return [] } + var newSlots = [TimeMatch]() + slots.forEach { timeMatch in + let durationLeft = timeMatch.durationLeft + if durationLeft - minimumDuration > 0 { + let timeMatch = TimeMatch(matchID: timeMatch.matchID, rotationIndex: timeMatch.rotationIndex + 1, courtIndex: timeMatch.courtIndex, groupIndex: timeMatch.groupIndex, startDate: nextStartDate, durationLeft: durationLeft, minimumBreakTime: timeMatch.minimumBreakTime, courtLocked: true) + newSlots.append(timeMatch) + } + } + return newSlots } - func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0) -> MatchDispatcher { + func getNextStartDate(fromPreviousRotationSlots slots: [TimeMatch], includeBreakTime: Bool) -> Date? { + slots.map { $0.estimatedEndDate(includeBreakTime: includeBreakTime) }.min() + } + + func roundDispatcher(numberOfCourtsAvailablePerRotation: Int, flattenedMatches: [Match], randomizeCourts: Bool, initialOccupiedCourt: Int = 0, dispatcherStartDate: Date) -> MatchDispatcher { var slots = [TimeMatch]() var availableMatchs = flattenedMatches var rotationIndex = 0 var freeCourtPerRotation = [Int: [Int]]() var groupLastRotation = [Int: Int]() - - while slots.count < flattenedMatches.count { + var courts = [Int]() + var timeToAdd = 0.0 + while availableMatchs.count > 0 { freeCourtPerRotation[rotationIndex] = [] var matchPerRound = [Int: Int]() var availableCourt = numberOfCourtsAvailablePerRotation if rotationIndex == 0 { availableCourt = availableCourt - initialOccupiedCourt } - (0.. 0 ? freeCourtPerRotation[rotationIndex - 1]!.count : 0 + + if previousRotationSlots.isEmpty && rotationIndex > 0 { + let previousPreviousRotationSlots = slots.filter({ $0.rotationIndex == rotationIndex - 2 }) + rotationStartDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: false) ?? dispatcherStartDate + } else if freeCourtPreviousRotation > 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 }) + if let previousEndDate = getNextStartDate(fromPreviousRotationSlots: previousPreviousRotationSlots, includeBreakTime: true) { + rotationStartDate = previousEndDate + courts = freeCourtPerRotation[rotationIndex - 1]! + } + } + + courts.forEach { courtIndex in //print(mt.map { ($0.bracket!.index.intValue, counts[$0.bracket!.index.intValue]) }) if let first = availableMatchs.first(where: { match in let roundObject = match.roundObject! - let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex) + let canBePlayed = roundMatchCanBePlayed(match, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate) 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) { + if canBePlayed && roundMatchCanBePlayed(nextMatch, roundObject: roundObject, slots: slots, rotationIndex: rotationIndex, targetedStartDate: rotationStartDate) { return true } else { return false @@ -184,11 +265,13 @@ 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, groupIndex: first.roundObject!.index, 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 } + timeToAdd = 0.0 } else { freeCourtPerRotation[rotationIndex]!.append(courtIndex) } @@ -199,7 +282,7 @@ class MatchScheduler { var organizedSlots = [TimeMatch]() for i in 0.. String { + self.groupStageTitle() + } +} +extension Round: Schedulable { + func titleLabel() -> String { + self.roundTitle() + } +} struct SchedulerView: View { var tournament: Tournament @@ -21,7 +29,7 @@ struct SchedulerView: View { ForEach(tournament.rounds()) { round in _schedulerView(round) ForEach(round.loserRoundsAndChildren()) { loserRound in - if round.isDisabled() == false { + if loserRound.isDisabled() == false { _schedulerView(loserRound) } } @@ -42,12 +50,12 @@ struct SchedulerView: View { GroupStageScheduleEditorView(groupStage: groupStage) } } - .navigationTitle(schedulable.selectionLabel()) + .navigationTitle(schedulable.titleLabel()) } label: { LabeledContent { Text(schedulable.matchFormat.format).font(.largeTitle) } label: { - if let startDate = schedulable.startDate { + if let startDate = schedulable.getStartDate() { Text(startDate.formatted(.dateTime.hour().minute())).font(.largeTitle) Text(startDate.formatted(.dateTime.weekday().day(.twoDigits).month().year())) } else { @@ -56,7 +64,7 @@ struct SchedulerView: View { } } } header: { - Text(schedulable.selectionLabel()) + Text(schedulable.titleLabel()) } } } diff --git a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift index 4fccc36..83795fc 100644 --- a/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift +++ b/PadelClub/Views/Tournament/Screen/TournamentScheduleView.swift @@ -7,12 +7,19 @@ import SwiftUI -protocol Schedulable: Selectable, Identifiable { +protocol Schedulable: Identifiable { var startDate: Date? { get set } var matchFormat: MatchFormat { get set } func playedMatches() -> [Match] + func titleLabel() -> String } +extension Schedulable { + func getStartDate() -> Date? { + startDate ?? playedMatches().first?.startDate + } + +} enum ScheduleDestination: Identifiable, Selectable { var id: String { switch self {