DEV-1479 ensure snake elimination turn is set (#93)

* ensure snake elimination turn is set

- centralise elimination update logic to a single place to ensure consistency

* doc the method

* testing
This commit is contained in:
Torben 2022-07-21 14:26:56 -07:00 committed by GitHub
parent 663c377cc4
commit e1289af5fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 143 additions and 73 deletions

View file

@ -57,6 +57,7 @@ func (prevState *BoardState) Clone() *BoardState {
nextState.Snakes[i].Health = prevState.Snakes[i].Health nextState.Snakes[i].Health = prevState.Snakes[i].Health
nextState.Snakes[i].Body = append([]Point{}, prevState.Snakes[i].Body...) nextState.Snakes[i].Body = append([]Point{}, prevState.Snakes[i].Body...)
nextState.Snakes[i].EliminatedCause = prevState.Snakes[i].EliminatedCause nextState.Snakes[i].EliminatedCause = prevState.Snakes[i].EliminatedCause
nextState.Snakes[i].EliminatedOnTurn = prevState.Snakes[i].EliminatedOnTurn
nextState.Snakes[i].EliminatedBy = prevState.Snakes[i].EliminatedBy nextState.Snakes[i].EliminatedBy = prevState.Snakes[i].EliminatedBy
} }
return nextState return nextState
@ -551,3 +552,13 @@ func getDistanceBetweenPoints(a, b Point) int {
func isSquareBoard(b *BoardState) bool { func isSquareBoard(b *BoardState) bool {
return b.Width == b.Height return b.Width == b.Height
} }
// EliminateSnake updates a snake's state to reflect that it was eliminated.
// - "cause" identifies what type of event caused the snake to be eliminated
// - "by" identifies which snake (if any, use empty string "" if none) eliminated the snake.
// - "turn" is the turn number that this snake was eliminated on.
func EliminateSnake(s *Snake, cause, by string, turn int) {
s.EliminatedCause = cause
s.EliminatedBy = by
s.EliminatedOnTurn = turn
}

View file

@ -881,3 +881,11 @@ func TestPlaceFoodRandomly(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, len(b.Food), 0) require.Equal(t, len(b.Food), 0)
} }
func TestEliminateSnake(t *testing.T) {
s := &Snake{}
EliminateSnake(s, "test-cause", "", 2)
require.Equal(t, "test-cause", s.EliminatedCause)
require.Equal(t, "", s.EliminatedBy)
require.Equal(t, 2, s.EliminatedOnTurn)
}

View file

