diff --git a/cli/README.md b/cli/README.md index 45e9f36..a048d08 100644 --- a/cli/README.md +++ b/cli/README.md @@ -78,6 +78,25 @@ Example creating a 7x7 Standard game with two Battlesnakes: battlesnake play --width 7 --height 7 --name Snake1 --url http://snake1-url-whatever --name Snake2 --url http://snake2-url-whatever ``` +### Maps +The `map` command provides map information for use with the `play` command. + +List all available maps using the `list` subcommand: +``` +battlesnake map list +``` +Display map information using the `info` subcommand: +``` +battlesnake map info standard +Name: Standard +Author: Battlesnake +Description: Standard snake placement and food spawning +Version: 2 +Min Players: 1 +Max Players: 16 +Board Sizes (WxH): 7x7 9x9 11x11 13x13 15x15 17x17 19x19 21x21 23x23 25x25 +``` + ### Sample Output ``` $ battlesnake play --width 3 --height 3 --url http://redacted:4567/ --url http://redacted:4568/ --name Bob --name Sue diff --git a/cli/commands/info.go b/cli/commands/info.go new file mode 100644 index 0000000..37df2a8 --- /dev/null +++ b/cli/commands/info.go @@ -0,0 +1,78 @@ +package commands + +import ( + "fmt" + "log" + + "github.com/BattlesnakeOfficial/rules/maps" + "github.com/spf13/cobra" +) + +type mapInfo struct { + All bool +} + +func NewMapInfoCommand() *cobra.Command { + info := mapInfo{} + var infoCmd = &cobra.Command{ + Use: "info [flags] map_name [...map_name]", + Short: "Display metadata for given map(s)", + Long: "Display metadata for given map(s)", + Run: func(cmd *cobra.Command, args []string) { + // handle --all flag first as there would be no args + if info.All { + mapList := maps.List() + for i, m := range mapList { + info.display(m) + if i < (len(mapList) - 1) { + fmt.Print("\n") + } + } + return + } + + // display help when no map(s) provided via args + if len(args) < 1 { + err := cmd.Help() + if err != nil { + log.Fatal(err) + } + return + } + + // display all maps via command args + for i, m := range args { + info.display(m) + if i < (len(args) - 1) { + fmt.Print("\n") + } + } + + }, + } + + infoCmd.Flags().BoolVarP(&info.All, "all", "a", false, "Display information for all maps") + + return infoCmd +} + +func (m *mapInfo) display(id string) { + gameMap, err := maps.GetMap(id) + if err != nil { + log.Fatalf("Failed to load game map %#v: %v", id, err) + } + meta := gameMap.Meta() + fmt.Println("Name:", meta.Name) + fmt.Println("Author:", meta.Author) + fmt.Println("Description:", meta.Description) + fmt.Println("Version:", meta.Version) + fmt.Println("Min Players:", meta.MinPlayers) + fmt.Println("Max Players:", meta.MaxPlayers) + fmt.Print("Board Sizes (WxH):") + for i, s := range meta.BoardSizes { + fmt.Printf(" %dx%d", s.Width, s.Height) + if i == (len(meta.BoardSizes) - 1) { + fmt.Print("\n") + } + } +} diff --git a/cli/commands/list.go b/cli/commands/list.go new file mode 100644 index 0000000..c8a8d20 --- /dev/null +++ b/cli/commands/list.go @@ -0,0 +1,22 @@ +package commands + +import ( + "fmt" + + "github.com/BattlesnakeOfficial/rules/maps" + "github.com/spf13/cobra" +) + +func NewMapListCommand() *cobra.Command { + var listCmd = &cobra.Command{ + Use: "list", + Short: "List available game maps", + Long: "List available game maps", + Run: func(cmd *cobra.Command, args []string) { + for _, m := range maps.List() { + fmt.Println(m) + } + }, + } + return listCmd +} diff --git a/cli/commands/map.go b/cli/commands/map.go new file mode 100644 index 0000000..d310016 --- /dev/null +++ b/cli/commands/map.go @@ -0,0 +1,24 @@ +package commands + +import ( + "log" + + "github.com/spf13/cobra" +) + +func NewMapCommand() *cobra.Command { + + var mapCmd = &cobra.Command{ + Use: "map", + Short: "Display map information", + Long: "Display map information", + Run: func(cmd *cobra.Command, args []string) { + err := cmd.Help() + if err != nil { + log.Fatal(err) + } + }, + } + + return mapCmd +} diff --git a/cli/commands/root.go b/cli/commands/root.go index df8ec66..7fb3a03 100644 --- a/cli/commands/root.go +++ b/cli/commands/root.go @@ -21,6 +21,12 @@ var rootCmd = &cobra.Command{ func Execute() { rootCmd.AddCommand(NewPlayCommand()) + mapCommand := NewMapCommand() + mapCommand.AddCommand(NewMapListCommand()) + mapCommand.AddCommand(NewMapInfoCommand()) + + rootCmd.AddCommand(mapCommand) + if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/maps/registry.go b/maps/registry.go index de79de3..7fe0b01 100644 --- a/maps/registry.go +++ b/maps/registry.go @@ -2,6 +2,7 @@ package maps import ( "fmt" + "sort" "github.com/BattlesnakeOfficial/rules" ) @@ -21,6 +22,16 @@ func (registry MapRegistry) RegisterMap(id string, m GameMap) { registry[id] = m } +// List returns all registered map IDs in alphabetical order +func (registry MapRegistry) List() []string { + var keys []string + for k, _ := range registry { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + // GetMap returns the map associated with the given ID. func (registry MapRegistry) GetMap(id string) (GameMap, error) { if m, ok := registry[id]; ok { @@ -34,6 +45,11 @@ func GetMap(id string) (GameMap, error) { return globalRegistry.GetMap(id) } +// List returns a list of maps registered to the global registry. +func List() []string { + return globalRegistry.List() +} + // RegisterMap adds a map to the global registry. func RegisterMap(id string, m GameMap) { globalRegistry.RegisterMap(id, m) diff --git a/maps/registry_test.go b/maps/registry_test.go index 6b8b840..4ed88cf 100644 --- a/maps/registry_test.go +++ b/maps/registry_test.go @@ -112,3 +112,15 @@ func pickSize(meta Metadata) Dimensions { // For fixed, just pick the first supported size return meta.BoardSizes[0] } + +func TestListRegisteredMaps(t *testing.T) { + keys := globalRegistry.List() + mapCount := 0 + for k := range globalRegistry { + // every registry key should exist in List results + require.Contains(t, keys, k) + mapCount++ + } + // List should equal number of maps in the global registry + require.Equal(t, len(keys), mapCount) +}