Skip to content

Commit

Permalink
feat: Custom brackets and bracket view (#7)
Browse files Browse the repository at this point in the history
* feat: Allow customizing shots per game round

* feat: Initial work for bracket view

* feat: Parent view for list vs bracket views

* feat: Store useBracketView, last round and region viewed

* feat: Add settings page

* feat: Collapsible helper for shots per round

* refactor: Streamline labels in tournament list view

* refactor: Generalize BracketCreationController for custom brackets

* feat: Select custom bracket size

* feat: Select initial fill type custom bracket

* feat: Support random fill of custom bracket

* feat: Support existing bracket fill of custom bracket

* feat: Update teams and seeds on custom brackets, sans dupe checks

* feat: Alert and block if updating to duplicate team

* feat: Finalize custom bracket creation

* feat: Allow changing region names

* feat: Update historical probabilities after 2024 tourneys

* chore: Update version to 1.10

* docs: Update ABOUT text for customization
  • Loading branch information
mvirgo authored May 19, 2024
1 parent 1a372b3 commit be12924
Show file tree
Hide file tree
Showing 42 changed files with 2,509 additions and 439 deletions.
148 changes: 133 additions & 15 deletions foam-madness.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions foam-madness/Controller/AppConstants.swift
Original file line number Diff line number Diff line change
@@ -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
}
258 changes: 220 additions & 38 deletions foam-madness/Controller/BracketCreationController.swift

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions foam-madness/Controller/GameStatsController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
55 changes: 55 additions & 0 deletions foam-madness/Controller/Helpers/GameHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
}
}
8 changes: 6 additions & 2 deletions foam-madness/Controller/Helpers/TeamHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Foundation
struct LoadedTeams {
var teams: [String]
var reverseTeamDict: [String: [String: String]]
var teamsNamesByIdDict: [String: String]
}

class TeamHelper {
Expand All @@ -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
}
Expand Down
139 changes: 127 additions & 12 deletions foam-madness/Controller/Helpers/TourneyHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSFetchRequestResult>(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<NSFetchRequestResult>(entityName: entity)
Expand Down Expand Up @@ -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]
Expand All @@ -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<Game>
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."
}
}
37 changes: 19 additions & 18 deletions foam-madness/Controller/TournamentStatsController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
}
}
Loading

0 comments on commit be12924

Please sign in to comment.