@ -15,6 +15,7 @@ func TestConstrictorRulesetInterface(t *testing.T) {
var constrictorMoveAndCollideMAD = gameTestCase{ var constrictorMoveAndCollideMAD = gameTestCase{
"Constrictor Case Move and Collide", "Constrictor Case Move and Collide",
&BoardState{ &BoardState{
Turn: 41,
Width: 10, Width: 10,
Height: 10, Height: 10,
Snakes: []Snake{ Snakes: []Snake{
@ -42,18 +43,20 @@ var constrictorMoveAndCollideMAD = gameTestCase{
Height: 10, Height: 10,
Snakes: []Snake{ Snakes: []Snake{
{ {
ID: "one", ID: "one",
Body: []Point{{1, 2}, {1, 1}, {1, 1}}, Body: []Point{{1, 2}, {1, 1}, {1, 1}},
Health: 100, Health: 100,
EliminatedCause: EliminatedByCollision, EliminatedCause: EliminatedByCollision,
EliminatedBy: "two", EliminatedBy: "two",
EliminatedOnTurn: 42,
}, },
{ {
ID: "two", ID: "two",
Body: []Point{{1, 1}, {1, 2}, {1, 2}}, Body: []Point{{1, 1}, {1, 2}, {1, 2}},
Health: 100, Health: 100,
EliminatedCause: EliminatedByCollision, EliminatedCause: EliminatedByCollision,
EliminatedBy: "one", EliminatedBy: "one",
EliminatedOnTurn: 42,
}, },
}, },
Food: []Point{}, Food: []Point{},

View file

@ -116,3 +116,34 @@ func TestSoloCreateNextBoardState(t *testing.T) {
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeSolo, NewPipeline(soloRulesetStages...))) gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeSolo, NewPipeline(soloRulesetStages...)))
} }
} }
// Test a snake running right into the wall is properly eliminated
func TestSoloEliminationOutOfBounds(t *testing.T) {
r := SoloRuleset{}
// Using MaxRand is important because it ensures that the snakes are consistently placed in a way this test will work.
// Actually random placement could result in the assumptions made by this test being incorrect.
initialState, err := CreateDefaultBoardState(MaxRand, 2, 2, []string{"one"})
require.NoError(t, err)
_, next, err := r.Execute(
initialState,
r.Settings(),
[]SnakeMove{{ID: "one", Move: "right"}},
)
require.NoError(t, err)
require.NotNil(t, initialState)
ended, next, err := r.Execute(
next,
r.Settings(),
[]SnakeMove{{ID: "one", Move: "right"}},
)
require.NoError(t, err)
require.NotNil(t, initialState)
require.True(t, ended)
require.Equal(t, EliminatedByOutOfBounds, next.Snakes[0].EliminatedCause)
require.Equal(t, "", next.Snakes[0].EliminatedBy)
require.Equal(t, 1, next.Snakes[0].EliminatedOnTurn)
}

View file

@ -184,7 +184,7 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove)
snake.Health = SnakeMaxHealth snake.Health = SnakeMaxHealth
} }
if snakeIsOutOfHealth(snake) { if snakeIsOutOfHealth(snake) {
snake.EliminatedCause = EliminatedByOutOfHealth EliminateSnake(snake, EliminatedByOutOfHealth, "", b.Turn+1)
} }
} }
} }
@ -221,12 +221,12 @@ func EliminateSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove
} }
if snakeIsOutOfHealth(snake) { if snakeIsOutOfHealth(snake) {
snake.EliminatedCause = EliminatedByOutOfHealth EliminateSnake(snake, EliminatedByOutOfHealth, "", b.Turn+1)
continue continue
} }
if snakeIsOutOfBounds(snake, b.Width, b.Height) { if snakeIsOutOfBounds(snake, b.Width, b.Height) {
snake.EliminatedCause = EliminatedByOutOfBounds EliminateSnake(snake, EliminatedByOutOfBounds, "", b.Turn+1)
continue continue
} }
} }
@ -306,8 +306,7 @@ func EliminateSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove
for i := 0; i < len(b.Snakes); i++ { for i := 0; i < len(b.Snakes); i++ {
snake := &b.Snakes[i] snake := &b.Snakes[i]
if snake.ID == elimination.ID { if snake.ID == elimination.ID {
snake.EliminatedCause = elimination.Cause EliminateSnake(snake, elimination.Cause, elimination.By, b.Turn+1)
snake.EliminatedBy = elimination.By
break break
} }
} }

View file

