From e1289af5fbc310b402db5df2ecccf383c26b73ae Mon Sep 17 00:00:00 2001 From: Torben Date: Thu, 21 Jul 2022 14:26:56 -0700 Subject: [PATCH] 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 --- board.go | 11 ++++ board_test.go | 8 +++ constrictor_test.go | 23 ++++---- solo_test.go | 31 ++++++++++ standard.go | 9 ++- standard_test.go | 134 +++++++++++++++++++++++++------------------- 6 files changed, 143 insertions(+), 73 deletions(-) diff --git a/board.go b/board.go index fa36f68..3ff2823 100644 --- a/board.go +++ b/board.go @@ -57,6 +57,7 @@ func (prevState *BoardState) Clone() *BoardState { 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].EliminatedOnTurn = prevState.Snakes[i].EliminatedOnTurn nextState.Snakes[i].EliminatedBy = prevState.Snakes[i].EliminatedBy } return nextState @@ -551,3 +552,13 @@ func getDistanceBetweenPoints(a, b Point) int { func isSquareBoard(b *BoardState) bool { 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 +} diff --git a/board_test.go b/board_test.go index 24bc370..10a0008 100644 --- a/board_test.go +++ b/board_test.go @@ -881,3 +881,11 @@ func TestPlaceFoodRandomly(t *testing.T) { require.NoError(t, err) 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) +} diff --git a/constrictor_test.go b/constrictor_test.go index 6befbd7..68df8ff 100644 --- a/constrictor_test.go +++ b/constrictor_test.go @@ -15,6 +15,7 @@ func TestConstrictorRulesetInterface(t *testing.T) { var constrictorMoveAndCollideMAD = gameTestCase{ "Constrictor Case Move and Collide", &BoardState{ + Turn: 41, Width: 10, Height: 10, Snakes: []Snake{ @@ -42,18 +43,20 @@ var constrictorMoveAndCollideMAD = gameTestCase{ Height: 10, Snakes: []Snake{ { - ID: "one", - Body: []Point{{1, 2}, {1, 1}, {1, 1}}, - Health: 100, - EliminatedCause: EliminatedByCollision, - EliminatedBy: "two", + ID: "one", + Body: []Point{{1, 2}, {1, 1}, {1, 1}}, + Health: 100, + EliminatedCause: EliminatedByCollision, + EliminatedBy: "two", + EliminatedOnTurn: 42, }, { - ID: "two", - Body: []Point{{1, 1}, {1, 2}, {1, 2}}, - Health: 100, - EliminatedCause: EliminatedByCollision, - EliminatedBy: "one", + ID: "two", + Body: []Point{{1, 1}, {1, 2}, {1, 2}}, + Health: 100, + EliminatedCause: EliminatedByCollision, + EliminatedBy: "one", + EliminatedOnTurn: 42, }, }, Food: []Point{}, diff --git a/solo_test.go b/solo_test.go index 42e32c5..96555f0 100644 --- a/solo_test.go +++ b/solo_test.go @@ -116,3 +116,34 @@ func TestSoloCreateNextBoardState(t *testing.T) { 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) +} diff --git a/standard.go b/standard.go index 0a8ed71..835cbeb 100644 --- a/standard.go +++ b/standard.go @@ -184,7 +184,7 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove) snake.Health = SnakeMaxHealth } 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) { - snake.EliminatedCause = EliminatedByOutOfHealth + EliminateSnake(snake, EliminatedByOutOfHealth, "", b.Turn+1) continue } if snakeIsOutOfBounds(snake, b.Width, b.Height) { - snake.EliminatedCause = EliminatedByOutOfBounds + EliminateSnake(snake, EliminatedByOutOfBounds, "", b.Turn+1) continue } } @@ -306,8 +306,7 @@ func EliminateSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove for i := 0; i < len(b.Snakes); i++ { snake := &b.Snakes[i] if snake.ID == elimination.ID { - snake.EliminatedCause = elimination.Cause - snake.EliminatedBy = elimination.By + EliminateSnake(snake, elimination.Cause, elimination.By, b.Turn+1) break } } diff --git a/standard_test.go b/standard_test.go index c5582bf..86c0849 100644 --- a/standard_test.go +++ b/standard_test.go @@ -164,6 +164,7 @@ var standardCaseMoveEatAndGrow = gameTestCase{ var standardMoveAndCollideMAD = gameTestCase{ "Standard Case Move and Collide", &BoardState{ + Turn: 0, Width: 10, Height: 10, Snakes: []Snake{ @@ -191,18 +192,20 @@ var standardMoveAndCollideMAD = gameTestCase{ Height: 10, Snakes: []Snake{ { - ID: "one", - Body: []Point{{1, 2}, {1, 1}}, - Health: 98, - EliminatedCause: EliminatedByCollision, - EliminatedBy: "two", + ID: "one", + Body: []Point{{1, 2}, {1, 1}}, + Health: 98, + EliminatedCause: EliminatedByCollision, + EliminatedBy: "two", + EliminatedOnTurn: 1, }, { - ID: "two", - Body: []Point{{1, 1}, {1, 2}}, - Health: 98, - EliminatedCause: EliminatedByCollision, - EliminatedBy: "one", + ID: "two", + Body: []Point{{1, 1}, {1, 2}}, + Health: 98, + EliminatedCause: EliminatedByCollision, + EliminatedBy: "one", + EliminatedOnTurn: 1, }, }, Food: []Point{}, @@ -275,10 +278,11 @@ func TestEatingOnLastMove(t *testing.T) { Health: 100, }, { - ID: "two", - Body: []Point{{3, 1}, {3, 2}, {3, 3}}, - Health: 0, - EliminatedCause: EliminatedByOutOfHealth, + ID: "two", + Body: []Point{{3, 1}, {3, 2}, {3, 3}}, + Health: 0, + EliminatedCause: EliminatedByOutOfHealth, + EliminatedOnTurn: 1, }, }, Food: []Point{{9, 9}}, @@ -315,6 +319,7 @@ func TestHeadToHeadOnFood(t *testing.T) { }{ { &BoardState{ + Turn: 41, Width: 10, Height: 10, Snakes: []Snake{ @@ -341,18 +346,20 @@ func TestHeadToHeadOnFood(t *testing.T) { Height: 10, Snakes: []Snake{ { - ID: "one", - Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}}, - Health: 100, - EliminatedCause: EliminatedByHeadToHeadCollision, - EliminatedBy: "two", + ID: "one", + Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}}, + Health: 100, + EliminatedCause: EliminatedByHeadToHeadCollision, + EliminatedBy: "two", + EliminatedOnTurn: 42, }, { - ID: "two", - Body: []Point{{0, 3}, {0, 4}, {0, 5}, {0, 5}}, - Health: 100, - EliminatedCause: EliminatedByHeadToHeadCollision, - EliminatedBy: "one", + ID: "two", + Body: []Point{{0, 3}, {0, 4}, {0, 5}, {0, 5}}, + Health: 100, + EliminatedCause: EliminatedByHeadToHeadCollision, + EliminatedBy: "one", + EliminatedOnTurn: 42, }, }, Food: []Point{{9, 9}}, @@ -360,6 +367,7 @@ func TestHeadToHeadOnFood(t *testing.T) { }, { &BoardState{ + Turn: 41, Width: 10, Height: 10, Snakes: []Snake{ @@ -386,11 +394,12 @@ func TestHeadToHeadOnFood(t *testing.T) { Height: 10, Snakes: []Snake{ { - ID: "one", - Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}}, - Health: 100, - EliminatedCause: EliminatedByHeadToHeadCollision, - EliminatedBy: "two", + ID: "one", + Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}}, + Health: 100, + EliminatedCause: EliminatedByHeadToHeadCollision, + EliminatedBy: "two", + EliminatedOnTurn: 42, }, { ID: "two", @@ -1272,66 +1281,75 @@ func TestMaybeEliminateSnakesPriority(t *testing.T) { func TestMaybeDamageHazards(t *testing.T) { tests := []struct { - Snakes []Snake - Hazards []Point - Food []Point - ExpectedEliminatedCauses []string - ExpectedEliminatedByIDs []string + Snakes []Snake + Hazards []Point + Food []Point + ExpectedEliminatedCauses []string + ExpectedEliminatedByIDs []string + ExpectedEliminatedOnTurns []int }{ {}, { - Snakes: []Snake{{Body: []Point{{0, 0}}}}, - Hazards: []Point{}, - ExpectedEliminatedCauses: []string{NotEliminated}, - ExpectedEliminatedByIDs: []string{""}, + Snakes: []Snake{{Body: []Point{{0, 0}}}}, + Hazards: []Point{}, + ExpectedEliminatedCauses: []string{NotEliminated}, + ExpectedEliminatedByIDs: []string{""}, + ExpectedEliminatedOnTurns: []int{0}, }, { - Snakes: []Snake{{Body: []Point{{0, 0}}}}, - Hazards: []Point{{0, 0}}, - ExpectedEliminatedCauses: []string{EliminatedByOutOfHealth}, - ExpectedEliminatedByIDs: []string{""}, + Snakes: []Snake{{Body: []Point{{0, 0}}}}, + Hazards: []Point{{0, 0}}, + ExpectedEliminatedCauses: []string{EliminatedByOutOfHealth}, + ExpectedEliminatedByIDs: []string{""}, + ExpectedEliminatedOnTurns: []int{42}, }, { - Snakes: []Snake{{Body: []Point{{0, 0}}}}, - Hazards: []Point{{0, 0}}, - Food: []Point{{0, 0}}, - ExpectedEliminatedCauses: []string{NotEliminated}, - ExpectedEliminatedByIDs: []string{""}, + Snakes: []Snake{{Body: []Point{{0, 0}}}}, + Hazards: []Point{{0, 0}}, + Food: []Point{{0, 0}}, + ExpectedEliminatedCauses: []string{NotEliminated}, + ExpectedEliminatedByIDs: []string{""}, + ExpectedEliminatedOnTurns: []int{0}, }, { - Snakes: []Snake{{Body: []Point{{0, 0}, {1, 0}, {2, 0}}}}, - Hazards: []Point{{1, 0}, {2, 0}}, - ExpectedEliminatedCauses: []string{NotEliminated}, - ExpectedEliminatedByIDs: []string{""}, + Snakes: []Snake{{Body: []Point{{0, 0}, {1, 0}, {2, 0}}}}, + Hazards: []Point{{1, 0}, {2, 0}}, + ExpectedEliminatedCauses: []string{NotEliminated}, + ExpectedEliminatedByIDs: []string{""}, + ExpectedEliminatedOnTurns: []int{0}, }, { Snakes: []Snake{ {Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, }, - Hazards: []Point{{1, 0}, {2, 0}, {3, 4}, {3, 5}, {3, 6}}, - ExpectedEliminatedCauses: []string{NotEliminated, NotEliminated}, - ExpectedEliminatedByIDs: []string{"", ""}, + Hazards: []Point{{1, 0}, {2, 0}, {3, 4}, {3, 5}, {3, 6}}, + ExpectedEliminatedCauses: []string{NotEliminated, NotEliminated}, + ExpectedEliminatedByIDs: []string{"", ""}, + ExpectedEliminatedOnTurns: []int{0, 0}, }, { Snakes: []Snake{ {Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, }, - Hazards: []Point{{3, 3}}, - ExpectedEliminatedCauses: []string{NotEliminated, EliminatedByOutOfHealth}, - ExpectedEliminatedByIDs: []string{"", ""}, + Hazards: []Point{{3, 3}}, + ExpectedEliminatedCauses: []string{NotEliminated, EliminatedByOutOfHealth}, + ExpectedEliminatedByIDs: []string{"", ""}, + ExpectedEliminatedOnTurns: []int{0, 42}, }, } 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} _, err := DamageHazardsStandard(b, r.Settings(), mockSnakeMoves()) require.NoError(t, err) for i, snake := range b.Snakes { require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause) + require.Equal(t, test.ExpectedEliminatedByIDs[i], snake.EliminatedBy) + require.Equal(t, test.ExpectedEliminatedOnTurns[i], snake.EliminatedOnTurn) } }