diff --git a/maps/arcade_maze.go b/maps/arcade_maze.go index 5c26821..c503eac 100644 --- a/maps/arcade_maze.go +++ b/maps/arcade_maze.go @@ -20,6 +20,9 @@ func (m ArcadeMazeMap) Meta() Metadata { Description: "Generic arcade maze map with deadly hazard walls.", Author: "Battlesnake", Version: 1, + MinPlayers: 1, + MaxPlayers: 6, + BoardSizes: FixedSizes(Dimensions{19, 21}), } } diff --git a/maps/empty.go b/maps/empty.go index 80c0955..023792f 100644 --- a/maps/empty.go +++ b/maps/empty.go @@ -20,6 +20,9 @@ func (m EmptyMap) Meta() Metadata { Description: "Default snake placement with no food", Author: "Battlesnake", Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } diff --git a/maps/game_map.go b/maps/game_map.go index 9d3ec93..e3c88b7 100644 --- a/maps/game_map.go +++ b/maps/game_map.go @@ -18,6 +18,41 @@ type GameMap interface { UpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error } +// Dimensions describes the size of a Battlesnake board. +type Dimensions struct { + // Width is the width, in number of board squares, of the board. + // The value 0 has a special meaning to mean unlimited. + Width uint + // Height is the height, in number of board squares, of the board. + // The value 0 has a special meaning to mean unlimited. + Height uint +} + +// sizes is a list of board sizes that a map supports. +type sizes []Dimensions + +// IsUnlimited reports whether the supported sizes are unlimited. +// Note that even for unlimited sizes, there will be an upper bound that can actually be run and visualised. +func (d sizes) IsUnlimited() bool { + return len(d) == 1 && d[0].Width == 0 +} + +// AnySize creates sizes for a board that has no fixed sizes (supports unlimited sizes). +func AnySize() sizes { + return sizes{Dimensions{Width: 0, Height: 0}} +} + +// FixedSizes creates dimensions for a board that has 1 or more fixed sizes. +// Examples: +// - FixedSizes(Dimension{9,11}) supports only a width of 9 and a height of 11. +// - FixedSizes(Dimensions{11,11},Dimensions{19,19}) supports sizes 11x11 and 19x19 +func FixedSizes(a Dimensions, b ...Dimensions) sizes { + s := make(sizes, 0, 1+len(b)) + s = append(s, a) + s = append(s, b...) + return s +} + type Metadata struct { Name string Author string @@ -25,6 +60,15 @@ type Metadata struct { // Version is the current version of the game map. // Each time a map is changed, the version number should be incremented by 1. Version uint + // MinPlayers is the minimum number of players that the map supports. + MinPlayers uint + // MaxPlayers is the maximum number of players that the map supports. + MaxPlayers uint + // BoardSizes is a list of supported board sizes. Board sizes can fall into one of 3 categories: + // 1. one fixed size (i.e. [11x11]) + // 2. multiple, fixed sizes (i.e. [11x11, 19x19, 25x25]) + // 3. "unlimited" sizes (the board is not fixed and can scale to any reasonable size) + BoardSizes sizes } // Editor is used by GameMap implementations to modify the board state. diff --git a/maps/game_map_test.go b/maps/game_map_test.go index 176fafe..e0cae3a 100644 --- a/maps/game_map_test.go +++ b/maps/game_map_test.go @@ -62,3 +62,18 @@ func TestBoardStateEditor(t *testing.T) { editor.ClearHazards() require.Equal(t, []rules.Point{}, boardState.Hazards) } + +func TestMapSizes(t *testing.T) { + s := FixedSizes(Dimensions{11, 12}) + require.Equal(t, s[0].Width, uint(11)) + require.Equal(t, s[0].Height, uint(12)) + + s = FixedSizes(Dimensions{11, 11}, Dimensions{19, 25}) + require.Len(t, s, 2) + require.Equal(t, s[1].Width, uint(19)) + require.Equal(t, s[1].Height, uint(25)) + + s = AnySize() + require.Len(t, s, 1, "unlimited maps should have just one dimensions") + require.True(t, s.IsUnlimited()) +} diff --git a/maps/hazards.go b/maps/hazards.go index 6a48a9c..4ba0f66 100644 --- a/maps/hazards.go +++ b/maps/hazards.go @@ -30,6 +30,9 @@ func (m InnerBorderHazardsMap) Meta() Metadata { Description: "Creates a static map on turn 0 that is a 1-square wall of hazard that is inset 2 squares from the edge of the board", Author: "Battlesnake", Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } @@ -67,6 +70,9 @@ func (m ConcentricRingsHazardsMap) Meta() Metadata { Description: "Creates a static map where there are rings of hazard sauce starting from the center with a 1 square space between the rings that has no sauce", Author: "Battlesnake", Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } @@ -105,6 +111,9 @@ func (m ColumnsHazardsMap) Meta() Metadata { Description: "Creates a static map on turn 0 that fills in odd squares, i.e. (1,1), (1,3), (3,3) ... with hazard sauce", Author: "Battlesnake", Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } @@ -139,8 +148,11 @@ func (m SpiralHazardsMap) Meta() Metadata { Name: "hz_spiral", Description: `Generates a dynamic hazard map that grows in a spiral pattern clockwise from a random point on the map. Each 2 turns a new hazard square is added to the map`, - Author: "altersaddle", - Version: 1, + Author: "altersaddle", + Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } @@ -230,6 +242,9 @@ func (m ScatterFillMap) Meta() Metadata { Name: "hz_scatter", Description: `Fills the entire board with hazard squares that are set to appear on regular turn schedule. Each square is picked at random.`, Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } @@ -278,6 +293,9 @@ func (m DirectionalExpandingBoxMap) Meta() Metadata { Name: "hz_grow_box", Description: `Creates an area of hazard that expands from a point with one random side growing on a turn schedule.`, Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } @@ -389,6 +407,9 @@ func (m ExpandingBoxMap) Meta() Metadata { Name: "hz_expand_box", Description: `Generates an area of hazard that expands from a random point on the board outward in concentric rings on a periodic turn schedule.`, Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } @@ -461,6 +482,9 @@ func (m ExpandingScatterMap) Meta() Metadata { Name: "hz_expand_scatter", Description: `Builds an expanding hazard area that grows from a central point in rings that are randomly filled in on a regular turn schedule.`, Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } @@ -538,8 +562,11 @@ func (m RiverAndBridgesHazardsMap) Meta() Metadata { Name: "hz_rivers_bridges", Description: `Creates fixed maps that have a lake of hazard in the middle with rivers going in the cardinal directions. Each river has one or two 1-square "bridges" over them`, - Author: "Battlesnake", - Version: 1, + Author: "Battlesnake", + Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: FixedSizes(Dimensions{11, 11}, Dimensions{19, 19}, Dimensions{25, 25}), } } diff --git a/maps/registry_test.go b/maps/registry_test.go index 654072e..5602ec1 100644 --- a/maps/registry_test.go +++ b/maps/registry_test.go @@ -23,9 +23,42 @@ func TestRegisteredMaps(t *testing.T) { for mapName, gameMap := range globalRegistry { t.Run(mapName, func(t *testing.T) { require.Equalf(t, mapName, gameMap.ID(), "%#v game map doesn't return its own ID", mapName) - require.True(t, gameMap.Meta().Version > 0, fmt.Sprintf("registered maps must have a valid version (>= 1) - '%d' is invalid", gameMap.Meta().Version)) + meta := gameMap.Meta() + require.True(t, meta.Version > 0, fmt.Sprintf("registered maps must have a valid version (>= 1) - '%d' is invalid", meta.Version)) + require.NotZero(t, meta.MinPlayers, "registered maps must have minimum players declared") + require.NotZero(t, meta.MaxPlayers, "registered maps must have maximum players declared") + require.LessOrEqual(t, meta.MaxPlayers, meta.MaxPlayers, "max players should always be >= min players") + require.NotEmpty(t, meta.BoardSizes, "registered maps must have at least one supported size declared") var setupBoardState *rules.BoardState + // "fuzz test" supported players + mapSize := pickSize(meta) + for i := meta.MinPlayers; i < meta.MaxPlayers; i++ { + t.Run(fmt.Sprintf("%d players", i), func(t *testing.T) { + initialBoardState := rules.NewBoardState(int(mapSize.Width), int(mapSize.Height)) + for j := uint(0); j < i; j++ { + initialBoardState.Snakes = append(initialBoardState.Snakes, rules.Snake{ID: fmt.Sprint(j), Body: []rules.Point{}}) + } + err := gameMap.SetupBoard(initialBoardState, testSettings, NewBoardStateEditor(initialBoardState)) + require.NoError(t, err, fmt.Sprintf("%d players should be supported by this map", i)) + }) + } + + // "fuzz test" supported map sizes + if !meta.BoardSizes.IsUnlimited() { + for _, mapSize := range meta.BoardSizes { + t.Run(fmt.Sprintf("%dx%d map size", mapSize.Width, mapSize.Height), func(t *testing.T) { + initialBoardState := rules.NewBoardState(int(mapSize.Width), int(mapSize.Height)) + for i := uint(0); i < meta.MaxPlayers; i++ { + initialBoardState.Snakes = append(initialBoardState.Snakes, rules.Snake{ID: fmt.Sprint(i), Body: []rules.Point{}}) + } + err := gameMap.SetupBoard(initialBoardState, testSettings, NewBoardStateEditor(initialBoardState)) + require.NoError(t, err, "error setting up map") + }) + } + } + + // Check that at least one map size can be setup without error for width := 0; width < maxBoardWidth; width++ { for height := 0; height < maxBoardHeight; height++ { initialBoardState := rules.NewBoardState(width, height) @@ -67,3 +100,13 @@ func TestRegisteredMaps(t *testing.T) { }) } } + +func pickSize(meta Metadata) Dimensions { + // For unlimited, we can pick any size + if meta.BoardSizes.IsUnlimited() { + return Dimensions{Width: 11, Height: 11} + } + + // For fixed, just pick the first supported size + return meta.BoardSizes[0] +} diff --git a/maps/royale.go b/maps/royale.go index b43618b..1e56d2a 100644 --- a/maps/royale.go +++ b/maps/royale.go @@ -22,6 +22,9 @@ func (m RoyaleHazardsMap) Meta() Metadata { Description: "A map where hazards are generated every N turns", Author: "Battlesnake", Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } } diff --git a/maps/standard.go b/maps/standard.go index 1814de9..14b8051 100644 --- a/maps/standard.go +++ b/maps/standard.go @@ -20,6 +20,9 @@ func (m StandardMap) Meta() Metadata { Description: "Standard snake placement and food spawning", Author: "Battlesnake", Version: 1, + MinPlayers: 1, + MaxPlayers: 8, + BoardSizes: AnySize(), } }