diff --git a/README.md b/README.md index a9dbcf1..4361ce8 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,14 @@ You will need to compile the binary from source or download a release from githu To learn how to use the CLI run: ```bash -bc --help +bracket-creator --help +bracket-creator create-pools --help +bracket-creator create-playoffs --help ``` -Example usage: +Example to build the tool from source: ```bash -make go/build && ./bin/bracket-creator create -s -f ./mock_data.csv -o ./output.xlsx +make go/build ``` CSV format for individual matches should be (see mock_data.csv for an example): @@ -61,30 +63,62 @@ First_Name Last_Name, Dojo For teams, it shoud be one team per line. -### Parameters +### Parameters to create Pools +Example command line to create pools with 5 players and 3 winners per pool: +```bash +bracket-creator create-pools -s -p 5 -w 3 -f ./mock_data_medium.csv -o ./pools-example.xlsx +``` * `-d` / `-determined` - Do not shuffle the names read from the input file * `-f` / `-file` - Path to the CSV file containing the players/teams in `Name, Dojo` format. `Dojo` is a field to ensure players/teams don't endup fighting someone of the same dojo * `-h` / `-help` - Show help -* `--no-pools` - Do not create pools and have only straight knockouts * `-o` / `-output` - Path to write the output excel file * `-p` / `-players` - Minimum number of players/teams per pool. Extra players are added to the end of the pool if there are more than expected. The default is 3 +* `-w` / `-pool-winners` - Number of players/teams that can qualify from each pool. The default is 2 * `-r` / `-round-robin` - Round robin, to ensure that in a pool of 4 or more, everyone would fight everyone. Otherwise, everyone fights only twice in their pool. The default is False * `-s` / `-sanatize` - Sanatize print names into first name initial and capitalize the last name. This is useful for individual player tournaments. * `-t` / `-team-matches` - Create team matches with x players per team. Default is 0, which means these are not team matches +### Parameters to create Playoffs +Example command line to create team playoffs with 5 players per team: +```bash +bracket-creator create-playoffs -t 5 -f ./mock_data_small.csv -o ./playoffs-example.xlsx +``` + +* `-d` / `-determined` - Do not shuffle the names read from the input file +* `-f` / `-file` - Path to the CSV file containing the players/teams in `Name, Dojo` format. `Dojo` is a field to ensure players/teams don't endup fighting someone of the same dojo +* `-h` / `-help` - Show help +* `-o` / `-output` - Path to write the output excel file +* `-s` / `-sanatize` - Sanatize print names into first name initial and capitalize the last name. This is useful for individual player tournaments. +* `-t` / `-team-matches` - Create team matches with x players per team. Default is 0, which means these are not team matches + ### Examples -**Individual player tournament** +**Individual pool player tournament** + +With 4 players and 2 winners per pool with sanatized names: ```bash -bc create -s -p 4 -f mock_data.csv -o output.xlsx +./bin/bracket-creator create-pools -s -p 4 -f mock_data.csv -o output.xlsx ``` -**Team tournament** +**Team pool tournament** + +With 5 players per team: ```bash -bc create -t 5 -f mock_data.csv -o output.xlsx +./bin/bracket-creator create-pools -t 5 -f mock_data.csv -o output.xlsx ``` +**Individual playoffs player tournament** +Straight knockout with sanatized names: +```bash +./bin/bracket-creator create-playoffs -s -f mock_data.csv -o output.xlsx +``` + +**Team pool tournament** +Straight knockout team competition with teams of 3: +```bash +./bin/bracket-creator create-playoffs -t 3 -f mock_data.csv -o output.xlsx +``` ## Install - WIP diff --git a/TODO.md b/TODO.md deleted file mode 100644 index b16188a..0000000 --- a/TODO.md +++ /dev/null @@ -1,3 +0,0 @@ -## TODO -1. flag to not create pools and only do straight knockout matches -1. customise the number of players that come out of each pool \ No newline at end of file diff --git a/cmd/create-playoffs.go b/cmd/create-playoffs.go new file mode 100644 index 0000000..19a2b44 --- /dev/null +++ b/cmd/create-playoffs.go @@ -0,0 +1,137 @@ +package cmd + +import ( + "bufio" + "fmt" + "log" + "math/rand" + "os" + + "github.com/gitrgoliveira/bracket-creator/internal/helper" + "github.com/spf13/cobra" + + "github.com/xuri/excelize/v2" +) + +type createPlayoffOptions struct { + teamMatches int + filePath string + outputPath string + sanatize bool + determined bool +} + +func newCreatePlayoffCmd() *cobra.Command { + + o := &createPlayoffOptions{} + + cmd := &cobra.Command{ + Use: "create-playoffs", + Short: "Creates playoff brackets only", + SilenceUsage: true, + // Args: cobra.ExactArgs(1), + RunE: o.run, + } + + cmd.Flags().BoolVarP(&o.determined, "determined", "d", false, "Do not shuffle the names read from the input file") + cmd.PersistentFlags().StringVarP(&o.filePath, "file", "f", "", "file with the list of players/teams") + cmd.PersistentFlags().StringVarP(&o.outputPath, "output", "o", "", "output path for the excel file") + cmd.Flags().BoolVarP(&o.sanatize, "sanatize", "s", false, "Sanatize names into first and last name and capitalize") + cmd.Flags().IntVarP(&o.teamMatches, "team-matches", "t", 0, "create team matches with x players per team (default 0)") + + cmd.MarkFlagRequired("file") + cmd.MarkFlagRequired("output") + + return cmd +} + +func (o *createPlayoffOptions) run(cmd *cobra.Command, args []string) error { + + fmt.Fprintf(cmd.OutOrStdout(), "Reading file: %s\n", o.filePath) + file, err := os.Open(o.filePath) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + entries := make([]string, 0) + scanner := bufio.NewScanner(file) + for scanner.Scan() { + entry := scanner.Text() + entries = append(entries, entry) + } + + entries = helper.RemoveDuplicates(entries) + + // Shuffle all entries + if !o.determined { + rand.Shuffle(len(entries), func(i, j int) { + entries[i], entries[j] = entries[j], entries[i] + }) + } + + players := helper.CreatePlayers(entries) + + // Openning the template Excel file. + f, err := excelize.OpenFile("template.xlsx") + if err != nil { + fmt.Println(err) + return nil + } + defer func() { + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() + + helper.AddPlayerDataToSheet(f, players, o.sanatize) + // gather all player names + var names []string + if o.sanatize { + for _, player := range players { + names = append(names, player.DisplayName) + } + } else { + for _, player := range players { + names = append(names, player.Name) + } + } + tree := helper.CreateBalancedTree(names, o.sanatize) + + depth := helper.CalculateDepth(tree) + fmt.Printf("Tree Depth: %d\n", depth) + helper.PrintLeafNodes(tree, f, "Tree", depth*2, 4, depth, false) + + // gathers a list of all of the matches + matches := helper.InOrderTraversal(tree) + matchMapping := helper.FillInMatches(f, matches) + eliminationMatchRounds := make([][]helper.EliminationMatch, depth-1) + // Get all the rounds + for i := depth; i > 1; i-- { + rounds := helper.TraverseRounds(tree, 1, i-1, matchMapping) + eliminationMatchRounds[depth-i] = rounds + fmt.Printf("Elimination matches for round %d: %d\n", i-1, len(eliminationMatchRounds[depth-i])) + } + + var matchWinners map[string]helper.MatchWinner + f.DeleteSheet("Pool Draw") + f.DeleteSheet("Pool Matches") + // hurray! they are all winners + matchWinners = helper.ConvertPlayersToWinners(players, o.sanatize) + helper.CreateNamesToPrint(f, players, o.sanatize) + + helper.PrintTeamEliminationMatches(f, matchWinners, matchMapping, eliminationMatchRounds, o.teamMatches) + + // Save the spreadsheet file + if err := f.SaveAs(o.outputPath); err != nil { + fmt.Println("Error saving Excel file:", err) + return err + } + + fmt.Println("Excel file created successfully:", o.outputPath) + return nil +} + +func init() { + rootCmd.AddCommand(newCreatePlayoffCmd()) +} diff --git a/cmd/create.go b/cmd/create-pools.go similarity index 55% rename from cmd/create.go rename to cmd/create-pools.go index 871c29e..ecf9536 100644 --- a/cmd/create.go +++ b/cmd/create-pools.go @@ -13,44 +13,55 @@ import ( "github.com/xuri/excelize/v2" ) -type createOptions struct { +type poolOptions struct { numPlayers int + poolWinners int teamMatches int filePath string outputPath string roundRobin bool sanatize bool determined bool - noPools bool } -func newCreateCmd() *cobra.Command { +func newCreatePoolCmd() *cobra.Command { - o := &createOptions{} + o := &poolOptions{} cmd := &cobra.Command{ - Use: "create", - Short: "subcommand to create brackets", + Use: "create-pools", + Short: "creates Pool brackets", SilenceUsage: true, // Args: cobra.ExactArgs(1), RunE: o.run, } - cmd.Flags().BoolVarP(&o.determined, "determined", "d", false, "Do not shuffle the names read from the input file") - cmd.Flags().StringVarP(&o.filePath, "file", "f", "", "file with the list of players/teams") - cmd.Flags().BoolVarP(&o.noPools, "no-pools", "", false, "Do not create pools and have only straight knockouts.") - cmd.Flags().StringVarP(&o.outputPath, "output", "o", "", "output path for the excel file") + cmd.PersistentFlags().BoolVarP(&o.determined, "determined", "d", false, "Do not shuffle the names read from the input file") + cmd.PersistentFlags().StringVarP(&o.filePath, "file", "f", "", "file with the list of players/teams") + cmd.PersistentFlags().StringVarP(&o.outputPath, "output", "o", "", "output path for the excel file") cmd.Flags().IntVarP(&o.numPlayers, "players", "p", 3, "minimum number of players/teams per pool") + cmd.Flags().IntVarP(&o.poolWinners, "pool-winners", "w", 2, "number of players/teams that can qualify from each pool") cmd.Flags().BoolVarP(&o.roundRobin, "round-robin", "r", false, "ensure all pools are round robin. Example, in a pool of 4, everyone would fight everyone") cmd.Flags().BoolVarP(&o.sanatize, "sanatize", "s", false, "Sanatize names into first and last name and capitalize") cmd.Flags().IntVarP(&o.teamMatches, "team-matches", "t", 0, "create team matches with x players per team (default 0)") + cmd.MarkFlagRequired("file") + cmd.MarkFlagRequired("output") + return cmd } -func (o *createOptions) run(cmd *cobra.Command, args []string) error { - fmt.Fprintf(cmd.OutOrStdout(), "Reading file: %s\n", o.filePath) +func (o *poolOptions) run(cmd *cobra.Command, args []string) error { + + // validation + if o.numPlayers < 2 { + return fmt.Errorf("number of players must be greater than 1") + } + if o.poolWinners >= o.numPlayers { + return fmt.Errorf("pool winners must be less than number of players per pool") + } + fmt.Fprintf(cmd.OutOrStdout(), "Reading file: %s\n", o.filePath) file, err := os.Open(o.filePath) if err != nil { log.Fatal(err) @@ -74,11 +85,7 @@ func (o *createOptions) run(cmd *cobra.Command, args []string) error { } players := helper.CreatePlayers(entries) - var pools []helper.Pool - - if !o.noPools { - pools = helper.CreatePools(players, o.numPlayers) - } + pools := helper.CreatePools(players, o.numPlayers) // Openning the template Excel file. f, err := excelize.OpenFile("template.xlsx") @@ -92,40 +99,18 @@ func (o *createOptions) run(cmd *cobra.Command, args []string) error { } }() - if o.noPools { - helper.AddPlayerDataToSheet(f, players, o.sanatize) - } else { - helper.AddPoolDataToSheet(f, pools, o.sanatize) - } - var tree *helper.Node + helper.AddPoolDataToSheet(f, pools, o.sanatize) - if !o.noPools { - helper.AddPoolsToSheet(f, pools) - finals := helper.GenerateFinals(pools) - tree = helper.CreateBalancedTree(finals, false) - } else { - // gather all player names - var names []string - if o.sanatize { - for _, player := range players { - names = append(names, player.DisplayName) - } - } else { - for _, player := range players { - names = append(names, player.Name) - } - } - tree = helper.CreateBalancedTree(names, o.sanatize) - } + helper.AddPoolsToSheet(f, pools) + finals := helper.GenerateFinals(pools, o.poolWinners) + tree := helper.CreateBalancedTree(finals, false) // helper.calc depth := helper.CalculateDepth(tree) fmt.Printf("Tree Depth: %d\n", depth) - helper.PrintLeafNodes(tree, f, "Tree", depth*2, 4, depth) - - if !o.noPools { - helper.AddPoolsToTree(f, "Tree", pools) - } + helper.PrintLeafNodes(tree, f, "Tree", depth*2, 4, depth, true) + helper.PrintLeafNodes(tree, f, "Tree", depth*2, 4, depth, true) + helper.AddPoolsToTree(f, "Tree", pools) // gathers a list of all of the matches matches := helper.InOrderTraversal(tree) @@ -138,27 +123,14 @@ func (o *createOptions) run(cmd *cobra.Command, args []string) error { fmt.Printf("Elimination matches for round %d: %d\n", i-1, len(eliminationMatchRounds[depth-i])) } - if !o.noPools { - if o.roundRobin { - helper.CreatePoolRoundRobinMatches(pools) - } else { - helper.CreatePoolMatches(pools) - } - } - - var matchWinners map[string]helper.MatchWinner - if o.noPools { - f.DeleteSheet("Pool Draw") - f.DeleteSheet("Pool Matches") - // hurray! they are all winners - matchWinners = helper.ConvertPlayersToWinners(players, o.sanatize) - helper.CreateNamesToPrint(f, players, o.sanatize) - + if o.roundRobin { + helper.CreatePoolRoundRobinMatches(pools) } else { - matchWinners = helper.PrintPoolMatches(f, pools, o.teamMatches) - helper.CreateNamesWithPoolToPrint(f, pools, o.sanatize) + helper.CreatePoolMatches(pools) } + matchWinners := helper.PrintPoolMatches(f, pools, o.teamMatches, o.poolWinners) + helper.CreateNamesWithPoolToPrint(f, pools, o.sanatize) helper.PrintTeamEliminationMatches(f, matchWinners, matchMapping, eliminationMatchRounds, o.teamMatches) // Save the spreadsheet file @@ -172,5 +144,5 @@ func (o *createOptions) run(cmd *cobra.Command, args []string) error { } func init() { - rootCmd.AddCommand(newCreateCmd()) + rootCmd.AddCommand(newCreatePoolCmd()) } diff --git a/example.xlsx b/example.xlsx deleted file mode 100755 index d42f1dd..0000000 Binary files a/example.xlsx and /dev/null differ diff --git a/internal/helper/excel.go b/internal/helper/excel.go index 6f83ea9..9114fdd 100644 --- a/internal/helper/excel.go +++ b/internal/helper/excel.go @@ -21,7 +21,7 @@ func FillInMatches(f *excelize.File, matches []string) map[string]int { return mapping } -func PrintPoolMatches(f *excelize.File, pools []Pool, teamMatches int) map[string]MatchWinner { +func PrintPoolMatches(f *excelize.File, pools []Pool, teamMatches int, numWinners int) map[string]MatchWinner { matchWinners := make(map[string]MatchWinner) sheetName := "Pool Matches" @@ -127,7 +127,7 @@ func PrintPoolMatches(f *excelize.File, pools []Pool, teamMatches int) map[strin f.SetCellValue(sheetName, fmt.Sprintf("%s%d", resultCol, poolRow), fmt.Sprintf("%d. ", result)) f.SetCellStyle(sheetName, fmt.Sprintf("%s%d", endColName, poolRow), fmt.Sprintf("%s%d", endColName, poolRow), getBorderStyleBottom(f)) - if result <= 2 { + if result <= numWinners { matchWinners[fmt.Sprintf("%s.%d", pool.PoolName, result)] = MatchWinner{ sheetName: sheetName, cell: fmt.Sprintf("%s%d", endColName, poolRow), @@ -206,7 +206,6 @@ func PrintEliminationMatches(f *excelize.File, poolMatchWinners map[string]Match matchRow++ ////////////////////////////////////// - // eliminationMatch.Left checks if it is a pool winner startCell = startColName + fmt.Sprint(matchRow) var leftCellValue, rightCellValue string diff --git a/internal/helper/tree.go b/internal/helper/tree.go index e9f3ce7..d66ddf3 100644 --- a/internal/helper/tree.go +++ b/internal/helper/tree.go @@ -3,6 +3,7 @@ package helper import ( "fmt" "math" + "strconv" "strings" "github.com/xuri/excelize/v2" @@ -62,25 +63,37 @@ func Walk(t *Node, ch chan int) { } } -func PrintLeafNodes(node *Node, f *excelize.File, sheetName string, startCol int, startRow int, depth int) { +func PrintLeafNodes(node *Node, f *excelize.File, sheetName string, startCol int, startRow int, depth int, pools bool) { if node == nil { return } emptyRows := int(math.Pow(2, float64(depth))) - 1 - // Need to ensure pools winners stay on top - // For that we need to ensure the last charater of the left (i.e. top) node is the number 1 - if !node.LeafNode && node.Left.LeafNode && node.Right.LeafNode { - if node.Left.LeafNode && strings.HasSuffix(node.Left.LeafVal, "2") { - node.Left, node.Right = node.Right, node.Left + if pools { + // Need to ensure pools winners stay on top + // For that we need to ensure the last charater of the left (i.e. top) node is the number 1 + if !node.LeafNode && node.Left.LeafNode && node.Right.LeafNode { + leftPool := strings.Split(node.Left.LeafVal, ".") + leftPos, _ := strconv.ParseInt(leftPool[1], 10, 64) + rightPool := strings.Split(node.Right.LeafVal, ".") + rightPos, _ := strconv.ParseInt(rightPool[1], 10, 64) + + if leftPos > rightPos { + node.Left, node.Right = node.Right, node.Left + } } - } - // Need to ensure pools winners are the ones that get a bye - if !node.LeafNode && node.Left.LeafNode && !node.Right.LeafNode { - if strings.HasSuffix(node.Left.LeafVal, "2") { - // find a second placed pool winner on the other branch - node.Left, node.Right.Left = node.Right.Left, node.Left + // Need to ensure pools winners are the ones that get a bye + if !node.LeafNode && node.Left.LeafNode && !node.Right.LeafNode { + leftPool := strings.Split(node.Left.LeafVal, ".") + leftPos, _ := strconv.ParseInt(leftPool[1], 10, 64) + rightPool := strings.Split(node.Right.Left.LeafVal, ".") + rightPos, _ := strconv.ParseInt(rightPool[1], 10, 64) + + if leftPos > rightPos { + // find a second placed pool winner on the other branch + node.Left, node.Right.Left = node.Right.Left, node.Left + } } } @@ -91,28 +104,26 @@ func PrintLeafNodes(node *Node, f *excelize.File, sheetName string, startCol int node.LeafVal = CreateTreeBracket(f, sheetName, startCol, emptyRows/2+startRow, emptyRows, false, fmt.Sprintf("%d", depth)) } - PrintLeafNodes(node.Left, f, sheetName, startCol-2, startRow, depth-1) - PrintLeafNodes(node.Right, f, sheetName, startCol-2, startRow+emptyRows+1, depth-1) + PrintLeafNodes(node.Left, f, sheetName, startCol-2, startRow, depth-1, pools) + PrintLeafNodes(node.Right, f, sheetName, startCol-2, startRow+emptyRows+1, depth-1, pools) } -func GenerateFinals(pools []Pool) []string { - finals := make([]string, 0) +func GenerateFinals(pools []Pool, poolWinners int) []string { + finalists := make([]string, 0) - for i, j := 0, len(pools)-1; j > i; i, j = i+1, j-1 { - finals = append(finals, fmt.Sprintf("%s.1", pools[i].PoolName)) - finals = append(finals, fmt.Sprintf("%s.2", pools[j].PoolName)) - } - for i, j := 0, len(pools)-1; i < j; i, j = i+1, j-1 { - finals = append(finals, fmt.Sprintf("%s.1", pools[j].PoolName)) - finals = append(finals, fmt.Sprintf("%s.2", pools[i].PoolName)) + for i := 0; i < len(pools); i++ { + for j := 0; j < poolWinners; j++ { + finalists = append(finalists, fmt.Sprintf("%s.%d", pools[i].PoolName, j+1)) + } } - // for an odd number of pools, add the middle pool to the finals - if len(pools)%2 != 0 { - finals = append(finals, fmt.Sprintf("%s.1", pools[len(pools)/2].PoolName)) - finals = append(finals, fmt.Sprintf("%s.2", pools[len(pools)/2].PoolName)) + + matches := make([]string, 0) + for i, j := 0, len(finalists)-1; j > i; i, j = i+1, j-1 { + matches = append(matches, finalists[i]) + matches = append(matches, finalists[j]) } - return finals + return matches } // Function to calculate the depth of a balanced tree for a given number of leaf nodes @@ -195,9 +206,6 @@ func TraverseRounds(node *Node, depth int, maxDepth int, matchMapping map[string var matches []EliminationMatch - // if depth == maxDepth && - // (node.Left.LeafNode || node.Right.LeafNode) { - if depth == maxDepth { //LeafVal // fmt.Printf("%s ", node.LeafVal) diff --git a/playoffs-example.xlsx b/playoffs-example.xlsx new file mode 100755 index 0000000..0c978b9 Binary files /dev/null and b/playoffs-example.xlsx differ diff --git a/pools-example.xlsx b/pools-example.xlsx new file mode 100755 index 0000000..62a5bab Binary files /dev/null and b/pools-example.xlsx differ