From 663c377cc4ea6b9eafbeaa3874d6ceecb492b35a Mon Sep 17 00:00:00 2001 From: Torben Date: Thu, 7 Jul 2022 11:14:30 -0700 Subject: [PATCH] change map support for large #'s of snakes (#92) * change map support for large #'s of snakes * test square board fn * format comment * add whitespace * better support large #'s of snakes on small boards * include an intermediate xlarge size --- board.go | 65 +++++++++++++++---------- board_test.go | 107 ++++++++++++++++++++++++++++-------------- constants.go | 8 ++-- maps/empty.go | 10 ++-- maps/empty_test.go | 4 +- maps/game_map.go | 13 +++++ maps/hazards.go | 48 +++++++++---------- maps/royale.go | 6 +-- maps/standard.go | 10 ++-- maps/standard_test.go | 4 +- 10 files changed, 174 insertions(+), 101 deletions(-) diff --git a/board.go b/board.go index f41745e..fa36f68 100644 --- a/board.go +++ b/board.go @@ -84,14 +84,25 @@ func CreateDefaultBoardState(rand Rand, width int, height int, snakeIDs []string // PlaceSnakesAutomatically initializes the array of snakes based on the provided snake IDs and the size of the board. func PlaceSnakesAutomatically(rand Rand, b *BoardState, snakeIDs []string) error { - if isFixedBoardSize(b) { - return PlaceSnakesFixed(rand, b, snakeIDs) - } - - if isExtraLargeBoardSize(b) { - return PlaceManySnakesDistributed(rand, b, snakeIDs) + + if isSquareBoard(b) { + // we don't allow > 8 snakes on very small boards + if len(snakeIDs) > 8 && b.Width < BoardSizeSmall { + return ErrorTooManySnakes + } + + // we can do fixed placement for up to 8 snakes on minimum sized boards + if len(snakeIDs) <= 8 && b.Width >= BoardSizeSmall { + return PlaceSnakesFixed(rand, b, snakeIDs) + } + + // for > 8 snakes, we can do distributed placement + if b.Width >= BoardSizeMedium { + return PlaceManySnakesDistributed(rand, b, snakeIDs) + } } + // last resort for unexpected board sizes we'll just randomly place snakes return PlaceSnakesRandomly(rand, b, snakeIDs) } @@ -294,7 +305,7 @@ func PlaceSnakesRandomly(rand Rand, b *BoardState, snakeIDs []string) error { } for i := 0; i < len(b.Snakes); i++ { - unoccupiedPoints := GetEvenUnoccupiedPoints(b) + unoccupiedPoints := removeCenterCoord(b, GetEvenUnoccupiedPoints(b)) if len(unoccupiedPoints) <= 0 { return ErrorNoRoomForSnake } @@ -340,7 +351,7 @@ func PlaceSnake(b *BoardState, snakeID string, body []Point) error { // PlaceFoodAutomatically initializes the array of food based on the size of the board and the number of snakes. func PlaceFoodAutomatically(rand Rand, b *BoardState) error { - if isFixedBoardSize(b) || isExtraLargeBoardSize(b) { + if isSquareBoard(b) && b.Width >= BoardSizeSmall { return PlaceFoodFixed(rand, b) } @@ -350,7 +361,7 @@ func PlaceFoodAutomatically(rand Rand, b *BoardState) error { func PlaceFoodFixed(rand Rand, b *BoardState) error { centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2} - isSmallBoard := b.Width*b.Height <= BoardSizeSmall*BoardSizeSmall + isSmallBoard := b.Width*b.Height < BoardSizeMedium*BoardSizeMedium // Up to 4 snakes can be placed such that food is nearby on small boards. // Otherwise, we skip this and only try to place food in the center. if len(b.Snakes) <= 4 || !isSmallBoard { @@ -368,6 +379,11 @@ func PlaceFoodFixed(rand Rand, b *BoardState) error { availableFoodLocations := []Point{} for _, p := range possibleFoodLocations { + // Don't place in the center + if centerCoord == p { + continue + } + // Ignore points already occupied by food isOccupiedAlready := false for _, food := range b.Food { @@ -464,6 +480,19 @@ func GetEvenUnoccupiedPoints(b *BoardState) []Point { return evenUnoccupiedPoints } +// removeCenterCoord filters out the board's center point from a list of points. +func removeCenterCoord(b *BoardState, points []Point) []Point { + centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2} + var noCenterPoints []Point + for _, p := range points { + if p != centerCoord { + noCenterPoints = append(noCenterPoints, p) + } + } + + return noCenterPoints +} + func GetUnoccupiedPoints(b *BoardState, includePossibleMoves bool) []Point { pointIsOccupied := map[int]map[int]bool{} for _, p := range b.Food { @@ -519,20 +548,6 @@ func getDistanceBetweenPoints(a, b Point) int { return absInt(a.X-b.X) + absInt(a.Y-b.Y) } -func isExtraLargeBoardSize(b *BoardState) bool { - // We can do placement for any square, large board using the distributed placement algorithm - return b.Width == b.Height && b.Width >= 21 -} - -func isFixedBoardSize(b *BoardState) bool { - if b.Height == BoardSizeSmall && b.Width == BoardSizeSmall { - return true - } - if b.Height == BoardSizeMedium && b.Width == BoardSizeMedium { - return true - } - if b.Height == BoardSizeLarge && b.Width == BoardSizeLarge { - return true - } - return false +func isSquareBoard(b *BoardState) bool { + return b.Width == b.Height } diff --git a/board_test.go b/board_test.go index d9117d4..24bc370 100644 --- a/board_test.go +++ b/board_test.go @@ -53,18 +53,47 @@ func TestCreateDefaultBoardState(t *testing.T) { ExpectedNumFood int Err error }{ - {1, 1, []string{"one"}, 0, nil}, - {1, 2, []string{"one"}, 0, nil}, + {1, 1, []string{"one"}, 0, ErrorNoRoomForSnake}, + {1, 2, []string{"one"}, 0, ErrorNoRoomForSnake}, {1, 4, []string{"one"}, 1, nil}, {2, 2, []string{"one"}, 1, nil}, {9, 8, []string{"one"}, 1, nil}, - {2, 2, []string{"one", "two"}, 0, nil}, + {2, 2, []string{"one", "two"}, 0, ErrorNoRoomForSnake}, {1, 1, []string{"one", "two"}, 2, ErrorNoRoomForSnake}, {1, 2, []string{"one", "two"}, 2, ErrorNoRoomForSnake}, {BoardSizeSmall, BoardSizeSmall, []string{"one", "two"}, 3, nil}, + { + BoardSizeSmall, + BoardSizeSmall, + []string{"1", "2", "3", "4"}, + 5, // <= 4 snakes on a small board we get more than just center food + nil, + }, + { + BoardSizeSmall, + BoardSizeSmall, + []string{"1", "2", "3", "4", "5"}, + 1, // for this size and this many snakes, food is only placed in the center + nil, + }, + { + BoardSizeSmall, + BoardSizeSmall, + []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"}, + 1, // for this size and this many snakes, food is only placed in the center + nil, + }, + { + BoardSizeMedium, + BoardSizeMedium, + []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"}, + 17, // > small boards and we get non-center food + nil, + }, } for testNum, test := range tests { + t.Logf("test case %d", testNum) state, err := CreateDefaultBoardState(MaxRand, test.Width, test.Height, test.IDs) require.Equal(t, test.Err, err) if err != nil { @@ -87,6 +116,7 @@ func TestPlaceSnakesDefault(t *testing.T) { // Because placement is random, we only test to ensure // that snake bodies are populated correctly // Note: because snakes are randomly spawned on even diagonal points, the board can accomodate number of snakes equal to: width*height/2 + // Update: because we exclude the center point now, we can accommodate 1 less snake now (width*height/2 - 1) tests := []struct { BoardState *BoardState SnakeIDs []string @@ -98,7 +128,7 @@ func TestPlaceSnakesDefault(t *testing.T) { Height: 1, }, make([]string, 1), - nil, + ErrorNoRoomForSnake, // we avoid placing snakes in the center, so a board size of 1 will error }, { &BoardState{ @@ -137,9 +167,17 @@ func TestPlaceSnakesDefault(t *testing.T) { Width: 5, Height: 10, }, - make([]string, 25), + make([]string, 24), nil, }, + { + &BoardState{ + Width: 5, + Height: 10, + }, + make([]string, 25), + ErrorNoRoomForSnake, + }, { &BoardState{ Width: 10, @@ -180,14 +218,6 @@ func TestPlaceSnakesDefault(t *testing.T) { make([]string, 8), nil, }, - { - &BoardState{ - Width: BoardSizeSmall, - Height: BoardSizeSmall, - }, - make([]string, 9), - ErrorTooManySnakes, - }, { &BoardState{ Width: BoardSizeMedium, @@ -201,7 +231,7 @@ func TestPlaceSnakesDefault(t *testing.T) { Width: BoardSizeMedium, Height: BoardSizeMedium, }, - make([]string, 9), + make([]string, 17), ErrorTooManySnakes, }, { @@ -217,7 +247,7 @@ func TestPlaceSnakesDefault(t *testing.T) { Width: BoardSizeLarge, Height: BoardSizeLarge, }, - make([]string, 9), + make([]string, 17), ErrorTooManySnakes, }, } @@ -422,17 +452,20 @@ func TestPlaceFood(t *testing.T) { }, } - for _, test := range tests { - require.Len(t, test.BoardState.Food, 0) - err := PlaceFoodAutomatically(MaxRand, test.BoardState) - require.NoError(t, err) - require.Equal(t, test.ExpectedFood, len(test.BoardState.Food)) - for _, point := range test.BoardState.Food { - require.GreaterOrEqual(t, point.X, 0) - require.GreaterOrEqual(t, point.Y, 0) - require.Less(t, point.X, test.BoardState.Width) - require.Less(t, point.Y, test.BoardState.Height) - } + for i, test := range tests { + t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + + require.Len(t, test.BoardState.Food, 0) + err := PlaceFoodAutomatically(MaxRand, test.BoardState) + require.NoError(t, err) + require.Equal(t, test.ExpectedFood, len(test.BoardState.Food)) + for _, point := range test.BoardState.Food { + require.GreaterOrEqual(t, point.X, 0) + require.GreaterOrEqual(t, point.Y, 0) + require.Less(t, point.X, test.BoardState.Width) + require.Less(t, point.Y, test.BoardState.Height) + } + }) } } @@ -637,14 +670,14 @@ func TestGetDistanceBetweenPoints(t *testing.T) { } } -func TestIsKnownBoardSize(t *testing.T) { +func TestIsSquareBoard(t *testing.T) { tests := []struct { Width int Height int Expected bool }{ - {1, 1, false}, - {0, 0, false}, + {1, 1, true}, + {0, 0, true}, {0, 45, false}, {45, 1, false}, {7, 7, true}, @@ -656,7 +689,7 @@ func TestIsKnownBoardSize(t *testing.T) { } for _, test := range tests { - result := isFixedBoardSize(&BoardState{Width: test.Width, Height: test.Height}) + result := isSquareBoard(&BoardState{Width: test.Width, Height: test.Height}) require.Equal(t, test.Expected, result) } } @@ -824,12 +857,14 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) { }, } - for _, test := range tests { - evenUnoccupiedPoints := GetEvenUnoccupiedPoints(test.Board) - require.Equal(t, len(test.Expected), len(evenUnoccupiedPoints)) - for i, e := range test.Expected { - require.Equal(t, e, evenUnoccupiedPoints[i]) - } + for i, test := range tests { + t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { + evenUnoccupiedPoints := GetEvenUnoccupiedPoints(test.Board) + require.Equal(t, len(test.Expected), len(evenUnoccupiedPoints)) + for i, e := range test.Expected { + require.Equal(t, e, evenUnoccupiedPoints[i]) + } + }) } } diff --git a/constants.go b/constants.go index 0ef46f2..c6bb45b 100644 --- a/constants.go +++ b/constants.go @@ -10,9 +10,11 @@ const ( MoveRight = "right" MoveLeft = "left" - BoardSizeSmall = 7 - BoardSizeMedium = 11 - BoardSizeLarge = 19 + BoardSizeSmall = 7 + BoardSizeMedium = 11 + BoardSizeLarge = 19 + BoardSizeXLarge = 21 + BoardSizeXXLarge = 25 SnakeMaxHealth = 100 SnakeStartSize = 3 diff --git a/maps/empty.go b/maps/empty.go index 023792f..e204feb 100644 --- a/maps/empty.go +++ b/maps/empty.go @@ -19,16 +19,20 @@ func (m EmptyMap) Meta() Metadata { Name: "Empty", Description: "Default snake placement with no food", Author: "Battlesnake", - Version: 1, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } func (m EmptyMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { rand := settings.GetRand(0) + if len(initialBoardState.Snakes) > int(m.Meta().MaxPlayers) { + return rules.ErrorTooManySnakes + } + snakeIDs := make([]string, 0, len(initialBoardState.Snakes)) for _, snake := range initialBoardState.Snakes { snakeIDs = append(snakeIDs, snake.ID) diff --git a/maps/empty_test.go b/maps/empty_test.go index b962051..9bfa3ed 100644 --- a/maps/empty_test.go +++ b/maps/empty_test.go @@ -42,7 +42,7 @@ func TestEmptyMapSetupBoard(t *testing.T) { &rules.BoardState{ Width: 7, Height: 7, - Snakes: generateSnakes(9), + Snakes: generateSnakes(17), Food: []rules.Point{}, Hazards: []rules.Point{}, }, @@ -61,7 +61,7 @@ func TestEmptyMapSetupBoard(t *testing.T) { }, rules.MinRand, nil, - rules.ErrorNoRoomForSnake, + rules.ErrorTooManySnakes, }, { "full 11x11 min", diff --git a/maps/game_map.go b/maps/game_map.go index e3c88b7..258c718 100644 --- a/maps/game_map.go +++ b/maps/game_map.go @@ -42,6 +42,19 @@ func AnySize() sizes { return sizes{Dimensions{Width: 0, Height: 0}} } +// OddSizes generates square (width = height) board sizes with an odd number of positions +// 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 { + var s sizes + for i := min; i <= max; i += 2 { + s = append(s, Dimensions{Width: i, Height: i}) + } + + return s +} + // 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. diff --git a/maps/hazards.go b/maps/hazards.go index 8b59fbb..973fbb6 100644 --- a/maps/hazards.go +++ b/maps/hazards.go @@ -29,10 +29,10 @@ func (m InnerBorderHazardsMap) Meta() Metadata { Name: "hz_inner_wall", 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, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } @@ -69,10 +69,10 @@ func (m ConcentricRingsHazardsMap) Meta() Metadata { Name: "hz_rings", 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, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } @@ -110,10 +110,10 @@ func (m ColumnsHazardsMap) Meta() Metadata { Name: "hz_columns", 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, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } @@ -149,10 +149,10 @@ func (m SpiralHazardsMap) Meta() Metadata { 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, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } @@ -241,10 +241,10 @@ func (m ScatterFillMap) Meta() Metadata { return 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, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } @@ -292,10 +292,10 @@ func (m DirectionalExpandingBoxMap) Meta() Metadata { return 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, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } @@ -406,10 +406,10 @@ func (m ExpandingBoxMap) Meta() Metadata { return 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, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } @@ -481,10 +481,10 @@ func (m ExpandingScatterMap) Meta() Metadata { return 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, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } diff --git a/maps/royale.go b/maps/royale.go index 1e56d2a..1870757 100644 --- a/maps/royale.go +++ b/maps/royale.go @@ -21,10 +21,10 @@ func (m RoyaleHazardsMap) Meta() Metadata { Name: "Royale", Description: "A map where hazards are generated every N turns", Author: "Battlesnake", - Version: 1, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } diff --git a/maps/standard.go b/maps/standard.go index 10e769e..bcdb19d 100644 --- a/maps/standard.go +++ b/maps/standard.go @@ -19,16 +19,20 @@ func (m StandardMap) Meta() Metadata { Name: "Standard", Description: "Standard snake placement and food spawning", Author: "Battlesnake", - Version: 1, + Version: 2, MinPlayers: 1, - MaxPlayers: 8, - BoardSizes: AnySize(), + MaxPlayers: 16, + BoardSizes: OddSizes(rules.BoardSizeSmall, rules.BoardSizeXXLarge), } } func (m StandardMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { rand := settings.GetRand(0) + if len(initialBoardState.Snakes) > int(m.Meta().MaxPlayers) { + return rules.ErrorTooManySnakes + } + snakeIDs := make([]string, 0, len(initialBoardState.Snakes)) for _, snake := range initialBoardState.Snakes { snakeIDs = append(snakeIDs, snake.ID) diff --git a/maps/standard_test.go b/maps/standard_test.go index 87dff36..b567a7c 100644 --- a/maps/standard_test.go +++ b/maps/standard_test.go @@ -43,7 +43,7 @@ func TestStandardMapSetupBoard(t *testing.T) { &rules.BoardState{ Width: 7, Height: 7, - Snakes: generateSnakes(9), + Snakes: generateSnakes(17), Food: []rules.Point{}, Hazards: []rules.Point{}, }, @@ -62,7 +62,7 @@ func TestStandardMapSetupBoard(t *testing.T) { }, rules.MinRand, nil, - rules.ErrorNoRoomForSnake, + rules.ErrorTooManySnakes, }, { "full 11x11 min",