From c4247945cad9ba4487dcccba800146e9430eaec8 Mon Sep 17 00:00:00 2001 From: Rob O'Dwyer Date: Tue, 13 Sep 2022 13:11:43 -0700 Subject: [PATCH] DEV 1676: Add maps helper functions (#111) * add utility methods to Editor and BoardStateEditor * add Meta.Validate * allow setting Meta.MinPlayers to zero * remove uints in map sizes * use Meta.Validate in HazardPitsMap --- maps/castle_wall.go | 2 +- maps/castle_wall_test.go | 6 +- maps/game_map.go | 303 +++++++++++++++++---- maps/game_map_test.go | 450 +++++++++++++++++++++++++++++++- maps/hazard_pits.go | 12 +- maps/registry_test.go | 5 +- maps/rivers_and_bridges_test.go | 6 +- 7 files changed, 701 insertions(+), 83 deletions(-) diff --git a/maps/castle_wall.go b/maps/castle_wall.go index c24d1d2..9f30f83 100644 --- a/maps/castle_wall.go +++ b/maps/castle_wall.go @@ -10,7 +10,7 @@ func init() { globalRegistry.RegisterMap("hz_castle_wall_xl", CastleWallExtraLargeHazardsMap{}) } -func setupCastleWallBoard(maxPlayers uint, startingPositions []rules.Point, hazards []rules.Point, initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func setupCastleWallBoard(maxPlayers int, startingPositions []rules.Point, hazards []rules.Point, initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { rand := settings.GetRand(initialBoardState.Turn) if len(initialBoardState.Snakes) > int(maxPlayers) { diff --git a/maps/castle_wall_test.go b/maps/castle_wall_test.go index 341e311..84b0ff7 100644 --- a/maps/castle_wall_test.go +++ b/maps/castle_wall_test.go @@ -21,8 +21,8 @@ func TestCastleWallHazardsMap(t *testing.T) { tests := []struct { Map maps.GameMap - Width uint - Height uint + Width int + Height int }{ {maps.CastleWallMediumHazardsMap{}, 11, 11}, {maps.CastleWallLargeHazardsMap{}, 19, 19}, @@ -31,7 +31,7 @@ func TestCastleWallHazardsMap(t *testing.T) { // check all the supported sizes for _, test := range tests { - state = rules.NewBoardState(int(test.Width), int(test.Height)) + state = rules.NewBoardState(test.Width, test.Height) state.Snakes = append(state.Snakes, rules.Snake{ID: "1", Body: []rules.Point{}}) editor = maps.NewBoardStateEditor(state) require.Empty(t, state.Hazards) diff --git a/maps/game_map.go b/maps/game_map.go index ddeae74..5ab76b4 100644 --- a/maps/game_map.go +++ b/maps/game_map.go @@ -1,6 +1,9 @@ package maps import ( + "fmt" + "strings" + "github.com/BattlesnakeOfficial/rules" ) @@ -25,14 +28,55 @@ type GameMap interface { UpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error } +type Metadata struct { + Name string + Author string + Description string + // Version is the current version of the game map. + // Each time a map is changed, the version number should be incremented by 1. + Version int + // MinPlayers is the minimum number of players that the map supports. + MinPlayers int + // MaxPlayers is the maximum number of players that the map supports. + MaxPlayers int + // 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 + // Tags is a list of strings use to categorize the map. + Tags []string +} + +func (meta Metadata) Validate(boardState *rules.BoardState) error { + if !meta.BoardSizes.IsAllowable(boardState.Width, boardState.Height) { + var sizesStrings []string + for _, size := range meta.BoardSizes { + sizesStrings = append(sizesStrings, fmt.Sprintf("%dx%d", size.Width, size.Height)) + } + + return rules.RulesetError("This map can only be played on these board sizes: " + strings.Join(sizesStrings, ", ")) + } + + if meta.MinPlayers != 0 && len(boardState.Snakes) < int(meta.MinPlayers) { + return rules.RulesetError(fmt.Sprintf("This map can only be played with %d-%d players", meta.MinPlayers, meta.MaxPlayers)) + } + + if meta.MaxPlayers != 0 && len(boardState.Snakes) > int(meta.MaxPlayers) { + return rules.RulesetError(fmt.Sprintf("This map can only be played with %d-%d players", meta.MinPlayers, meta.MaxPlayers)) + } + + return nil +} + // 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 + Width int // Height is the height, in number of board squares, of the board. // The value 0 has a special meaning to mean unlimited. - Height uint + Height int } // sizes is a list of board sizes that a map supports. @@ -50,7 +94,7 @@ func (d sizes) IsAllowable(Width int, Height int) bool { } for _, size := range d { - if size.Width == uint(Width) && size.Height == uint(Height) { + if size.Width == Width && size.Height == Height { return true } } @@ -67,7 +111,7 @@ func AnySize() sizes { // in the vertical and horizontal directions. // Examples: // - OddSizes(11,21) produces [(11,11), (13,13), (15,15), (17,17), (19,19), (21,21)] -func OddSizes(min, max uint) sizes { +func OddSizes(min, max int) sizes { var s sizes for i := min; i <= max; i += 2 { s = append(s, Dimensions{Width: i, Height: i}) @@ -87,107 +131,262 @@ func FixedSizes(a Dimensions, b ...Dimensions) sizes { return s } -type Metadata struct { - Name string - Author string - Description string - // 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 - // Tags is a list of strings use to categorize the map. - Tags []string -} - // Editor is used by GameMap implementations to modify the board state. type Editor interface { // Clears all food from the board. ClearFood() - // Clears all hazards from the board. - ClearHazards() - // Adds a food to the board. Does not check for duplicates. AddFood(rules.Point) - // Adds a hazard to the board. Does not check for duplicates. - AddHazard(rules.Point) - // Removes all food from a specific tile on the board. RemoveFood(rules.Point) + // Get the locations of food currently on the board. + // Note: the return value is a copy and modifying it won't affect the board. + Food() []rules.Point + + // Clears all hazards from the board. + ClearHazards() + + // Adds a hazard to the board. Does not check for duplicates. + AddHazard(rules.Point) + // Removes all hazards from a specific tile on the board. RemoveHazard(rules.Point) + // Get the locations of hazards currently on the board. + // Note: the return value is a copy and modifying it won't affect the board. + Hazards() []rules.Point + // Updates the body and health of a snake. PlaceSnake(id string, body []rules.Point, health int) + + // Get the bodies of all non-eliminated snakes currently on the board, keyed by Snake ID + // Note: the body values in the return value are a copy and modifying them won't affect the board. + SnakeBodies() map[string][]rules.Point + + // Given a list of Snakes and a list of head coordinates, randomly place + // the snakes on those coordinates, or return an error if placement of all + // Snakes is impossible. + PlaceSnakesRandomlyAtPositions(rand rules.Rand, snakes []rules.Snake, heads []rules.Point, bodyLength int) error + + // Returns true if the provided point on the board is occupied by a snake body, food, and/or hazard. + IsOccupied(point rules.Point, snakes, hazards, food bool) bool + + // Get a set of all points on the board the are occupied by snake bodies, food, and/or hazards. + // The value for each point will be set to true in the return value if that point is occupied by one of the selected objects. + OccupiedPoints(snakes, hazards, food bool) map[rules.Point]bool + + // Given a list of points, return only those that are unoccupied by snake bodies, food, and/or hazards. + FilterUnoccupiedPoints(targets []rules.Point, snakes, hazards, food bool) []rules.Point + + // Shuffle the provided slice of points randomly using the provided rules.Rand + ShufflePoints(rules.Rand, []rules.Point) } // An Editor backed by a BoardState. type BoardStateEditor struct { - *rules.BoardState + boardState *rules.BoardState } func NewBoardStateEditor(boardState *rules.BoardState) *BoardStateEditor { return &BoardStateEditor{ - BoardState: boardState, + boardState: boardState, } } func (editor *BoardStateEditor) ClearFood() { - editor.Food = []rules.Point{} -} - -func (editor *BoardStateEditor) ClearHazards() { - editor.Hazards = []rules.Point{} + editor.boardState.Food = []rules.Point{} } func (editor *BoardStateEditor) AddFood(p rules.Point) { - editor.Food = append(editor.Food, rules.Point{X: p.X, Y: p.Y}) -} - -func (editor *BoardStateEditor) AddHazard(p rules.Point) { - editor.Hazards = append(editor.Hazards, rules.Point{X: p.X, Y: p.Y}) + editor.boardState.Food = append(editor.boardState.Food, rules.Point{X: p.X, Y: p.Y}) } func (editor *BoardStateEditor) RemoveFood(p rules.Point) { - for index, food := range editor.Food { + for index, food := range editor.boardState.Food { if food.X == p.X && food.Y == p.Y { - editor.Food[index] = editor.Food[len(editor.Food)-1] - editor.Food = editor.Food[:len(editor.Food)-1] + editor.boardState.Food[index] = editor.boardState.Food[len(editor.boardState.Food)-1] + editor.boardState.Food = editor.boardState.Food[:len(editor.boardState.Food)-1] } } } +// Get the locations of food currently on the board. +// Note: the return value is read-only. +func (editor *BoardStateEditor) Food() []rules.Point { + return append([]rules.Point(nil), editor.boardState.Food...) +} + +func (editor *BoardStateEditor) ClearHazards() { + editor.boardState.Hazards = []rules.Point{} +} + +func (editor *BoardStateEditor) AddHazard(p rules.Point) { + editor.boardState.Hazards = append(editor.boardState.Hazards, rules.Point{X: p.X, Y: p.Y}) +} + func (editor *BoardStateEditor) RemoveHazard(p rules.Point) { - for index, food := range editor.Hazards { + for index, food := range editor.boardState.Hazards { if food.X == p.X && food.Y == p.Y { - editor.Hazards[index] = editor.Hazards[len(editor.Hazards)-1] - editor.Hazards = editor.Hazards[:len(editor.Hazards)-1] + editor.boardState.Hazards[index] = editor.boardState.Hazards[len(editor.boardState.Hazards)-1] + editor.boardState.Hazards = editor.boardState.Hazards[:len(editor.boardState.Hazards)-1] } } } +// Get the locations of hazards currently on the board. +// Note: the return value is read-only. +func (editor *BoardStateEditor) Hazards() []rules.Point { + return append([]rules.Point(nil), editor.boardState.Hazards...) +} + func (editor *BoardStateEditor) PlaceSnake(id string, body []rules.Point, health int) { - for index, snake := range editor.Snakes { + for index, snake := range editor.boardState.Snakes { if snake.ID == id { - editor.Snakes[index].Body = body - editor.Snakes[index].Health = health + editor.boardState.Snakes[index].Body = body + editor.boardState.Snakes[index].Health = health return } } - editor.Snakes = append(editor.Snakes, rules.Snake{ + editor.boardState.Snakes = append(editor.boardState.Snakes, rules.Snake{ ID: id, Health: health, Body: body, }) } + +// Get the bodies of all non-eliminated snakes currently on the board. +// Note: the return value is read-only. +func (editor *BoardStateEditor) SnakeBodies() map[string][]rules.Point { + result := make(map[string][]rules.Point, len(editor.boardState.Snakes)) + + for _, snake := range editor.boardState.Snakes { + result[snake.ID] = append([]rules.Point(nil), snake.Body...) + } + + return result +} + +// Given a list of Snakes and a list of head coordinates, randomly place +// the snakes on those coordinates, or return an error if placement of all +// Snakes is impossible. +func (editor *BoardStateEditor) PlaceSnakesRandomlyAtPositions(rand rules.Rand, snakes []rules.Snake, heads []rules.Point, bodyLength int) error { + if len(snakes) > len(heads) { + return rules.ErrorTooManySnakes + } + + // Shuffle starting points + editor.ShufflePoints(rand, heads) + + // Assign starting points to snakes in order + for index, snake := range snakes { + head := heads[index] + body := make([]rules.Point, bodyLength) + for i := 0; i < bodyLength; i++ { + body[i] = head + } + editor.PlaceSnake(snake.ID, body, rules.SnakeMaxHealth) + } + + return nil +} + +// Returns true if the provided point on the board is occupied by a snake body, food, and/or hazard. +func (editor *BoardStateEditor) IsOccupied(point rules.Point, snakes, hazards, food bool) bool { + if food { + for _, food := range editor.boardState.Food { + if food == point { + return true + } + } + } + if hazards { + for _, hazard := range editor.boardState.Hazards { + if hazard == point { + return true + } + } + } + if snakes { + for _, snake := range editor.boardState.Snakes { + for _, body := range snake.Body { + if body == point { + return true + } + } + } + } + return false +} + +// Get a set of all points on the board the are occupied by snake bodies, food, and/or hazards. +// The value for each point will be set to true in the return value if that point is occupied by one of the selected objects. +func (editor *BoardStateEditor) OccupiedPoints(snakes, hazards, food bool) map[rules.Point]bool { + boardState := editor.boardState + result := make(map[rules.Point]bool, len(boardState.Food)+len(boardState.Hazards)+len(boardState.Snakes)*3) + + if food { + for _, food := range editor.boardState.Food { + result[food] = true + } + } + if hazards { + for _, hazard := range editor.boardState.Hazards { + result[hazard] = true + } + } + if snakes { + for _, snake := range editor.boardState.Snakes { + for _, body := range snake.Body { + result[body] = true + } + } + } + + return result +} + +// Given a list of points, return only those that are unoccupied by snake bodies, food, and/or hazards. +func (editor *BoardStateEditor) FilterUnoccupiedPoints(targets []rules.Point, snakes, hazards, food bool) []rules.Point { + result := make([]rules.Point, 0, len(targets)) + +targetLoop: + for _, point := range targets { + if food { + for _, food := range editor.boardState.Food { + if food == point { + continue targetLoop + } + } + } + if hazards { + for _, hazard := range editor.boardState.Hazards { + if hazard == point { + continue targetLoop + } + } + } + if snakes { + for _, snake := range editor.boardState.Snakes { + for _, body := range snake.Body { + if body == point { + continue targetLoop + } + } + } + } + + result = append(result, point) + } + + return result +} + +func (editor *BoardStateEditor) ShufflePoints(rand rules.Rand, points []rules.Point) { + rand.Shuffle(len(points), func(i int, j int) { + points[i], points[j] = points[j], points[i] + }) +} diff --git a/maps/game_map_test.go b/maps/game_map_test.go index e0cae3a..7ce3865 100644 --- a/maps/game_map_test.go +++ b/maps/game_map_test.go @@ -7,6 +7,110 @@ import ( "github.com/stretchr/testify/require" ) +func TestMetadataValidate(t *testing.T) { + for label, test := range map[string]struct { + metadata Metadata + boardState *rules.BoardState + expected error + }{ + "unlimited": { + Metadata{ + BoardSizes: AnySize(), + }, + rules.NewBoardState(99, 99), + nil, + }, + "in sizes": { + Metadata{ + BoardSizes: OddSizes(7, 25), + }, + rules.NewBoardState(7, 7), + nil, + }, + "too small": { + Metadata{ + BoardSizes: OddSizes(7, 25), + }, + rules.NewBoardState(6, 6), + rules.RulesetError("This map can only be played on these board sizes: 7x7, 9x9, 11x11, 13x13, 15x15, 17x17, 19x19, 21x21, 23x23, 25x25"), + }, + "too large": { + Metadata{ + BoardSizes: OddSizes(7, 25), + }, + rules.NewBoardState(26, 26), + rules.RulesetError("This map can only be played on these board sizes: 7x7, 9x9, 11x11, 13x13, 15x15, 17x17, 19x19, 21x21, 23x23, 25x25"), + }, + "valid players": { + Metadata{ + BoardSizes: AnySize(), + MinPlayers: 4, + MaxPlayers: 4, + }, + &rules.BoardState{ + Snakes: []rules.Snake{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + {ID: "4"}, + }, + }, + nil, + }, + "too few players": { + Metadata{ + BoardSizes: AnySize(), + MinPlayers: 3, + MaxPlayers: 4, + }, + &rules.BoardState{ + Snakes: []rules.Snake{ + {ID: "1"}, + {ID: "2"}, + }, + }, + rules.RulesetError("This map can only be played with 3-4 players"), + }, + "too many players": { + Metadata{ + BoardSizes: AnySize(), + MinPlayers: 3, + MaxPlayers: 4, + }, + &rules.BoardState{ + Snakes: []rules.Snake{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + {ID: "4"}, + {ID: "5"}, + }, + }, + rules.RulesetError("This map can only be played with 3-4 players"), + }, + } { + t.Run(label, func(t *testing.T) { + actual := test.metadata.Validate(test.boardState) + require.Equal(t, test.expected, actual) + }) + } +} + +func TestMapSizes(t *testing.T) { + s := FixedSizes(Dimensions{11, 12}) + require.Equal(t, s[0].Width, 11) + require.Equal(t, s[0].Height, 12) + + s = FixedSizes(Dimensions{11, 11}, Dimensions{19, 25}) + require.Len(t, s, 2) + require.Equal(t, s[1].Width, 19) + require.Equal(t, s[1].Height, 25) + + s = AnySize() + require.Len(t, s, 1, "unlimited maps should have just one dimensions") + require.True(t, s.IsUnlimited()) +} + func TestBoardStateEditorInterface(t *testing.T) { var _ Editor = (*BoardStateEditor)(nil) } @@ -18,7 +122,7 @@ func TestBoardStateEditor(t *testing.T) { Health: 100, }) - editor := BoardStateEditor{BoardState: boardState} + editor := BoardStateEditor{boardState: boardState} editor.AddFood(rules.Point{X: 1, Y: 3}) editor.AddFood(rules.Point{X: 3, Y: 6}) @@ -56,6 +160,26 @@ func TestBoardStateEditor(t *testing.T) { }, }, boardState) + require.Equal(t, []rules.Point{ + {X: 1, Y: 3}, + {X: 3, Y: 7}, + }, editor.Food()) + + require.Equal(t, []rules.Point{ + {X: 1, Y: 3}, + {X: 3, Y: 7}, + }, editor.Hazards()) + + require.Equal(t, map[string][]rules.Point{ + "existing_snake": { + {X: 5, Y: 2}, {X: 5, Y: 1}, {X: 5, Y: 0}, + }, + "new_snake": { + + {X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, + }, + }, editor.SnakeBodies()) + editor.ClearFood() require.Equal(t, []rules.Point{}, boardState.Food) @@ -63,17 +187,317 @@ func TestBoardStateEditor(t *testing.T) { 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)) +func TestBoardStateEditorPlaceSnakesRandomlyAtPositions(t *testing.T) { + for label, test := range map[string]struct { + rand rules.Rand + initialSnakes []rules.Snake + heads []rules.Point + bodyLength int + expectedError error + expectedSnakes []rules.Snake + }{ + "empty": { + rules.MinRand, + []rules.Snake{}, + []rules.Point{}, + 0, + nil, + []rules.Snake{}, + }, + "too many snakes": { + rules.MinRand, + []rules.Snake{ + {ID: "1"}, {ID: "2"}, {ID: "3"}, + }, + []rules.Point{{X: 3, Y: 3}, {X: 6, Y: 2}}, + 3, + rules.ErrorTooManySnakes, + nil, + }, + "success unshuffled": { + rules.MinRand, + []rules.Snake{ + {ID: "1"}, {ID: "2"}, + }, + []rules.Point{{X: 3, Y: 3}, {X: 6, Y: 2}}, + 3, + nil, + []rules.Snake{ + { + ID: "1", + Body: []rules.Point{{X: 3, Y: 3}, {X: 3, Y: 3}, {X: 3, Y: 3}}, + Health: rules.SnakeMaxHealth, + }, { + ID: "2", + Body: []rules.Point{{X: 6, Y: 2}, {X: 6, Y: 2}, {X: 6, Y: 2}}, + Health: rules.SnakeMaxHealth, + }, + }, + }, + "success shuffled": { + rules.MaxRand, + []rules.Snake{ + {ID: "1"}, {ID: "2"}, + }, + []rules.Point{{X: 3, Y: 3}, {X: 6, Y: 2}}, + 3, + nil, + []rules.Snake{ + { + ID: "1", + Body: []rules.Point{{X: 6, Y: 2}, {X: 6, Y: 2}, {X: 6, Y: 2}}, + Health: rules.SnakeMaxHealth, + }, { + ID: "2", + Body: []rules.Point{{X: 3, Y: 3}, {X: 3, Y: 3}, {X: 3, Y: 3}}, + Health: rules.SnakeMaxHealth, + }, + }, + }, + } { + t.Run(label, func(t *testing.T) { + boardState := rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall) + boardState.Snakes = test.initialSnakes + editor := NewBoardStateEditor(boardState) - 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()) + err := editor.PlaceSnakesRandomlyAtPositions(test.rand, test.initialSnakes, test.heads, test.bodyLength) + if test.expectedError != nil { + require.Equal(t, test.expectedError, err) + } else { + require.Equal(t, test.expectedSnakes, boardState.Snakes) + } + }) + } +} + +func TestBoardStateEditorIsOccupied(t *testing.T) { + for label, test := range map[string]struct { + boardState *rules.BoardState + point rules.Point + snakes, hazards, food bool + expected bool + }{ + "empty board": { + rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall), + rules.Point{X: 3, Y: 3}, + true, true, true, + false, + }, + "unoccupied": { + &rules.BoardState{ + Food: []rules.Point{{X: 1, Y: 1}}, + Hazards: []rules.Point{{X: 2, Y: 2}}, + Snakes: []rules.Snake{ + { + ID: "1", + Body: []rules.Point{{X: 3, Y: 3}}, + }, + }, + }, + rules.Point{X: 2, Y: 3}, + true, true, true, + false, + }, + "food": { + &rules.BoardState{ + Food: []rules.Point{{X: 1, Y: 1}}, + }, + rules.Point{X: 1, Y: 1}, + false, false, true, + true, + }, + "ignored food": { + &rules.BoardState{ + Food: []rules.Point{{X: 1, Y: 1}}, + }, + rules.Point{X: 1, Y: 1}, + false, false, false, + false, + }, + "hazard": { + &rules.BoardState{ + Hazards: []rules.Point{{X: 1, Y: 1}}, + }, + rules.Point{X: 1, Y: 1}, + false, true, false, + true, + }, + "ignored hazard": { + &rules.BoardState{ + Food: []rules.Point{{X: 1, Y: 1}}, + }, + rules.Point{X: 1, Y: 1}, + false, false, false, + false, + }, + "snake": { + &rules.BoardState{ + Snakes: []rules.Snake{ + { + ID: "1", + Body: []rules.Point{{X: 1, Y: 1}}, + }, + }, + }, + rules.Point{X: 1, Y: 1}, + true, false, false, + true, + }, + "ignored snake": { + &rules.BoardState{ + Snakes: []rules.Snake{ + { + ID: "1", + Body: []rules.Point{{X: 1, Y: 1}}, + }, + }, + }, + rules.Point{X: 1, Y: 1}, + false, false, false, + false, + }, + } { + t.Run(label, func(t *testing.T) { + editor := NewBoardStateEditor(test.boardState) + + actual := editor.IsOccupied(test.point, test.snakes, test.hazards, test.food) + + require.Equal(t, test.expected, actual) + }) + } +} + +func TestBoardStateEditorOccupiedPoints(t *testing.T) { + testBoardState := &rules.BoardState{ + Food: []rules.Point{{X: 1, Y: 1}}, + Hazards: []rules.Point{{X: 2, Y: 2}}, + Snakes: []rules.Snake{ + { + ID: "1", + Body: []rules.Point{{X: 3, Y: 3}}, + }, + }, + } + + for label, test := range map[string]struct { + boardState *rules.BoardState + snakes, hazards, food bool + expected map[rules.Point]bool + }{ + "empty board": { + rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall), + true, true, true, + map[rules.Point]bool{}, + }, + "all types": { + testBoardState, + true, true, true, + map[rules.Point]bool{ + {X: 1, Y: 1}: true, + {X: 2, Y: 2}: true, + {X: 3, Y: 3}: true, + }, + }, + "ignore snakes": { + testBoardState, + false, true, true, + map[rules.Point]bool{ + {X: 1, Y: 1}: true, + {X: 2, Y: 2}: true, + }, + }, + "ignore hazards": { + testBoardState, + true, false, true, + map[rules.Point]bool{ + {X: 1, Y: 1}: true, + {X: 3, Y: 3}: true, + }, + }, + "ignore food": { + testBoardState, + true, true, false, + map[rules.Point]bool{ + {X: 2, Y: 2}: true, + {X: 3, Y: 3}: true, + }, + }, + } { + t.Run(label, func(t *testing.T) { + editor := NewBoardStateEditor(test.boardState) + + actual := editor.OccupiedPoints(test.snakes, test.hazards, test.food) + + require.Equal(t, test.expected, actual) + }) + } +} + +func TestBoardStateEditorFilterUnoccupiedPoints(t *testing.T) { + testBoardState := &rules.BoardState{ + Food: []rules.Point{{X: 1, Y: 1}}, + Hazards: []rules.Point{{X: 2, Y: 2}}, + Snakes: []rules.Snake{ + { + ID: "1", + Body: []rules.Point{{X: 3, Y: 3}}, + }, + }, + } + + for label, test := range map[string]struct { + boardState *rules.BoardState + targets []rules.Point + snakes, hazards, food bool + expected []rules.Point + }{ + "empty": { + rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall), + []rules.Point{}, + true, true, true, + []rules.Point{}, + }, + "all types": { + testBoardState, + []rules.Point{{X: 3, Y: 3}, {X: 1, Y: 1}, {X: 2, Y: 2}, {X: 2, Y: 1}}, + true, true, true, + []rules.Point{{X: 2, Y: 1}}, + }, + "ignore snakes": { + testBoardState, + []rules.Point{{X: 3, Y: 3}, {X: 1, Y: 1}, {X: 2, Y: 2}, {X: 2, Y: 1}}, + false, true, true, + []rules.Point{{X: 3, Y: 3}, {X: 2, Y: 1}}, + }, + "ignore hazards": { + testBoardState, + []rules.Point{{X: 3, Y: 3}, {X: 1, Y: 1}, {X: 2, Y: 2}, {X: 2, Y: 1}}, + true, false, true, + []rules.Point{{X: 2, Y: 2}, {X: 2, Y: 1}}, + }, + "ignore food": { + testBoardState, + []rules.Point{{X: 3, Y: 3}, {X: 1, Y: 1}, {X: 2, Y: 2}, {X: 2, Y: 1}}, + true, true, false, + []rules.Point{{X: 1, Y: 1}, {X: 2, Y: 1}}, + }, + } { + t.Run(label, func(t *testing.T) { + editor := NewBoardStateEditor(test.boardState) + + actual := editor.FilterUnoccupiedPoints(test.targets, test.snakes, test.hazards, test.food) + + require.Equal(t, test.expected, actual) + }) + } +} + +func TestBoardStateEditorShufflePoints(t *testing.T) { + editor := NewBoardStateEditor(rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall)) + points := []rules.Point{{X: 4, Y: 0}, {X: 3, Y: 1}, {X: 2, Y: 2}, {X: 1, Y: 3}, {X: 0, Y: 4}} + + editor.ShufflePoints(rules.MaxRand, points) + expected := []rules.Point{{X: 3, Y: 1}, {X: 2, Y: 2}, {X: 1, Y: 3}, {X: 0, Y: 4}, {X: 4, Y: 0}} + + require.Equal(t, expected, points) } diff --git a/maps/hazard_pits.go b/maps/hazard_pits.go index 0d24d5f..f37f9b2 100644 --- a/maps/hazard_pits.go +++ b/maps/hazard_pits.go @@ -20,8 +20,8 @@ func (m HazardPitsMap) Meta() Metadata { Description: "A map that that fills in grid-like pattern of squares with pits filled with hazard sauce. Every N turns the pits will fill with another layer of sauce up to a maximum of 4 layers which last a few cycles, then the pits drain and the pattern repeats", Author: "Battlesnake", Version: 1, - MinPlayers: 1, - MaxPlayers: 4, + MinPlayers: 0, + MaxPlayers: len(hazardPitStartPositions), BoardSizes: FixedSizes(Dimensions{11, 11}), Tags: []string{TAG_FOOD_PLACEMENT, TAG_HAZARD_PLACEMENT, TAG_SNAKE_PLACEMENT}, } @@ -47,12 +47,8 @@ func (m HazardPitsMap) AddHazardPits(board *rules.BoardState, settings rules.Set } func (m HazardPitsMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - if !m.Meta().BoardSizes.IsAllowable(initialBoardState.Width, initialBoardState.Height) { - return rules.RulesetError("This map can only be played on a 11x11 board") - } - - if len(initialBoardState.Snakes) > len(hazardPitStartPositions) { - return rules.ErrorTooManySnakes + if err := m.Meta().Validate(initialBoardState); err != nil { + return err } rand := settings.GetRand(0) diff --git a/maps/registry_test.go b/maps/registry_test.go index 7250671..e70999a 100644 --- a/maps/registry_test.go +++ b/maps/registry_test.go @@ -25,7 +25,6 @@ func TestRegisteredMaps(t *testing.T) { require.Equalf(t, mapName, gameMap.ID(), "%#v game map doesn't return its own ID", mapName) 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") @@ -37,7 +36,7 @@ func TestRegisteredMaps(t *testing.T) { 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++ { + for j := 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)) @@ -50,7 +49,7 @@ func TestRegisteredMaps(t *testing.T) { 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++ { + for i := 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)) diff --git a/maps/rivers_and_bridges_test.go b/maps/rivers_and_bridges_test.go index 1f71928..4d9e675 100644 --- a/maps/rivers_and_bridges_test.go +++ b/maps/rivers_and_bridges_test.go @@ -21,8 +21,8 @@ func TestRiversAndBridgetsHazardsMap(t *testing.T) { tests := []struct { Map maps.GameMap - Width uint - Height uint + Width int + Height int }{ {maps.RiverAndBridgesMediumHazardsMap{}, 11, 11}, {maps.RiverAndBridgesLargeHazardsMap{}, 19, 19}, @@ -33,7 +33,7 @@ func TestRiversAndBridgetsHazardsMap(t *testing.T) { // check all the supported sizes for _, test := range tests { - state = rules.NewBoardState(int(test.Width), int(test.Height)) + state = rules.NewBoardState(test.Width, test.Height) state.Snakes = append(state.Snakes, rules.Snake{ID: "1", Body: []rules.Point{}}) editor = maps.NewBoardStateEditor(state) require.Empty(t, state.Hazards)