DEV-280: Extract board generation out of rules.Ruleset (#51)

* extract board generation out of rules.Ruleset

* update comment and remove redundant interface check

* clone boardState in constrictor to respect the ModifyBoardState interface
This commit is contained in:
Rob O'Dwyer 2021-08-23 17:13:58 -07:00 committed by GitHub
parent e416384007
commit 015b681f14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1006 additions and 917 deletions

297
board.go Normal file
View file

@ -0,0 +1,297 @@
package rules
import "math/rand"
type BoardState struct {
Height int32
Width int32
Food []Point
Snakes []Snake
Hazards []Point
}
// NewBoardState returns an empty but fully initialized BoardState
func NewBoardState(width, height int32) *BoardState {
return &BoardState{
Height: height,
Width: width,
Food: []Point{},
Snakes: []Snake{},
Hazards: []Point{},
}
}
// Clone returns a deep copy of prevState that can be safely modified inside Ruleset.CreateNextBoardState
func (prevState *BoardState) Clone() *BoardState {
nextState := &BoardState{
Height: prevState.Height,
Width: prevState.Width,
Food: append([]Point{}, prevState.Food...),
Snakes: make([]Snake, len(prevState.Snakes)),
Hazards: append([]Point{}, prevState.Hazards...),
}
for i := 0; i < len(prevState.Snakes); i++ {
nextState.Snakes[i].ID = prevState.Snakes[i].ID
nextState.Snakes[i].Health = prevState.Snakes[i].Health
nextState.Snakes[i].Body = append([]Point{}, prevState.Snakes[i].Body...)
nextState.Snakes[i].EliminatedCause = prevState.Snakes[i].EliminatedCause
nextState.Snakes[i].EliminatedBy = prevState.Snakes[i].EliminatedBy
}
return nextState
}
// CreateDefaultBoardState is a convenience function for fully initializing a
// "default" board state with snakes and food.
// In a real game, the engine may generate the board without calling this
// function, or customize the results based on game-specific settings.
func CreateDefaultBoardState(width int32, height int32, snakeIDs []string) (*BoardState, error) {
initialBoardState := NewBoardState(width, height)
err := PlaceSnakesAutomatically(initialBoardState, snakeIDs)
if err != nil {
return nil, err
}
err = PlaceFoodAutomatically(initialBoardState)
if err != nil {
return nil, err
}
return initialBoardState, nil
}
// PlaceSnakesAutomatically initializes the array of snakes based on the provided snake IDs and the size of the board.
func PlaceSnakesAutomatically(b *BoardState, snakeIDs []string) error {
if isKnownBoardSize(b) {
return PlaceSnakesFixed(b, snakeIDs)
}
return PlaceSnakesRandomly(b, snakeIDs)
}
func PlaceSnakesFixed(b *BoardState, snakeIDs []string) error {
b.Snakes = make([]Snake, len(snakeIDs))
for i := 0; i < len(snakeIDs); i++ {
b.Snakes[i] = Snake{
ID: snakeIDs[i],
Health: SnakeMaxHealth,
}
}
// Create start 8 points
mn, md, mx := int32(1), (b.Width-1)/2, b.Width-2
startPoints := []Point{
{mn, mn},
{mn, md},
{mn, mx},
{md, mn},
{md, mx},
{mx, mn},
{mx, md},
{mx, mx},
}
// Sanity check
if len(b.Snakes) > len(startPoints) {
return ErrorTooManySnakes
}
// Randomly order them
rand.Shuffle(len(startPoints), func(i int, j int) {
startPoints[i], startPoints[j] = startPoints[j], startPoints[i]
})
// Assign to snakes in order given
for i := 0; i < len(b.Snakes); i++ {
for j := 0; j < SnakeStartSize; j++ {
b.Snakes[i].Body = append(b.Snakes[i].Body, startPoints[i])
}
}
return nil
}
func PlaceSnakesRandomly(b *BoardState, snakeIDs []string) error {
b.Snakes = make([]Snake, len(snakeIDs))
for i := 0; i < len(snakeIDs); i++ {
b.Snakes[i] = Snake{
ID: snakeIDs[i],
Health: SnakeMaxHealth,
}
}
for i := 0; i < len(b.Snakes); i++ {
unoccupiedPoints := getEvenUnoccupiedPoints(b)
if len(unoccupiedPoints) <= 0 {
return ErrorNoRoomForSnake
}
p := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
for j := 0; j < SnakeStartSize; j++ {
b.Snakes[i].Body = append(b.Snakes[i].Body, p)
}
}
return nil
}
// PlaceSnake adds a snake to the board with the given ID and body coordinates.
func PlaceSnake(b *BoardState, snakeID string, body []Point) error {
b.Snakes = append(b.Snakes, Snake{
ID: snakeID,
Health: SnakeMaxHealth,
Body: body,
})
return nil
}
// PlaceFoodAutomatically initializes the array of food based on the size of the board and the number of snakes.
func PlaceFoodAutomatically(b *BoardState) error {
if isKnownBoardSize(b) {
return PlaceFoodFixed(b)
}
return PlaceFoodRandomly(b, int32(len(b.Snakes)))
}
func PlaceFoodFixed(b *BoardState) error {
// Place 1 food within exactly 2 moves of each snake
for i := 0; i < len(b.Snakes); i++ {
snakeHead := b.Snakes[i].Body[0]
possibleFoodLocations := []Point{
{snakeHead.X - 1, snakeHead.Y - 1},
{snakeHead.X - 1, snakeHead.Y + 1},
{snakeHead.X + 1, snakeHead.Y - 1},
{snakeHead.X + 1, snakeHead.Y + 1},
}
availableFoodLocations := []Point{}
for _, p := range possibleFoodLocations {
isOccupiedAlready := false
for _, food := range b.Food {
if food.X == p.X && food.Y == p.Y {
isOccupiedAlready = true
break
}
}
if !isOccupiedAlready {
availableFoodLocations = append(availableFoodLocations, p)
}
}
if len(availableFoodLocations) <= 0 {
return ErrorNoRoomForFood
}
// Select randomly from available locations
placedFood := availableFoodLocations[rand.Intn(len(availableFoodLocations))]
b.Food = append(b.Food, placedFood)
}
// Finally, always place 1 food in center of board for dramatic purposes
isCenterOccupied := true
centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2}
unoccupiedPoints := getUnoccupiedPoints(b, true)
for _, point := range unoccupiedPoints {
if point == centerCoord {
isCenterOccupied = false
break
}
}
if isCenterOccupied {
return ErrorNoRoomForFood
}
b.Food = append(b.Food, centerCoord)
return nil
}
// PlaceFoodRandomly adds up to n new food to the board in random unoccupied squares
func PlaceFoodRandomly(b *BoardState, n int32) error {
for i := int32(0); i < n; i++ {
unoccupiedPoints := getUnoccupiedPoints(b, false)
if len(unoccupiedPoints) > 0 {
newFood := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
b.Food = append(b.Food, newFood)
}
}
return nil
}
func isKnownBoardSize(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 getUnoccupiedPoints(b *BoardState, includePossibleMoves bool) []Point {
pointIsOccupied := map[int32]map[int32]bool{}
for _, p := range b.Food {
if _, xExists := pointIsOccupied[p.X]; !xExists {
pointIsOccupied[p.X] = map[int32]bool{}
}
pointIsOccupied[p.X][p.Y] = true
}
for _, snake := range b.Snakes {
if snake.EliminatedCause != NotEliminated {
continue
}
for i, p := range snake.Body {
if _, xExists := pointIsOccupied[p.X]; !xExists {
pointIsOccupied[p.X] = map[int32]bool{}
}
pointIsOccupied[p.X][p.Y] = true
if i == 0 && !includePossibleMoves {
nextMovePoints := []Point{
{X: p.X - 1, Y: p.Y},
{X: p.X + 1, Y: p.Y},
{X: p.X, Y: p.Y - 1},
{X: p.X, Y: p.Y + 1},
}
for _, nextP := range nextMovePoints {
if _, xExists := pointIsOccupied[nextP.X]; !xExists {
pointIsOccupied[nextP.X] = map[int32]bool{}
}
pointIsOccupied[nextP.X][nextP.Y] = true
}
}
}
}
unoccupiedPoints := []Point{}
for x := int32(0); x < b.Width; x++ {
for y := int32(0); y < b.Height; y++ {
if _, xExists := pointIsOccupied[x]; xExists {
if isOccupied, yExists := pointIsOccupied[x][y]; yExists {
if isOccupied {
continue
}
}
}
unoccupiedPoints = append(unoccupiedPoints, Point{X: x, Y: y})
}
}
return unoccupiedPoints
}
func getEvenUnoccupiedPoints(b *BoardState) []Point {
// Start by getting unoccupied points
unoccupiedPoints := getUnoccupiedPoints(b, true)
// Create a new array to hold points that are even
evenUnoccupiedPoints := []Point{}
for _, point := range unoccupiedPoints {
if ((point.X + point.Y) % 2) == 0 {
evenUnoccupiedPoints = append(evenUnoccupiedPoints, point)
}
}
return evenUnoccupiedPoints
}

678
board_test.go Normal file
View file

@ -0,0 +1,678 @@
package rules
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestCreateDefaultBoardState(t *testing.T) {
tests := []struct {
Height int32
Width int32
IDs []string
ExpectedNumFood int
Err error
}{
{1, 1, []string{"one"}, 0, nil},
{1, 2, []string{"one"}, 0, nil},
{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},
{1, 1, []string{"one", "two"}, 2, ErrorNoRoomForSnake},
{1, 2, []string{"one", "two"}, 2, ErrorNoRoomForSnake},
{BoardSizeSmall, BoardSizeSmall, []string{"one", "two"}, 3, nil},
}
for testNum, test := range tests {
state, err := CreateDefaultBoardState(test.Width, test.Height, test.IDs)
require.Equal(t, test.Err, err)
if err != nil {
require.Nil(t, state)
continue
}
require.NotNil(t, state)
require.Equal(t, test.Width, state.Width)
require.Equal(t, test.Height, state.Height)
require.Equal(t, len(test.IDs), len(state.Snakes))
for i, id := range test.IDs {
require.Equal(t, id, state.Snakes[i].ID)
}
require.Len(t, state.Food, test.ExpectedNumFood, testNum)
require.Len(t, state.Hazards, 0, testNum)
}
}
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
tests := []struct {
BoardState *BoardState
SnakeIDs []string
Err error
}{
{
&BoardState{
Width: 1,
Height: 1,
},
make([]string, 1),
nil,
},
{
&BoardState{
Width: 1,
Height: 1,
},
make([]string, 2),
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 2,
Height: 1,
},
make([]string, 2),
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 1,
Height: 2,
},
make([]string, 2),
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 10,
Height: 5,
},
make([]string, 24),
nil,
},
{
&BoardState{
Width: 5,
Height: 10,
},
make([]string, 25),
nil,
},
{
&BoardState{
Width: 10,
Height: 5,
},
make([]string, 49),
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 5,
Height: 10,
},
make([]string, 50),
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 25,
Height: 2,
},
make([]string, 51),
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
},
make([]string, 1),
nil,
},
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
},
make([]string, 8),
nil,
},
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
},
make([]string, 9),
ErrorTooManySnakes,
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
},
make([]string, 8),
nil,
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
},
make([]string, 9),
ErrorTooManySnakes,
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
},
make([]string, 8),
nil,
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
},
make([]string, 9),
ErrorTooManySnakes,
},
}
for _, test := range tests {
t.Run(fmt.Sprint(test.BoardState.Width, test.BoardState.Height, len(test.SnakeIDs)), func(t *testing.T) {
require.Equal(t, test.BoardState.Width*test.BoardState.Height, int32(len(getUnoccupiedPoints(test.BoardState, true))))
err := PlaceSnakesAutomatically(test.BoardState, test.SnakeIDs)
require.Equal(t, test.Err, err, "Snakes: %d", len(test.BoardState.Snakes))
if err == nil {
for i := 0; i < len(test.BoardState.Snakes); i++ {
require.Len(t, test.BoardState.Snakes[i].Body, 3)
for _, point := range test.BoardState.Snakes[i].Body {
require.GreaterOrEqual(t, point.X, int32(0))
require.GreaterOrEqual(t, point.Y, int32(0))
require.Less(t, point.X, test.BoardState.Width)
require.Less(t, point.Y, test.BoardState.Height)
}
for j := 0; j < len(test.BoardState.Snakes); j++ {
if j == i {
continue
}
require.NotEqual(t, test.BoardState.Snakes[j].Body[0], test.BoardState.Snakes[i].Body[0], "Snakes placed at same square")
}
// All snakes are expected to be placed on an even square - this is true even of fixed positions for known board sizes
var snakePlacedOnEvenSquare bool = ((test.BoardState.Snakes[i].Body[0].X + test.BoardState.Snakes[i].Body[0].Y) % 2) == 0
require.Equal(t, true, snakePlacedOnEvenSquare)
}
}
})
}
}
func TestPlaceSnake(t *testing.T) {
// TODO: Should PlaceSnake check for boundaries?
boardState := NewBoardState(BoardSizeSmall, BoardSizeSmall)
require.Empty(t, boardState.Snakes)
_ = PlaceSnake(boardState, "a", []Point{{0, 0}, {1, 0}, {1, 1}})
require.Len(t, boardState.Snakes, 1)
require.Equal(t, Snake{
ID: "a",
Body: []Point{{0, 0}, {1, 0}, {1, 1}},
Health: SnakeMaxHealth,
EliminatedCause: NotEliminated,
EliminatedBy: "",
}, boardState.Snakes[0])
_ = PlaceSnake(boardState, "b", []Point{{0, 2}, {1, 2}, {3, 2}})
require.Len(t, boardState.Snakes, 2)
require.Equal(t, Snake{
ID: "b",
Body: []Point{{0, 2}, {1, 2}, {3, 2}},
Health: SnakeMaxHealth,
EliminatedCause: NotEliminated,
EliminatedBy: "",
}, boardState.Snakes[1])
}
func TestPlaceFood(t *testing.T) {
tests := []struct {
BoardState *BoardState
ExpectedFood int
}{
{
&BoardState{
Width: 1,
Height: 1,
Snakes: make([]Snake, 1),
},
1,
},
{
&BoardState{
Width: 1,
Height: 2,
Snakes: make([]Snake, 2),
},
2,
},
{
&BoardState{
Width: 101,
Height: 202,
Snakes: make([]Snake, 17),
},
17,
},
{
&BoardState{
Width: 10,
Height: 20,
Snakes: make([]Snake, 305),
},
200,
},
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
Snakes: []Snake{
{Body: []Point{{5, 1}}},
{Body: []Point{{5, 3}}},
{Body: []Point{{5, 5}}},
},
},
4, // +1 because of fixed spawn locations
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
{Body: []Point{{1, 5}}},
{Body: []Point{{1, 9}}},
{Body: []Point{{5, 1}}},
{Body: []Point{{5, 9}}},
{Body: []Point{{9, 1}}},
{Body: []Point{{9, 5}}},
{Body: []Point{{9, 9}}},
},
},
9, // +1 because of fixed spawn locations
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
{Body: []Point{{1, 9}}},
{Body: []Point{{1, 17}}},
{Body: []Point{{17, 1}}},
{Body: []Point{{17, 9}}},
{Body: []Point{{17, 17}}},
},
},
7, // +1 because of fixed spawn locations
},
}
for _, test := range tests {
require.Len(t, test.BoardState.Food, 0)
err := PlaceFoodAutomatically(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, int32(0))
require.GreaterOrEqual(t, point.Y, int32(0))
require.Less(t, point.X, test.BoardState.Width)
require.Less(t, point.Y, test.BoardState.Height)
}
}
}
func TestPlaceFoodFixed(t *testing.T) {
tests := []struct {
BoardState *BoardState
}{
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
Snakes: []Snake{
{Body: []Point{{1, 3}}},
},
},
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
{Body: []Point{{1, 5}}},
{Body: []Point{{9, 5}}},
{Body: []Point{{9, 9}}},
},
},
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
{Body: []Point{{1, 9}}},
{Body: []Point{{1, 17}}},
{Body: []Point{{9, 1}}},
{Body: []Point{{9, 17}}},
{Body: []Point{{17, 1}}},
{Body: []Point{{17, 9}}},
{Body: []Point{{17, 17}}},
},
},
},
}
for _, test := range tests {
require.Len(t, test.BoardState.Food, 0)
err := PlaceFoodFixed(test.BoardState)
require.NoError(t, err)
require.Equal(t, len(test.BoardState.Snakes)+1, len(test.BoardState.Food))
// Make sure every snake has food within 2 moves of it
for _, snake := range test.BoardState.Snakes {
head := snake.Body[0]
bottomLeft := Point{head.X - 1, head.Y - 1}
topLeft := Point{head.X - 1, head.Y + 1}
bottomRight := Point{head.X + 1, head.Y - 1}
topRight := Point{head.X + 1, head.Y + 1}
foundFoodInTwoMoves := false
for _, food := range test.BoardState.Food {
if food == bottomLeft || food == topLeft || food == bottomRight || food == topRight {
foundFoodInTwoMoves = true
break
}
}
require.True(t, foundFoodInTwoMoves)
}
// Make sure one food exists in center of board
foundFoodInCenter := false
midPoint := Point{(test.BoardState.Width - 1) / 2, (test.BoardState.Height - 1) / 2}
for _, food := range test.BoardState.Food {
if food == midPoint {
foundFoodInCenter = true
break
}
}
require.True(t, foundFoodInCenter)
}
}
func TestPlaceFoodFixedNoRoom(t *testing.T) {
boardState := &BoardState{
Width: 3,
Height: 3,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
},
Food: []Point{},
}
err := PlaceFoodFixed(boardState)
require.Error(t, err)
boardState = &BoardState{
Width: 7,
Height: 7,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
},
Food: []Point{},
}
err = PlaceFoodFixed(boardState)
require.NoError(t, err)
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
require.Equal(t, 1, len(boardState.Food))
err = PlaceFoodFixed(boardState)
require.NoError(t, err)
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
require.Equal(t, 2, len(boardState.Food))
err = PlaceFoodFixed(boardState)
require.NoError(t, err)
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
require.Equal(t, 3, len(boardState.Food))
err = PlaceFoodFixed(boardState)
require.NoError(t, err)
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
require.Equal(t, 4, len(boardState.Food))
// And now there should be no more room.
err = PlaceFoodFixed(boardState)
require.Error(t, err)
}
func TestIsKnownBoardSize(t *testing.T) {
tests := []struct {
Width int32
Height int32
Expected bool
}{
{1, 1, false},
{0, 0, false},
{0, 45, false},
{45, 1, false},
{7, 7, true},
{11, 11, true},
{19, 19, true},
{7, 11, false},
{11, 19, false},
{19, 7, false},
}
for _, test := range tests {
result := isKnownBoardSize(&BoardState{Width: test.Width, Height: test.Height})
require.Equal(t, test.Expected, result)
}
}
func TestGetUnoccupiedPoints(t *testing.T) {
tests := []struct {
Board *BoardState
Expected []Point
}{
{
&BoardState{
Height: 1,
Width: 1,
},
[]Point{{0, 0}},
},
{
&BoardState{
Height: 1,
Width: 2,
},
[]Point{{0, 0}, {1, 0}},
},
{
&BoardState{
Height: 1,
Width: 1,
Food: []Point{{0, 0}, {101, 202}, {-4, -5}},
},
[]Point{},
},
{
&BoardState{
Height: 2,
Width: 2,
Food: []Point{{0, 0}, {1, 0}},
},
[]Point{{0, 1}, {1, 1}},
},
{
&BoardState{
Height: 2,
Width: 2,
Food: []Point{{0, 0}, {0, 1}, {1, 0}, {1, 1}},
},
[]Point{},
},
{
&BoardState{
Height: 4,
Width: 1,
Snakes: []Snake{
{Body: []Point{{0, 0}}},
},
},
[]Point{{0, 1}, {0, 2}, {0, 3}},
},
{
&BoardState{
Height: 2,
Width: 3,
Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
},
},
[]Point{{0, 1}, {2, 0}, {2, 1}},
},
{
&BoardState{
Height: 2,
Width: 3,
Food: []Point{{0, 0}, {1, 0}, {1, 1}, {2, 0}},
Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
{Body: []Point{{0, 1}}},
},
},
[]Point{{2, 1}},
},
}
for _, test := range tests {
unoccupiedPoints := getUnoccupiedPoints(test.Board, true)
require.Equal(t, len(test.Expected), len(unoccupiedPoints))
for i, e := range test.Expected {
require.Equal(t, e, unoccupiedPoints[i])
}
}
}
func TestGetEvenUnoccupiedPoints(t *testing.T) {
tests := []struct {
Board *BoardState
Expected []Point
}{
{
&BoardState{
Height: 1,
Width: 1,
},
[]Point{{0, 0}},
},
{
&BoardState{
Height: 2,
Width: 2,
},
[]Point{{0, 0}, {1, 1}},
},
{
&BoardState{
Height: 1,
Width: 1,
Food: []Point{{0, 0}, {101, 202}, {-4, -5}},
},
[]Point{},
},
{
&BoardState{
Height: 2,
Width: 2,
Food: []Point{{0, 0}, {1, 0}},
},
[]Point{{1, 1}},
},
{
&BoardState{
Height: 4,
Width: 4,
Food: []Point{{0, 0}, {0, 2}, {1, 1}, {1, 3}, {2, 0}, {2, 2}, {3, 1}, {3, 3}},
},
[]Point{},
},
{
&BoardState{
Height: 4,
Width: 1,
Snakes: []Snake{
{Body: []Point{{0, 0}}},
},
},
[]Point{{0, 2}},
},
{
&BoardState{
Height: 2,
Width: 3,
Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
},
},
[]Point{{2, 0}},
},
{
&BoardState{
Height: 2,
Width: 3,
Food: []Point{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
{Body: []Point{{0, 1}}},
},
},
[]Point{{2, 0}},
},
}
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])
}
}
}
func TestPlaceFoodRandomly(t *testing.T) {
b := &BoardState{
Height: 1,
Width: 3,
Snakes: []Snake{
{Body: []Point{{1, 0}}},
},
}
// Food should never spawn, no room
err := PlaceFoodRandomly(b, 99)
require.NoError(t, err)
require.Equal(t, len(b.Food), 0)
}

