Skip to content

Commit

Permalink
change map support for large #'s of snakes (#92)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
torbensky authored Jul 7, 2022
1 parent 08cb7ae commit 663c377
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 100 deletions.
63 changes: 39 additions & 24 deletions board.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
107 changes: 71 additions & 36 deletions board_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -201,7 +231,7 @@ func TestPlaceSnakesDefault(t *testing.T) {
Width: BoardSizeMedium,
Height: BoardSizeMedium,
},
make([]string, 9),
make([]string, 17),
ErrorTooManySnakes,
},
{
Expand All @@ -217,7 +247,7 @@ func TestPlaceSnakesDefault(t *testing.T) {
Width: BoardSizeLarge,
Height: BoardSizeLarge,
},
make([]string, 9),
make([]string, 17),
ErrorTooManySnakes,
},
}
Expand Down Expand Up @@ -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)
}
})
}
}

Expand Down Expand Up @@ -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},
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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])
}
})
}
}

Expand Down
8 changes: 5 additions & 3 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions maps/empty.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions maps/empty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{},
},
Expand All @@ -61,7 +61,7 @@ func TestEmptyMapSetupBoard(t *testing.T) {
},
rules.MinRand,
nil,
rules.ErrorNoRoomForSnake,
rules.ErrorTooManySnakes,
},
{
"full 11x11 min",
Expand Down
13 changes: 13 additions & 0 deletions maps/game_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 663c377

Please sign in to comment.