From 015b681f1468ee1edefa98c4ea1c6ac03f879ae1 Mon Sep 17 00:00:00 2001 From: Rob O'Dwyer Date: Mon, 23 Aug 2021 17:13:58 -0700 Subject: [PATCH] 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 --- board.go | 297 +++++++++++++++++++ board_test.go | 678 +++++++++++++++++++++++++++++++++++++++++++ cli/commands/play.go | 8 +- constrictor.go | 12 +- constrictor_test.go | 7 +- ruleset.go | 10 +- standard.go | 257 +--------------- standard_test.go | 648 +---------------------------------------- 8 files changed, 1003 insertions(+), 914 deletions(-) create mode 100644 board.go create mode 100644 board_test.go diff --git a/board.go b/board.go new file mode 100644 index 0000000..8eedda2 --- /dev/null +++ b/board.go @@ -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 +} diff --git a/board_test.go b/board_test.go new file mode 100644 index 0000000..5250059 --- /dev/null +++ b/board_test.go @@ -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) +} diff --git a/cli/commands/play.go b/cli/commands/play.go index a322ffa..ac57ae1 100644 --- a/cli/commands/play.go +++ b/cli/commands/play.go @@ -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) diff --git a/constrictor.go b/constrictor.go index 52f0b57..6acd787 100644 --- a/constrictor.go +++ b/constrictor.go @@ -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 } - - err = r.applyConstrictorRules(initialBoardState) + newBoardState := initialBoardState.Clone() + err = r.applyConstrictorRules(newBoardState) if err != nil { return nil, err } - return initialBoardState, nil + return newBoardState, nil } func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { diff --git a/constrictor_test.go b/constrictor_test.go index 6097ba4..36bce42 100644 --- a/constrictor_test.go +++ b/constrictor_test.go @@ -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) diff --git a/ruleset.go b/ruleset.go index bc442a8..0486abd 100644 --- a/ruleset.go +++ b/ruleset.go @@ -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) } diff --git a/standard.go b/standard.go index 19125b8..9cee9c6 100644 --- a/standard.go +++ b/standard.go @@ -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++ { diff --git a/standard_test.go b/standard_test.go index e103bb1..33d5878 100644 --- a/standard_test.go +++ b/standard_test.go @@ -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