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

View file

@ -1,6 +1,7 @@
package rulesets package rulesets
import ( import (
"errors"
"math" "math"
"math/rand" "math/rand"
"testing" "testing"
@ -10,26 +11,172 @@ import (
func TestSanity(t *testing.T) { func TestSanity(t *testing.T) {
r := StandardRuleset{} 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( next, err := r.ResolveMoves(
&BoardState{}, &BoardState{},
[]*SnakeMove{}, []SnakeMove{},
) )
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, next) 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) { func TestMoveSnakes(t *testing.T) {
b := &BoardState{ b := &BoardState{
Snakes: []*Snake{ Snakes: []Snake{
{ {
ID: "one", ID: "one",
Body: []*Point{{10, 110}, {11, 110}}, Body: []Point{{10, 110}, {11, 110}},
Health: 111111, Health: 111111,
}, },
{ {
ID: "two", ID: "two",
Body: []*Point{{23, 220}, {22, 220}, {21, 220}, {20, 220}}, Body: []Point{{23, 220}, {22, 220}, {21, 220}, {20, 220}},
Health: 222222, Health: 222222,
}, },
}, },
@ -37,47 +184,37 @@ func TestMoveSnakes(t *testing.T) {
tests := []struct { tests := []struct {
MoveOne string MoveOne string
ExpectedOne []*Point ExpectedOne []Point
MoveTwo string MoveTwo string
ExpectedTwo []*Point ExpectedTwo []Point
}{ }{
{ {
MOVE_UP, MoveUp, []Point{{10, 109}, {10, 110}},
[]*Point{{10, 109}, {10, 110}}, MoveDown, []Point{{23, 221}, {23, 220}, {22, 220}, {21, 220}},
MOVE_DOWN,
[]*Point{{23, 221}, {23, 220}, {22, 220}, {21, 220}},
}, },
{ {
MOVE_RIGHT, MoveRight, []Point{{11, 109}, {10, 109}},
[]*Point{{11, 109}, {10, 109}}, MoveLeft, []Point{{22, 221}, {23, 221}, {23, 220}, {22, 220}},
MOVE_LEFT,
[]*Point{{22, 221}, {23, 221}, {23, 220}, {22, 220}},
}, },
{ {
MOVE_RIGHT, MoveRight, []Point{{12, 109}, {11, 109}},
[]*Point{{12, 109}, {11, 109}}, MoveLeft, []Point{{21, 221}, {22, 221}, {23, 221}, {23, 220}},
MOVE_LEFT,
[]*Point{{21, 221}, {22, 221}, {23, 221}, {23, 220}},
}, },
{ {
MOVE_RIGHT, MoveRight, []Point{{13, 109}, {12, 109}},
[]*Point{{13, 109}, {12, 109}}, MoveLeft, []Point{{20, 221}, {21, 221}, {22, 221}, {23, 221}},
MOVE_LEFT,
[]*Point{{20, 221}, {21, 221}, {22, 221}, {23, 221}},
}, },
{ {
MOVE_UP, MoveUp, []Point{{13, 108}, {13, 109}},
[]*Point{{13, 108}, {13, 109}}, MoveDown, []Point{{20, 222}, {20, 221}, {21, 221}, {22, 221}},
MOVE_DOWN,
[]*Point{{20, 222}, {20, 221}, {21, 221}, {22, 221}},
}, },
} }
r := StandardRuleset{} r := StandardRuleset{}
for _, test := range tests { for _, test := range tests {
moves := []*SnakeMove{ moves := []SnakeMove{
{Snake: b.Snakes[0], Move: test.MoveOne}, {ID: "one", Move: test.MoveOne},
{Snake: b.Snakes[1], Move: test.MoveTwo}, {ID: "two", Move: test.MoveTwo},
} }
err := r.moveSnakes(b, moves) 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)) require.Equal(t, len(b.Snakes[0].Body), len(test.ExpectedOne))
for i, e := range 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)) require.Equal(t, len(b.Snakes[1].Body), len(test.ExpectedTwo))
for i, e := range 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) { func TestMoveSnakesDefault(t *testing.T) {
tests := []struct { tests := []struct {
Body []*Point Body []Point
Move string Move string
Expected []*Point Expected []Point
}{ }{
{ {
Body: []*Point{{0, 0}}, Body: []Point{{0, 0}},
Move: "asdf", Move: "asdf",
Expected: []*Point{{0, -1}}, Expected: []Point{{0, -1}},
}, },
{ {
Body: []*Point{{5, 5}, {5, 5}}, Body: []Point{{5, 5}, {5, 5}},
Move: "", Move: "",
Expected: []*Point{{5, 4}, {5, 5}}, Expected: []Point{{5, 4}, {5, 5}},
}, },
{ {
Body: []*Point{{5, 5}, {5, 4}}, Body: []Point{{5, 5}, {5, 4}},
Expected: []*Point{{5, 6}, {5, 5}}, Expected: []Point{{5, 6}, {5, 5}},
}, },
{ {
Body: []*Point{{5, 4}, {5, 5}}, Body: []Point{{5, 4}, {5, 5}},
Expected: []*Point{{5, 3}, {5, 4}}, Expected: []Point{{5, 3}, {5, 4}},
}, },
{ {
Body: []*Point{{5, 4}, {5, 5}}, Body: []Point{{5, 4}, {5, 5}},
Expected: []*Point{{5, 3}, {5, 4}}, Expected: []Point{{5, 3}, {5, 4}},
}, },
{ {
Body: []*Point{{4, 5}, {5, 5}}, Body: []Point{{4, 5}, {5, 5}},
Expected: []*Point{{3, 5}, {4, 5}}, Expected: []Point{{3, 5}, {4, 5}},
}, },
{ {
Body: []*Point{{5, 5}, {4, 5}}, Body: []Point{{5, 5}, {4, 5}},
Expected: []*Point{{6, 5}, {5, 5}}, Expected: []Point{{6, 5}, {5, 5}},
}, },
} }
r := StandardRuleset{} r := StandardRuleset{}
for _, test := range tests { for _, test := range tests {
b := &BoardState{ b := &BoardState{
Snakes: []*Snake{ Snakes: []Snake{
{Body: test.Body}, {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) err := r.moveSnakes(b, moves)
require.NoError(t, err) 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.Body), len(b.Snakes[0].Body))
require.Equal(t, len(test.Expected), len(b.Snakes[0].Body)) require.Equal(t, len(test.Expected), len(b.Snakes[0].Body))
for i, e := range test.Expected { 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) { func TestReduceSnakeHealth(t *testing.T) {
var err error
r := StandardRuleset{}
b := &BoardState{ b := &BoardState{
Snakes: []*Snake{ Snakes: []Snake{
&Snake{ {
Body: []*Point{{0, 0}, {0, 1}}, Body: []Point{{0, 0}, {0, 1}},
Health: 99, Health: 99,
}, },
&Snake{ {
Body: []*Point{{5, 8}, {6, 8}, {7, 8}}, Body: []Point{{5, 8}, {6, 8}, {7, 8}},
Health: 2, Health: 2,
}, },
}, },
} }
err = r.reduceSnakeHealth(b) r := StandardRuleset{}
err := r.reduceSnakeHealth(b)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, b.Snakes[0].Health, int32(98)) require.Equal(t, b.Snakes[0].Health, int32(98))
require.Equal(t, b.Snakes[1].Health, int32(1)) require.Equal(t, b.Snakes[1].Health, int32(1))
@ -218,8 +354,8 @@ func TestSnakeHasStarved(t *testing.T) {
} }
func TestSnakeIsOutOfBounds(t *testing.T) { func TestSnakeIsOutOfBounds(t *testing.T) {
var boardWidth int32 = 10 boardWidth := int32(10)
var boardHeight int32 = 100 boardHeight := int32(100)
tests := []struct { tests := []struct {
Point Point Point Point
@ -252,172 +388,166 @@ func TestSnakeIsOutOfBounds(t *testing.T) {
{Point{X: math.MaxInt32, Y: math.MaxInt32}, true}, {Point{X: math.MaxInt32, Y: math.MaxInt32}, true},
} }
var s *Snake
r := StandardRuleset{} r := StandardRuleset{}
for _, test := range tests { for _, test := range tests {
// Test with point as head // Test with point as head
s = &Snake{Body: []*Point{&test.Point}} s := Snake{Body: []Point{test.Point}}
require.Equal(t, test.Expected, r.snakeIsOutOfBounds(s, boardWidth, boardHeight), "Head%+v", test.Point) require.Equal(t, test.Expected, r.snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Head%+v", test.Point)
// Test with point as body // Test with point as body
s = &Snake{Body: []*Point{&Point{0, 0}, &Point{0, 0}, &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) require.Equal(t, test.Expected, r.snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Body%+v", test.Point)
} }
} }
func TestSnakeHasBodyCollidedSelf(t *testing.T) { func TestSnakeHasBodyCollidedSelf(t *testing.T) {
tests := []struct { tests := []struct {
Body []*Point Body []Point
Expected bool Expected bool
}{ }{
{[]*Point{{1, 1}}, false}, {[]Point{{1, 1}}, false},
// Self stacks should self collide // Self stacks should self collide
// (we rely on snakes moving before we check self-collision on turn one) // (we rely on snakes moving before we check self-collision on turn one)
{[]*Point{{2, 2}, {2, 2}}, true}, {[]Point{{2, 2}, {2, 2}}, true},
{[]*Point{{3, 3}, {3, 3}, {3, 3}}, true}, {[]Point{{3, 3}, {3, 3}, {3, 3}}, true},
{[]*Point{{5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, true}, {[]Point{{5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, true},
// Non-collision cases // Non-collision cases
{[]*Point{{0, 0}, {1, 0}, {1, 0}}, false}, {[]Point{{0, 0}, {1, 0}, {1, 0}}, false},
{[]*Point{{0, 0}, {1, 0}, {2, 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}, {2, 0}, {2, 0}}, false},
{[]*Point{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 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}}, false},
{[]*Point{{0, 0}, {0, 1}, {0, 2}, {0, 2}, {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}, {0, 1}, {0, 2}, {0, 3}, {0, 4}}, false},
// Collision cases // Collision cases
{[]*Point{{0, 0}, {1, 0}, {0, 0}}, true}, {[]Point{{0, 0}, {1, 0}, {0, 0}}, true},
{[]*Point{{0, 0}, {0, 0}, {1, 0}}, true}, {[]Point{{0, 0}, {0, 0}, {1, 0}}, true},
{[]*Point{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 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{{4, 4}, {3, 4}, {3, 3}, {4, 4}, {4, 4}}, true},
{[]*Point{{3, 3}, {3, 4}, {3, 3}, {4, 4}, {4, 5}}, true}, {[]Point{{3, 3}, {3, 4}, {3, 3}, {4, 4}, {4, 5}}, true},
} }
var s *Snake
r := StandardRuleset{} r := StandardRuleset{}
for _, test := range tests { for _, test := range tests {
s = &Snake{Body: test.Body} s := Snake{Body: test.Body}
require.Equal(t, test.Expected, r.snakeHasBodyCollided(s, s), "Body%q", s.Body) require.Equal(t, test.Expected, r.snakeHasBodyCollided(&s, &s), "Body%q", s.Body)
} }
} }
func TestSnakeHasBodyCollidedOther(t *testing.T) { func TestSnakeHasBodyCollidedOther(t *testing.T) {
tests := []struct { tests := []struct {
SnakeBody []*Point SnakeBody []Point
OtherBody []*Point OtherBody []Point
Expected bool Expected bool
}{ }{
{ {
// Just heads // Just heads
[]*Point{{0, 0}}, []Point{{0, 0}},
[]*Point{{1, 1}}, []Point{{1, 1}},
false, false,
}, },
{ {
// Head-to-heads are not considered in body collisions // Head-to-heads are not considered in body collisions
[]*Point{{0, 0}}, []Point{{0, 0}},
[]*Point{{0, 0}}, []Point{{0, 0}},
false, false,
}, },
{ {
// Stacked bodies // Stacked bodies
[]*Point{{0, 0}}, []Point{{0, 0}},
[]*Point{{0, 0}, {0, 0}}, []Point{{0, 0}, {0, 0}},
true, true,
}, },
{ {
// Separate stacked bodies // Separate stacked bodies
[]*Point{{0, 0}, {0, 0}, {0, 0}}, []Point{{0, 0}, {0, 0}, {0, 0}},
[]*Point{{1, 1}, {1, 1}, {1, 1}}, []Point{{1, 1}, {1, 1}, {1, 1}},
false, false,
}, },
{ {
// Stacked bodies, separated heads // Stacked bodies, separated heads
[]*Point{{0, 0}, {1, 0}, {1, 0}}, []Point{{0, 0}, {1, 0}, {1, 0}},
[]*Point{{2, 0}, {1, 0}, {1, 0}}, []Point{{2, 0}, {1, 0}, {1, 0}},
false, false,
}, },
{ {
// Mid-snake collision // Mid-snake collision
[]*Point{{1, 1}}, []Point{{1, 1}},
[]*Point{{0, 1}, {1, 1}, {2, 1}}, []Point{{0, 1}, {1, 1}, {2, 1}},
true, true,
}, },
} }
var s *Snake
var o *Snake
r := StandardRuleset{} r := StandardRuleset{}
for _, test := range tests { for _, test := range tests {
s = &Snake{Body: test.SnakeBody} s := &Snake{Body: test.SnakeBody}
o = &Snake{Body: test.OtherBody} o := &Snake{Body: test.OtherBody}
require.Equal(t, test.Expected, r.snakeHasBodyCollided(s, o), "Snake%q Other%q", s.Body, o.Body) require.Equal(t, test.Expected, r.snakeHasBodyCollided(s, o), "Snake%q Other%q", s.Body, o.Body)
} }
} }
func TestSnakeHasLostHeadToHead(t *testing.T) { func TestSnakeHasLostHeadToHead(t *testing.T) {
tests := []struct { tests := []struct {
SnakeBody []*Point SnakeBody []Point
OtherBody []*Point OtherBody []Point
Expected bool Expected bool
ExpectedOpposite bool ExpectedOpposite bool
}{ }{
{ {
// Just heads // Just heads
[]*Point{{0, 0}}, []Point{{0, 0}},
[]*Point{{1, 1}}, []Point{{1, 1}},
false, false, false, false,
}, },
{ {
// Just heads colliding // Just heads colliding
[]*Point{{0, 0}}, []Point{{0, 0}},
[]*Point{{0, 0}}, []Point{{0, 0}},
true, true, true, true,
}, },
{ {
// One snake larger // One snake larger
[]*Point{{0, 0}, {1, 0}, {2, 0}}, []Point{{0, 0}, {1, 0}, {2, 0}},
[]*Point{{0, 0}}, []Point{{0, 0}},
false, true, false, true,
}, },
{ {
// Other snake equal // Other snake equal
[]*Point{{0, 0}, {1, 0}, {2, 0}}, []Point{{0, 0}, {1, 0}, {2, 0}},
[]*Point{{0, 0}, {0, 1}, {0, 2}}, []Point{{0, 0}, {0, 1}, {0, 2}},
true, true, true, true,
}, },
{ {
// Other snake longer // Other snake longer
[]*Point{{0, 0}, {1, 0}, {2, 0}}, []Point{{0, 0}, {1, 0}, {2, 0}},
[]*Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}}, []Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
true, false, true, false,
}, },
{ {
// Body collision // Body collision
[]*Point{{0, 1}, {1, 1}, {2, 1}}, []Point{{0, 1}, {1, 1}, {2, 1}},
[]*Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}}, []Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}},
false, false, false, false,
}, },
{ {
// Separate stacked bodies, head collision // Separate stacked bodies, head collision
[]*Point{{3, 10}, {2, 10}, {2, 10}}, []Point{{3, 10}, {2, 10}, {2, 10}},
[]*Point{{3, 10}, {4, 10}, {4, 10}}, []Point{{3, 10}, {4, 10}, {4, 10}},
true, true, true, true,
}, },
{ {
// Separate stacked bodies, head collision // Separate stacked bodies, head collision
[]*Point{{10, 3}, {10, 2}, {10, 1}, {10, 0}}, []Point{{10, 3}, {10, 2}, {10, 1}, {10, 0}},
[]*Point{{10, 3}, {10, 4}, {10, 5}}, []Point{{10, 3}, {10, 4}, {10, 5}},
false, true, false, true,
}, },
} }
var s *Snake
var o *Snake
r := StandardRuleset{} r := StandardRuleset{}
for _, test := range tests { for _, test := range tests {
s = &Snake{Body: test.SnakeBody} s := Snake{Body: test.SnakeBody}
o = &Snake{Body: test.OtherBody} 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.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) 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) { func TestFeedSnakes(t *testing.T) {
r := StandardRuleset{} r := StandardRuleset{}
b := &BoardState{ b := &BoardState{
Snakes: []*Snake{ Snakes: []Snake{
{Body: []*Point{ {Body: []Point{{2, 1}, {1, 1}, {1, 2}, {2, 2}}},
{2, 1}, {1, 1}, {1, 2}, {2, 2},
}},
},
Food: []*Point{
{2, 1},
}, },
Food: []Point{{2, 1}},
} }
err := r.feedSnakes(b) err := r.feedSnakes(b)
@ -444,77 +570,77 @@ func TestFeedSnakes(t *testing.T) {
func TestGetUnoccupiedPoints(t *testing.T) { func TestGetUnoccupiedPoints(t *testing.T) {
tests := []struct { tests := []struct {
Board *BoardState Board *BoardState
Expected []*Point Expected []Point
}{ }{
{ {
&BoardState{ &BoardState{
Height: 1, Height: 1,
Width: 1, Width: 1,
}, },
[]*Point{{0, 0}}, []Point{{0, 0}},
}, },
{ {
&BoardState{ &BoardState{
Height: 1, Height: 1,
Width: 2, Width: 2,
}, },
[]*Point{{0, 0}, {1, 0}}, []Point{{0, 0}, {1, 0}},
}, },
{ {
&BoardState{ &BoardState{
Height: 1, Height: 1,
Width: 1, Width: 1,
Food: []*Point{{0, 0}, {101, 202}, {-4, -5}}, Food: []Point{{0, 0}, {101, 202}, {-4, -5}},
}, },
[]*Point{}, []Point{},
}, },
{ {
&BoardState{ &BoardState{
Height: 2, Height: 2,
Width: 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{ &BoardState{
Height: 2, Height: 2,
Width: 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{ &BoardState{
Height: 4, Height: 4,
Width: 1, Width: 1,
Snakes: []*Snake{ Snakes: []Snake{
{Body: []*Point{{0, 0}}}, {Body: []Point{{0, 0}}},
}, },
}, },
[]*Point{{0, 1}, {0, 2}, {0, 3}}, []Point{{0, 1}, {0, 2}, {0, 3}},
}, },
{ {
&BoardState{ &BoardState{
Height: 2, Height: 2,
Width: 3, Width: 3,
Snakes: []*Snake{ Snakes: []Snake{
{Body: []*Point{{0, 0}, {1, 0}, {1, 1}}}, {Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
}, },
}, },
[]*Point{{0, 1}, {2, 0}, {2, 1}}, []Point{{0, 1}, {2, 0}, {2, 1}},
}, },
{ {
&BoardState{ &BoardState{
Height: 2, Height: 2,
Width: 3, Width: 3,
Food: []*Point{{0, 0}, {1, 0}, {1, 1}, {2, 0}}, Food: []Point{{0, 0}, {1, 0}, {1, 1}, {2, 0}},
Snakes: []*Snake{ Snakes: []Snake{
{Body: []*Point{{0, 0}, {1, 0}, {1, 1}}}, {Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
{Body: []*Point{{0, 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) unoccupiedPoints := r.getUnoccupiedPoints(test.Board)
require.Equal(t, len(test.Expected), len(unoccupiedPoints)) require.Equal(t, len(test.Expected), len(unoccupiedPoints))
for i, e := range test.Expected { 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) { func TestMaybeSpawnFood(t *testing.T) {
tests := []struct { tests := []struct {
Seed int64 Seed int64
ExpectedFood []*Point ExpectedFood []Point
}{ }{
// Use pre-tested seeds and results // Use pre-tested seeds and results
{123, []*Point{}}, {123, []Point{}},
{456, []*Point{}}, {456, []Point{}},
{789, []*Point{}}, {789, []Point{}},
{1024, []*Point{{2, 1}}}, {1024, []Point{{2, 1}}},
{511, []*Point{{2, 0}}}, {511, []Point{{2, 0}}},
{165, []*Point{{3, 1}}}, {165, []Point{{3, 1}}},
} }
r := StandardRuleset{} r := StandardRuleset{}
@ -547,9 +673,9 @@ func TestMaybeSpawnFood(t *testing.T) {
b := &BoardState{ b := &BoardState{
Height: 4, Height: 4,
Width: 5, Width: 5,
Snakes: []*Snake{ Snakes: []Snake{
{Body: []*Point{{1, 0}, {1, 1}}}, {Body: []Point{{1, 0}, {1, 1}}},
{Body: []*Point{{0, 1}, {0, 2}, {0, 3}}}, {Body: []Point{{0, 1}, {0, 2}, {0, 3}}},
}, },
} }
@ -558,7 +684,7 @@ func TestMaybeSpawnFood(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, len(test.ExpectedFood), len(b.Food), "Seed %d", test.Seed) require.Equal(t, len(test.ExpectedFood), len(b.Food), "Seed %d", test.Seed)
for i, e := range test.ExpectedFood { 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)
} }
} }
} }