diff --git a/foam-madness.xcodeproj/project.pbxproj b/foam-madness.xcodeproj/project.pbxproj index af94e0d..08a4eef 100644 --- a/foam-madness.xcodeproj/project.pbxproj +++ b/foam-madness.xcodeproj/project.pbxproj @@ -27,11 +27,20 @@ 7AECF9C8245E779E0037E126 /* womensBracketology2020.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7AECF9C7245E779E0037E126 /* womensBracketology2020.plist */; }; 7AEFD6672514704E007BB16B /* ABOUT.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 7AEFD6662514704E007BB16B /* ABOUT.rtf */; }; 7AF31A862431267400DF1605 /* GameHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF31A852431267400DF1605 /* GameHelper.swift */; }; + D1074A982BF13D4200569080 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1074A972BF13D4200569080 /* SettingsView.swift */; }; + D1074A9B2BF13E9300569080 /* SettingBracketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1074A9A2BF13E9300569080 /* SettingBracketView.swift */; }; + D113300D2BE5952B00F6DF30 /* BracketGameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D113300C2BE5952B00F6DF30 /* BracketGameCell.swift */; }; D11E4F8F2BA7410100CA9FDD /* womensBracket2024.plist in Resources */ = {isa = PBXBuildFile; fileRef = D11E4F8D2BA7410100CA9FDD /* womensBracket2024.plist */; }; D11E4F902BA7410100CA9FDD /* mensBracket2024.plist in Resources */ = {isa = PBXBuildFile; fileRef = D11E4F8E2BA7410100CA9FDD /* mensBracket2024.plist */; }; + D12027002BF79FFF00A10EA7 /* SelectNumTeamsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12026FF2BF79FFF00A10EA7 /* SelectNumTeamsView.swift */; }; + D12027022BF7A00B00A10EA7 /* SelectYearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12027012BF7A00B00A10EA7 /* SelectYearView.swift */; }; + D137201D2BF940C00057EA8D /* SelectCustomTeamsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D137201C2BF940C00057EA8D /* SelectCustomTeamsView.swift */; }; + D137201F2BF948290057EA8D /* SeedSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D137201E2BF948290057EA8D /* SeedSelector.swift */; }; D1386BDE2B929752006C676C /* NavigationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1386BDD2B929752006C676C /* NavigationUtil.swift */; }; + D13EEA582BF9903B00F61B51 /* UpdateRegionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D13EEA572BF9903B00F61B51 /* UpdateRegionsView.swift */; }; D14720182B60D22500453304 /* PreviewDataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14720172B60D22500453304 /* PreviewDataController.swift */; }; D147201A2B60D6FC00453304 /* TeamHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14720192B60D6FC00453304 /* TeamHelper.swift */; }; + D1472C202BF086CC00EECAFF /* TournamentGamesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1472C1F2BF086CC00EECAFF /* TournamentGamesView.swift */; }; D14C59E927DED2FE001378F1 /* mensBracket2022.plist in Resources */ = {isa = PBXBuildFile; fileRef = D14C59E827DED2FE001378F1 /* mensBracket2022.plist */; }; D14C59EB27DED30F001378F1 /* womensBracket2022.plist in Resources */ = {isa = PBXBuildFile; fileRef = D14C59EA27DED30F001378F1 /* womensBracket2022.plist */; }; D15865732B574D4D009E486A /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15865722B574D4D009E486A /* MenuView.swift */; }; @@ -45,23 +54,31 @@ D15865842B575118009E486A /* GameStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15865832B575118009E486A /* GameStatsView.swift */; }; D15865882B575176009E486A /* SelectInitialBracketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15865872B575176009E486A /* SelectInitialBracketView.swift */; }; D158658A2B575185009E486A /* BracketCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15865892B575185009E486A /* BracketCreationView.swift */; }; - D158658C2B575197009E486A /* TournamentGamesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D158658B2B575197009E486A /* TournamentGamesView.swift */; }; + D158658C2B575197009E486A /* TournamentListGamesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D158658B2B575197009E486A /* TournamentListGamesView.swift */; }; D15865902B5751BE009E486A /* SelectTournamentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D158658F2B5751BE009E486A /* SelectTournamentView.swift */; }; D15865922B5751CE009E486A /* TournamentStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15865912B5751CE009E486A /* TournamentStatsView.swift */; }; D15865952B575ECE009E486A /* PrimaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15865942B575ECE009E486A /* PrimaryButtonStyle.swift */; }; D158B32D2B9A96720002D2F6 /* GameStatsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D158B32C2B9A96720002D2F6 /* GameStatsController.swift */; }; + D16485722BE9D7AC006BB362 /* BracketWinnerLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16485712BE9D7AB006BB362 /* BracketWinnerLine.swift */; }; + D16BEEB42BF7E605008D06B0 /* SelectInitialBracketShellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16BEEB32BF7E605008D06B0 /* SelectInitialBracketShellView.swift */; }; D17FF6012BA140DA00149D63 /* SearchTeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17FF6002BA140DA00149D63 /* SearchTeamView.swift */; }; D17FF6052BA1454900149D63 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D17FF6042BA1454900149D63 /* SearchBar.swift */; }; + D181D2872BB529340067B4F1 /* BracketIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D181D2862BB529340067B4F1 /* BracketIcon.swift */; }; + D181D28A2BB537D80067B4F1 /* BracketGamesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D181D2892BB537D80067B4F1 /* BracketGamesView.swift */; }; D18D756E29BEA5AC00DE609B /* mensBracket2023.plist in Resources */ = {isa = PBXBuildFile; fileRef = D18D756D29BEA5AC00DE609B /* mensBracket2023.plist */; }; D18D757029BEA5B500DE609B /* womensBracket2023.plist in Resources */ = {isa = PBXBuildFile; fileRef = D18D756F29BEA5B500DE609B /* womensBracket2023.plist */; }; D199087B2B996D6200A57746 /* ShootModeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D199087A2B996D6200A57746 /* ShootModeController.swift */; }; D199087D2B99703500A57746 /* SaveHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D199087C2B99703500A57746 /* SaveHelper.swift */; }; + D19AF8AC2BF69808003A1273 /* CreateCustomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19AF8AB2BF69808003A1273 /* CreateCustomView.swift */; }; + D19AF8B02BF69F7D003A1273 /* CustomTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19AF8AF2BF69F7D003A1273 /* CustomTypeView.swift */; }; + D19AF8B22BF69FFE003A1273 /* ListCustomTeamsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19AF8B12BF69FFE003A1273 /* ListCustomTeamsView.swift */; }; D1A3E0092B76F94D001E461A /* BracketHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A3E0082B76F94D001E461A /* BracketHelper.swift */; }; D1A67BE629BD48E200E1D55B /* foam_madness_bracket_file_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A67BE529BD48E200E1D55B /* foam_madness_bracket_file_tests.swift */; }; D1A67BED29BD631D00E1D55B /* foam_madness_probability_file_test.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A67BEC29BD631D00E1D55B /* foam_madness_probability_file_test.swift */; }; D1A67BEF29BD6E4D00E1D55B /* foam_madness_teams_file_test.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A67BEE29BD6E4D00E1D55B /* foam_madness_teams_file_test.swift */; }; D1A8B19E2B9FD8A500354C9F /* MailHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A8B19D2B9FD8A500354C9F /* MailHandler.swift */; }; - D1A8B1A02B9FF03100354C9F /* TournamentGameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A8B19F2B9FF03100354C9F /* TournamentGameCell.swift */; }; + D1A8B1A02B9FF03100354C9F /* TournamentListGameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A8B19F2B9FF03100354C9F /* TournamentListGameCell.swift */; }; + D1BF49392BA805AA00A00669 /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BF49382BA805AA00A00669 /* AppConstants.swift */; }; D1C757A12B9A62C500817E1A /* TournamentStatsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C757A02B9A62C500817E1A /* TournamentStatsController.swift */; }; D1D4F9C32B97E4D0004B9C18 /* BracketCreationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D4F9C22B97E4D0004B9C18 /* BracketCreationController.swift */; }; D1E16E1128334BB0007280EC /* bracketIndex.plist in Resources */ = {isa = PBXBuildFile; fileRef = D1E16E1028334BB0007280EC /* bracketIndex.plist */; }; @@ -112,11 +129,20 @@ 7AECF9C7245E779E0037E126 /* womensBracketology2020.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = womensBracketology2020.plist; sourceTree = ""; }; 7AEFD6662514704E007BB16B /* ABOUT.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = ABOUT.rtf; sourceTree = ""; }; 7AF31A852431267400DF1605 /* GameHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameHelper.swift; sourceTree = ""; }; + D1074A972BF13D4200569080 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + D1074A9A2BF13E9300569080 /* SettingBracketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingBracketView.swift; sourceTree = ""; }; + D113300C2BE5952B00F6DF30 /* BracketGameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketGameCell.swift; sourceTree = ""; }; D11E4F8D2BA7410100CA9FDD /* womensBracket2024.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = womensBracket2024.plist; sourceTree = ""; }; D11E4F8E2BA7410100CA9FDD /* mensBracket2024.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = mensBracket2024.plist; sourceTree = ""; }; + D12026FF2BF79FFF00A10EA7 /* SelectNumTeamsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectNumTeamsView.swift; sourceTree = ""; }; + D12027012BF7A00B00A10EA7 /* SelectYearView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectYearView.swift; sourceTree = ""; }; + D137201C2BF940C00057EA8D /* SelectCustomTeamsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCustomTeamsView.swift; sourceTree = ""; }; + D137201E2BF948290057EA8D /* SeedSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedSelector.swift; sourceTree = ""; }; D1386BDD2B929752006C676C /* NavigationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationUtil.swift; sourceTree = ""; }; + D13EEA572BF9903B00F61B51 /* UpdateRegionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateRegionsView.swift; sourceTree = ""; }; D14720172B60D22500453304 /* PreviewDataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewDataController.swift; sourceTree = ""; }; D14720192B60D6FC00453304 /* TeamHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeamHelper.swift; sourceTree = ""; }; + D1472C1F2BF086CC00EECAFF /* TournamentGamesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentGamesView.swift; sourceTree = ""; }; D14C59E827DED2FE001378F1 /* mensBracket2022.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = mensBracket2022.plist; sourceTree = ""; }; D14C59EA27DED30F001378F1 /* womensBracket2022.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = womensBracket2022.plist; sourceTree = ""; }; D15865722B574D4D009E486A /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; @@ -130,24 +156,33 @@ D15865832B575118009E486A /* GameStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameStatsView.swift; sourceTree = ""; }; D15865872B575176009E486A /* SelectInitialBracketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectInitialBracketView.swift; sourceTree = ""; }; D15865892B575185009E486A /* BracketCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketCreationView.swift; sourceTree = ""; }; - D158658B2B575197009E486A /* TournamentGamesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentGamesView.swift; sourceTree = ""; }; + D158658B2B575197009E486A /* TournamentListGamesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentListGamesView.swift; sourceTree = ""; }; D158658F2B5751BE009E486A /* SelectTournamentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTournamentView.swift; sourceTree = ""; }; D15865912B5751CE009E486A /* TournamentStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentStatsView.swift; sourceTree = ""; }; D15865942B575ECE009E486A /* PrimaryButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButtonStyle.swift; sourceTree = ""; }; D158B32C2B9A96720002D2F6 /* GameStatsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameStatsController.swift; sourceTree = ""; }; + D16485712BE9D7AB006BB362 /* BracketWinnerLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketWinnerLine.swift; sourceTree = ""; }; + D16BEEB32BF7E605008D06B0 /* SelectInitialBracketShellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectInitialBracketShellView.swift; sourceTree = ""; }; D17FF6002BA140DA00149D63 /* SearchTeamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTeamView.swift; sourceTree = ""; }; D17FF6042BA1454900149D63 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; + D181D2862BB529340067B4F1 /* BracketIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketIcon.swift; sourceTree = ""; }; + D181D2892BB537D80067B4F1 /* BracketGamesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketGamesView.swift; sourceTree = ""; }; D18D756D29BEA5AC00DE609B /* mensBracket2023.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = mensBracket2023.plist; sourceTree = ""; }; D18D756F29BEA5B500DE609B /* womensBracket2023.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = womensBracket2023.plist; sourceTree = ""; }; D199087A2B996D6200A57746 /* ShootModeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShootModeController.swift; sourceTree = ""; }; D199087C2B99703500A57746 /* SaveHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveHelper.swift; sourceTree = ""; }; + D19AF8AB2BF69808003A1273 /* CreateCustomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateCustomView.swift; sourceTree = ""; }; + D19AF8AF2BF69F7D003A1273 /* CustomTypeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTypeView.swift; sourceTree = ""; }; + D19AF8B12BF69FFE003A1273 /* ListCustomTeamsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCustomTeamsView.swift; sourceTree = ""; }; D1A3E0082B76F94D001E461A /* BracketHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketHelper.swift; sourceTree = ""; }; D1A67BE329BD48E200E1D55B /* foam-madness-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "foam-madness-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D1A67BE529BD48E200E1D55B /* foam_madness_bracket_file_tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = foam_madness_bracket_file_tests.swift; sourceTree = ""; }; D1A67BEC29BD631D00E1D55B /* foam_madness_probability_file_test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = foam_madness_probability_file_test.swift; sourceTree = ""; }; D1A67BEE29BD6E4D00E1D55B /* foam_madness_teams_file_test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = foam_madness_teams_file_test.swift; sourceTree = ""; }; D1A8B19D2B9FD8A500354C9F /* MailHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailHandler.swift; sourceTree = ""; }; - D1A8B19F2B9FF03100354C9F /* TournamentGameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentGameCell.swift; sourceTree = ""; }; + D1A8B19F2B9FF03100354C9F /* TournamentListGameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentListGameCell.swift; sourceTree = ""; }; + D1BF49372BA7FFC100A00669 /* foam-madness-v4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "foam-madness-v4.xcdatamodel"; sourceTree = ""; }; + D1BF49382BA805AA00A00669 /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; D1C757A02B9A62C500817E1A /* TournamentStatsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentStatsController.swift; sourceTree = ""; }; D1D4F9C22B97E4D0004B9C18 /* BracketCreationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BracketCreationController.swift; sourceTree = ""; }; D1E16E1028334BB0007280EC /* bracketIndex.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = bracketIndex.plist; sourceTree = ""; }; @@ -265,6 +300,7 @@ D158B32C2B9A96720002D2F6 /* GameStatsController.swift */, 7A2ECEE8242EDE920065E369 /* AppDelegate.swift */, 7A2D2E3C262E78A000AB5935 /* Helpers */, + D1BF49382BA805AA00A00669 /* AppConstants.swift */, ); path = Controller; sourceTree = ""; @@ -279,6 +315,27 @@ path = "Live Games API"; sourceTree = ""; }; + D1074A992BF13E5500569080 /* SettingsView */ = { + isa = PBXGroup; + children = ( + D15865752B574D61009E486A /* AboutView.swift */, + D1074A972BF13D4200569080 /* SettingsView.swift */, + D1074A9A2BF13E9300569080 /* SettingBracketView.swift */, + ); + path = SettingsView; + sourceTree = ""; + }; + D12026FE2BF79FB300A10EA7 /* InitialBracket */ = { + isa = PBXGroup; + children = ( + D16BEEB32BF7E605008D06B0 /* SelectInitialBracketShellView.swift */, + D15865872B575176009E486A /* SelectInitialBracketView.swift */, + D12026FF2BF79FFF00A10EA7 /* SelectNumTeamsView.swift */, + D12027012BF7A00B00A10EA7 /* SelectYearView.swift */, + ); + path = InitialBracket; + sourceTree = ""; + }; D1386BDA2B9292DA006C676C /* Navigation */ = { isa = PBXGroup; children = ( @@ -287,6 +344,17 @@ path = Navigation; sourceTree = ""; }; + D1472C1E2BF0861500EECAFF /* TournamentGamesView */ = { + isa = PBXGroup; + children = ( + D19AF8AA2BF697ED003A1273 /* CreateCustomView */, + D181D2882BB537BC0067B4F1 /* BracketView */, + D181D2852BB529140067B4F1 /* ListView */, + D1472C1F2BF086CC00EECAFF /* TournamentGamesView.swift */, + ); + path = TournamentGamesView; + sourceTree = ""; + }; D14C59E627DECEC0001378F1 /* brackets */ = { isa = PBXGroup; children = ( @@ -327,8 +395,8 @@ D158656D2B574CFC009E486A /* Menu */ = { isa = PBXGroup; children = ( - D15865752B574D61009E486A /* AboutView.swift */, D15865722B574D4D009E486A /* MenuView.swift */, + D1074A992BF13E5500569080 /* SettingsView */, ); path = Menu; sourceTree = ""; @@ -346,10 +414,9 @@ D158656F2B574D07009E486A /* Tournament */ = { isa = PBXGroup; children = ( - D15865872B575176009E486A /* SelectInitialBracketView.swift */, + D12026FE2BF79FB300A10EA7 /* InitialBracket */, + D1472C1E2BF0861500EECAFF /* TournamentGamesView */, D15865892B575185009E486A /* BracketCreationView.swift */, - D1A8B19F2B9FF03100354C9F /* TournamentGameCell.swift */, - D158658B2B575197009E486A /* TournamentGamesView.swift */, D158658F2B5751BE009E486A /* SelectTournamentView.swift */, D15865912B5751CE009E486A /* TournamentStatsView.swift */, ); @@ -384,6 +451,39 @@ path = Button; sourceTree = ""; }; + D181D2852BB529140067B4F1 /* ListView */ = { + isa = PBXGroup; + children = ( + D1A8B19F2B9FF03100354C9F /* TournamentListGameCell.swift */, + D158658B2B575197009E486A /* TournamentListGamesView.swift */, + D181D2862BB529340067B4F1 /* BracketIcon.swift */, + ); + path = ListView; + sourceTree = ""; + }; + D181D2882BB537BC0067B4F1 /* BracketView */ = { + isa = PBXGroup; + children = ( + D181D2892BB537D80067B4F1 /* BracketGamesView.swift */, + D113300C2BE5952B00F6DF30 /* BracketGameCell.swift */, + D16485712BE9D7AB006BB362 /* BracketWinnerLine.swift */, + ); + path = BracketView; + sourceTree = ""; + }; + D19AF8AA2BF697ED003A1273 /* CreateCustomView */ = { + isa = PBXGroup; + children = ( + D19AF8AB2BF69808003A1273 /* CreateCustomView.swift */, + D19AF8AF2BF69F7D003A1273 /* CustomTypeView.swift */, + D19AF8B12BF69FFE003A1273 /* ListCustomTeamsView.swift */, + D137201C2BF940C00057EA8D /* SelectCustomTeamsView.swift */, + D137201E2BF948290057EA8D /* SeedSelector.swift */, + D13EEA572BF9903B00F61B51 /* UpdateRegionsView.swift */, + ); + path = CreateCustomView; + sourceTree = ""; + }; D1A67BE429BD48E200E1D55B /* foam-madness-tests */ = { isa = PBXGroup; children = ( @@ -531,38 +631,55 @@ 7A494BC5243046A50042173A /* DataController.swift in Sources */, D14720182B60D22500453304 /* PreviewDataController.swift in Sources */, D15865762B574D61009E486A /* AboutView.swift in Sources */, + D19AF8B22BF69FFE003A1273 /* ListCustomTeamsView.swift in Sources */, D1A8B19E2B9FD8A500354C9F /* MailHandler.swift in Sources */, D15865922B5751CE009E486A /* TournamentStatsView.swift in Sources */, D158657C2B574FB9009E486A /* SelectTeamsView.swift in Sources */, 7A494BC3243041C20042173A /* foam-madness.xcdatamodeld in Sources */, + D19AF8B02BF69F7D003A1273 /* CustomTypeView.swift in Sources */, D199087B2B996D6200A57746 /* ShootModeController.swift in Sources */, + D12027022BF7A00B00A10EA7 /* SelectYearView.swift in Sources */, D1C757A12B9A62C500817E1A /* TournamentStatsController.swift in Sources */, D15865802B5750F9009E486A /* ShootModeView.swift in Sources */, + D113300D2BE5952B00F6DF30 /* BracketGameCell.swift in Sources */, + D13EEA582BF9903B00F61B51 /* UpdateRegionsView.swift in Sources */, + D1BF49392BA805AA00A00669 /* AppConstants.swift in Sources */, + D12027002BF79FFF00A10EA7 /* SelectNumTeamsView.swift in Sources */, D17FF6012BA140DA00149D63 /* SearchTeamView.swift in Sources */, D158657A2B574F94009E486A /* LiveGamesCell.swift in Sources */, + D1074A9B2BF13E9300569080 /* SettingBracketView.swift in Sources */, 7ACBCB3B250EC7FC00EBA5B1 /* ErrorResponse.swift in Sources */, D15865882B575176009E486A /* SelectInitialBracketView.swift in Sources */, D158B32D2B9A96720002D2F6 /* GameStatsController.swift in Sources */, + D1472C202BF086CC00EECAFF /* TournamentGamesView.swift in Sources */, 7ACBCB37250EC73900EBA5B1 /* APIClient.swift in Sources */, D15865732B574D4D009E486A /* MenuView.swift in Sources */, - D1A8B1A02B9FF03100354C9F /* TournamentGameCell.swift in Sources */, + D1A8B1A02B9FF03100354C9F /* TournamentListGameCell.swift in Sources */, D15865902B5751BE009E486A /* SelectTournamentView.swift in Sources */, D15865782B574F58009E486A /* LiveGamesView.swift in Sources */, 7A2D2E42262E81DB00AB5935 /* ProbabilityHelper.swift in Sources */, + D137201D2BF940C00057EA8D /* SelectCustomTeamsView.swift in Sources */, D158658A2B575185009E486A /* BracketCreationView.swift in Sources */, 7ACBCB39250EC7DA00EBA5B1 /* LiveGamesResponse.swift in Sources */, D1A3E0092B76F94D001E461A /* BracketHelper.swift in Sources */, + D181D2872BB529340067B4F1 /* BracketIcon.swift in Sources */, 7A10A5F7243847B10070302F /* TourneyHelper.swift in Sources */, D147201A2B60D6FC00453304 /* TeamHelper.swift in Sources */, + D1074A982BF13D4200569080 /* SettingsView.swift in Sources */, D199087D2B99703500A57746 /* SaveHelper.swift in Sources */, + D19AF8AC2BF69808003A1273 /* CreateCustomView.swift in Sources */, D1D4F9C32B97E4D0004B9C18 /* BracketCreationController.swift in Sources */, + D16BEEB42BF7E605008D06B0 /* SelectInitialBracketShellView.swift in Sources */, D158657E2B5750E7009E486A /* PlayGameView.swift in Sources */, 7AF31A862431267400DF1605 /* GameHelper.swift in Sources */, + D181D28A2BB537D80067B4F1 /* BracketGamesView.swift in Sources */, D15865842B575118009E486A /* GameStatsView.swift in Sources */, D1386BDE2B929752006C676C /* NavigationUtil.swift in Sources */, D15865952B575ECE009E486A /* PrimaryButtonStyle.swift in Sources */, 7A2ECEE9242EDE920065E369 /* AppDelegate.swift in Sources */, - D158658C2B575197009E486A /* TournamentGamesView.swift in Sources */, + D158658C2B575197009E486A /* TournamentListGamesView.swift in Sources */, + D137201F2BF948290057EA8D /* SeedSelector.swift in Sources */, + D16485722BE9D7AC006BB362 /* BracketWinnerLine.swift in Sources */, D17FF6052BA1454900149D63 /* SearchBar.swift in Sources */, 7A2D2E3F262E7FBE00AB5935 /* SimHelper.swift in Sources */, D15865822B57510D009E486A /* GameScoreView.swift in Sources */, @@ -728,7 +845,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_TEAM = 5X3GA242G5; INFOPLIST_FILE = "foam-madness/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -736,7 +853,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9; + MARKETING_VERSION = 1.10; PRODUCT_BUNDLE_IDENTIFIER = "com.mvirgo.foam-madness"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -749,7 +866,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 13; + CURRENT_PROJECT_VERSION = 14; DEVELOPMENT_TEAM = 5X3GA242G5; INFOPLIST_FILE = "foam-madness/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -757,7 +874,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.9; + MARKETING_VERSION = 1.10; PRODUCT_BUNDLE_IDENTIFIER = "com.mvirgo.foam-madness"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -849,11 +966,12 @@ 7A494BC1243041C20042173A /* foam-madness.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + D1BF49372BA7FFC100A00669 /* foam-madness-v4.xcdatamodel */, 7ABF40392456A55600A18B3F /* foam-madness-v3.xcdatamodel */, 7A73579E243ED4A800691B26 /* foam-madness-v2.xcdatamodel */, 7A494BC2243041C20042173A /* foam-madness.xcdatamodel */, ); - currentVersion = 7ABF40392456A55600A18B3F /* foam-madness-v3.xcdatamodel */; + currentVersion = D1BF49372BA7FFC100A00669 /* foam-madness-v4.xcdatamodel */; path = "foam-madness.xcdatamodeld"; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/foam-madness/Controller/AppConstants.swift b/foam-madness/Controller/AppConstants.swift new file mode 100644 index 0000000..b793346 --- /dev/null +++ b/foam-madness/Controller/AppConstants.swift @@ -0,0 +1,12 @@ +// +// AppConstants.swift +// foam-madness +// +// Created by Michael Virgo on 3/18/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +struct AppConstants { + static let defaultShotsPerRound = 10 + static let defaultUseBracketView = false +} diff --git a/foam-madness/Controller/BracketCreationController.swift b/foam-madness/Controller/BracketCreationController.swift index b2afce1..66f4ed5 100644 --- a/foam-madness/Controller/BracketCreationController.swift +++ b/foam-madness/Controller/BracketCreationController.swift @@ -17,12 +17,28 @@ struct LoadedBracketOutput { var firstFour: [String: [String: String]] } -struct TournamentOutput { - var tournament: Tournament - var hasFirstFour: Bool -} +let startingRoundByNumTeams = [ + 68: 0, + 64: 1, + 32: 2, + 16: 3, + 8: 4, + 4: 5 +] + +// Takes in the "startingRound" for a bracket +let nextGameStartForLaterRounds = [ + 36, // 68 teams + 36, // 64 teams + 36, // 32 teams + 52, // 16 teams + 60, // 8 teams + 64 // 4 teams (doesn't matter, ignored) +] class BracketCreationController { + @AppStorage("useBracketView") var useBracketView = AppConstants.defaultUseBracketView + var context: NSManagedObjectContext! init(context: NSManagedObjectContext!) { @@ -30,17 +46,45 @@ class BracketCreationController { } // MARK: Public methods - func createBracket(bracketLocation: String, tournamentName: String, isSimulated: Bool, useLeft: Bool) -> TournamentOutput { + func createBracketFromFile(bracketLocation: String, tournamentName: String, isSimulated: Bool, useLeft: Bool, shotsPerRound: Int) -> Tournament { // Load the bracket let loadedBracket = loadBracket(bracketLocation: bracketLocation) // Create any teams that aren't created yet createAnyNewTeams() // Create the tournament - let tournament = createTournamentObject(tournamentName: tournamentName, isSimulated: isSimulated, isWomens: loadedBracket.isWomens) + let tournament = createTournamentObject( + tournamentName: tournamentName, + isSimulated: isSimulated, + isWomens: loadedBracket.isWomens, + shotsPerRound: shotsPerRound + ) // Create all games and add to tourney - createTournamentGames(loadedBracket: loadedBracket, tournament: tournament, useLeft: useLeft) + let hasFirstFour = loadedBracket.hasFirstFour + let startingRound = hasFirstFour ? 0 : 1 + createTournamentGames(loadedBracket: loadedBracket, tournament: tournament, useLeft: useLeft, startingRound: startingRound) + tournament.ready = true - return TournamentOutput(tournament: tournament, hasFirstFour: loadedBracket.hasFirstFour) + return tournament + } + + func createCustomBracket(numTeams: Int, isWomens: Bool, tournamentName: String, isSimulated: Bool, useLeft: Bool, shotsPerRound: Int) -> Tournament { + // Mock the bracket + let hasFirstFour = numTeams == 68 + let mockBracket = mockBracket(isWomens: isWomens, hasFirstFour: hasFirstFour) + // Create any teams that aren't created yet + createAnyNewTeams() + // Create the tournament + let tournament = createTournamentObject( + tournamentName: tournamentName, + isSimulated: isSimulated, + isWomens: isWomens, + shotsPerRound: shotsPerRound + ) + // Create all games and add to tourney + createTournamentGames(loadedBracket: mockBracket, tournament: tournament, useLeft: useLeft, startingRound: startingRoundByNumTeams[numTeams]!) + tournament.ready = false + + return tournament } func checkExistingNames(_ name: String) -> Bool { @@ -49,15 +93,10 @@ class BracketCreationController { return TourneyHelper.fetchDataFromContext(context, predicate, "Tournament", []).count == 0 } - func simulateTournament(tournament: Tournament, hasFirstFour: Bool) -> String { - var id: Int16 + func simulateTournament(tournament: Tournament) -> String { var winner: String = "" - // Get correct starting id (hard-coded for current bracket style) - if !hasFirstFour { - id = 4 - } else { - id = 0 - } + // Get correct starting id (minimum tourneyGameId) + var id = (tournament.games as! Set).min(by: { $0.tourneyGameId < $1.tourneyGameId })?.tourneyGameId ?? 0 // Loop through and simulate all games while true { let game = tournament.games?.filtered(using: NSPredicate(format: "tourneyGameId == %@", NSNumber(value: id))).first as! Game @@ -87,6 +126,87 @@ class BracketCreationController { return winner } + // For randomly filling custom brackets + func fillTournamentWithRandomTeams(_ tournament: Tournament) { + let games = tournament.games?.allObjects as! [Game] + let initialRound = games.min(by: { $0.round < $1.round })!.round + let gamesInInitialRound = games.filter({ $0.round == initialRound }) + + // Load all teams + let teams = TeamHelper.loadTeams() + + // Add teams to each game, avoiding duplicates + var teamIds: Set = [] + for game in gamesInInitialRound { + while game.teams?.count != 2 { + let teamId = Int16(getRandomTeamId(teams))! + if !teamIds.contains(teamId) { + teamIds.insert(teamId) + if game.teams?.count == 0 { + game.team1Id = teamId + } else { + game.team2Id = teamId + } + let team = TeamHelper.fetchTeamById([teamId], context).first as! Team + team.addToGames(game) + } + } + } + + saveData() + } + + // For filling custom bracket from existing base + func fillTournamentFromExistingCustom(_ tournament: Tournament, _ bracketLocation: String) { + // Load the bracket + let loadedBracket = loadBracket(bracketLocation: bracketLocation) + let regionSeedTeams = loadedBracket.regionSeedTeams + let games = tournament.games?.allObjects as! [Game] + + // Replace region names + for game in games { + useRegionNameExistingCustom(game, loadedBracket.regionOrder) + } + + // Fill in teams + let minRound = games.min(by: { $0.round < $1.round })?.round ?? 0 + let initialRoundGames = games.filter({ $0.round == minRound }) + for game in initialRoundGames { + game.team1Id = regionSeedTeams[game.region ?? ""]![String(game.team1Seed)] ?? -1 + game.team2Id = regionSeedTeams[game.region ?? ""]![String(game.team2Seed)] ?? -1 + if (game.team2Id == -1) { + // Team 2 may not actually exist yet due to First Four, randomly pull one + let team2Seed = String(game.team2Seed) + var firstFourTeams: [Int16] = [] + firstFourTeams.append(regionSeedTeams[game.region ?? ""]![team2Seed + "1"]!) + firstFourTeams.append(regionSeedTeams[game.region ?? ""]![team2Seed + "2"]!) + game.team2Id = firstFourTeams.randomElement() ?? -1 + } + // Fetch the teams by id + let results = TeamHelper.fetchTeamById([game.team1Id, game.team2Id], context) + // Add teams to the game + for team in results { + (team as! Team).addToGames(game) + } + } + + saveData() + } + + func updateRegionNameForGames(originalName: String, newName: String, gamesSet: NSSet) { + let games = Array(gamesSet) as! [Game] + let regionGames = games.filter({ $0.region == originalName }) + for game in regionGames { + game.region = newName + } + saveData() + } + + func finalizeCustomBracket(_ tournament: Tournament) { + tournament.ready = true + saveData() + } + // MARK: Private methods private func loadBracket(bracketLocation: String) -> LoadedBracketOutput { // Load in bracket @@ -115,6 +235,17 @@ class BracketCreationController { ) } + // For mocking custom loaded bracket + private func mockBracket(isWomens: Bool, hasFirstFour: Bool) -> LoadedBracketOutput { + return LoadedBracketOutput( + hasFirstFour: hasFirstFour, + isWomens: isWomens, + regionOrder: ["Region 1", "Region 2", "Region 3", "Region 4"], + regionSeedTeams: [:], + firstFour: [:] + ) + } + private func createAnyNewTeams() { // Start by getting all existing team ids let existingIds = TeamHelper.getExistingTeams(context) @@ -135,13 +266,15 @@ class BracketCreationController { } } - private func createTournamentObject(tournamentName: String, isSimulated: Bool, isWomens: Bool) -> Tournament { + private func createTournamentObject(tournamentName: String, isSimulated: Bool, isWomens: Bool, shotsPerRound: Int) -> Tournament { // Create the tournament let tournament = Tournament(context: context) tournament.name = tournamentName tournament.createdDate = Date() tournament.isWomens = isWomens tournament.isSimulated = isSimulated + tournament.shotsPerRound = Int16(shotsPerRound) + tournament.useBracketView = useBracketView // Make sure it is saved saveData() @@ -151,22 +284,24 @@ class BracketCreationController { private func createTournamentGames( loadedBracket: LoadedBracketOutput, tournament: Tournament, - useLeft: Bool + useLeft: Bool, + startingRound: Int ) { let regionOrder = loadedBracket.regionOrder let firstFour = loadedBracket.firstFour let regionSeedTeams = loadedBracket.regionSeedTeams - let hasFirstFour = loadedBracket.hasFirstFour let isWomens = loadedBracket.isWomens // Note: This function is essentially hard-coded for current bracket style - // Create First Four if Men's tournament - if hasFirstFour { + if startingRound == 0 { + // Create First Four if included createFirstFour(tournament: tournament, firstFour: firstFour, regionSeedTeams: regionSeedTeams, isWomens: isWomens, useLeft: useLeft) } - // Create Round 1 with initial teams - createFirstRound(tournament: tournament, regionOrder: regionOrder, regionSeedTeams: regionSeedTeams, isWomens: isWomens, useLeft: useLeft) - // Create Round 2-Championship with no teams - createLaterRounds(tournament: tournament, regionOrder: regionOrder, isWomens: isWomens, useLeft: useLeft) + if startingRound <= 1 { + // Create Round 1 with initial teams + createFirstRound(tournament: tournament, regionOrder: regionOrder, regionSeedTeams: regionSeedTeams, isWomens: isWomens, useLeft: useLeft) + } + // Create Round 2 (or higher) to Championship with no teams + createLaterRounds(tournament: tournament, regionOrder: regionOrder, isWomens: isWomens, useLeft: useLeft, startingRound: startingRound) // Save the data saveData() } @@ -180,6 +315,7 @@ class BracketCreationController { game.region = gameInfo["Region"] game.useLeft = useLeft game.isWomens = isWomens + game.shotsPerRound = tournament.shotsPerRound // Add both team ids and seeds game.team1Seed = Int16(gameInfo["Seed"]!)! game.team2Seed = Int16(gameInfo["Seed"]!)! @@ -214,21 +350,24 @@ class BracketCreationController { game.region = region game.useLeft = useLeft game.isWomens = isWomens + game.shotsPerRound = tournament.shotsPerRound game.team1Seed = Int16(i) game.team2Seed = Int16(17-i) - // Team 2 may not actually exist yet due to First Four - game.team1Id = regionSeedTeams[region]![String(i)] ?? -1 - game.team2Id = regionSeedTeams[region]![String(17-i)] ?? -1 + if regionSeedTeams.count > 0 { // not a custom bracket, set teams + // Team 2 may not actually exist yet due to First Four + game.team1Id = regionSeedTeams[region]![String(i)] ?? -1 + game.team2Id = regionSeedTeams[region]![String(17-i)] ?? -1 + // Fetch the teams by id + let results = TeamHelper.fetchTeamById([game.team1Id, game.team2Id], context) + // Add teams to the game + for team in results { + (team as! Team).addToGames(game) + } + } // Set tourney game id and next game game.tourneyGameId = Int16(gameId) game.nextGame = Int16((gameId / 2) + 34) gameId += 1 - // Fetch the teams by id - let results = TeamHelper.fetchTeamById([game.team1Id, game.team2Id], context) - // Add teams to the game - for team in results { - (team as! Team).addToGames(game) - } // Add the game to the tournament tournament.addToGames(game) // Save the data @@ -237,21 +376,28 @@ class BracketCreationController { } } - private func createLaterRounds(tournament: Tournament, regionOrder: [String], isWomens: Bool, useLeft: Bool) { + private func createLaterRounds(tournament: Tournament, regionOrder: [String], isWomens: Bool, useLeft: Bool, startingRound: Int) { let gamesPerRoundPerRegion = [4, 2, 1, 2, 1] // Make counter for tourney game id (start at 36 to avoid early rounds) - var gameId = 36 + var gameId = nextGameStartForLaterRounds[startingRound] + // Start the loop at a min of round 2 for later rounds, or higher if input (e.g. 16 team tourney) + let adjustedStartRound = startingRound < 2 ? 2 : startingRound // Loop through rounds - for i in 2...6 { + for i in adjustedStartRound...6 { if i < 5 { // Before final four // Loop through regions for region in regionOrder { - for _ in 1...gamesPerRoundPerRegion[i-2] { + for j in 1...gamesPerRoundPerRegion[i-2] { let game = Game(context: context) game.round = Int16(i) game.region = region game.useLeft = useLeft game.isWomens = isWomens + game.shotsPerRound = tournament.shotsPerRound + if startingRound == i { + // Add seeds for custom bracket + setCustomSeeds(game: game, index: j, gamesPerRegion: gamesPerRoundPerRegion[i-2]) + } // Set tourney game id and next game game.tourneyGameId = Int16(gameId) game.nextGame = Int16((gameId / 2) + 34) @@ -268,10 +414,15 @@ class BracketCreationController { game.round = Int16(i) game.useLeft = useLeft game.isWomens = isWomens + game.shotsPerRound = tournament.shotsPerRound if i == 5 { game.region = "Final Four" game.tourneyGameId = Int16(63 + j) game.nextGame = 66 + if startingRound == i { + // Add seeds for custom bracket + setCustomSeeds(game: game, index: j, gamesPerRegion: gamesPerRoundPerRegion[i-2]) + } } else { game.region = "Championship" game.tourneyGameId = 66 @@ -285,6 +436,37 @@ class BracketCreationController { } } + // This could maybe be improved, but given at most 64 teams allowed, + // and 350 teams, this should never get stuck too long + private func getRandomTeamId(_ teams: LoadedTeams) -> String { + let teamName = teams.teams.randomElement()! + return teams.reverseTeamDict[teamName]!["id"]! + } + + private func setCustomSeeds(game: Game, index: Int, gamesPerRegion: Int) { + let seed1: Int16 + if game.round == 2 && (index == 2 || index == 4) { + // Hard-coded for 32 team custom brackets + // (Puts two seed at bottom of region) + seed1 = index == 2 ? 4 : 2 + } else { + seed1 = Int16(index) + } + + let maxSeed = gamesPerRegion * 2 + game.team1Seed = seed1 + game.team2Seed = Int16(maxSeed) - seed1 + 1 // 1-indexed for w/e reason + } + + // Replace the region name on a custom bracket using existing base (e.g. "Region 1" replaced by `regionOrder[0]`) + private func useRegionNameExistingCustom(_ game: Game, _ regionOrder: [String]) { + if let regionNumber = Int(game.region?.replacingOccurrences(of: "Region ", with: "") ?? "0"), + regionNumber > 0, regionNumber <= regionOrder.count { + game.region = regionOrder[regionNumber - 1] + } + } + + // MARK: Utility private func saveData() { SaveHelper.saveData(context, "BracketCreationController") } diff --git a/foam-madness/Controller/GameStatsController.swift b/foam-madness/Controller/GameStatsController.swift index 029eddf..abf3253 100644 --- a/foam-madness/Controller/GameStatsController.swift +++ b/foam-madness/Controller/GameStatsController.swift @@ -31,13 +31,13 @@ class GameStatsController { game.team1Twos, game.team1Threes, game.team1Fours - ]) + ], game.shotsPerRound) team2Stats += calcFGPercent([ game.team2Ones, game.team2Twos, game.team2Threes, game.team2Fours - ]) + ], game.shotsPerRound) // Add overtime stats if necessary, or else hide @@ -51,19 +51,18 @@ class GameStatsController { return GameStatsArrays(team1Stats: team1Stats, team2Stats: team2Stats, hasOvertimeStats: hasOvertimeStats) } - private func calcFGPercent(_ shotCounts: [Int16]) -> [String] { + private func calcFGPercent(_ shotCounts: [Int16], _ shotsPerRound: Int16) -> [String] { var out: [String] = [] - let shots: Int16 = 10 // hard-coded number of shots per round var totalMade: Int16 = 0 // counter for total FG% // Loop through shotCounts and calculate related FG% for shotType in shotCounts { totalMade += shotType - out.append(shotPercentageString(shotsMade: shotType, shotsTaken: shots)) + out.append(shotPercentageString(shotsMade: shotType, shotsTaken: shotsPerRound)) } // Add total FG% at start of out array - out = [shotPercentageString(shotsMade: totalMade, shotsTaken: (shots * 4))] + out + out = [shotPercentageString(shotsMade: totalMade, shotsTaken: (shotsPerRound * 4))] + out return out } diff --git a/foam-madness/Controller/Helpers/GameHelper.swift b/foam-madness/Controller/Helpers/GameHelper.swift index fe5dd3a..0e355a4 100644 --- a/foam-madness/Controller/Helpers/GameHelper.swift +++ b/foam-madness/Controller/Helpers/GameHelper.swift @@ -123,11 +123,13 @@ class GameHelper { static func prepareSingleGame( _ team1Name: String, _ team2Name: String, + _ shotsPerRound: Int, _ reverseTeamDict: [String: [String: String]], _ context: NSManagedObjectContext ) -> Game { // Create a game let game = Game(context: context) + game.shotsPerRound = Int16(shotsPerRound) // Hide the region and round from Play Game view game.region = "" game.round = -1 @@ -143,4 +145,57 @@ class GameHelper { return game } + + static func getGameWinnerAbbreviation(_ game: Game) -> String { + if !game.completion { + return "" + } + let winningId = game.team1Score > game.team2Score ? game.team1Id : game.team2Id + let winningTeam = (game.teams?.allObjects as! [Team]).filter({ $0.id == winningId }).first + return winningTeam?.abbreviation ?? "" + } + + static func getTeamIdsForGame(_ game: Game) -> [Int16] { + var output: [Int16] = [] + if let teams = game.teams { + for team in teams { + output.append((team as! Team).id) + } + } + return output + } + + static func updateTeamsInGame( + _ team1Name: String, + _ team2Name: String, + _ game: Game, + _ reverseTeamDict: [String: [String: String]], + _ context: NSManagedObjectContext + ) { + // Remove existing teams + if game.teams?.count ?? 0 > 0 { + for teamAny in game.teams!.allObjects { + let team = teamAny as! Team + team.removeFromGames(game) + } + } + + if team1Name != "" { + let team1 = TeamHelper.lookupOrCreateTeam(teamName: team1Name, reverseTeamDict: reverseTeamDict, context: context) + game.team1Id = team1.id + team1.addToGames(game) + } else { + game.team1Id = -1 + } + + if team2Name != "" { + let team2 = TeamHelper.lookupOrCreateTeam(teamName: team2Name, reverseTeamDict: reverseTeamDict, context: context) + game.team2Id = team2.id + team2.addToGames(game) + } else { + game.team2Id = -1 + } + + SaveHelper.saveData(context, "updateTeamsInGame") + } } diff --git a/foam-madness/Controller/Helpers/TeamHelper.swift b/foam-madness/Controller/Helpers/TeamHelper.swift index 4e08475..e9449a8 100644 --- a/foam-madness/Controller/Helpers/TeamHelper.swift +++ b/foam-madness/Controller/Helpers/TeamHelper.swift @@ -12,6 +12,7 @@ import Foundation struct LoadedTeams { var teams: [String] var reverseTeamDict: [String: [String: String]] + var teamsNamesByIdDict: [String: String] } class TeamHelper { @@ -21,17 +22,20 @@ class TeamHelper { // Add team name to a temporary array var tempTeams: [String] = [String]() var reverseTeamDict = [String: [String: String]]() + var teamNamesByIdDict = [String: String]() // avoids bug due to name lengthening in v1.9 for key in dict.allKeys { let name = (dict.value(forKey: key as! String) as! NSDictionary).value(forKey: "name") as? String tempTeams.append(name!) // Add to reverseTeamDict for lookup of abbreviation/id later let abbreviation = (dict.value(forKey: key as! String) as! NSDictionary).value(forKey: "abbreviation") as? String - reverseTeamDict[name!] = ["id": String(describing: key), "abbreviation": abbreviation!] + let id = String(describing: key) + reverseTeamDict[name!] = ["id": id, "abbreviation": abbreviation!] + teamNamesByIdDict[id] = name } // Sort the temporary array for easy selection tempTeams.sort() - let loadedTeams = LoadedTeams(teams: tempTeams, reverseTeamDict: reverseTeamDict) + let loadedTeams = LoadedTeams(teams: tempTeams, reverseTeamDict: reverseTeamDict, teamsNamesByIdDict: teamNamesByIdDict) return loadedTeams } diff --git a/foam-madness/Controller/Helpers/TourneyHelper.swift b/foam-madness/Controller/Helpers/TourneyHelper.swift index 85471ff..28510fb 100644 --- a/foam-madness/Controller/Helpers/TourneyHelper.swift +++ b/foam-madness/Controller/Helpers/TourneyHelper.swift @@ -8,19 +8,12 @@ import CoreData +struct ExistingTeamData { + var region: String + var seed: Int16 +} + class TourneyHelper { - static func fetchData(_ dataController: DataController, _ predicate: NSPredicate, _ entity: String) -> [Any] { - // Get view context - let context = dataController.viewContext - // Get tournaments from Core Data - let fetchRequest = NSFetchRequest(entityName: entity) - fetchRequest.predicate = predicate - // Fetch the results - let results = try! context.fetch(fetchRequest) - - return results - } - static func fetchDataFromContext(_ context: NSManagedObjectContext, _ predicate: NSPredicate?, _ entity: String, _ sortDescriptors: [NSSortDescriptor]) -> [Any] { // Get tournaments from Core Data let fetchRequest = NSFetchRequest(entityName: entity) @@ -67,6 +60,13 @@ class TourneyHelper { } static func getTourneyGameText(game: Game) -> String { + if (game.teams?.count == 0) { + return "Pending participants" + } else if (game.teams?.count == 1) { + let team = (game.teams?.allObjects as! [Team])[0] + let seed = game.team1Id == team.id ? game.team1Seed : game.team2Seed + return "\(seed) \(team.name ?? "") vs. Pending participant" + } let teams = GameHelper.getOrderedTeams(game) let team1 = teams[0] let team2 = teams[1] @@ -92,4 +92,119 @@ class TourneyHelper { return gameText } + + // Since the original implementation didn't guarantee team1Id comes from the "top" game + // of a bracket, we have to find for second round and later which had the "top" previous game + private static func orderTeamsByPreviousRound(game: Game) -> [Int16] { + if game.teams?.count == 0 { + return [-1, -1] + } + + if let tournament = game.tournament { + let previousGames = tournament.games!.filtered(using: NSPredicate(format: "nextGame == %i", game.tourneyGameId)) as! Set + if (previousGames.isEmpty) { + // May not have previous games for custom brackets with <64 games + let teams = GameHelper.getOrderedTeams(game) + return [teams[0].id, teams[1].id] + } + let firstPreviousGame = previousGames.min(by: { $0.tourneyGameId < $1.tourneyGameId })! + + if game.teams?.count == 1 { + let teamId = GameHelper.getTeamIdsForGame(game).first! + if teamId == firstPreviousGame.team1Id || teamId == firstPreviousGame.team2Id { + return [teamId, -1] + } + return [-1, teamId] + } + + let teamIds = GameHelper.getTeamIdsForGame(game) + // If teamIds[0] in firstPreviousGame, return it first in order + if teamIds[0] == firstPreviousGame.team1Id || teamIds[0] == firstPreviousGame.team2Id { + return [teamIds[0], teamIds[1]] + } + return [teamIds[1], teamIds[0]] + } + + return [-1, -1] + } + + private static func createBracketLineText(seed: Int16, team: String, score: Int16, completion: Bool) -> String { + return "\(seed) \(team)\(completion ? ": \(score)" : "")" + } + + static func getBracketGameText(game: Game) -> [String] { + var output: [String] = [] + if game.teams?.count ?? 0 == 0 { + return ["Pending", "Pending"] + } + + if game.teams?.count == 1 { + let setTeam = game.teams?.allObjects[0] as! Team + // Note: Seed will always be team1Seed, since it is set first, regardless of top/bottom bracket part + output.append(createBracketLineText(seed: game.team1Seed, team: setTeam.abbreviation ?? "", score: 0, completion: game.completion)) + + if game.round <= 1 { + // Order will be correct for First Four and Round of 64 + output.append("Pending") + } else { + // Need to check previous round + let teamIds = orderTeamsByPreviousRound(game: game) + if teamIds[0] == -1 { + output.insert("Pending", at: 0) + } else { + output.append("Pending") + } + } + + return output + } + + let topTeam, bottomTeam: Team + if game.round <= 1 { + // Order will be correct for First Four and Round of 64 + let teams = GameHelper.getOrderedTeams(game) + topTeam = teams[0] + bottomTeam = teams[1] + } else { + let teamIds = orderTeamsByPreviousRound(game: game) + let teams = game.teams?.allObjects as! [Team] + if teamIds[0] == teams[0].id { + topTeam = teams[0] + bottomTeam = teams[1] + } else { + topTeam = teams[1] + bottomTeam = teams[0] + } + } + + if (game.team1Id == topTeam.id) { + output.append(createBracketLineText(seed: game.team1Seed, team: topTeam.abbreviation ?? "", score: game.team1Score, completion: game.completion)) + output.append(createBracketLineText(seed: game.team2Seed, team: bottomTeam.abbreviation ?? "", score: game.team2Score, completion: game.completion)) + } else { + output.append(createBracketLineText(seed: game.team2Seed, team: topTeam.abbreviation ?? "", score: game.team2Score, completion: game.completion)) + output.append(createBracketLineText(seed: game.team1Seed, team: bottomTeam.abbreviation ?? "", score: game.team1Score, completion: game.completion)) + } + + return output + } + + // Used with updating teams in custom tournaments + static func checkForDuplicateTeamInTournament(_ tournament: Tournament, currentGameId: Int16, teamId: Int16) -> ExistingTeamData? { + let games = tournament.games?.allObjects as! [Game] + let filteredGames = games.filter({ $0.teams?.count ?? 0 > 0 && $0.tourneyGameId != currentGameId }) + + for game in filteredGames { + if game.team1Id == teamId { + return ExistingTeamData(region: game.region ?? "", seed: game.team1Seed) + } else if game.team2Id == teamId { + return ExistingTeamData(region: game.region ?? "", seed: game.team2Seed) + } + } + + return nil + } + + static func duplicateTeamAlertMessage(_ teamName: String, _ existingTeamData: ExistingTeamData?) -> String { + return "\(teamName) is already seed #\(existingTeamData!.seed) in the \(existingTeamData!.region) region. Please choose another team, or remove them from the other game first." + } } diff --git a/foam-madness/Controller/TournamentStatsController.swift b/foam-madness/Controller/TournamentStatsController.swift index 7c66d0d..8de93c0 100644 --- a/foam-madness/Controller/TournamentStatsController.swift +++ b/foam-madness/Controller/TournamentStatsController.swift @@ -159,7 +159,7 @@ class TournamentStatsController { leftOTTaken += Int(game.team2OTTaken) } - private func setTotalStatsArray(_ games: [Game]) { + private func setTotalStatsArray(_ games: [Game], _ shotsPerRound: Int) { totalStatsArray[0] = games.count // Total games totalStatsArray[1] = totalUpsets // Total upsets // Note: 2 and 3 are skipped - only relate to hands @@ -169,16 +169,16 @@ class TournamentStatsController { let totalRightMade = rightOnesMade + rightTwosMade + rightThreesMade + rightFoursMade + rightOTMade let totalTaken = (games.count * 2 * 40) + leftOTTaken + rightOTTaken totalStatsArray[5] = Int((Float(totalLeftMade + totalRightMade) / Float(totalTaken)) * 100) // Total FG% - totalStatsArray[6] = Int((Float(leftOnesMade + rightOnesMade) / Float(games.count * 2 * 10)) * 100) // 1pt% - totalStatsArray[7] = Int((Float(leftTwosMade + rightTwosMade) / Float(games.count * 2 * 10)) * 100) // 2pt% - totalStatsArray[8] = Int((Float(leftThreesMade + rightThreesMade) / Float(games.count * 2 * 10)) * 100) // 3pt% - totalStatsArray[9] = Int((Float(leftFoursMade + rightFoursMade) / Float(games.count * 2 * 10)) * 100) // 4pt% + totalStatsArray[6] = Int((Float(leftOnesMade + rightOnesMade) / Float(games.count * 2 * shotsPerRound)) * 100) // 1pt% + totalStatsArray[7] = Int((Float(leftTwosMade + rightTwosMade) / Float(games.count * 2 * shotsPerRound)) * 100) // 2pt% + totalStatsArray[8] = Int((Float(leftThreesMade + rightThreesMade) / Float(games.count * 2 * shotsPerRound)) * 100) // 3pt% + totalStatsArray[9] = Int((Float(leftFoursMade + rightFoursMade) / Float(games.count * 2 * shotsPerRound)) * 100) // 4pt% if leftOTTaken + rightOTTaken > 0 { // avoid division by zero totalStatsArray[10] = Int((Float(leftOTMade + rightOTMade) / Float(leftOTTaken + rightOTTaken)) * 100) // OT% } } - private func setLeftStatsArray() { + private func setLeftStatsArray(_ shotsPerRound: Int) { let leftGames = leftVsRight + (2 * leftVsLeft) if leftGames == 0 {return} // No need to calculate leftStatsArray[0] = leftVsRight // Games vs. Opposite @@ -192,16 +192,16 @@ class TournamentStatsController { let totalLeftMade = leftOnesMade + leftTwosMade + leftThreesMade + leftFoursMade + leftOTMade let totalTaken = (leftGames * 40) + leftOTTaken leftStatsArray[5] = Int((Float(totalLeftMade) / Float(totalTaken)) * 100) // Total FG% - leftStatsArray[6] = Int((Float(leftOnesMade) / Float(leftGames * 10)) * 100) // 1pt% - leftStatsArray[7] = Int((Float(leftTwosMade) / Float(leftGames * 10)) * 100) // 2pt% - leftStatsArray[8] = Int((Float(leftThreesMade) / Float(leftGames * 10)) * 100) // 3pt% - leftStatsArray[9] = Int((Float(leftFoursMade) / Float(leftGames * 10)) * 100) // 4pt% + leftStatsArray[6] = Int((Float(leftOnesMade) / Float(leftGames * shotsPerRound)) * 100) // 1pt% + leftStatsArray[7] = Int((Float(leftTwosMade) / Float(leftGames * shotsPerRound)) * 100) // 2pt% + leftStatsArray[8] = Int((Float(leftThreesMade) / Float(leftGames * shotsPerRound)) * 100) // 3pt% + leftStatsArray[9] = Int((Float(leftFoursMade) / Float(leftGames * shotsPerRound)) * 100) // 4pt% if leftOTTaken > 0 { // avoid division by zero leftStatsArray[10] = Int((Float(leftOTMade) / Float(leftOTTaken)) * 100) // OT% } } - private func setRightStatsArray() { + private func setRightStatsArray(_ shotsPerRound: Int) { let rightGames = leftVsRight + (2 * rightVsRight) if rightGames == 0 {return} // No need to calculate rightStatsArray[0] = leftVsRight // Games vs. Opposite @@ -215,19 +215,20 @@ class TournamentStatsController { let totalRightMade = rightOnesMade + rightTwosMade + rightThreesMade + rightFoursMade + rightOTMade let totalTaken = (rightGames * 40) + rightOTTaken rightStatsArray[5] = Int((Float(totalRightMade) / Float(totalTaken)) * 100) // Total FG% - rightStatsArray[6] = Int((Float(rightOnesMade) / Float(rightGames * 10)) * 100) // 1pt% - rightStatsArray[7] = Int((Float(rightTwosMade) / Float(rightGames * 10)) * 100) // 2pt% - rightStatsArray[8] = Int((Float(rightThreesMade) / Float(rightGames * 10)) * 100) // 3pt% - rightStatsArray[9] = Int((Float(rightFoursMade) / Float(rightGames * 10)) * 100) // 4pt% + rightStatsArray[6] = Int((Float(rightOnesMade) / Float(rightGames * shotsPerRound)) * 100) // 1pt% + rightStatsArray[7] = Int((Float(rightTwosMade) / Float(rightGames * shotsPerRound)) * 100) // 2pt% + rightStatsArray[8] = Int((Float(rightThreesMade) / Float(rightGames * shotsPerRound)) * 100) // 3pt% + rightStatsArray[9] = Int((Float(rightFoursMade) / Float(rightGames * shotsPerRound)) * 100) // 4pt% if rightOTTaken > 0 { // avoid division by zero rightStatsArray[10] = Int((Float(rightOTMade) / Float(rightOTTaken)) * 100) // OT% } } private func setStatsArrays(_ games: [Game]) { + let shotsPerRound = Int(games[0].shotsPerRound) // Set all three stats arrays - setTotalStatsArray(games) - setLeftStatsArray() - setRightStatsArray() + setTotalStatsArray(games, shotsPerRound) + setLeftStatsArray(shotsPerRound) + setRightStatsArray(shotsPerRound) } } diff --git a/foam-madness/Model/ABOUT.rtf b/foam-madness/Model/ABOUT.rtf index 774390c..3830e2c 100644 --- a/foam-madness/Model/ABOUT.rtf +++ b/foam-madness/Model/ABOUT.rtf @@ -1,4 +1,4 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2578 +{\rtf1\ansi\ansicpg1252\cocoartf2761 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;\f1\fnil\fcharset0 Menlo-Regular;\f2\fnil\fcharset0 HelveticaNeue-Bold; } {\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red255\green255\blue255;} @@ -15,7 +15,7 @@ \f1\b0\fs24 \ \ -\f0 Games are built with four rounds, made up of 10 shots of escalating value: 1 point, 2 points, 3 points, and 4 points. Clicking on an individual basketball to make it orange will count it as a made shot; leaving it gray will not be counted. +\f0 Games are built with four rounds, made up of 10 shots (by default, but customizable from 3 to 15 shots) of escalating value: 1 point, 2 points, 3 points, and 4 points. Clicking on an individual basketball to make it orange will count it as a made shot; leaving it gray will not be counted. \f1 \ \ @@ -23,7 +23,7 @@ \f1 \ \ -\f0 If overtime is needed, an additional 10 shots is given per team, with each basket worth one point. An additional 10 shots is added per additional overtime period, as necessary. +\f0 If overtime is needed, an additional round of shots is given per team, with each basket worth one point. An additional round of shots is added per additional overtime period, as necessary. \f1 \ \ @@ -51,7 +51,7 @@ \f1\b0\fs24 \ \ -\f0 Tournament mode requires the user to select an initial bracket, and then can select individual games to play by round. The currently viewed round can be changed with the +/- button from within a single tournament's menu, although games will be unplayable until both teams have been decided for a given game (typically applicable to later rounds). +\f0 Tournament mode requires the user to either select an initial bracket or make a customizable bracket, and then they can select individual games to play by round or region. The currently viewed round can be changed with the +/- button from within a single tournament's menu (if in List view style), or the currently viewed region can be changed by selecting a different region (if in Bracket view style), although games will be unplayable until both teams have been decided for a given game (typically applicable to later rounds). \f1 \ \ diff --git a/foam-madness/Model/PreviewDataController.swift b/foam-madness/Model/PreviewDataController.swift index 7158059..bf9fb93 100644 --- a/foam-madness/Model/PreviewDataController.swift +++ b/foam-madness/Model/PreviewDataController.swift @@ -61,8 +61,25 @@ struct PreviewDataController { private func makeMockTournaments() { let context = container.viewContext + // Regular tournament let _ = BracketCreationController(context: context) - .createBracket(bracketLocation: "mensBracket2023", tournamentName: "Example Tournament 1", isSimulated: false, useLeft: false) + .createBracketFromFile( + bracketLocation: "mensBracket2023", + tournamentName: "Example Tournament 1", + isSimulated: false, + useLeft: false, + shotsPerRound: AppConstants.defaultShotsPerRound + ) + // Custom tournament + let _ = BracketCreationController(context: context) + .createCustomBracket( + numTeams: 64, + isWomens: false, + tournamentName: "Custom Tournament 1", + isSimulated: false, + useLeft: false, + shotsPerRound: AppConstants.defaultShotsPerRound + ) } private func loadMockData() { diff --git a/foam-madness/Model/foam-madness.xcdatamodeld/.xccurrentversion b/foam-madness/Model/foam-madness.xcdatamodeld/.xccurrentversion index b4ce3d5..4b8242e 100644 --- a/foam-madness/Model/foam-madness.xcdatamodeld/.xccurrentversion +++ b/foam-madness/Model/foam-madness.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - foam-madness-v3.xcdatamodel + foam-madness-v4.xcdatamodel diff --git a/foam-madness/Model/foam-madness.xcdatamodeld/foam-madness-v4.xcdatamodel/contents b/foam-madness/Model/foam-madness.xcdatamodeld/foam-madness-v4.xcdatamodel/contents new file mode 100644 index 0000000..242ca7e --- /dev/null +++ b/foam-madness/Model/foam-madness.xcdatamodeld/foam-madness-v4.xcdatamodel/contents @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/foam-madness/View/Game/ShootModeView.swift b/foam-madness/View/Game/ShootModeView.swift index d808ebf..4e67b92 100644 --- a/foam-madness/View/Game/ShootModeView.swift +++ b/foam-madness/View/Game/ShootModeView.swift @@ -18,8 +18,10 @@ struct ShootModeView: View { @State private var currentTeam: String? @State private var shotType: String? @State private var hand: String? - @State private var grid: [[Bool]] = Array(repeating: Array(repeating: false, count: 5), count: 2) + @State private var grid: [[Bool]] @State private var madeShots: Int16 = 0 + let maxShotsPerRow: Int = 5 // hard-coded bball images per row + var numColsArray: [Int] // MARK: Other variables @State private var teamFlag = true // True = Team 1 shooting, False = Team 2 shooting @@ -27,47 +29,78 @@ struct ShootModeView: View { @State private var isFinished = false @State private var scoreMultiplier: Int16 = 1 // Increment by 1 per shot type @State private var shootModeController: ShootModeController! - let overtimeShots: Int16 = 10 // hard-coded number of shots per OT + let shotsPerRound: Int16 + + init(game: Game, isSimulated: Bool) { + self.game = game + self.isSimulated = isSimulated + self.shotsPerRound = game.shotsPerRound + + // Prepare the grid + numColsArray = [] + var remainingShots = Int(shotsPerRound) + while remainingShots > 0 { + if (remainingShots > maxShotsPerRow) { + numColsArray.append(maxShotsPerRow) + remainingShots -= maxShotsPerRow + } else { + numColsArray.append(remainingShots) + remainingShots = 0 + } + } + + grid = Array( + repeating: Array(repeating: false, count: maxShotsPerRow), + count: numColsArray.count + ) + } var body: some View { - VStack { - VStack(spacing: 10) { - Text(currentTeam ?? "") - .font(.largeTitle) - .fontWeight(.bold) - .minimumScaleFactor(0.5) - .multilineTextAlignment(.center) - Text(shotType ?? "").font(.title) - Text(hand ?? "").font(.title2) - } - + GeometryReader { geometry in VStack { - ForEach(0..<2) { row in - HStack { - ForEach(0..<5) { column in - Button(action: { - grid[row][column].toggle() - madeShots += grid[row][column] ? 1 : -1 - }) { - let num = column + (5 * row) + 1 - ZStack { - Image(grid[row][column] ? "basketball" : "basketball-gray") - .resizable() - .aspectRatio(contentMode: .fit) - Text("\(num)").foregroundColor(.primary).fontWeight(.bold).font(.system(size: getBasketballFontSize())) + VStack(spacing: 10) { + Text(currentTeam ?? "") + .font(.largeTitle) + .fontWeight(.bold) + .minimumScaleFactor(0.5) + .multilineTextAlignment(.center) + Text(shotType ?? "").font(.title) + Text(hand ?? "").font(.title2) + } + + VStack { + ForEach(0.. 0 && shownTeamName == "") { List { ForEach(searchResults, id: \.self) { team in - Button(team, action: { - teamName = team // update for parent - showParentButton = true // update for parent - shownTeamName = team // hide the list - searchText = team // show user the selected team - }) - .tag(String?.some(team)) + Button(team, action: { setFields(team) }) + .tag(String?.some(team)) } } .listStyle(.plain) } } + .onAppear { + // Custom games selector may have an existing team + if teamName != "" { + searchText = teamName + shownTeamName = teamName + } + } .onChange(of: searchText) { _ in if searchText == "" { // Cleared the search bar @@ -52,6 +55,13 @@ struct SearchTeamView: View { clearFields() } } + .onChange(of: searchResults + [shownTeamName, searchText]) { _ in + if searchResults.count > 0 && shownTeamName != searchText { + isTyping = true + } else { + isTyping = false + } + } } func clearFields() { @@ -59,6 +69,21 @@ struct SearchTeamView: View { teamName = "" // update for parent showParentButton = false // update for parent } + + func setFields(_ team: String) { + teamName = team // update for parent + showParentButton = true // update for parent + shownTeamName = team // hide the list + searchText = team // show user the selected team + UIApplication.shared.endEditing() + } +} + +// Hide the keyboard +extension UIApplication { + func endEditing() { + sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } } struct SearchTeamView_Previews: PreviewProvider { @@ -66,6 +91,12 @@ struct SearchTeamView_Previews: PreviewProvider { static var previews: some View { let teams = TeamHelper.loadTeams().teams - return SearchTeamView(teamName: .constant(""), showParentButton: .constant(false), label: "Team 1", teams: teams).environment(\.managedObjectContext, viewContext) + return SearchTeamView( + isTyping: .constant(false), + teamName: .constant(""), + showParentButton: .constant(false), + label: "Team 1", + teams: teams + ).environment(\.managedObjectContext, viewContext) } } diff --git a/foam-madness/View/Team/SelectTeamsView.swift b/foam-madness/View/Team/SelectTeamsView.swift index 40df718..abb7fe4 100644 --- a/foam-madness/View/Team/SelectTeamsView.swift +++ b/foam-madness/View/Team/SelectTeamsView.swift @@ -19,28 +19,50 @@ struct SelectTeamsView: View { @State private var team2: String = "" @State private var showButton1 = false @State private var showButton2 = false + @State private var shotsPerRound = AppConstants.defaultShotsPerRound @State private var progressToGame = false @State private var createdGame: Game? + + @State private var hideTeam1Search = false + @State private var hideTeam2Search = false var body: some View { VStack { Text("Select Teams").foregroundColor(commonBlue).font(.largeTitle).fontWeight(.bold) VStack() { - SearchTeamView(teamName: $team1, showParentButton: $showButton1, label: "Team 1", teams: teams) - SearchTeamView(teamName: $team2, showParentButton: $showButton2, label: "Team 2", teams: teams) - } - - if (showButton1 && showButton2) { - if (progressToGame) { - NavigationLink("", destination: PlayGameView(game: createdGame!), isActive: $progressToGame) - } else { - Button("Continue") { - createGame() + if !hideTeam1Search { + SearchTeamView( + isTyping: $hideTeam2Search, + teamName: $team1, + showParentButton: $showButton1, + label: "Team 1", + teams: teams + ) + } + if !hideTeam2Search { + SearchTeamView( + isTyping: $hideTeam1Search, + teamName: $team2, + showParentButton: $showButton2, + label: "Team 2", + teams: teams + ) + } + + if (showButton1 && showButton2) { + Stepper("Shots per round: \(shotsPerRound)", value: $shotsPerRound, in: 3...15) + .padding([.leading, .trailing]) + if (progressToGame) { + NavigationLink("", destination: PlayGameView(game: createdGame!), isActive: $progressToGame) + } else { + Button("Continue") { + createGame() + } + .buttonStyle(PrimaryButtonFullWidthStyle()) + .padding() } - .buttonStyle(PrimaryButtonFullWidthStyle()) - .padding() } } } @@ -59,7 +81,7 @@ struct SelectTeamsView: View { alertUser(title: "Invalid Teams", message: "Selected teams must be different.") return } - createdGame = GameHelper.prepareSingleGame(team1, team2, reverseTeamDict, viewContext) + createdGame = GameHelper.prepareSingleGame(team1, team2, shotsPerRound, reverseTeamDict, viewContext) progressToGame = true } diff --git a/foam-madness/View/Tournament/BracketCreationView.swift b/foam-madness/View/Tournament/BracketCreationView.swift index 485320a..49462dc 100644 --- a/foam-madness/View/Tournament/BracketCreationView.swift +++ b/foam-madness/View/Tournament/BracketCreationView.swift @@ -11,13 +11,18 @@ import SwiftUI struct BracketCreationView: View { @Environment(\.managedObjectContext) private var viewContext // Passed from previous + @State var isCustom: Bool @State var isSimulated: Bool + @State var isWomens: Bool + @State var numTeams: Int // ignored if non-custom @State var chosenBracketFile: String @State private var showProgress = false @State private var progress = 0.0 + @State private var shotsPerRound = AppConstants.defaultShotsPerRound @State private var tournamentName = "" @State private var rightHanded = true + @State private var showShotsHelper = false @State private var tournamentReady = false @State private var tournament: Tournament! @@ -36,12 +41,33 @@ struct BracketCreationView: View { Toggle("\(rightHanded ? "Right" : "Left")-Hand Dominant", isOn: $rightHanded) .toggleStyle(SwitchToggleStyle(tint: commonBlue)) .font(.title2) + HStack { + Text("Shots per round: \(shotsPerRound)") + Button(action: onClickShotsHelper) { + Image(systemName: "info.circle") + .foregroundColor(commonBlue) + } + Spacer() + Stepper("", value: $shotsPerRound, in: 3...15) + .labelsHidden() + }.font(.title2) + if showShotsHelper { + Text("Shots per round controls how many 1 point, 2 point, etc., shot attempts per team each game.") + .font(.callout) + .foregroundColor(.secondary) + } } if (tournamentReady) { - NavigationLink("", destination: TournamentGamesView(tournament: tournament), isActive: $tournamentReady) + NavigationLink("", destination: TournamentGamesView( + showBracketView: tournament.useBracketView, + tournament: tournament + ), isActive: $tournamentReady) } else { - Button("\(isSimulated ? "Sim" : "Create") Tournament", action: { createTournament() }) + let buttonText = isCustom + ? "Continue" + : "\(isSimulated ? "Sim" : "Create") Tournament" + Button(buttonText, action: { createTournament() }) .buttonStyle(PrimaryButtonFullWidthStyle()) } @@ -56,15 +82,16 @@ struct BracketCreationView: View { if !isValidName() { return } - let tournamentOutput = BracketCreationController(context: viewContext) - .createBracket(bracketLocation: chosenBracketFile, tournamentName: tournamentName, isSimulated: isSimulated, useLeft: !rightHanded) - tournament = tournamentOutput.tournament - if (isSimulated) { + if (isCustom) { + tournament = BracketCreationController(context: viewContext) + .createCustomBracket(numTeams: numTeams, isWomens: isWomens, tournamentName: tournamentName, isSimulated: isSimulated, useLeft: !rightHanded, shotsPerRound: shotsPerRound) + } else { + tournament = BracketCreationController(context: viewContext) + .createBracketFromFile(bracketLocation: chosenBracketFile, tournamentName: tournamentName, isSimulated: isSimulated, useLeft: !rightHanded, shotsPerRound: shotsPerRound) + } + if (isSimulated && !isCustom) { let winner = BracketCreationController(context: viewContext) - .simulateTournament( - tournament: tournamentOutput.tournament, - hasFirstFour: tournamentOutput.hasFirstFour - ) + .simulateTournament(tournament: tournament) // Notify user of winner let title = "Tournament Complete" let message = "\(winner) wins the tournament! (Sim)" @@ -94,6 +121,10 @@ struct BracketCreationView: View { } } + private func onClickShotsHelper() { + showShotsHelper = !showShotsHelper + } + private func alertUser(title: String, message: String, _ endTournament: Bool) { let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) if endTournament { @@ -115,7 +146,7 @@ struct BracketCreationView_Previews: PreviewProvider { static var previews: some View { NavigationView { BracketCreationView( - isSimulated: false, chosenBracketFile: "mensBracket2023" + isCustom: false, isSimulated: false, isWomens: false, numTeams: 0, chosenBracketFile: "mensBracket2023" ).environment(\.managedObjectContext, PreviewDataController.shared.container.viewContext) }.navigationViewStyle(StackNavigationViewStyle()) } diff --git a/foam-madness/View/Tournament/InitialBracket/SelectInitialBracketShellView.swift b/foam-madness/View/Tournament/InitialBracket/SelectInitialBracketShellView.swift new file mode 100644 index 0000000..434a29b --- /dev/null +++ b/foam-madness/View/Tournament/InitialBracket/SelectInitialBracketShellView.swift @@ -0,0 +1,30 @@ +// +// SelectInitialBracketShellView.swift +// foam-madness +// +// Created by Michael Virgo on 5/17/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +// Shell view to allow use of chosenBracketFile outside of SelectInitialBracketView +struct SelectInitialBracketShellView: View { + @State var isSimulated: Bool + @State private var chosenBracketFile: String? + + var body: some View { + SelectInitialBracketView( + isSimulated: isSimulated, + chosenBracketFile: $chosenBracketFile + ) + } +} + +struct SelectInitialBracketShellView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + SelectInitialBracketShellView(isSimulated: false).environment(\.managedObjectContext, PreviewDataController.shared.container.viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/InitialBracket/SelectInitialBracketView.swift b/foam-madness/View/Tournament/InitialBracket/SelectInitialBracketView.swift new file mode 100644 index 0000000..51a03f3 --- /dev/null +++ b/foam-madness/View/Tournament/InitialBracket/SelectInitialBracketView.swift @@ -0,0 +1,140 @@ +// +// SelectInitialBracketView.swift +// foam-madness +// +// Created by Michael Virgo on 1/16/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct SelectInitialBracketView: View { + @State var isSimulated: Bool + // Hide existing/custom when coming from later custom view + @State var showCustomSelector = true + @Binding var chosenBracketFile: String? + @State private var chosenBracketName: String? + @State private var chosenYear: String? + @State private var isCustom = false + @State private var isWomens = false + @State private var numTeams = 64 + @State private var brackets: [BracketItem] = [] + let gridPadding = 10.0 + + var body: some View { + GeometryReader { geometry in + VStack(spacing: geometry.size.height * 0.03) { + if showCustomSelector { + Spacer() + + HStack { + Text("Existing/Custom") + .font(.title2) + .lineLimit(1) + Picker("", selection: $isCustom) { + Text("Existing").tag(false) + Text("Custom").tag(true) + } + .pickerStyle(.segmented) + }.padding([.leading, .trailing], 10) + } + + HStack(spacing: 10) { + Text("Men's/Women's") + .font(.title2) + .lineLimit(1) + Picker("", selection: $isWomens) { + Text("Men").tag(false) + Text("Women").tag(true) + } + .pickerStyle(.segmented) + }.padding([.leading, .trailing], 10) + + if isCustom { + SelectNumTeamsView(numTeams: $numTeams) + } else { + SelectYearView(chosenYear: $chosenYear, brackets: $brackets) + } + + VStack(spacing: 10) { + Text("Current Bracket Chosen") + .font(.title2) + Text(chosenBracketName ?? "") + .font(.title2) + .fontWeight(.bold) + } + + if showCustomSelector { + Spacer() + + NavigationLink( + "Continue", + destination: + BracketCreationView( + isCustom: isCustom, + isSimulated: isSimulated, + isWomens: isWomens, + numTeams: numTeams, // unused if not custom + chosenBracketFile: chosenBracketFile ?? "" + ) + ) + .buttonStyle(PrimaryButtonFullWidthStyle()) + .padding([.leading, .trailing, .bottom]) + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + if showCustomSelector { + Text("Choose a Starting Bracket") + .fontWeight(.bold) + } + } + } + .onAppear() { + brackets = BracketHelper.loadBrackets() + } + .onChange(of: chosenYear) { _ in + getChosenBracket() + } + .onChange(of: isCustom) { _ in + getChosenBracket() + } + .onChange(of: isWomens) { _ in + getChosenBracket() + } + .onChange(of: numTeams) { _ in + getChosenBracket() + } + } + } + + private func getBracketName() -> String { + if brackets.count > 0 { + return brackets.filter({ $0.year == Int(chosenYear ?? "") && $0.isWomens == isWomens})[0].name + } + return "" + } + + private func getChosenBracket() { + if (chosenYear != nil && brackets.count > 0 && !isCustom) { + let chosenBracket = brackets.filter({ $0.year == Int(chosenYear ?? "") && $0.isWomens == isWomens})[0] + chosenBracketFile = chosenBracket.file + chosenBracketName = chosenBracket.name + } else if (isCustom) { + chosenBracketFile = "custom" + chosenBracketName = "Custom Bracket - \(numTeams) teams \n(\(isWomens ? "Women's" : "Men's") probabilities)" + } + } +} + +struct SelectInitialBracketView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + SelectInitialBracketView( + isSimulated: false, + chosenBracketFile: .constant("") + ).environment(\.managedObjectContext, PreviewDataController.shared.container.viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/InitialBracket/SelectNumTeamsView.swift b/foam-madness/View/Tournament/InitialBracket/SelectNumTeamsView.swift new file mode 100644 index 0000000..7dc8321 --- /dev/null +++ b/foam-madness/View/Tournament/InitialBracket/SelectNumTeamsView.swift @@ -0,0 +1,52 @@ +// +// SelectNumTeamsView.swift +// foam-madness +// +// Created by Michael Virgo on 5/17/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +let numTeamsArray = [64, 32, 16, 8, 4] + +struct SelectNumTeamsView: View { + @Binding var numTeams: Int + let gridPadding = 10.0 + + var body: some View { + VStack { + Text("Number of Teams").font(.title2) + LazyVGrid(columns: [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ], spacing: gridPadding) { + ForEach(numTeamsArray, id: \.self) { teams in + Button(action: { + numTeams = teams + }) { + Text("\(teams)") + .lineLimit(1) + .font(.headline) + .foregroundColor(numTeams == teams ? Color.white : Color.primary) + .padding() + } + .frame(maxWidth: .infinity) + .background(numTeams == teams ? commonBlue : Color.secondary) + .cornerRadius(5.0) + } + }.padding(gridPadding) + } + } +} + +struct SelectNumTeamsView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + SelectNumTeamsView(numTeams: .constant(64)).environment(\.managedObjectContext, PreviewDataController.shared.container.viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/InitialBracket/SelectYearView.swift b/foam-madness/View/Tournament/InitialBracket/SelectYearView.swift new file mode 100644 index 0000000..f65d57c --- /dev/null +++ b/foam-madness/View/Tournament/InitialBracket/SelectYearView.swift @@ -0,0 +1,73 @@ +// +// SelectYearView.swift +// foam-madness +// +// Created by Michael Virgo on 5/17/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct SelectYearView: View { + @Binding var chosenYear: String? + @Binding var brackets: [BracketItem] + @State private var yearsArray: [String] = [] + let gridPadding = 10.0 + + var body: some View { + VStack { + Text("Choose a Year").font(.title2) + LazyVGrid(columns: [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ], spacing: gridPadding) { + ForEach(yearsArray, id: \.self) { year in + Button(action: { + chosenYear = year + }) { + Text(year) + .lineLimit(1) + .font(.headline) + .foregroundColor(chosenYear == year ? Color.white : Color.primary) + .padding() + } + .frame(maxWidth: .infinity) + .background(chosenYear == year ? commonBlue : Color.secondary) + .cornerRadius(5.0) + } + }.padding(gridPadding) + } + .onAppear { + getYears() + } + .onChange(of: brackets.count) { _ in + getYears() + } + } + + private func getYears() { + let initialYearsArray = brackets.map { $0.year } + let uniqueYears = Set(initialYearsArray) + // sort descending + let sortedYears = uniqueYears.sorted(by: >) + yearsArray = sortedYears.map { String($0) } + if !yearsArray.isEmpty { + chosenYear = yearsArray[0] + } + } +} + +struct SelectYearView_Previews: PreviewProvider { + static var previews: some View { + let brackets = BracketHelper.loadBrackets() + + NavigationView { + SelectYearView( + chosenYear: .constant(String(brackets[0].year)), + brackets: .constant(brackets) + ).environment(\.managedObjectContext, PreviewDataController.shared.container.viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/SelectInitialBracketView.swift b/foam-madness/View/Tournament/SelectInitialBracketView.swift deleted file mode 100644 index 69842a6..0000000 --- a/foam-madness/View/Tournament/SelectInitialBracketView.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// SelectInitialBracketView.swift -// foam-madness -// -// Created by Michael Virgo on 1/16/24. -// Copyright © 2024 mvirgo. All rights reserved. -// - -import SwiftUI - -struct SelectInitialBracketView: View { - @State var isSimulated: Bool - @State private var chosenBracketFile: String? - @State private var chosenBracketName: String? - @State private var chosenYear: String? - @State private var isWomens = false - @State private var yearsArray: [String] = [] - @State private var brackets: [BracketItem] = [] - let gridPadding = 10.0 - - var body: some View { - GeometryReader { geometry in - VStack(spacing: geometry.size.height * 0.03) { - Spacer() - - VStack { - Text("Choose a Year").font(.title2) - LazyVGrid(columns: [ - GridItem(.flexible()), - GridItem(.flexible()), - GridItem(.flexible()), - GridItem(.flexible()) - ], spacing: gridPadding) { - ForEach(yearsArray, id: \.self) { year in - Button(action: { - chosenYear = year - }) { - Text(year) - .lineLimit(1) - .font(.headline) - .foregroundColor(chosenYear == year ? Color.white : Color.primary) - .padding() - } - .frame(maxWidth: .infinity) - .background(chosenYear == year ? commonBlue : Color.secondary) - .cornerRadius(5.0) - } - }.padding(gridPadding) - } - - VStack { - Text("Men's or Women's?").font(.title2) - Picker("", selection: $isWomens) { - Text("Men's").tag(false) - Text("Women's").tag(true) - } - .pickerStyle(.segmented) - .padding([.leading, .trailing], 30) - } - - VStack(spacing: 10) { - Text("Current Bracket Chosen") - .font(.title2) - Text(chosenBracketName ?? "") - .font(.title2) - .fontWeight(.bold) - } - - Spacer() - - NavigationLink( - "Continue", - destination: - BracketCreationView( - isSimulated: isSimulated, - chosenBracketFile: chosenBracketFile ?? "" - ) - ) - .buttonStyle(PrimaryButtonFullWidthStyle()) - .padding([.leading, .trailing, .bottom]) - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text("Choose a Starting Bracket") - .fontWeight(.bold) - } - } - .onAppear() { - brackets = BracketHelper.loadBrackets() - getYears() - } - .onChange(of: chosenYear) { _ in - getChosenBracket() - } - .onChange(of: isWomens) { _ in - getChosenBracket() - } - } - } - - private func getYears() { - let initialYearsArray = brackets.map { $0.year } - let uniqueYears = Set(initialYearsArray) - // sort descending - let sortedYears = uniqueYears.sorted(by: >) - yearsArray = sortedYears.map { String($0) } - chosenYear = yearsArray[0] - } - - private func getBracketName() -> String { - if brackets.count > 0 { - return brackets.filter({ $0.year == Int(chosenYear ?? "") && $0.isWomens == isWomens})[0].name - } - return "" - } - - private func getChosenBracket() { - if (chosenYear != nil && brackets.count > 0) { - let chosenBracket = brackets.filter({ $0.year == Int(chosenYear ?? "") && $0.isWomens == isWomens})[0] - chosenBracketFile = chosenBracket.file - chosenBracketName = chosenBracket.name - } - } -} - -struct SelectInitialBracketView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - SelectInitialBracketView(isSimulated: false).environment(\.managedObjectContext, PreviewDataController.shared.container.viewContext) - }.navigationViewStyle(StackNavigationViewStyle()) - } -} diff --git a/foam-madness/View/Tournament/SelectTournamentView.swift b/foam-madness/View/Tournament/SelectTournamentView.swift index 377a280..a37ba2a 100644 --- a/foam-madness/View/Tournament/SelectTournamentView.swift +++ b/foam-madness/View/Tournament/SelectTournamentView.swift @@ -18,10 +18,14 @@ struct SelectTournamentView: View { var body: some View { List { ForEach(shownTournaments, id: \.id) { tournament in - NavigationLink(tournament.name ?? "", destination: TournamentGamesView(tournament: tournament)) + NavigationLink(tournament.name ?? "", destination: TournamentGamesView( + showBracketView: tournament.useBracketView, + tournament: tournament + )) } .onDelete(perform: deleteItems) } + .listStyle(.plain) .navigationBarTitleDisplayMode(.inline) .navigationTitle(completedTournaments ? "Choose a Completed Tournament" diff --git a/foam-madness/View/Tournament/TournamentGamesView.swift b/foam-madness/View/Tournament/TournamentGamesView.swift deleted file mode 100644 index b16b43e..0000000 --- a/foam-madness/View/Tournament/TournamentGamesView.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// TournamentGamesView.swift -// foam-madness -// -// Created by Michael Virgo on 1/16/24. -// Copyright © 2024 mvirgo. All rights reserved. -// - -import SwiftUI - -struct TournamentGamesView: View { - @State private var initialRound: Int16 = 0 - @State private var finalRound: Int16 = 6 - @State private var roundStepper: Int16 = 0 - @State private var roundText = "ROUND" - @State private var games: [Game] = [] - @State var tournament: Tournament - - var body: some View { - VStack { - Text(GameHelper.getRoundString(roundStepper)) - .foregroundColor(commonBlue) - .font(.largeTitle) - .fontWeight(.bold) - - List { - ForEach(games.filter({ $0.round == roundStepper }), id: \.id) { game in - TournamentGameCell(game: game) - } - } - Text("Change Round") - .foregroundColor(commonBlue) - .font(.title2) - .fontWeight(.bold) - Stepper(value: $roundStepper, in: initialRound...finalRound) { - Text("") - } - .labelsHidden() - .padding([.bottom]) - } - .navigationBarTitleDisplayMode(.inline) - .navigationBarBackButtonHidden(true) - .navigationTitle(tournament.name ?? "Tournament Games") - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button("Main Menu", action: { - NavigationUtil.popToRootView() - }) - } - ToolbarItem(placement: .topBarTrailing) { - if !tournament.isSimulated { - NavigationLink("Stats", destination: TournamentStatsView(tournament: tournament)) - } - } - } - .onAppear { - getSortedGames() - } - .tag("TournamentGames") - } - - func getSortedGames() { - let gamesArray = Array(tournament.games!) as! [Game] - games = gamesArray.sorted() { $0.tourneyGameId < $1.tourneyGameId } - - let minRound = games.min(by: { $0.round < $1.round })?.round ?? 0 - finalRound = games.max(by: { $0.round < $1.round })?.round ?? 6 - initialRound = minRound - roundStepper = minRound - } -} - -struct TournamentGamesView_Previews: PreviewProvider { - static var previews: some View { - let viewContext = PreviewDataController.shared.container.viewContext - let tournaments = TourneyHelper.fetchDataFromContext(viewContext, nil, "Tournament", []) as! [Tournament] - return NavigationView { - TournamentGamesView(tournament: tournaments[0]).environment(\.managedObjectContext, viewContext) - }.navigationViewStyle(StackNavigationViewStyle()) - } -} diff --git a/foam-madness/View/Tournament/TournamentGamesView/BracketView/BracketGameCell.swift b/foam-madness/View/Tournament/TournamentGamesView/BracketView/BracketGameCell.swift new file mode 100644 index 0000000..02b755d --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/BracketView/BracketGameCell.swift @@ -0,0 +1,103 @@ +// +// BracketGameCell.swift +// foam-madness +// +// Created by Michael Virgo on 5/3/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct BracketGameCell: View { + @StateObject var game: Game + @State var spacing: CGFloat + @State private var team1Text: String = "" + @State private var team2Text: String = "" + + var body: some View { + Group { + if game.teams?.count == 2 { + if game.completion == false { + NavigationLink { + PlayGameView(game: game) + } label: { + getLinkLabel + } + .buttonStyle(.plain) + } else { + NavigationLink { + GameScoreView(game: game) + } label: { + getLinkLabel + } + .buttonStyle(.plain) + } + } else { + getLinkLabel + } + } + .onAppear { + let teamsText = TourneyHelper.getBracketGameText(game: game) + team1Text = teamsText[0] + team2Text = teamsText[1] + } + } + + var getLinkLabel: some View { + return ZStack(alignment: .center) { + VStack(alignment: .leading, spacing: 1) { + Text(team1Text) + .frame(minWidth: 140, alignment: .leading) + .padding([.leading, .trailing]) + Text(team2Text) + .frame(minWidth: 140, alignment: .leading) + .padding([.top], spacing) + .padding([.leading, .trailing]) + .padding([.bottom], 5) + .border(width: 5, edges: [.top, .bottom, .trailing], color: commonBlue) + } + .contentShape(Rectangle()) // allow click on open space in bracket + + // List the region if First Four + if (game.round == 0) { + Text(game.region ?? "") + .font(.caption) + .foregroundColor(.secondary) + } + } + } +} + +// 3-sided edge code below from: https://stackoverflow.com/a/58632759 +extension View { + func border(width: CGFloat, edges: [Edge], color: Color) -> some View { + overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color)) + } +} + +struct EdgeBorder: Shape { + var width: CGFloat + var edges: [Edge] + + func path(in rect: CGRect) -> Path { + edges.map { edge -> Path in + switch edge { + case .top: return Path(.init(x: rect.minX, y: rect.minY, width: rect.width, height: width)) + case .bottom: return Path(.init(x: rect.minX, y: rect.maxY - width, width: rect.width, height: width)) + case .leading: return Path(.init(x: rect.minX, y: rect.minY, width: width, height: rect.height)) + case .trailing: return Path(.init(x: rect.maxX - width, y: rect.minY, width: width, height: rect.height)) + } + }.reduce(into: Path()) { $0.addPath($1) } + } +} + +struct BracketGameCell_Previews: PreviewProvider { + static var previews: some View { + let viewContext = PreviewDataController.shared.container.viewContext + let predicate = NSPredicate(format: "completion == YES") + let games = TourneyHelper.fetchDataFromContext(viewContext, predicate, "Game", []) as! [Game] + return NavigationView { + BracketGameCell(game: games[0], spacing: 15.0).environment(\.managedObjectContext, PreviewDataController.shared.container.viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/BracketView/BracketGamesView.swift b/foam-madness/View/Tournament/TournamentGamesView/BracketView/BracketGamesView.swift new file mode 100644 index 0000000..63e6e65 --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/BracketView/BracketGamesView.swift @@ -0,0 +1,183 @@ +// +// BracketGamesView.swift +// foam-madness +// +// Created by Michael Virgo on 3/28/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct BracketGamesView: View { + @Environment(\.presentationMode) var presentationMode + @State var tournament: Tournament + @State private var chosenRegion = "" + @State private var regionWinnerName = "" + @State private var games: [Game] = [] + @State private var maxRoundForRegion: Int = 0 + @State private var minRoundForRegion: Int = 0 + @State private var regions: [String] = [] + @Binding var hideListView: Bool + + let gridPadding = 10.0 + let baseBracketSpacing: CGFloat = 20.0 + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack { + Spacer() + // Bracket itself + ScrollView([.horizontal, .vertical]) { + LazyHStack(spacing: 0) { + ForEach(minRoundForRegion.. CGFloat { + let multiplier = index == minRoundForRegion ? 1.0 : pow(CGFloat(index - minRoundForRegion), 1.33) * 3.5 + return baseBracketSpacing * multiplier + } + + private func getOuterSpacing(index: Int) -> CGFloat { + let multiplier = index == minRoundForRegion ? 1.0 : pow(CGFloat(index - minRoundForRegion), 1.1) * 1.6 + return baseBracketSpacing * multiplier + } +} + +struct BracketGamesView_Previews: PreviewProvider { + static var previews: some View { + let viewContext = PreviewDataController.shared.container.viewContext + let tournaments = TourneyHelper.fetchDataFromContext(viewContext, nil, "Tournament", []) as! [Tournament] + return NavigationView { + BracketGamesView(tournament: tournaments[0], hideListView: .constant(true)).environment(\.managedObjectContext, viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/BracketView/BracketWinnerLine.swift b/foam-madness/View/Tournament/TournamentGamesView/BracketView/BracketWinnerLine.swift new file mode 100644 index 0000000..184fe0b --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/BracketView/BracketWinnerLine.swift @@ -0,0 +1,38 @@ +// +// BracketWinnerLine.swift +// foam-madness +// +// Created by Michael Virgo on 5/6/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct BracketWinnerLine: View { + @Binding var winnerName: String + @Binding var maxRoundForRegion: Int + + var body: some View { + VStack { + // Skip for First Four + if maxRoundForRegion == 0 { + EmptyView() + } else { + Text(winnerName == "" ? "Pending" : winnerName) + .frame(minWidth: 140, alignment: .leading) + .padding([.leading, .trailing]) + .padding([.bottom], 5) + .border(width: 5, edges: [.bottom], color: commonBlue) + } + } + } +} + +struct BracketWinnerLine_Previews: PreviewProvider { + static var previews: some View { + return BracketWinnerLine( + winnerName: .constant("KU"), + maxRoundForRegion: .constant(1) + ) + } +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/CreateCustomView.swift b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/CreateCustomView.swift new file mode 100644 index 0000000..16cec27 --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/CreateCustomView.swift @@ -0,0 +1,47 @@ +// +// CreateCustomView.swift +// foam-madness +// +// Created by Michael Virgo on 5/16/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct CreateCustomView: View { + @State var tournament: Tournament + @State private var showGames: Bool = false + @Binding var isReady: Bool + + var body: some View { + VStack { + if !showGames { + CustomTypeView(tournament: tournament, showGames: $showGames) + } else { + ListCustomTeamsView(tournament: tournament, isReady: $isReady) + } + } + .onAppear { + // Move past selection screen if already added any teams + let games = tournament.games?.allObjects as! [Game] + let gamesWithTeams = games.filter({ $0.teams?.count ?? 0 > 0 }) + if gamesWithTeams.count > 0 { + showGames = true + } + } + } +} + +struct CreateCustomView_Previews: PreviewProvider { + static var previews: some View { + let viewContext = PreviewDataController.shared.container.viewContext + let predicate = NSPredicate(format: "ready == NO") + let tournaments = TourneyHelper.fetchDataFromContext(viewContext, predicate, "Tournament", []) as! [Tournament] + return NavigationView { + CreateCustomView( + tournament: tournaments[0], + isReady: .constant(false) + ).environment(\.managedObjectContext, viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/CustomTypeView.swift b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/CustomTypeView.swift new file mode 100644 index 0000000..f67c1c5 --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/CustomTypeView.swift @@ -0,0 +1,100 @@ +// +// CustomTypeView.swift +// foam-madness +// +// Created by Michael Virgo on 5/16/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +enum CustomType { + case random + case existing + case selectAll +} + +struct CustomTypeView: View { + @Environment(\.managedObjectContext) private var viewContext + @State var tournament: Tournament + @State var customType = CustomType.random + @State private var numTeams: Int = 0 + @State private var chosenBracketFile: String? // existing + @Binding var showGames: Bool + + var body: some View { + VStack(spacing: 10) { + Text("How do you want to fill your custom bracket?") + + Picker("", selection: $customType) { + Text("Randomly").tag(CustomType.random) + if numTeams > 4 { + Text("Base on Existing").tag(CustomType.existing) + } + Text("Select All").tag(CustomType.selectAll) + } + .pickerStyle(.menu) + .accentColor(commonBlue) + .scaleEffect(1.4) + + descriptionLabel + .font(.footnote) + .foregroundColor(.secondary) + + if customType == CustomType.existing { + SelectInitialBracketView( + isSimulated: tournament.isSimulated, + showCustomSelector: false, + chosenBracketFile: $chosenBracketFile + ) + .scaleEffect(0.9) + .aspectRatio(contentMode: .fit) + .background(commonBlue.opacity(0.2)) + .cornerRadius(5.0) + } + + Button(action: handleContinue, label: { + Text("Continue") + }) + .buttonStyle(PrimaryButtonFullWidthStyle()) + .scaleEffect(0.8) + } + .padding([.leading, .trailing], 10) + .onAppear { + numTeams = (tournament.games?.count ?? 0) + 1 + } + } + + var descriptionLabel: some View { + let standardEndText = ", and then you can edit teams and seeds from there." + switch customType { + case .random: + return Text("The bracket will be initially filled with \(numTeams) random teams" + standardEndText) + case .existing: + return Text("You'll choose an existing bracket to fill the teams initially" + standardEndText + " Fewer than 64 teams uses original seeds, not real results of later rounds. Men/Women selection here only affects the bracket used, not probabilities.") + case .selectAll: + return Text("You'll need to choose all \(numTeams) teams for the bracket, and can edit seeds as well.") + } + } + + func handleContinue() { + if customType == CustomType.random { + BracketCreationController(context: viewContext).fillTournamentWithRandomTeams(tournament) + } else if customType == CustomType.existing { + BracketCreationController(context: viewContext).fillTournamentFromExistingCustom(tournament, chosenBracketFile ?? "") + } + // selectAll has no pre-processing + showGames = true + } +} + +struct CustomTypeView_Previews: PreviewProvider { + static var previews: some View { + let viewContext = PreviewDataController.shared.container.viewContext + let predicate = NSPredicate(format: "ready == NO") + let tournaments = TourneyHelper.fetchDataFromContext(viewContext, predicate, "Tournament", []) as! [Tournament] + return NavigationView { + CustomTypeView(tournament: tournaments[0], showGames: .constant(false)).environment(\.managedObjectContext, viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/ListCustomTeamsView.swift b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/ListCustomTeamsView.swift new file mode 100644 index 0000000..a04a170 --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/ListCustomTeamsView.swift @@ -0,0 +1,143 @@ +// +// ListCustomTeamsView.swift +// foam-madness +// +// Created by Michael Virgo on 5/16/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct ListCustomTeamsView: View { + @Environment(\.presentationMode) var presentationMode + @Environment(\.managedObjectContext) private var viewContext + @State var tournament: Tournament + @Binding var isReady: Bool + @State private var games: [Game] = [] + @State private var showHelper = false + @State private var showReadyGames = false + @State private var readyTotal = 0 + + var body: some View { + VStack(spacing: 10) { + HStack { + Text(showReadyGames ? "Ready Games" : "Select Teams") + .font(.title) + .fontWeight(.bold) + Button(action: { showHelper = !showHelper }) { + Image(systemName: "info.circle") + }.font(.title2) + } + .foregroundColor(commonBlue) + + if showHelper { + Text(showReadyGames + ? "These games have both teams selected, but you can still change them." + : "These games need one or both teams selected still." + ) + .font(.callout) + .foregroundColor(.secondary) + } + + List { + ForEach(games.filter({ + showReadyGames + ? $0.teams?.count == 2 + : $0.teams?.count != 2 + }), id: \.id) { game in + TournamentListGameCell(game: game, allowSelection: true) + .listRowBackground(showReadyGames ? Color.green.opacity(0.2) : Color.gray) + } + }.listStyle(.plain) + + Picker("", selection: $showReadyGames) { + Text("Need Teams").tag(false) + Text("Ready").tag(true) + } + .pickerStyle(.segmented) + + Text("\(readyTotal) / \(games.count) games ready") + .font(.headline) + + if games.count > 2 { + // Final Four can't change + NavigationLink("Change Region Names", destination: UpdateRegionsView(games: tournament.games!)) + } + + if readyTotal == games.count { + Button("\(tournament.isSimulated ? "Sim" : "Create") Tournament") { + handleContinue() + } + .buttonStyle(PrimaryButtonFullWidthStyle()) + .scaleEffect(0.8) + } + } + .padding() + .onAppear { + getSortedInitialRoundGames() + } + } + + private func getSortedInitialRoundGames() { + let gamesArray = Array(tournament.games!) as! [Game] + let minRound = gamesArray.min(by: { $0.round < $1.round })?.round ?? 0 + let initialRoundGames = gamesArray.filter({ $0.round == minRound }) + games = initialRoundGames.sorted() { $0.tourneyGameId < $1.tourneyGameId } + + // Default to ready tab if no games need selection + let gamesWithoutBothTeams = games.filter({ $0.teams?.count ?? 0 < 2}) + if gamesWithoutBothTeams.count == 0 { + showReadyGames = true + } + + readyTotal = games.count - gamesWithoutBothTeams.count + } + + private func handleContinue() { + alertUser( + title: "Locked In", + message: "Once you continue, you can no longer make changes to the bracket, and your tournament will be \(tournament.isSimulated ? "simulated" : "created").", + isContinueCheck: true + ) + } + + private func createTournament() { + BracketCreationController(context: viewContext).finalizeCustomBracket(tournament) + if tournament.isSimulated { + let winner = BracketCreationController(context: viewContext) + .simulateTournament(tournament: tournament) + // Notify user of winner + let title = "Tournament Complete" + let message = "\(winner) wins the tournament! (Sim)" + alertUser(title: title, message: message, isContinueCheck: false) + } + isReady = true + } + + private func alertUser(title: String, message: String, isContinueCheck: Bool) { + let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertVC.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil)) + alertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: {_ in + if isContinueCheck { + createTournament() + } + })) + + let viewController = UIApplication.shared.windows.first!.rootViewController! + viewController.present(alertVC, animated: true, completion: nil) + } +} + +struct ListCustomTeamsView_Previews: PreviewProvider { + static var previews: some View { + let viewContext = PreviewDataController.shared.container.viewContext + let predicate = NSPredicate(format: "ready == NO") + let tournaments = TourneyHelper.fetchDataFromContext(viewContext, predicate, "Tournament", []) as! [Tournament] + return NavigationView { + ListCustomTeamsView( + tournament: tournaments[0], + isReady: .constant(false) + ).environment(\.managedObjectContext, viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/SeedSelector.swift b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/SeedSelector.swift new file mode 100644 index 0000000..fca8930 --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/SeedSelector.swift @@ -0,0 +1,28 @@ +// +// SeedSelector.swift +// foam-madness +// +// Created by Michael Virgo on 5/18/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct SeedSelector: View { + @State var teamNum: Int + @Binding var selectedSeed: Int16 + + var body: some View { + HStack { + Text("Team \(teamNum) Seed: \(selectedSeed)") + .fontWeight(.bold) + Spacer() + Stepper("", value: $selectedSeed, in: 1...16) + .labelsHidden() + }.padding([.leading, .trailing]) + } +} + +#Preview { + SeedSelector(teamNum: 1, selectedSeed: .constant(1)) +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/SelectCustomTeamsView.swift b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/SelectCustomTeamsView.swift new file mode 100644 index 0000000..77b4ce4 --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/SelectCustomTeamsView.swift @@ -0,0 +1,222 @@ +// +// SelectCustomTeamsView.swift +// foam-madness +// +// Created by Michael Virgo on 5/18/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct SelectCustomTeamsView: View { + @Environment(\.presentationMode) var presentationMode + @Environment(\.managedObjectContext) private var viewContext + + @State var game: Game + @State var tournament: Tournament + + @State private var teams: [String] = [] + @State private var reverseTeamDict: [String: [String: String]] = [:] + + @State private var originalSeed1: Int16 = 1 + @State private var originalSeed2: Int16 = 1 + @State private var selectedSeed1: Int16 = 1 + @State private var selectedSeed2: Int16 = 1 + + @State private var originalTeam1: String = "" + @State private var originalTeam2: String = "" + @State private var selectedTeam1: String = "" + @State private var selectedTeam2: String = "" + + @State private var team1Searching = false + @State private var team2Searching = false + @State private var isValidating = false + + var body: some View { + VStack(spacing: 5) { + Spacer() + + if !(team1Searching || team2Searching) { + Text("Update Teams & Seeds") + .foregroundColor(commonBlue) + .font(.largeTitle) + .fontWeight(.bold) + } + + if !team2Searching { + SearchTeamView( + isTyping: $team1Searching, + teamName: $selectedTeam1, + showParentButton: .constant(true), + label: "Team 1", + teams: teams + ) + if !team1Searching { + SeedSelector(teamNum: 1, selectedSeed: $selectedSeed1) + } + } + + if !team1Searching { + SearchTeamView( + isTyping: $team2Searching, + teamName: $selectedTeam2, + showParentButton: .constant(true), + label: "Team 2", + teams: teams + ) + if !team2Searching { + SeedSelector(teamNum: 2, selectedSeed: $selectedSeed2) + } + } + + if !(team2Searching || team1Searching) { + Spacer() + if originalSeed1 != selectedSeed1 || originalSeed2 != selectedSeed2 { + Text("Changing seeds changes the probabilities used, but won't change game position in the bracket.") + .fixedSize(horizontal: false, vertical: true) + .font(.footnote) + .foregroundColor(.secondary) + .padding([.leading, .trailing]) + } + + Button("Confirm Changes") { + handleConfirmChanges() + } + .disabled(isValidating) + .buttonStyle(PrimaryButtonFullWidthStyle()) + .scaleEffect(0.75) + .padding([.leading, .trailing]) + + } + } + .onAppear { + setupView() + } + } + + private func setupView() { + let loadedTeams = TeamHelper.loadTeams() + let teamsNamesByIdDict = loadedTeams.teamsNamesByIdDict + teams = loadedTeams.teams + reverseTeamDict = loadedTeams.reverseTeamDict + + originalSeed1 = game.team1Seed + originalSeed2 = game.team2Seed + selectedSeed1 = game.team1Seed + selectedSeed2 = game.team2Seed + + var team1 = "" + var team2 = "" + // Use teamsNamesByIdDict to avoid bug caused by lengthening names in v1.9 + if game.teams?.count == 2 { + let orderedTeams = GameHelper.getOrderedTeams(game) + team1 = teamsNamesByIdDict[String(orderedTeams[0].id)] ?? "" + team2 = teamsNamesByIdDict[String(orderedTeams[1].id)] ?? "" + } else if game.teams?.count == 1 { + let team = game.teams?.allObjects.first as! Team + let teamName = teamsNamesByIdDict[String(team.id)] + if team.id == game.team1Id { + team1 = teamName ?? "" + } else { + team2 = teamName ?? "" + } + } + + originalTeam1 = team1 + originalTeam2 = team2 + selectedTeam1 = team1 + selectedTeam2 = team2 + } + + private func handleConfirmChanges() { + isValidating = true + let isValidated = validateChange() + isValidating = false + if isValidated { + updateTeamsAndSeeds() + } + } + + private func validateChange() -> Bool { + if selectedTeam1 == "" && selectedTeam2 == "" { + return true + } + + if selectedTeam1 == selectedTeam2 { + alertUser(title: "Invalid Teams", message: "Selected teams must be different.") + return false + } + + if selectedTeam1 != "" && originalTeam1 != selectedTeam1 { + let teamId = Int16(reverseTeamDict[selectedTeam1]?["id"] ?? "-1") ?? -1 + let validation = TourneyHelper.checkForDuplicateTeamInTournament( + tournament, + currentGameId: game.tourneyGameId, + teamId: teamId + ) + if validation != nil { + alertUser( + title: "Team Already Chosen", + message: TourneyHelper.duplicateTeamAlertMessage( + selectedTeam1, + validation + ) + ) + return false + } + } + + if selectedTeam2 != "" && originalTeam2 != selectedTeam2 { + let teamId = Int16(reverseTeamDict[selectedTeam2]?["id"] ?? "-1") ?? -1 + let validation = TourneyHelper.checkForDuplicateTeamInTournament( + tournament, + currentGameId: game.tourneyGameId, + teamId: teamId + ) + if validation != nil { + alertUser( + title: "Team Already Chosen", + message: TourneyHelper.duplicateTeamAlertMessage( + selectedTeam2, + validation + ) + ) + return false + } + } + + return true + } + + private func updateTeamsAndSeeds() { + game.team1Seed = selectedSeed1 + game.team2Seed = selectedSeed2 + GameHelper.updateTeamsInGame(selectedTeam1, selectedTeam2, game, reverseTeamDict, viewContext) + presentationMode.wrappedValue.dismiss() + } + + private func alertUser(title: String, message: String) { + let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + + let viewController = UIApplication.shared.windows.first!.rootViewController! + viewController.present(alertVC, animated: true, completion: nil) + } +} + +struct SelectCustomTeamsView_Previews: PreviewProvider { + static var previews: some View { + let viewContext = PreviewDataController.shared.container.viewContext + let predicate = NSPredicate(format: "ready == NO") + let tournaments = TourneyHelper.fetchDataFromContext(viewContext, predicate, "Tournament", []) as! [Tournament] + let tournament = tournaments[0] + let games = tournament.games?.allObjects as! [Game] + let game = games.filter({ $0.tourneyGameId == 5}).first! + return NavigationView { + SelectCustomTeamsView( + game: game, + tournament: tournament + ).environment(\.managedObjectContext, viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/UpdateRegionsView.swift b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/UpdateRegionsView.swift new file mode 100644 index 0000000..f1685bd --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/CreateCustomView/UpdateRegionsView.swift @@ -0,0 +1,106 @@ +// +// UpdateRegionsView.swift +// foam-madness +// +// Created by Michael Virgo on 5/18/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct UpdateRegionsView: View { + @Environment(\.presentationMode) var presentationMode + @Environment(\.managedObjectContext) private var viewContext + + @State var games: NSSet + @State private var originalRegions: [String] = [] + @State private var currentRegions: [String] = [] + + var body: some View { + VStack(spacing: 10) { + Text("Update Region Names") + .font(.title) + .fontWeight(.bold) + .foregroundColor(commonBlue) + + LazyVStack(spacing: 20) { + ForEach(currentRegions.indices, id: \.self) { index in + TextField(originalRegions[index], text: $currentRegions[index]) + .disableAutocorrection(true) + .textFieldStyle(.roundedBorder) + .padding([.leading, .trailing], 20) + } + } + .padding([.top, .bottom], 20) + .background(commonBlue.opacity(0.2)) + + Button("Confirm Names") { + handleConfirmNames() + } + .buttonStyle(PrimaryButtonFullWidthStyle()) + .scaleEffect(0.8) + + Spacer() + } + .padding([.top]) + .onAppear { + let gamesArray = Array(games) as! [Game] + let sortedGames = gamesArray.sorted() { $0.tourneyGameId < $1.tourneyGameId } + + let tournamentRegions = sortedGames.compactMap { $0.region } + let uniqueTournamentRegions = Array(Set(tournamentRegions)) + let filteredRegions = uniqueTournamentRegions.filter({ + $0 != "Final Four" && $0 != "Championship" + }).sorted() { $0 < $1 } + + originalRegions = filteredRegions + currentRegions = filteredRegions + } + } + + private func handleConfirmNames() { + for region in currentRegions { + if region == "" { + alertUser(title: "Missing Name", message: "Please give a name to all regions.") + return + } + } + + let uniqueRegions = Set(currentRegions) + if uniqueRegions.count != currentRegions.count { + alertUser(title: "Unique Regions", message: "All region names must be unique.") + return + } + + // Update all games for each updated region + for (i, region) in currentRegions.enumerated() { + if region != originalRegions[i] { + BracketCreationController(context: viewContext) + .updateRegionNameForGames( + originalName: originalRegions[i], newName: region, gamesSet: games + ) + } + } + + presentationMode.wrappedValue.dismiss() + } + + private func alertUser(title: String, message: String) { + let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertVC.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + + let viewController = UIApplication.shared.windows.first!.rootViewController! + viewController.present(alertVC, animated: true, completion: nil) + } +} + +struct UpdateRegionsView_Previews: PreviewProvider { + static var previews: some View { + let viewContext = PreviewDataController.shared.container.viewContext + let predicate = NSPredicate(format: "ready == NO") + let tournaments = TourneyHelper.fetchDataFromContext(viewContext, predicate, "Tournament", []) as! [Tournament] + return NavigationView { + UpdateRegionsView(games: tournaments[0].games!).environment(\.managedObjectContext, viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/ListView/BracketIcon.swift b/foam-madness/View/Tournament/TournamentGamesView/ListView/BracketIcon.swift new file mode 100644 index 0000000..43a209a --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/ListView/BracketIcon.swift @@ -0,0 +1,72 @@ +// +// BracketIcon.swift +// foam-madness +// +// Created by Michael Virgo on 3/27/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +let lineWidth: CGFloat = 10 + +private struct BracketShape: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + + let shift: CGFloat = 100 + let lineAdj = lineWidth / 2 // connect end of lines + let topY = rect.minY - shift * 1.5 + let bottomY = rect.maxY - (shift / 2) + let leftX = rect.minX - (shift * 1.25) + let midX = leftX + shift + let rightX = midX + shift + let farRightX = rightX + shift + + // Define the points of the bracket shape + // Left side + path.move(to: CGPoint(x: leftX, y: topY)) + path.addLine(to: CGPoint(x: midX, y: topY)) + path.move(to: CGPoint(x: leftX, y: bottomY)) + path.addLine(to: CGPoint(x: midX, y: bottomY)) + path.move(to: CGPoint(x: midX, y: topY - lineAdj)) + path.addLine(to: CGPoint(x: midX, y: bottomY + lineAdj)) + + path.move(to: CGPoint(x: leftX, y: topY + shift * 2)) + path.addLine(to: CGPoint(x: midX, y: topY + shift * 2)) + path.move(to: CGPoint(x: leftX, y: bottomY + shift * 2)) + path.addLine(to: CGPoint(x: midX, y: bottomY + shift * 2)) + path.move(to: CGPoint(x: midX, y: topY - lineAdj + shift * 2)) + path.addLine(to: CGPoint(x: midX, y: bottomY + lineAdj + shift * 2)) + + // Middle bracket + path.move(to: CGPoint(x: midX, y: rect.midY - shift)) + path.addLine(to: CGPoint(x: rightX, y: rect.midY - shift)) + + path.move(to: CGPoint(x: midX, y: rect.midY + shift)) + path.addLine(to: CGPoint(x: rightX, y: rect.midY + shift)) + + path.move(to: CGPoint(x: rightX, y: rect.midY - shift - lineAdj)) + path.addLine(to: CGPoint(x: rightX, y: rect.midY + shift + lineAdj)) + + // Final line + path.move(to: CGPoint(x: rightX, y: rect.midY)) + path.addLine(to: CGPoint(x: farRightX, y: rect.midY)) + + return path + } +} + +struct BracketIcon: View { + var body: some View { + VStack { + BracketShape() + .stroke(commonBlue, lineWidth: lineWidth) + .frame(width: 50, height: 50) + } + } +} + +#Preview { + BracketIcon() +} diff --git a/foam-madness/View/Tournament/TournamentGameCell.swift b/foam-madness/View/Tournament/TournamentGamesView/ListView/TournamentListGameCell.swift similarity index 58% rename from foam-madness/View/Tournament/TournamentGameCell.swift rename to foam-madness/View/Tournament/TournamentGamesView/ListView/TournamentListGameCell.swift index 9786edc..7d20d6b 100644 --- a/foam-madness/View/Tournament/TournamentGameCell.swift +++ b/foam-madness/View/Tournament/TournamentGamesView/ListView/TournamentListGameCell.swift @@ -1,5 +1,5 @@ // -// TournamentGameCell.swift +// TournamentListGameCell.swift // foam-madness // // Created by Michael Virgo on 3/11/24. @@ -8,11 +8,12 @@ import SwiftUI -struct TournamentGameCell: View { +struct TournamentListGameCell: View { @StateObject var game: Game + @State var allowSelection = false var body: some View { - if game.teams?.count == 2 { + if game.teams?.count == 2 && !allowSelection { if game.completion == false { NavigationLink { PlayGameView(game: game) @@ -29,7 +30,23 @@ struct TournamentGameCell: View { )) } } else { - Text("Pending participants").listRowBackground(Color.gray) + Group { + if allowSelection { + HStack { + NavigationLink { + SelectCustomTeamsView( + game: game, + tournament: game.tournament! + ) + } label: { + getLinkLabel + } + } + } else { + getLinkLabel + .listRowBackground(Color.gray) + } + } } } @@ -42,13 +59,13 @@ struct TournamentGameCell: View { } } -struct TournamentGameCell_Previews: PreviewProvider { +struct TournamentListGameCell_Previews: PreviewProvider { static var previews: some View { let viewContext = PreviewDataController.shared.container.viewContext let predicate = NSPredicate(format: "completion == YES") let games = TourneyHelper.fetchDataFromContext(viewContext, predicate, "Game", []) as! [Game] return NavigationView { - TournamentGameCell(game: games[0]).environment(\.managedObjectContext, PreviewDataController.shared.container.viewContext) + TournamentListGameCell(game: games[0]).environment(\.managedObjectContext, PreviewDataController.shared.container.viewContext) }.navigationViewStyle(StackNavigationViewStyle()) } } diff --git a/foam-madness/View/Tournament/TournamentGamesView/ListView/TournamentListGamesView.swift b/foam-madness/View/Tournament/TournamentGamesView/ListView/TournamentListGamesView.swift new file mode 100644 index 0000000..b074b43 --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/ListView/TournamentListGamesView.swift @@ -0,0 +1,96 @@ +// +// TournamentListGamesView.swift +// foam-madness +// +// Created by Michael Virgo on 1/16/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct TournamentListGamesView: View { + @State private var initialRound: Int16 = 0 + @State private var finalRound: Int16 = 6 + @State private var roundStepper: Int16 = 0 + @State private var roundText = "ROUND" + @State private var games: [Game] = [] + @State var tournament: Tournament + @Binding var bracketView: Bool + + var body: some View { + VStack { + Text(GameHelper.getRoundString(roundStepper)) + .foregroundColor(commonBlue) + .font(.largeTitle) + .fontWeight(.bold) + + List { + ForEach(games.filter({ $0.round == roundStepper }), id: \.id) { game in + TournamentListGameCell(game: game) + } + }.listStyle(.plain) + + HStack { + // Hide below to keep spacing + BracketIcon() + .scaleEffect(CGSize(width: 0.15, height: 0.15)) + .hidden() + .disabled(true) + Spacer() + VStack { + Text("Change Round") + .foregroundColor(commonBlue) + .font(.title2) + .fontWeight(.bold) + Stepper(value: $roundStepper, in: initialRound...finalRound) { + Text("") + } + .labelsHidden() + .padding([.bottom]) + } + Spacer() + Button(action: { useBracketView() }) { + BracketIcon() + .scaleEffect(CGSize(width: 0.15, height: 0.15)) + } + }.padding([.leading, .trailing]) + } + .onAppear { + getSortedGames() + } + .onDisappear { + tournament.lastRoundViewed = roundStepper + } + } + + func getSortedGames() { + let gamesArray = Array(tournament.games!) as! [Game] + games = gamesArray.sorted() { $0.tourneyGameId < $1.tourneyGameId } + + let minRound = games.min(by: { $0.round < $1.round })?.round ?? 0 + finalRound = games.max(by: { $0.round < $1.round })?.round ?? 6 + initialRound = minRound + + let lastRoundViewed = tournament.lastRoundViewed + if minRound > lastRoundViewed { + roundStepper = minRound + tournament.lastRoundViewed = minRound + } else { + roundStepper = lastRoundViewed + } + } + + func useBracketView() { + bracketView = true + } +} + +struct TournamentListGamesView_Previews: PreviewProvider { + static var previews: some View { + let viewContext = PreviewDataController.shared.container.viewContext + let tournaments = TourneyHelper.fetchDataFromContext(viewContext, nil, "Tournament", []) as! [Tournament] + return NavigationView { + TournamentListGamesView(tournament: tournaments[0], bracketView: .constant(false)).environment(\.managedObjectContext, viewContext) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/View/Tournament/TournamentGamesView/TournamentGamesView.swift b/foam-madness/View/Tournament/TournamentGamesView/TournamentGamesView.swift new file mode 100644 index 0000000..dacc8a6 --- /dev/null +++ b/foam-madness/View/Tournament/TournamentGamesView/TournamentGamesView.swift @@ -0,0 +1,65 @@ +// +// TournamentGamesView.swift +// foam-madness +// +// Created by Michael Virgo on 5/12/24. +// Copyright © 2024 mvirgo. All rights reserved. +// + +import SwiftUI + +struct TournamentGamesView: View { + @State var showBracketView: Bool + @State var tournament: Tournament + // Trigger a view update if custom tournament marked ready + @State private var customIsReady = false + + var body: some View { + Group { + if tournament.ready || customIsReady { + if showBracketView { + BracketGamesView(tournament: tournament, hideListView: $showBracketView) + } else { + TournamentListGamesView(tournament: tournament, bracketView: $showBracketView) + } + } else { + CreateCustomView(tournament: tournament, isReady: $customIsReady) + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .navigationTitle(tournament.name ?? "Tournament Games") + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Main Menu", action: { + NavigationUtil.popToRootView() + }) + } + ToolbarItem(placement: .topBarTrailing) { + if !tournament.isSimulated && tournament.ready { + NavigationLink("Stats", destination: TournamentStatsView(tournament: tournament)) + } + } + } + .onAppear { + showBracketView = tournament.useBracketView + } + .onDisappear { + tournament.useBracketView = showBracketView + } + .tag("TournamentGames") + } +} + +struct TournamentGamesView_Previews: PreviewProvider { + static var previews: some View { + let viewContext = PreviewDataController.shared.container.viewContext + let tournaments = TourneyHelper.fetchDataFromContext(viewContext, nil, "Tournament", []) as! [Tournament] + return NavigationView { + TournamentGamesView( + showBracketView: tournaments[0].useBracketView, + tournament: tournaments[0]).environment(\.managedObjectContext, viewContext + ) + }.navigationViewStyle(StackNavigationViewStyle()) + } +} diff --git a/foam-madness/probabilities/historicalProbabilities.plist b/foam-madness/probabilities/historicalProbabilities.plist index aedd61d..b1a4a04 100644 --- a/foam-madness/probabilities/historicalProbabilities.plist +++ b/foam-madness/probabilities/historicalProbabilities.plist @@ -5,7 +5,7 @@ Source Date Accessed - 2024-03-15T00:32:40Z + 2024-05-19T15:52:11Z URL http://mcubed.net/ncaab/seeds.shtml @@ -14,25 +14,25 @@ 1 0.5 2 - 0.545 + 0.551 3 - 0.634 + 0.643 4 - 0.705 + 0.6909999999999999 5 - 0.797 + 0.803 6 0.706 7 0.857 8 - 0.785 + 0.787 9 - 0.906 + 0.909 10 0.875 11 - 0.556 + 0.6 12 1 13 @@ -49,23 +49,23 @@ 2 0.5 3 - 0.606 + 0.603 4 0.5 5 0.25 6 - 0.722 + 0.703 7 - 0.696 + 0.705 8 0.4 9 0.667 10 - 0.641 + 0.646 11 - 0.842 + 0.8 12 1 13 @@ -73,7 +73,7 @@ 14 0.5 15 - 0.928 + 0.929 16 0.5 @@ -86,7 +86,7 @@ 5 0.5 6 - 0.583 + 0.577 7 0.632 8 @@ -96,13 +96,13 @@ 10 0.6919999999999999 11 - 0.667 + 0.677 12 0.5 13 0.5 14 - 0.855 + 0.853 15 0.667 16 @@ -113,9 +113,9 @@ 4 0.5 5 - 0.573 + 0.5669999999999999 6 - 0.333 + 0.429 7 0.333 8 @@ -125,11 +125,11 @@ 10 1 11 - 0.5 + 0 12 - 0.705 + 0.717 13 - 0.789 + 0.788 14 0.5 15 @@ -142,7 +142,7 @@ 5 0.5 6 - 1 + 0.667 7 0.5 8 @@ -154,9 +154,9 @@ 11 0.5 12 - 0.674 + 0.67 13 - 0.85 + 0.857 14 0.5 15 @@ -177,7 +177,7 @@ 10 0.6 11 - 0.628 + 0.619 12 0.5 13 @@ -198,7 +198,7 @@ 9 0.5 10 - 0.606 + 0.609 11 0 12 @@ -217,7 +217,7 @@ 8 0.5 9 - 0.511 + 0.506 10 0.5 11 diff --git a/foam-madness/probabilities/historicalProbabilitiesWomen.plist b/foam-madness/probabilities/historicalProbabilitiesWomen.plist index e569dae..4042ebe 100644 --- a/foam-madness/probabilities/historicalProbabilitiesWomen.plist +++ b/foam-madness/probabilities/historicalProbabilitiesWomen.plist @@ -5,7 +5,7 @@ Source Date Accessed - 2024-03-15T00:40:23Z + 2024-05-19T15:59:37Z URL http://mcubed.net/ncaabw/seeds.shtml @@ -16,17 +16,17 @@ 2 0.621 3 - 0.729 + 0.722 4 - 0.8090000000000001 + 0.8120000000000001 5 - 0.915 + 0.918 6 0.875 7 0.8 8 - 0.945 + 0.947 9 0.959 10 @@ -42,14 +42,14 @@ 15 0.5 16 - 0.991 + 0.992 2 2 0.5 3 - 0.62 + 0.6 4 0.533 5 @@ -57,7 +57,7 @@ 6 0.789 7 - 0.847 + 0.843 8 0 9 @@ -86,9 +86,9 @@ 5 1 6 - 0.68 + 0.6879999999999999 7 - 0.5 + 0.533 8 0.5 9 @@ -96,7 +96,7 @@ 10 0.5 11 - 0.698 + 0.705 12 0.5 13 @@ -113,7 +113,7 @@ 4 0.5 5 - 0.641 + 0.636 6 1 7 @@ -129,7 +129,7 @@ 12 0.862 13 - 0.9399999999999999 + 0.9419999999999999 14 0.5 15 @@ -154,7 +154,7 @@ 11 0.5 12 - 0.787 + 0.793 13 0.571 14 @@ -177,7 +177,7 @@ 10 0.5 11 - 0.6840000000000001 + 0.6860000000000001 12 0.5 13 @@ -198,7 +198,7 @@ 9 0.5 10 - 0.642 + 0.651 11 0 12 @@ -217,7 +217,7 @@ 8 0.5 9 - 0.52 + 0.532 10 0.5 11