@ -164,6 +164,7 @@ var standardCaseMoveEatAndGrow = gameTestCase{
var standardMoveAndCollideMAD = gameTestCase{ var standardMoveAndCollideMAD = gameTestCase{
"Standard Case Move and Collide", "Standard Case Move and Collide",
&BoardState{ &BoardState{
Turn: 0,
Width: 10, Width: 10,
Height: 10, Height: 10,
Snakes: []Snake{ Snakes: []Snake{
@ -191,18 +192,20 @@ var standardMoveAndCollideMAD = gameTestCase{
Height: 10, Height: 10,
Snakes: []Snake{ Snakes: []Snake{
{ {
ID: "one", ID: "one",
Body: []Point{{1, 2}, {1, 1}}, Body: []Point{{1, 2}, {1, 1}},
Health: 98, Health: 98,
EliminatedCause: EliminatedByCollision, EliminatedCause: EliminatedByCollision,
EliminatedBy: "two", EliminatedBy: "two",
EliminatedOnTurn: 1,
}, },
{ {
ID: "two", ID: "two",
Body: []Point{{1, 1}, {1, 2}}, Body: []Point{{1, 1}, {1, 2}},
Health: 98, Health: 98,
EliminatedCause: EliminatedByCollision, EliminatedCause: EliminatedByCollision,
EliminatedBy: "one", EliminatedBy: "one",
EliminatedOnTurn: 1,
}, },
}, },
Food: []Point{}, Food: []Point{},
@ -275,10 +278,11 @@ func TestEatingOnLastMove(t *testing.T) {
Health: 100, Health: 100,
}, },
{ {
ID: "two", ID: "two",
Body: []Point{{3, 1}, {3, 2}, {3, 3}}, Body: []Point{{3, 1}, {3, 2}, {3, 3}},
Health: 0, Health: 0,
EliminatedCause: EliminatedByOutOfHealth, EliminatedCause: EliminatedByOutOfHealth,
EliminatedOnTurn: 1,
}, },
}, },
Food: []Point{{9, 9}}, Food: []Point{{9, 9}},
@ -315,6 +319,7 @@ func TestHeadToHeadOnFood(t *testing.T) {
}{ }{
{ {
&BoardState{ &BoardState{
Turn: 41,
Width: 10, Width: 10,
Height: 10, Height: 10,
Snakes: []Snake{ Snakes: []Snake{
@ -341,18 +346,20 @@ func TestHeadToHeadOnFood(t *testing.T) {
Height: 10, Height: 10,
Snakes: []Snake{ Snakes: []Snake{
{ {
ID: "one", ID: "one",
Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}}, Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}},
Health: 100, Health: 100,
EliminatedCause: EliminatedByHeadToHeadCollision, EliminatedCause: EliminatedByHeadToHeadCollision,
EliminatedBy: "two", EliminatedBy: "two",
EliminatedOnTurn: 42,
}, },
{ {
ID: "two", ID: "two",
Body: []Point{{0, 3}, {0, 4}, {0, 5}, {0, 5}}, Body: []Point{{0, 3}, {0, 4}, {0, 5}, {0, 5}},
Health: 100, Health: 100,
EliminatedCause: EliminatedByHeadToHeadCollision, EliminatedCause: EliminatedByHeadToHeadCollision,
EliminatedBy: "one", EliminatedBy: "one",
EliminatedOnTurn: 42,
}, },
}, },
Food: []Point{{9, 9}}, Food: []Point{{9, 9}},
@ -360,6 +367,7 @@ func TestHeadToHeadOnFood(t *testing.T) {
}, },
{ {
&BoardState{ &BoardState{
Turn: 41,
Width: 10, Width: 10,
Height: 10, Height: 10,
Snakes: []Snake{ Snakes: []Snake{
@ -386,11 +394,12 @@ func TestHeadToHeadOnFood(t *testing.T) {
Height: 10, Height: 10,
Snakes: []Snake{ Snakes: []Snake{
{ {
ID: "one", ID: "one",
Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}}, Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}},
Health: 100, Health: 100,
EliminatedCause: EliminatedByHeadToHeadCollision, EliminatedCause: EliminatedByHeadToHeadCollision,
EliminatedBy: "two", EliminatedBy: "two",
EliminatedOnTurn: 42,
}, },
{ {
ID: "two", ID: "two",
@ -1272,66 +1281,75 @@ func TestMaybeEliminateSnakesPriority(t *testing.T) {
func TestMaybeDamageHazards(t *testing.T) { func TestMaybeDamageHazards(t *testing.T) {
tests := []struct { tests := []struct {
Snakes []Snake Snakes []Snake
Hazards []Point Hazards []Point
Food []Point Food []Point
ExpectedEliminatedCauses []string ExpectedEliminatedCauses []string
ExpectedEliminatedByIDs []string ExpectedEliminatedByIDs []string
ExpectedEliminatedOnTurns []int
}{ }{
{}, {},
{ {
Snakes: []Snake{{Body: []Point{{0, 0}}}}, Snakes: []Snake{{Body: []Point{{0, 0}}}},
Hazards: []Point{}, Hazards: []Point{},
ExpectedEliminatedCauses: []string{NotEliminated}, ExpectedEliminatedCauses: []string{NotEliminated},
ExpectedEliminatedByIDs: []string{""}, ExpectedEliminatedByIDs: []string{""},
ExpectedEliminatedOnTurns: []int{0},
}, },
{ {
Snakes: []Snake{{Body: []Point{{0, 0}}}}, Snakes: []Snake{{Body: []Point{{0, 0}}}},
Hazards: []Point{{0, 0}}, Hazards: []Point{{0, 0}},
ExpectedEliminatedCauses: []string{EliminatedByOutOfHealth}, ExpectedEliminatedCauses: []string{EliminatedByOutOfHealth},
ExpectedEliminatedByIDs: []string{""}, ExpectedEliminatedByIDs: []string{""},
ExpectedEliminatedOnTurns: []int{42},
}, },
{ {
Snakes: []Snake{{Body: []Point{{0, 0}}}}, Snakes: []Snake{{Body: []Point{{0, 0}}}},
Hazards: []Point{{0, 0}}, Hazards: []Point{{0, 0}},
Food: []Point{{0, 0}}, Food: []Point{{0, 0}},
ExpectedEliminatedCauses: []string{NotEliminated}, ExpectedEliminatedCauses: []string{NotEliminated},
ExpectedEliminatedByIDs: []string{""}, ExpectedEliminatedByIDs: []string{""},
ExpectedEliminatedOnTurns: []int{0},
}, },
{ {
Snakes: []Snake{{Body: []Point{{0, 0}, {1, 0}, {2, 0}}}}, Snakes: []Snake{{Body: []Point{{0, 0}, {1, 0}, {2, 0}}}},
Hazards: []Point{{1, 0}, {2, 0}}, Hazards: []Point{{1, 0}, {2, 0}},
ExpectedEliminatedCauses: []string{NotEliminated}, ExpectedEliminatedCauses: []string{NotEliminated},
ExpectedEliminatedByIDs: []string{""}, ExpectedEliminatedByIDs: []string{""},
ExpectedEliminatedOnTurns: []int{0},
}, },
{ {
Snakes: []Snake{ Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, {Body: []Point{{0, 0}, {1, 0}, {2, 0}}},
{Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}},
}, },
Hazards: []Point{{1, 0}, {2, 0}, {3, 4}, {3, 5}, {3, 6}}, Hazards: []Point{{1, 0}, {2, 0}, {3, 4}, {3, 5}, {3, 6}},
ExpectedEliminatedCauses: []string{NotEliminated, NotEliminated}, ExpectedEliminatedCauses: []string{NotEliminated, NotEliminated},
ExpectedEliminatedByIDs: []string{"", ""}, ExpectedEliminatedByIDs: []string{"", ""},
ExpectedEliminatedOnTurns: []int{0, 0},
}, },
{ {
Snakes: []Snake{ Snakes: []Snake{
{Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, {Body: []Point{{0, 0}, {1, 0}, {2, 0}}},
{Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}},
}, },
Hazards: []Point{{3, 3}}, Hazards: []Point{{3, 3}},
ExpectedEliminatedCauses: []string{NotEliminated, EliminatedByOutOfHealth}, ExpectedEliminatedCauses: []string{NotEliminated, EliminatedByOutOfHealth},
ExpectedEliminatedByIDs: []string{"", ""}, ExpectedEliminatedByIDs: []string{"", ""},
ExpectedEliminatedOnTurns: []int{0, 42},
}, },
} }
for _, test := range tests { for _, test := range tests {
b := &BoardState{Snakes: test.Snakes, Hazards: test.Hazards, Food: test.Food} b := &BoardState{Turn: 41, Snakes: test.Snakes, Hazards: test.Hazards, Food: test.Food}
r := StandardRuleset{HazardDamagePerTurn: 100} r := StandardRuleset{HazardDamagePerTurn: 100}
_, err := DamageHazardsStandard(b, r.Settings(), mockSnakeMoves()) _, err := DamageHazardsStandard(b, r.Settings(), mockSnakeMoves())
require.NoError(t, err) require.NoError(t, err)
for i, snake := range b.Snakes { for i, snake := range b.Snakes {
require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause) require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause)
require.Equal(t, test.ExpectedEliminatedByIDs[i], snake.EliminatedBy)
require.Equal(t, test.ExpectedEliminatedOnTurns[i], snake.EliminatedOnTurn)
} }
} }