Rename consts, remove pointers where not wanted/needed, snake placement tests.

This commit is contained in:
bvanvugt 2020-01-02 16:10:33 -08:00
parent 53d57d8e6a
commit 010b3aa08f
3 changed files with 419 additions and 281 deletions

View file

@ -1,10 +1,10 @@
package rulesets
const (
MOVE_UP = "up"
MOVE_DOWN = "down"
MOVE_RIGHT = "right"
MOVE_LEFT = "left"
MoveUp = "up"
MoveDown = "down"
MoveRight = "right"
MoveLeft = "left"
)
type Point struct {
@ -14,7 +14,7 @@ type Point struct {
type Snake struct {
ID string
Body []*Point
Body []Point
Health int32
EliminatedCause string
}
@ -22,16 +22,16 @@ type Snake struct {
type BoardState struct {
Height int32
Width int32
Food []*Point
Snakes []*Snake
Food []Point
Snakes []Snake
}
type SnakeMove struct {
Snake *Snake
Move string
ID string
Move string
}
type Ruleset interface {
CreateInitialBoardState(width int32, height int32, snakeIDs []string) (*BoardState, error)
ResolveMoves(prevState *BoardState, moves []*SnakeMove) (*BoardState, error)
ResolveMoves(prevState *BoardState, moves []SnakeMove) (*BoardState, error)
}

View file

@ -8,50 +8,40 @@ import (
type StandardRuleset struct{}
const (
BOARD_SIZE_SMALL = 7
BOARD_SIZE_MEDIUM = 11
BOARD_SIZE_LARGE = 19
FOOD_SPAWN_CHANCE = 0.1
SNAKE_MAX_HEALTH = 100
BoardSizeSmall = 7
BoardSizeMedium = 11
BoardSizeLarge = 19
FoodSpawnChance = 0.1
SnakeMaxHealth = 100
SnakeStartSize = 3
// bvanvugt - TODO: Just return formatted strings instead of codes?
ELIMINATED_COLLISION = "snake-collision"
ELIMINATED_SELF_COLLISION = "snake-self-collision"
ELIMINATED_STARVATION = "starvation"
ELIMINATED_HEAD_TO_HEAD = "head-collision"
ELIMINATED_OUT_OF_BOUNDS = "wall-collision"
EliminatedByColliision = "snake-collision"
EliminatedBySelfColliision = "snake-self-collision"
EliminatedByStarvation = "starvation"
EliminatedByHeadToHeadCollision = "head-collision"
EliminatedByOutOfBounds = "wall-collision"
)
func (r *StandardRuleset) CreateInitialBoard(width int32, height int32, snakeIDs []string) (*BoardState, error) {
var err error
snakes := []*Snake{}
for _, id := range snakeIDs {
snakes = append(snakes,
&Snake{
ID: id,
Health: SNAKE_MAX_HEALTH,
},
)
}
func (r *StandardRuleset) CreateInitialBoardState(width int32, height int32, snakeIDs []string) (*BoardState, error) {
initialBoardState := &BoardState{
Height: height,
Width: width,
Snakes: snakes,
Snakes: make([]Snake, len(snakeIDs)),
}
// Place Snakes
if r.isKnownBoardSize(initialBoardState) {
err = r.placeSnakesFixed(initialBoardState)
} else {
err = r.placeSnakesRandomly(initialBoardState)
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
}
// Place Food
err = r.placeInitialFood(initialBoardState)
if err != nil {
return nil, err
@ -60,23 +50,30 @@ func (r *StandardRuleset) CreateInitialBoard(width int32, height int32, snakeIDs
return initialBoardState, nil
}
func (r *StandardRuleset) placeSnakesFixed(b *BoardState) error {
// Sanity check
if len(b.Snakes) >= 8 {
return errors.New("too many snakes for fixed start positions")
func (r *StandardRuleset) placeSnakes(b *BoardState) error {
if r.isKnownBoardSize(b) {
return r.placeSnakesFixed(b)
}
return r.placeSnakesRandomly(b)
}
// Create start points
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},
Point{mn, mn},
Point{mn, md},
Point{mn, mx},
Point{md, mn},
Point{md, mx},
Point{mx, mn},
Point{mx, md},
Point{mx, mx},
}
// Sanity check
if len(b.Snakes) > len(startPoints) {
return errors.New("too many snakes for fixed start positions")
}
// Randomly order them
@ -85,10 +82,9 @@ func (r *StandardRuleset) placeSnakesFixed(b *BoardState) error {
})
// Assign to snakes in order given
for i, snake := range b.Snakes {
p := startPoints[i]
for j := 0; j < 3; j++ {
snake.Body = append(snake.Body, &Point{p.X, p.Y})
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])
}
}
@ -96,24 +92,27 @@ func (r *StandardRuleset) placeSnakesFixed(b *BoardState) error {
}
func (r *StandardRuleset) placeSnakesRandomly(b *BoardState) error {
for _, snake := range b.Snakes {
for i := 0; i < len(b.Snakes); i++ {
unoccupiedPoints := r.getUnoccupiedPoints(b)
if len(unoccupiedPoints) < len(b.Snakes)-i {
return errors.New("not enough empty squares to place snakes")
}
p := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
for j := 0; j < 3; j++ {
snake.Body = append(snake.Body, &Point{p.X, p.Y})
for j := 0; j < SnakeStartSize; j++ {
b.Snakes[i].Body = append(b.Snakes[i].Body, p)
}
}
return nil
}
func (r *StandardRuleset) isKnownBoardSize(b *BoardState) bool {
if b.Height == BOARD_SIZE_SMALL && b.Width == BOARD_SIZE_SMALL {
if b.Height == BoardSizeSmall && b.Width == BoardSizeSmall {
return true
}
if b.Height == BOARD_SIZE_MEDIUM && b.Width == BOARD_SIZE_MEDIUM {
if b.Height == BoardSizeMedium && b.Width == BoardSizeMedium {
return true
}
if b.Height == BOARD_SIZE_LARGE && b.Width == BOARD_SIZE_LARGE {
if b.Height == BoardSizeLarge && b.Width == BoardSizeLarge {
return true
}
return false
@ -124,12 +123,18 @@ func (r *StandardRuleset) placeInitialFood(b *BoardState) error {
return nil
}
func (r *StandardRuleset) ResolveMoves(prevState *BoardState, moves []*SnakeMove) (*BoardState, error) {
// TODO: DO NOT REFERENCE prevState directly!!!!
// we're technically altering both states
func (r *StandardRuleset) ResolveMoves(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
// We specifically want to copy prevState, so as not to alter it directly.
nextState := &BoardState{
Snakes: prevState.Snakes,
Food: prevState.Food,
Height: prevState.Height,
Width: prevState.Width,
Food: append([]Point{}, prevState.Food...),
Snakes: make([]Snake, len(prevState.Snakes)),
}
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...)
}
// TODO: Gut check the BoardState?
@ -146,6 +151,12 @@ func (r *StandardRuleset) ResolveMoves(prevState *BoardState, moves []*SnakeMove
return nil, err
}
// TODO: LOG?
err = r.eliminateSnakes(nextState)
if err != nil {
return nil, err
}
// TODO
// bvanvugt: we specifically want this to happen before elimination
// so that head-to-head collisions on food still remove the food.
@ -164,78 +175,79 @@ func (r *StandardRuleset) ResolveMoves(prevState *BoardState, moves []*SnakeMove
return nil, err
}
// TODO: LOG?
err = r.eliminateSnakes(nextState)
if err != nil {
return nil, err
}
return nextState, nil
}
func (r *StandardRuleset) moveSnakes(b *BoardState, moves []*SnakeMove) error {
func (r *StandardRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
for _, move := range moves {
var newHead = &Point{}
var snake *Snake
for i := 0; i < len(b.Snakes); i++ {
if b.Snakes[i].ID == move.ID {
snake = &b.Snakes[i]
}
}
var newHead = Point{}
switch move.Move {
case MOVE_DOWN:
newHead.X = move.Snake.Body[0].X
newHead.Y = move.Snake.Body[0].Y + 1
case MOVE_LEFT:
newHead.X = move.Snake.Body[0].X - 1
newHead.Y = move.Snake.Body[0].Y
case MOVE_RIGHT:
newHead.X = move.Snake.Body[0].X + 1
newHead.Y = move.Snake.Body[0].Y
case MOVE_UP:
newHead.X = move.Snake.Body[0].X
newHead.Y = move.Snake.Body[0].Y - 1
case MoveDown:
newHead.X = snake.Body[0].X
newHead.Y = snake.Body[0].Y + 1
case MoveLeft:
newHead.X = snake.Body[0].X - 1
newHead.Y = snake.Body[0].Y
case MoveRight:
newHead.X = snake.Body[0].X + 1
newHead.Y = snake.Body[0].Y
case MoveUp:
newHead.X = snake.Body[0].X
newHead.Y = snake.Body[0].Y - 1
default:
// Default to UP
var dX int32 = 0
var dY int32 = -1
// If neck is available, use neck to determine last direction
if len(move.Snake.Body) >= 2 {
dX = move.Snake.Body[0].X - move.Snake.Body[1].X
dY = move.Snake.Body[0].Y - move.Snake.Body[1].Y
if len(snake.Body) >= 2 {
dX = snake.Body[0].X - snake.Body[1].X
dY = snake.Body[0].Y - snake.Body[1].Y
if dX == 0 && dY == 0 {
dY = -1 // Move up if no last move was made
}
}
// Apply
newHead.X = move.Snake.Body[0].X + dX
newHead.Y = move.Snake.Body[0].Y + dY
newHead.X = snake.Body[0].X + dX
newHead.Y = snake.Body[0].Y + dY
}
// Append new head, pop old tail
move.Snake.Body = append([]*Point{newHead}, move.Snake.Body[:len(move.Snake.Body)-1]...)
snake.Body = append([]Point{newHead}, snake.Body[:len(snake.Body)-1]...)
}
return nil
}
func (r *StandardRuleset) reduceSnakeHealth(b *BoardState) error {
for _, snake := range b.Snakes {
snake.Health = snake.Health - 1
for i := 0; i < len(b.Snakes); i++ {
b.Snakes[i].Health = b.Snakes[i].Health - 1
}
return nil
}
func (r *StandardRuleset) eliminateSnakes(b *BoardState) error {
for _, snake := range b.Snakes {
if r.snakeHasStarved(snake) {
snake.EliminatedCause = ELIMINATED_STARVATION
} else if r.snakeIsOutOfBounds(snake, b.Width, b.Height) {
snake.EliminatedCause = ELIMINATED_OUT_OF_BOUNDS
if r.snakeHasStarved(&snake) {
snake.EliminatedCause = EliminatedByStarvation
} else if r.snakeIsOutOfBounds(&snake, b.Width, b.Height) {
snake.EliminatedCause = EliminatedByOutOfBounds
} else {
for _, other := range b.Snakes {
if r.snakeHasBodyCollided(snake, other) {
if r.snakeHasBodyCollided(&snake, &other) {
if snake.ID == other.ID {
snake.EliminatedCause = ELIMINATED_SELF_COLLISION
snake.EliminatedCause = EliminatedBySelfColliision
} else {
snake.EliminatedCause = ELIMINATED_COLLISION
snake.EliminatedCause = EliminatedByColliision
}
break
} else if r.snakeHasLostHeadToHead(snake, other) {
snake.EliminatedCause = ELIMINATED_HEAD_TO_HEAD
} else if r.snakeHasLostHeadToHead(&snake, &other) {
snake.EliminatedCause = EliminatedByHeadToHeadCollision
break
}
}
@ -280,8 +292,8 @@ func (r *StandardRuleset) snakeHasLostHeadToHead(s *Snake, other *Snake) bool {
}
func (r *StandardRuleset) feedSnakes(b *BoardState) error {
var newFood []*Point
var tail *Point
var newFood []Point
var tail Point
for _, food := range b.Food {
foodHasBeenEaten := false
@ -289,9 +301,9 @@ func (r *StandardRuleset) feedSnakes(b *BoardState) error {
if snake.Body[0].X == food.X && snake.Body[0].Y == food.Y {
foodHasBeenEaten = true
// Update snake
snake.Health = SNAKE_MAX_HEALTH
snake.Health = SnakeMaxHealth
tail = snake.Body[len(snake.Body)-1]
snake.Body = append(snake.Body, &Point{X: tail.X, Y: tail.Y})
snake.Body = append(snake.Body, tail)
}
}
// Persist food to next BoardState if not eaten
@ -305,7 +317,7 @@ func (r *StandardRuleset) feedSnakes(b *BoardState) error {
}
func (r *StandardRuleset) maybeSpawnFood(b *BoardState, n int) error {
if rand.Float32() <= FOOD_SPAWN_CHANCE {
if rand.Float32() <= FoodSpawnChance {
r.spawnFood(b, n)
}
return nil
@ -321,7 +333,7 @@ func (r *StandardRuleset) spawnFood(b *BoardState, n int) {
}
}
func (r *StandardRuleset) getUnoccupiedPoints(b *BoardState) []*Point {
func (r *StandardRuleset) getUnoccupiedPoints(b *BoardState) []Point {
pointIsOccupied := map[int32]map[int32]bool{}
for _, p := range b.Food {
if _, xExists := pointIsOccupied[p.X]; !xExists {
@ -338,7 +350,7 @@ func (r *StandardRuleset) getUnoccupiedPoints(b *BoardState) []*Point {
}
}
unoccupiedPoints := []*Point{}
unoccupiedPoints := []Point{}
for x := int32(0); x < b.Width; x++ {
for y := int32(0); y < b.Height; y++ {
if _, xExists := pointIsOccupied[x]; xExists {
@ -348,7 +360,7 @@ func (r *StandardRuleset) getUnoccupiedPoints(b *BoardState) []*Point {
}
}
}
unoccupiedPoints = append(unoccupiedPoints, &Point{X: x, Y: y})
unoccupiedPoints = append(unoccupiedPoints, Point{X: x, Y: y})
}
}
return unoccupiedPoints

View file

@ -1,6 +1,7 @@
package rulesets
import (
"errors"
"math"
"math/rand"
"testing"
@ -10,26 +11,172 @@ import (
func TestSanity(t *testing.T) {
r := StandardRuleset{}
state, err := r.CreateInitialBoardState(0, 0, []string{})
require.NoError(t, err)
require.NotNil(t, state)
require.Equal(t, int32(0), state.Width)
require.Equal(t, int32(0), state.Height)
require.Len(t, state.Food, 0)
require.Len(t, state.Snakes, 0)
next, err := r.ResolveMoves(
&BoardState{},
[]*SnakeMove{},
[]SnakeMove{},
)
require.NoError(t, err)
require.NotNil(t, next)
require.Equal(t, int32(0), state.Width)
require.Equal(t, int32(0), state.Height)
require.Len(t, state.Snakes, 0)
}
// Create Board
// placeSnakes
// placeFood
// knownBoardSize
// REsolveMoves
// eliminateSnakes
// --> related subs
// move, reduce, feed, need to consider dead snakes
func TestCreateInitialBoardState(t *testing.T) {
// TODO
}
func TestPlaceSnakes(t *testing.T) {
// Because placement is random, we only test to ensure
// that snake bodies are populated correctly
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),
},
errors.New("not enough empty squares to place snakes"),
},
{
&BoardState{
Width: 10,
Height: 5,
Snakes: make([]Snake, 49),
},
nil,
},
{
&BoardState{
Width: 5,
Height: 10,
Snakes: make([]Snake, 50),
},
nil,
},
{
&BoardState{
Width: 25,
Height: 2,
Snakes: make([]Snake, 51),
},
errors.New("not enough empty squares to place snakes"),
},
{
&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),
},
errors.New("too many snakes for fixed start positions"),
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
Snakes: make([]Snake, 8),
},
nil,
},
{
&BoardState{
Width: BoardSizeMedium,
Height: BoardSizeMedium,
Snakes: make([]Snake, 9),
},
errors.New("too many snakes for fixed start positions"),
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
Snakes: make([]Snake, 8),
},
nil,
},
{
&BoardState{
Width: BoardSizeLarge,
Height: BoardSizeLarge,
Snakes: make([]Snake, 9),
},
errors.New("too many snakes for fixed start positions"),
},
}
r := StandardRuleset{}
for _, test := range tests {
require.Equal(t, test.BoardState.Width*test.BoardState.Height, int32(len(r.getUnoccupiedPoints(test.BoardState))))
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)
}
}
}
}
func TestResolveMoves(t *testing.T) {
// TODO
}
func TestMoveSnakes(t *testing.T) {
b := &BoardState{
Snakes: []*Snake{
Snakes: []Snake{
{
ID: "one",
Body: []*Point{{10, 110}, {11, 110}},
Body: []Point{{10, 110}, {11, 110}},
Health: 111111,
},
{
ID: "two",
Body: []*Point{{23, 220}, {22, 220}, {21, 220}, {20, 220}},
Body: []Point{{23, 220}, {22, 220}, {21, 220}, {20, 220}},
Health: 222222,
},
},
@ -37,47 +184,37 @@ func TestMoveSnakes(t *testing.T) {
tests := []struct {
MoveOne string
ExpectedOne []*Point
ExpectedOne []Point
MoveTwo string
ExpectedTwo []*Point
ExpectedTwo []Point
}{
{
MOVE_UP,
[]*Point{{10, 109}, {10, 110}},
MOVE_DOWN,
[]*Point{{23, 221}, {23, 220}, {22, 220}, {21, 220}},
MoveUp, []Point{{10, 109}, {10, 110}},
MoveDown, []Point{{23, 221}, {23, 220}, {22, 220}, {21, 220}},
},
{
MOVE_RIGHT,
[]*Point{{11, 109}, {10, 109}},
MOVE_LEFT,
[]*Point{{22, 221}, {23, 221}, {23, 220}, {22, 220}},
MoveRight, []Point{{11, 109}, {10, 109}},
MoveLeft, []Point{{22, 221}, {23, 221}, {23, 220}, {22, 220}},
},
{
MOVE_RIGHT,
[]*Point{{12, 109}, {11, 109}},
MOVE_LEFT,
[]*Point{{21, 221}, {22, 221}, {23, 221}, {23, 220}},
MoveRight, []Point{{12, 109}, {11, 109}},
MoveLeft, []Point{{21, 221}, {22, 221}, {23, 221}, {23, 220}},
},
{
MOVE_RIGHT,
[]*Point{{13, 109}, {12, 109}},
MOVE_LEFT,
[]*Point{{20, 221}, {21, 221}, {22, 221}, {23, 221}},
MoveRight, []Point{{13, 109}, {12, 109}},
MoveLeft, []Point{{20, 221}, {21, 221}, {22, 221}, {23, 221}},
},
{
MOVE_UP,
[]*Point{{13, 108}, {13, 109}},
MOVE_DOWN,
[]*Point{{20, 222}, {20, 221}, {21, 221}, {22, 221}},
MoveUp, []Point{{13, 108}, {13, 109}},
MoveDown, []Point{{20, 222}, {20, 221}, {21, 221}, {22, 221}},
},
}
r := StandardRuleset{}
for _, test := range tests {
moves := []*SnakeMove{
{Snake: b.Snakes[0], Move: test.MoveOne},
{Snake: b.Snakes[1], Move: test.MoveTwo},
moves := []SnakeMove{
{ID: "one", Move: test.MoveOne},
{ID: "two", Move: test.MoveTwo},
}
err := r.moveSnakes(b, moves)
@ -90,61 +227,61 @@ func TestMoveSnakes(t *testing.T) {
require.Equal(t, len(b.Snakes[0].Body), len(test.ExpectedOne))
for i, e := range test.ExpectedOne {
require.Equal(t, *e, *b.Snakes[0].Body[i])
require.Equal(t, e, b.Snakes[0].Body[i])
}
require.Equal(t, len(b.Snakes[1].Body), len(test.ExpectedTwo))
for i, e := range test.ExpectedTwo {
require.Equal(t, *e, *b.Snakes[1].Body[i])
require.Equal(t, e, b.Snakes[1].Body[i])
}
}
}
func TestMoveSnakesDefault(t *testing.T) {
tests := []struct {
Body []*Point
Body []Point
Move string
Expected []*Point
Expected []Point
}{
{
Body: []*Point{{0, 0}},
Body: []Point{{0, 0}},
Move: "asdf",
Expected: []*Point{{0, -1}},
Expected: []Point{{0, -1}},
},
{
Body: []*Point{{5, 5}, {5, 5}},
Body: []Point{{5, 5}, {5, 5}},
Move: "",
Expected: []*Point{{5, 4}, {5, 5}},
Expected: []Point{{5, 4}, {5, 5}},
},
{
Body: []*Point{{5, 5}, {5, 4}},
Expected: []*Point{{5, 6}, {5, 5}},
Body: []Point{{5, 5}, {5, 4}},
Expected: []Point{{5, 6}, {5, 5}},
},
{
Body: []*Point{{5, 4}, {5, 5}},
Expected: []*Point{{5, 3}, {5, 4}},
Body: []Point{{5, 4}, {5, 5}},
Expected: []Point{{5, 3}, {5, 4}},
},
{
Body: []*Point{{5, 4}, {5, 5}},
Expected: []*Point{{5, 3}, {5, 4}},
Body: []Point{{5, 4}, {5, 5}},
Expected: []Point{{5, 3}, {5, 4}},
},
{
Body: []*Point{{4, 5}, {5, 5}},
Expected: []*Point{{3, 5}, {4, 5}},
Body: []Point{{4, 5}, {5, 5}},
Expected: []Point{{3, 5}, {4, 5}},
},
{
Body: []*Point{{5, 5}, {4, 5}},
Expected: []*Point{{6, 5}, {5, 5}},
Body: []Point{{5, 5}, {4, 5}},
Expected: []Point{{6, 5}, {5, 5}},
},
}
r := StandardRuleset{}
for _, test := range tests {
b := &BoardState{
Snakes: []*Snake{
{Body: test.Body},
Snakes: []Snake{
{ID: "one", Body: test.Body},
},
}
moves := []*SnakeMove{{Snake: b.Snakes[0], Move: test.Move}}
moves := []SnakeMove{{ID: "one", Move: test.Move}}
err := r.moveSnakes(b, moves)
require.NoError(t, err)
@ -152,28 +289,27 @@ func TestMoveSnakesDefault(t *testing.T) {
require.Equal(t, len(test.Body), len(b.Snakes[0].Body))
require.Equal(t, len(test.Expected), len(b.Snakes[0].Body))
for i, e := range test.Expected {
require.Equal(t, *e, *b.Snakes[0].Body[i])
require.Equal(t, e, b.Snakes[0].Body[i])
}
}
}
func TestReduceSnakeHealth(t *testing.T) {
var err error
r := StandardRuleset{}
b := &BoardState{
Snakes: []*Snake{
&Snake{
Body: []*Point{{0, 0}, {0, 1}},
Snakes: []Snake{
{
Body: []Point{{0, 0}, {0, 1}},
Health: 99,
},
&Snake{
Body: []*Point{{5, 8}, {6, 8}, {7, 8}},
{
Body: []Point{{5, 8}, {6, 8}, {7, 8}},
Health: 2,
},
},
}
err = r.reduceSnakeHealth(b)
r := StandardRuleset{}
err := r.reduceSnakeHealth(b)
require.NoError(t, err)
require.Equal(t, b.Snakes[0].Health, int32(98))
require.Equal(t, b.Snakes[1].Health, int32(1))
@ -218,8 +354,8 @@ func TestSnakeHasStarved(t *testing.T) {
}
func TestSnakeIsOutOfBounds(t *testing.T) {
var boardWidth int32 = 10
var boardHeight int32 = 100
boardWidth := int32(10)
boardHeight := int32(100)
tests := []struct {
Point Point
@ -252,172 +388,166 @@ func TestSnakeIsOutOfBounds(t *testing.T) {
{Point{X: math.MaxInt32, Y: math.MaxInt32}, true},
}
var s *Snake
r := StandardRuleset{}
for _, test := range tests {
// Test with point as head
s = &Snake{Body: []*Point{&test.Point}}
require.Equal(t, test.Expected, r.snakeIsOutOfBounds(s, boardWidth, boardHeight), "Head%+v", test.Point)
s := Snake{Body: []Point{test.Point}}
require.Equal(t, test.Expected, r.snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Head%+v", test.Point)
// Test with point as body
s = &Snake{Body: []*Point{&Point{0, 0}, &Point{0, 0}, &test.Point}}
require.Equal(t, test.Expected, r.snakeIsOutOfBounds(s, boardWidth, boardHeight), "Body%+v", test.Point)
s = Snake{Body: []Point{Point{0, 0}, Point{0, 0}, test.Point}}
require.Equal(t, test.Expected, r.snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Body%+v", test.Point)
}
}
func TestSnakeHasBodyCollidedSelf(t *testing.T) {
tests := []struct {
Body []*Point
Body []Point
Expected bool
}{
{[]*Point{{1, 1}}, false},
{[]Point{{1, 1}}, false},
// Self stacks should self collide
// (we rely on snakes moving before we check self-collision on turn one)
{[]*Point{{2, 2}, {2, 2}}, true},
{[]*Point{{3, 3}, {3, 3}, {3, 3}}, true},
{[]*Point{{5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, true},
{[]Point{{2, 2}, {2, 2}}, true},
{[]Point{{3, 3}, {3, 3}, {3, 3}}, true},
{[]Point{{5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, true},
// Non-collision cases
{[]*Point{{0, 0}, {1, 0}, {1, 0}}, false},
{[]*Point{{0, 0}, {1, 0}, {2, 0}}, false},
{[]*Point{{0, 0}, {1, 0}, {2, 0}, {2, 0}, {2, 0}}, false},
{[]*Point{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}}, false},
{[]*Point{{0, 0}, {0, 1}, {0, 2}}, false},
{[]*Point{{0, 0}, {0, 1}, {0, 2}, {0, 2}, {0, 2}}, false},
{[]*Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}}, false},
{[]Point{{0, 0}, {1, 0}, {1, 0}}, false},
{[]Point{{0, 0}, {1, 0}, {2, 0}}, false},
{[]Point{{0, 0}, {1, 0}, {2, 0}, {2, 0}, {2, 0}}, false},
{[]Point{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}}, false},
{[]Point{{0, 0}, {0, 1}, {0, 2}}, false},
{[]Point{{0, 0}, {0, 1}, {0, 2}, {0, 2}, {0, 2}}, false},
{[]Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}}, false},
// Collision cases
{[]*Point{{0, 0}, {1, 0}, {0, 0}}, true},
{[]*Point{{0, 0}, {0, 0}, {1, 0}}, true},
{[]*Point{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}, true},
{[]*Point{{4, 4}, {3, 4}, {3, 3}, {4, 4}, {4, 4}}, true},
{[]*Point{{3, 3}, {3, 4}, {3, 3}, {4, 4}, {4, 5}}, true},
{[]Point{{0, 0}, {1, 0}, {0, 0}}, true},
{[]Point{{0, 0}, {0, 0}, {1, 0}}, true},
{[]Point{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}, true},
{[]Point{{4, 4}, {3, 4}, {3, 3}, {4, 4}, {4, 4}}, true},
{[]Point{{3, 3}, {3, 4}, {3, 3}, {4, 4}, {4, 5}}, true},
}
var s *Snake
r := StandardRuleset{}
for _, test := range tests {
s = &Snake{Body: test.Body}
require.Equal(t, test.Expected, r.snakeHasBodyCollided(s, s), "Body%q", s.Body)
s := Snake{Body: test.Body}
require.Equal(t, test.Expected, r.snakeHasBodyCollided(&s, &s), "Body%q", s.Body)
}
}
func TestSnakeHasBodyCollidedOther(t *testing.T) {
tests := []struct {
SnakeBody []*Point
OtherBody []*Point
SnakeBody []Point
OtherBody []Point
Expected bool
}{
{
// Just heads
[]*Point{{0, 0}},
[]*Point{{1, 1}},
[]Point{{0, 0}},
[]Point{{1, 1}},
false,
},
{
// Head-to-heads are not considered in body collisions
[]*Point{{0, 0}},
[]*Point{{0, 0}},
[]Point{{0, 0}},
[]Point{{0, 0}},
false,
},
{
// Stacked bodies
[]*Point{{0, 0}},
[]*Point{{0, 0}, {0, 0}},
[]Point{{0, 0}},
[]Point{{0, 0}, {0, 0}},
true,
},
{
// Separate stacked bodies
[]*Point{{0, 0}, {0, 0}, {0, 0}},
[]*Point{{1, 1}, {1, 1}, {1, 1}},
[]Point{{0, 0}, {0, 0}, {0, 0}},
[]Point{{1, 1}, {1, 1}, {1, 1}},
false,
},
{
// Stacked bodies, separated heads
[]*Point{{0, 0}, {1, 0}, {1, 0}},
[]*Point{{2, 0}, {1, 0}, {1, 0}},
[]Point{{0, 0}, {1, 0}, {1, 0}},
[]Point{{2, 0}, {1, 0}, {1, 0}},
false,
},
{
// Mid-snake collision
[]*Point{{1, 1}},
[]*Point{{0, 1}, {1, 1}, {2, 1}},
[]Point{{1, 1}},
[]Point{{0, 1}, {1, 1}, {2, 1}},
true,
},
}
var s *Snake
var o *Snake
r := StandardRuleset{}
for _, test := range tests {
s = &Snake{Body: test.SnakeBody}
o = &Snake{Body: test.OtherBody}
s := &Snake{Body: test.SnakeBody}
o := &Snake{Body: test.OtherBody}
require.Equal(t, test.Expected, r.snakeHasBodyCollided(s, o), "Snake%q Other%q", s.Body, o.Body)
}
}
func TestSnakeHasLostHeadToHead(t *testing.T) {
tests := []struct {
SnakeBody []*Point
OtherBody []*Point
SnakeBody []Point
OtherBody []Point
Expected bool
ExpectedOpposite bool
}{
{
// Just heads
[]*Point{{0, 0}},
[]*Point{{1, 1}},
[]Point{{0, 0}},
[]Point{{1, 1}},
false, false,
},
{
// Just heads colliding
[]*Point{{0, 0}},
[]*Point{{0, 0}},
[]Point{{0, 0}},
[]Point{{0, 0}},
true, true,
},
{
// One snake larger
[]*Point{{0, 0}, {1, 0}, {2, 0}},
[]*Point{{0, 0}},
[]Point{{0, 0}, {1, 0}, {2, 0}},
[]Point{{0, 0}},
false, true,
},
{
// Other snake equal
[]*Point{{0, 0}, {1, 0}, {2, 0}},
[]*Point{{0, 0}, {0, 1}, {0, 2}},
[]Point{{0, 0}, {1, 0}, {2, 0}},
[]Point{{0, 0}, {0, 1}, {0, 2}},
true, true,
},
{
// Other snake longer
[]*Point{{0, 0}, {1, 0}, {2, 0}},
[]*Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
[]Point{{0, 0}, {1, 0}, {2, 0}},
[]Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
true, false,
},
{
// Body collision
[]*Point{{0, 1}, {1, 1}, {2, 1}},
[]*Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
[]Point{{0, 1}, {1, 1}, {2, 1}},
[]Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
false, false,
},
{
// Separate stacked bodies, head collision
[]*Point{{3, 10}, {2, 10}, {2, 10}},
[]*Point{{3, 10}, {4, 10}, {4, 10}},
[]Point{{3, 10}, {2, 10}, {2, 10}},
[]Point{{3, 10}, {4, 10}, {4, 10}},
true, true,
},
{
// Separate stacked bodies, head collision
[]*Point{{10, 3}, {10, 2}, {10, 1}, {10, 0}},
[]*Point{{10, 3}, {10, 4}, {10, 5}},
[]Point{{10, 3}, {10, 2}, {10, 1}, {10, 0}},
[]Point{{10, 3}, {10, 4}, {10, 5}},
false, true,
},
}
var s *Snake
var o *Snake
r := StandardRuleset{}
for _, test := range tests {
s = &Snake{Body: test.SnakeBody}
o = &Snake{Body: test.OtherBody}
require.Equal(t, test.Expected, r.snakeHasLostHeadToHead(s, o), "Snake%q Other%q", s.Body, o.Body)
require.Equal(t, test.ExpectedOpposite, r.snakeHasLostHeadToHead(o, s), "Snake%q Other%q", s.Body, o.Body)
s := Snake{Body: test.SnakeBody}
o := Snake{Body: test.OtherBody}
require.Equal(t, test.Expected, r.snakeHasLostHeadToHead(&s, &o), "Snake%q Other%q", s.Body, o.Body)
require.Equal(t, test.ExpectedOpposite, r.snakeHasLostHeadToHead(&o, &s), "Snake%q Other%q", s.Body, o.Body)
}
}
@ -425,14 +555,10 @@ func TestSnakeHasLostHeadToHead(t *testing.T) {
func TestFeedSnakes(t *testing.T) {
r := StandardRuleset{}
b := &BoardState{
Snakes: []*Snake{
{Body: []*Point{
{2, 1}, {1, 1}, {1, 2}, {2, 2},
}},
},
Food: []*Point{
{2, 1},
Snakes: []Snake{
{Body: []Point{{2, 1}, {1, 1}, {1, 2}, {2, 2}}},
},
Food: []Point{{2, 1}},
}
err := r.feedSnakes(b)
@ -444,77 +570,77 @@ func TestFeedSnakes(t *testing.T) {
func TestGetUnoccupiedPoints(t *testing.T) {
tests := []struct {
Board *BoardState
Expected []*Point
Expected []Point
}{
{
&BoardState{
Height: 1,
Width: 1,
},
[]*Point{{0, 0}},
[]Point{{0, 0}},
},
{
&BoardState{
Height: 1,
Width: 2,
},
[]*Point{{0, 0}, {1, 0}},
[]Point{{0, 0}, {1, 0}},
},
{
&BoardState{
Height: 1,
Width: 1,
Food: []*Point{{0, 0}, {101, 202}, {-4, -5}},
Food: []Point{{0, 0}, {101, 202}, {-4, -5}},
},
[]*Point{},
[]Point{},
},
{
&BoardState{
Height: 2,
Width: 2,
Food: []*Point{{0, 0}, {1, 0}},
Food: []Point{{0, 0}, {1, 0}},
},
[]*Point{{0, 1}, {1, 1}},
[]Point{{0, 1}, {1, 1}},
},
{
&BoardState{
Height: 2,
Width: 2,
Food: []*Point{{0, 0}, {0, 1}, {1, 0}, {1, 1}},
Food: []Point{{0, 0}, {0, 1}, {1, 0}, {1, 1}},
},
[]*Point{},
[]Point{},
},
{
&BoardState{
Height: 4,
Width: 1,
Snakes: []*Snake{
{Body: []*Point{{0, 0}}},
Snakes: []Snake{
{Body: []Point{{0, 0}}},
},
},
[]*Point{{0, 1}, {0, 2}, {0, 3}},
[]Point{{0, 1}, {0, 2}, {0, 3}},
},
{
&BoardState{
Height: 2,
Width: 3,
Snakes: []*Snake{
{Body: []*Point{{0, 0}, {1, 0}, {1, 1}}},
Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
},
},
[]*Point{{0, 1}, {2, 0}, {2, 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}}},
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}},
[]Point{{2, 1}},
},
}
@ -523,7 +649,7 @@ func TestGetUnoccupiedPoints(t *testing.T) {
unoccupiedPoints := r.getUnoccupiedPoints(test.Board)
require.Equal(t, len(test.Expected), len(unoccupiedPoints))
for i, e := range test.Expected {
require.Equal(t, *e, *unoccupiedPoints[i])
require.Equal(t, e, unoccupiedPoints[i])
}
}
}
@ -531,15 +657,15 @@ func TestGetUnoccupiedPoints(t *testing.T) {
func TestMaybeSpawnFood(t *testing.T) {
tests := []struct {
Seed int64
ExpectedFood []*Point
ExpectedFood []Point
}{
// Use pre-tested seeds and results
{123, []*Point{}},
{456, []*Point{}},
{789, []*Point{}},
{1024, []*Point{{2, 1}}},
{511, []*Point{{2, 0}}},
{165, []*Point{{3, 1}}},
{123, []Point{}},
{456, []Point{}},
{789, []Point{}},
{1024, []Point{{2, 1}}},
{511, []Point{{2, 0}}},
{165, []Point{{3, 1}}},
}
r := StandardRuleset{}
@ -547,9 +673,9 @@ func TestMaybeSpawnFood(t *testing.T) {
b := &BoardState{
Height: 4,
Width: 5,
Snakes: []*Snake{
{Body: []*Point{{1, 0}, {1, 1}}},
{Body: []*Point{{0, 1}, {0, 2}, {0, 3}}},
Snakes: []Snake{
{Body: []Point{{1, 0}, {1, 1}}},
{Body: []Point{{0, 1}, {0, 2}, {0, 3}}},
},
}
@ -558,7 +684,7 @@ func TestMaybeSpawnFood(t *testing.T) {
require.NoError(t, err)
require.Equal(t, len(test.ExpectedFood), len(b.Food), "Seed %d", test.Seed)
for i, e := range test.ExpectedFood {
require.Equal(t, *e, *b.Food[i], "Seed %d", test.Seed)
require.Equal(t, e, b.Food[i], "Seed %d", test.Seed)
}
}
}