View file

@ -236,11 +236,15 @@ func initializeBoardFromArgs(ruleset rules.Ruleset, snakes []Battlesnake) *rules
for _, snake := range snakes {
snakeIds = append(snakeIds, snake.ID)
}
state, err := ruleset.CreateInitialBoardState(Width, Height, snakeIds)
state, err := rules.CreateDefaultBoardState(Width, Height, snakeIds)
if err != nil {
log.Panic("[PANIC]: Error Initializing Board State")
panic(err)
}
state, err = ruleset.ModifyInitialBoardState(state)
if err != nil {
log.Panic("[PANIC]: Error Initializing Board State")
}
for _, snake := range snakes {
requestBody := getIndividualBoardStateForSnake(state, snake, ruleset)
u, _ := url.ParseRequestURI(snake.URL)

View file

@ -1,23 +1,21 @@
package rules
import ()
type ConstrictorRuleset struct {
StandardRuleset
}
func (r *ConstrictorRuleset) CreateInitialBoardState(width int32, height int32, snakeIDs []string) (*BoardState, error) {
initialBoardState, err := r.StandardRuleset.CreateInitialBoardState(width, height, snakeIDs)
func (r *ConstrictorRuleset) ModifyInitialBoardState(initialBoardState *BoardState) (*BoardState, error) {
initialBoardState, err := r.StandardRuleset.ModifyInitialBoardState(initialBoardState)
if err != nil {
return nil, err
}
newBoardState := initialBoardState.Clone()
err = r.applyConstrictorRules(newBoardState)
if err != nil {
return nil, err
}
err = r.applyConstrictorRules(initialBoardState)
if err != nil {
return nil, err
}
return initialBoardState, nil
return newBoardState, nil
}
func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {

View file

@ -10,7 +10,7 @@ func TestConstrictorRulesetInterface(t *testing.T) {
var _ Ruleset = (*ConstrictorRuleset)(nil)
}
func TestConstrictorCreateInitialBoardState(t *testing.T) {
func TestConstrictorModifyInitialBoardState(t *testing.T) {
tests := []struct {
Height int32
Width int32
@ -27,7 +27,10 @@ func TestConstrictorCreateInitialBoardState(t *testing.T) {
r := ConstrictorRuleset{}
for testNum, test := range tests {
state, err := r.CreateInitialBoardState(test.Width, test.Height, test.IDs)
state, err := CreateDefaultBoardState(test.Width, test.Height, test.IDs)
require.NoError(t, err)
require.NotNil(t, state)
state, err = r.ModifyInitialBoardState(state)
require.NoError(t, err)
require.NotNil(t, state)
require.Equal(t, test.Width, state.Width)

View file

@ -46,14 +46,6 @@ type Snake struct {
EliminatedBy string
}
type BoardState struct {
Height int32
Width int32
Food []Point
Snakes []Snake
Hazards []Point
}
type SnakeMove struct {
ID string
Move string
@ -61,7 +53,7 @@ type SnakeMove struct {
type Ruleset interface {
Name() string
CreateInitialBoardState(width int32, height int32, snakeIDs []string) (*BoardState, error)
ModifyInitialBoardState(initialState *BoardState) (*BoardState, error)
CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error)
IsGameOver(state *BoardState) (bool, error)
}

View file

@ -13,182 +13,14 @@ type StandardRuleset struct {
func (r *StandardRuleset) Name() string { return "standard" }
func (r *StandardRuleset) CreateInitialBoardState(width int32, height int32, snakeIDs []string) (*BoardState, error) {
initialBoardState := &BoardState{
Height: height,
Width: width,
Snakes: make([]Snake, len(snakeIDs)),
}
for i := 0; i < len(snakeIDs); i++ {
initialBoardState.Snakes[i] = Snake{
ID: snakeIDs[i],
Health: SnakeMaxHealth,
}
}
err := r.placeSnakes(initialBoardState)
if err != nil {
return nil, err
}
err = r.placeFood(initialBoardState)
if err != nil {
return nil, err
}
return initialBoardState, nil
}
func (r *StandardRuleset) placeSnakes(b *BoardState) error {
if r.isKnownBoardSize(b) {
return r.placeSnakesFixed(b)
}
return r.placeSnakesRandomly(b)
}
func (r *StandardRuleset) placeSnakesFixed(b *BoardState) error {
// Create start 8 points
mn, md, mx := int32(1), (b.Width-1)/2, b.Width-2
startPoints := []Point{
{mn, mn},
{mn, md},
{mn, mx},
{md, mn},
{md, mx},
{mx, mn},
{mx, md},
{mx, mx},
}
// Sanity check
if len(b.Snakes) > len(startPoints) {
return ErrorTooManySnakes
}
// Randomly order them
rand.Shuffle(len(startPoints), func(i int, j int) {
startPoints[i], startPoints[j] = startPoints[j], startPoints[i]
})
// Assign to snakes in order given
for i := 0; i < len(b.Snakes); i++ {
for j := 0; j < SnakeStartSize; j++ {
b.Snakes[i].Body = append(b.Snakes[i].Body, startPoints[i])
}
}
return nil
}
func (r *StandardRuleset) placeSnakesRandomly(b *BoardState) error {
for i := 0; i < len(b.Snakes); i++ {
unoccupiedPoints := r.getEvenUnoccupiedPoints(b)
if len(unoccupiedPoints) <= 0 {
return ErrorNoRoomForSnake
}
p := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
for j := 0; j < SnakeStartSize; j++ {
b.Snakes[i].Body = append(b.Snakes[i].Body, p)
}
}
return nil
}
func (r *StandardRuleset) placeFood(b *BoardState) error {
if r.isKnownBoardSize(b) {
return r.placeFoodFixed(b)
}
return r.placeFoodRandomly(b)
}
func (r *StandardRuleset) placeFoodFixed(b *BoardState) error {
// Place 1 food within exactly 2 moves of each snake
for i := 0; i < len(b.Snakes); i++ {
snakeHead := b.Snakes[i].Body[0]
possibleFoodLocations := []Point{
{snakeHead.X - 1, snakeHead.Y - 1},
{snakeHead.X - 1, snakeHead.Y + 1},
{snakeHead.X + 1, snakeHead.Y - 1},
{snakeHead.X + 1, snakeHead.Y + 1},
}
availableFoodLocations := []Point{}
for _, p := range possibleFoodLocations {
isOccupiedAlready := false
for _, food := range b.Food {
if food.X == p.X && food.Y == p.Y {
isOccupiedAlready = true
break
}
}
if !isOccupiedAlready {
availableFoodLocations = append(availableFoodLocations, p)
}
}
if len(availableFoodLocations) <= 0 {
return ErrorNoRoomForFood
}
// Select randomly from available locations
placedFood := availableFoodLocations[rand.Intn(len(availableFoodLocations))]
b.Food = append(b.Food, placedFood)
}
// Finally, always place 1 food in center of board for dramatic purposes
isCenterOccupied := true
centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2}
unoccupiedPoints := r.getUnoccupiedPoints(b, true)
for _, point := range unoccupiedPoints {
if point == centerCoord {
isCenterOccupied = false
break
}
}
if isCenterOccupied {
return ErrorNoRoomForFood
}
b.Food = append(b.Food, centerCoord)
return nil
}
func (r *StandardRuleset) placeFoodRandomly(b *BoardState) error {
return r.spawnFood(b, int32(len(b.Snakes)))
}
func (r *StandardRuleset) isKnownBoardSize(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 (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
// No-op
return initialState, nil
}
func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
// We specifically want to copy prevState, so as not to alter it directly.
nextState := &BoardState{
Height: prevState.Height,
Width: prevState.Width,
Food: append([]Point{}, prevState.Food...),
Snakes: make([]Snake, len(prevState.Snakes)),
Hazards: append([]Point{}, prevState.Hazards...),
}
for i := 0; i < len(prevState.Snakes); i++ {
nextState.Snakes[i].ID = prevState.Snakes[i].ID
nextState.Snakes[i].Health = prevState.Snakes[i].Health
nextState.Snakes[i].Body = append([]Point{}, prevState.Snakes[i].Body...)
nextState.Snakes[i].EliminatedCause = prevState.Snakes[i].EliminatedCause
nextState.Snakes[i].EliminatedBy = prevState.Snakes[i].EliminatedBy
}
nextState := prevState.Clone()
// TODO: Gut check the BoardState?
@ -545,90 +377,13 @@ func (r *StandardRuleset) growSnake(snake *Snake) {
func (r *StandardRuleset) maybeSpawnFood(b *BoardState) error {
numCurrentFood := int32(len(b.Food))
if numCurrentFood < r.MinimumFood {
return r.spawnFood(b, r.MinimumFood-numCurrentFood)
return PlaceFoodRandomly(b, r.MinimumFood-numCurrentFood)
} else if r.FoodSpawnChance > 0 && int32(rand.Intn(100)) < r.FoodSpawnChance {
return r.spawnFood(b, 1)
return PlaceFoodRandomly(b, 1)
}
return nil
}
func (r *StandardRuleset) spawnFood(b *BoardState, n int32) error {
for i := int32(0); i < n; i++ {
unoccupiedPoints := r.getUnoccupiedPoints(b, false)
if len(unoccupiedPoints) > 0 {
newFood := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
b.Food = append(b.Food, newFood)
}
}
return nil
}
func (r *StandardRuleset) getUnoccupiedPoints(b *BoardState, includePossibleMoves bool) []Point {
pointIsOccupied := map[int32]map[int32]bool{}
for _, p := range b.Food {
if _, xExists := pointIsOccupied[p.X]; !xExists {
pointIsOccupied[p.X] = map[int32]bool{}
}
pointIsOccupied[p.X][p.Y] = true
}
for _, snake := range b.Snakes {
if snake.EliminatedCause != NotEliminated {
continue
}
for i, p := range snake.Body {
if _, xExists := pointIsOccupied[p.X]; !xExists {
pointIsOccupied[p.X] = map[int32]bool{}
}
pointIsOccupied[p.X][p.Y] = true
if i == 0 && !includePossibleMoves {
nextMovePoints := []Point{
{X: p.X - 1, Y: p.Y},
{X: p.X + 1, Y: p.Y},
{X: p.X, Y: p.Y - 1},
{X: p.X, Y: p.Y + 1},
}
for _, nextP := range nextMovePoints {
if _, xExists := pointIsOccupied[nextP.X]; !xExists {
pointIsOccupied[nextP.X] = map[int32]bool{}
}
pointIsOccupied[nextP.X][nextP.Y] = true
}
}
}
}
unoccupiedPoints := []Point{}
for x := int32(0); x < b.Width; x++ {
for y := int32(0); y < b.Height; y++ {
if _, xExists := pointIsOccupied[x]; xExists {
if isOccupied, yExists := pointIsOccupied[x][y]; yExists {
if isOccupied {
continue
}
}
}
unoccupiedPoints = append(unoccupiedPoints, Point{X: x, Y: y})
}
}
return unoccupiedPoints
}
func (r *StandardRuleset) getEvenUnoccupiedPoints(b *BoardState) []Point {
// Start by getting unoccupied points
unoccupiedPoints := r.getUnoccupiedPoints(b, true)
// Create a new array to hold points that are even
evenUnoccupiedPoints := []Point{}
for _, point := range unoccupiedPoints {
if ((point.X + point.Y) % 2) == 0 {
evenUnoccupiedPoints = append(evenUnoccupiedPoints, point)
}
}
return evenUnoccupiedPoints
}
func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) {
numSnakesRemaining := 0
for i := 0; i < len(b.Snakes); i++ {

View file

@ -15,7 +15,11 @@ func TestStandardRulesetInterface(t *testing.T) {
func TestSanity(t *testing.T) {
r := StandardRuleset{}
state, err := r.CreateInitialBoardState(0, 0, []string{})
state, err := CreateDefaultBoardState(0, 0, []string{})
require.NoError(t, err)
require.NotNil(t, state)
state, err = r.ModifyInitialBoardState(state)
require.NoError(t, err)
require.NotNil(t, state)
require.Equal(t, int32(0), state.Width)
@ -39,434 +43,6 @@ func TestStandardName(t *testing.T) {
require.Equal(t, "standard", r.Name())
}
func TestCreateInitialBoardState(t *testing.T) {
tests := []struct {
Height int32
Width int32
IDs []string
ExpectedNumFood int
Err error
}{
{1, 1, []string{"one"}, 0, nil},
{1, 2, []string{"one"}, 0, nil},
{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},
{1, 1, []string{"one", "two"}, 2, ErrorNoRoomForSnake},
{1, 2, []string{"one", "two"}, 2, ErrorNoRoomForSnake},
{BoardSizeSmall, BoardSizeSmall, []string{"one", "two"}, 3, nil},
}
r := StandardRuleset{}
for testNum, test := range tests {
state, err := r.CreateInitialBoardState(test.Width, test.Height, test.IDs)
require.Equal(t, test.Err, err)
if err != nil {
require.Nil(t, state)
continue
}
require.NotNil(t, state)
require.Equal(t, test.Width, state.Width)
require.Equal(t, test.Height, state.Height)
require.Equal(t, len(test.IDs), len(state.Snakes))
for i, id := range test.IDs {
require.Equal(t, id, state.Snakes[i].ID)
}
require.Len(t, state.Food, test.ExpectedNumFood, testNum)
require.Len(t, state.Hazards, 0, testNum)
}
}
func TestPlaceSnakes(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
tests := []struct {
BoardState *BoardState
Err error
}{
{
&BoardState{
Width: 1,
Height: 1,
Snakes: make([]Snake, 1),
},
nil,
},
{
&BoardState{
Width: 1,
Height: 1,
Snakes: make([]Snake, 2),
},
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 2,
Height: 1,
Snakes: make([]Snake, 2),
},
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 1,
Height: 2,
Snakes: make([]Snake, 2),
},
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 10,
Height: 5,
Snakes: make([]Snake, 24),
},
nil,
},
{
&BoardState{
Width: 5,
Height: 10,
Snakes: make([]Snake, 25),
},
nil,
},
{
&BoardState{
Width: 10,
Height: 5,
Snakes: make([]Snake, 49),
},
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 5,
Height: 10,
Snakes: make([]Snake, 50),
},
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: 25,
Height: 2,
Snakes: make([]Snake, 51),
},
ErrorNoRoomForSnake,
},
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
Snakes: make([]Snake, 1),
},
nil,
},
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
Snakes: make([]Snake, 8),
},
nil,
},
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
Snakes: make([]Snake, 9),
},
ErrorTooManySnakes,
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
Snakes: make([]Snake, 8),
},
nil,
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
Snakes: make([]Snake, 9),
},
ErrorTooManySnakes,
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
Snakes: make([]Snake, 8),
},
nil,
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
Snakes: make([]Snake, 9),
},
ErrorTooManySnakes,
},
}
r := StandardRuleset{}
for _, test := range tests {
require.Equal(t, test.BoardState.Width*test.BoardState.Height, int32(len(r.getUnoccupiedPoints(test.BoardState, true))))
err := r.placeSnakes(test.BoardState)
require.Equal(t, test.Err, err, "Snakes: %d", len(test.BoardState.Snakes))
if err == nil {
for i := 0; i < len(test.BoardState.Snakes); i++ {
require.Len(t, test.BoardState.Snakes[i].Body, 3)
for _, point := range test.BoardState.Snakes[i].Body {
require.GreaterOrEqual(t, point.X, int32(0))
require.GreaterOrEqual(t, point.Y, int32(0))
require.Less(t, point.X, test.BoardState.Width)
require.Less(t, point.Y, test.BoardState.Height)
}
// All snakes are expected to be placed on an even square - this is true even of fixed positions for known board sizes
var snakePlacedOnEvenSquare bool = ((test.BoardState.Snakes[i].Body[0].X + test.BoardState.Snakes[i].Body[0].Y) % 2) == 0
require.Equal(t, true, snakePlacedOnEvenSquare)
}
}
}
}
func TestPlaceFood(t *testing.T) {
tests := []struct {
BoardState *BoardState
ExpectedFood int
}{
{
&BoardState{
Width: 1,
Height: 1,
Snakes: make([]Snake, 1),
},
1,
},
{
&BoardState{
Width: 1,
Height: 2,
Snakes: make([]Snake, 2),
},
2,
},
{
&BoardState{
Width: 101,
Height: 202,
Snakes: make([]Snake, 17),
},
17,
},
{
&BoardState{
Width: 10,
Height: 20,
Snakes: make([]Snake, 305),
},
200,
},
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
Snakes: []Snake{
{Body: []Point{{5, 1}}},
{Body: []Point{{5, 3}}},
{Body: []Point{{5, 5}}},
},
},
4, // +1 because of fixed spawn locations
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
{Body: []Point{{1, 5}}},
{Body: []Point{{1, 9}}},
{Body: []Point{{5, 1}}},
{Body: []Point{{5, 9}}},
{Body: []Point{{9, 1}}},
{Body: []Point{{9, 5}}},
{Body: []Point{{9, 9}}},
},
},
9, // +1 because of fixed spawn locations
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
{Body: []Point{{1, 9}}},
{Body: []Point{{1, 17}}},
{Body: []Point{{17, 1}}},
{Body: []Point{{17, 9}}},
{Body: []Point{{17, 17}}},
},
},
7, // +1 because of fixed spawn locations
},
}
r := StandardRuleset{}
for _, test := range tests {
require.Len(t, test.BoardState.Food, 0)
err := r.placeFood(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, int32(0))
require.GreaterOrEqual(t, point.Y, int32(0))
require.Less(t, point.X, test.BoardState.Width)
require.Less(t, point.Y, test.BoardState.Height)
}
}
}
func TestPlaceFoodFixed(t *testing.T) {
tests := []struct {
BoardState *BoardState
}{
{
&BoardState{
Width: BoardSizeSmall,
Height: BoardSizeSmall,
Snakes: []Snake{
{Body: []Point{{1, 3}}},
},
},
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
{Body: []Point{{1, 5}}},
{Body: []Point{{9, 5}}},
{Body: []Point{{9, 9}}},
},
},
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
{Body: []Point{{1, 9}}},
{Body: []Point{{1, 17}}},
{Body: []Point{{9, 1}}},
{Body: []Point{{9, 17}}},
{Body: []Point{{17, 1}}},
{Body: []Point{{17, 9}}},
{Body: []Point{{17, 17}}},
},
},
},
}
r := StandardRuleset{}
for _, test := range tests {
require.Len(t, test.BoardState.Food, 0)
err := r.placeFoodFixed(test.BoardState)
require.NoError(t, err)
require.Equal(t, len(test.BoardState.Snakes)+1, len(test.BoardState.Food))
// Make sure every snake has food within 2 moves of it
for _, snake := range test.BoardState.Snakes {
head := snake.Body[0]
bottomLeft := Point{head.X - 1, head.Y - 1}
topLeft := Point{head.X - 1, head.Y + 1}
bottomRight := Point{head.X + 1, head.Y - 1}
topRight := Point{head.X + 1, head.Y + 1}
foundFoodInTwoMoves := false
for _, food := range test.BoardState.Food {
if food == bottomLeft || food == topLeft || food == bottomRight || food == topRight {
foundFoodInTwoMoves = true
break
}
}
require.True(t, foundFoodInTwoMoves)
}
// Make sure one food exists in center of board
foundFoodInCenter := false
midPoint := Point{(test.BoardState.Width - 1) / 2, (test.BoardState.Height - 1) / 2}
for _, food := range test.BoardState.Food {
if food == midPoint {
foundFoodInCenter = true
break
}
}
require.True(t, foundFoodInCenter)
}
}
func TestPlaceFoodFixedNoRoom(t *testing.T) {
r := StandardRuleset{}
boardState := &BoardState{
Width: 3,
Height: 3,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
},
Food: []Point{},
}
err := r.placeFoodFixed(boardState)
require.Error(t, err)
boardState = &BoardState{
Width: 7,
Height: 7,
Snakes: []Snake{
{Body: []Point{{1, 1}}},
},
Food: []Point{},
}
err = r.placeFoodFixed(boardState)
require.NoError(t, err)
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
require.Equal(t, 1, len(boardState.Food))
err = r.placeFoodFixed(boardState)
require.NoError(t, err)
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
require.Equal(t, 2, len(boardState.Food))
err = r.placeFoodFixed(boardState)
require.NoError(t, err)
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
require.Equal(t, 3, len(boardState.Food))
err = r.placeFoodFixed(boardState)
require.NoError(t, err)
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
require.Equal(t, 4, len(boardState.Food))
// And now there should be no more room.
err = r.placeFoodFixed(boardState)
require.Error(t, err)
}
func TestCreateNextBoardState(t *testing.T) {
tests := []struct {
prevState *BoardState
@ -1024,31 +600,6 @@ func TestMoveSnakesExtraMovesIgnored(t *testing.T) {
require.Equal(t, []Point{{1, 0}}, b.Snakes[0].Body)
}
func TestIsKnownBoardSize(t *testing.T) {
tests := []struct {
Width int32
Height int32
Expected bool
}{
{1, 1, false},
{0, 0, false},
{0, 45, false},
{45, 1, false},
{7, 7, true},
{11, 11, true},
{19, 19, true},
{7, 11, false},
{11, 19, false},
{19, 7, false},
}
r := StandardRuleset{}
for _, test := range tests {
result := r.isKnownBoardSize(&BoardState{Width: test.Width, Height: test.Height})
require.Equal(t, test.Expected, result)
}
}
func TestMoveSnakesDefault(t *testing.T) {
tests := []struct {
Body []Point
@ -1789,180 +1340,6 @@ func TestMaybeFeedSnakes(t *testing.T) {
}
}
func TestGetUnoccupiedPoints(t *testing.T) {
tests := []struct {
Board *BoardState
Expected []Point
}{
{
&BoardState{
Height: 1,
Width: 1,
},
[]Point{{0, 0}},
},
{
&BoardState{
Height: 1,
Width: 2,
},
[]Point{{0, 0}, {1, 0}},
},
{
&BoardState{
Height: 1,
Width: 1,
Food: []Point{{0, 0}, {101, 202}, {-4, -5}},
},
[]Point{},
},
{
&BoardState{
Height: 2,
Width: 2,
Food: []Point{{0, 0}, {1, 0}},
},
[]Point{{0, 1}, {1, 1}},
},
{
&BoardState{
Height: 2,
Width: 2,
Food: []Point{{0, 0}, {0, 1}, {1, 0}, {1, 1}},
},
[]Point{},
},
{
&BoardState{
Height: 4,
Width: 1,
Snakes: []Snake{
{Body: []Point{{0, 0}}},
},
},
[]Point{{0, 1}, {0, 2}, {0, 3}},
},
{
&BoardState{
Height: 2,
Width: 3,
Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
},
},
[]Point{{0, 1}, {2, 0}, {2, 1}},
},
{
&BoardState{
Height: 2,
Width: 3,
Food: []Point{{0, 0}, {1, 0}, {1, 1}, {2, 0}},
Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
{Body: []Point{{0, 1}}},
},
},
[]Point{{2, 1}},
},
}
r := StandardRuleset{}
for _, test := range tests {
unoccupiedPoints := r.getUnoccupiedPoints(test.Board, true)
require.Equal(t, len(test.Expected), len(unoccupiedPoints))
for i, e := range test.Expected {
require.Equal(t, e, unoccupiedPoints[i])
}
}
}
func TestGetEvenUnoccupiedPoints(t *testing.T) {
tests := []struct {
Board *BoardState
Expected []Point
}{
{
&BoardState{
Height: 1,
Width: 1,
},
[]Point{{0, 0}},
},
{
&BoardState{
Height: 2,
Width: 2,
},
[]Point{{0, 0}, {1, 1}},
},
{
&BoardState{
Height: 1,
Width: 1,
Food: []Point{{0, 0}, {101, 202}, {-4, -5}},
},
[]Point{},
},
{
&BoardState{
Height: 2,
Width: 2,
Food: []Point{{0, 0}, {1, 0}},
},
[]Point{{1, 1}},
},
{
&BoardState{
Height: 4,
Width: 4,
Food: []Point{{0, 0}, {0, 2}, {1, 1}, {1, 3}, {2, 0}, {2, 2}, {3, 1}, {3, 3}},
},
[]Point{},
},
{
&BoardState{
Height: 4,
Width: 1,
Snakes: []Snake{
{Body: []Point{{0, 0}}},
},
},
[]Point{{0, 2}},
},
{
&BoardState{
Height: 2,
Width: 3,
Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
},
},
[]Point{{2, 0}},
},
{
&BoardState{
Height: 2,
Width: 3,
Food: []Point{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
{Body: []Point{{0, 1}}},
},
},
[]Point{{2, 0}},
},
}
r := StandardRuleset{}
for _, test := range tests {
evenUnoccupiedPoints := r.getEvenUnoccupiedPoints(test.Board)
require.Equal(t, len(test.Expected), len(evenUnoccupiedPoints))
for i, e := range test.Expected {
require.Equal(t, e, evenUnoccupiedPoints[i])
}
}
}
func TestMaybeSpawnFoodMinimum(t *testing.T) {
tests := []struct {
MinimumFood int32
@ -2064,21 +1441,6 @@ func TestMaybeSpawnFoodHalfChance(t *testing.T) {
}
}
func TestSpawnFood(t *testing.T) {
b := &BoardState{
Height: 1,
Width: 3,
Snakes: []Snake{
{Body: []Point{{1, 0}}},
},
}
// Food should never spawn, no room
r := StandardRuleset{}
err := r.spawnFood(b, 99)
require.NoError(t, err)
require.Equal(t, len(b.Food), 0)
}
func TestIsGameOver(t *testing.T) {
tests := []struct {
Snakes []Snake