Skip to content

Commit

Permalink
Merge pull request #8 from gitrgoliveira/playoffs-new-ux
Browse files Browse the repository at this point in the history
Playoffs feature and new UX
  • Loading branch information
gitrgoliveira authored Oct 29, 2023
2 parents a4ce353 + 381b25d commit 136d327
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 111 deletions.
52 changes: 43 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
3 changes: 0 additions & 3 deletions TODO.md

This file was deleted.

137 changes: 137 additions & 0 deletions cmd/create-playoffs.go
Original file line number Diff line number Diff line change
@@ -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())
}
102 changes: 37 additions & 65 deletions cmd/create.go → cmd/create-pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -172,5 +144,5 @@ func (o *createOptions) run(cmd *cobra.Command, args []string) error {
}

func init() {
rootCmd.AddCommand(newCreateCmd())
rootCmd.AddCommand(newCreatePoolCmd())
}
Binary file removed example.xlsx
Binary file not shown.
Loading

0 comments on commit 136d327

Please sign in to comment.