Add "EliminatedBy" to snake eliminations. (#11)

* add eliminated by

* add test

* make sure largest snake is listed as eliminator on head to head collisions

* remove unused type def

* Reduce memory usage during elimination checks.

Co-authored-by: Daniel Steuernol <dlsteuer@gmail.com>
This commit is contained in:
Brad Van Vugt 2020-02-19 11:44:48 -08:00 committed by GitHub
parent a241c526b2
commit 8153585f57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 70 additions and 21 deletions

1
go.sum
View file

@ -5,6 +5,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -17,6 +17,7 @@ type Snake struct {
Body []Point
Health int32
EliminatedCause string
EliminatedBy string
}
type BoardState struct {

View file

@ -3,6 +3,7 @@ package rules
import (
"errors"
"math/rand"
"sort"
)
type StandardRuleset struct{}
@ -256,6 +257,19 @@ func (r *StandardRuleset) reduceSnakeHealth(b *BoardState) error {
}
func (r *StandardRuleset) eliminateSnakes(b *BoardState) error {
// First order snake indices by length.
// In multi-collision scenarios we want to always attribute elimination to the longest snake.
snakeIndicesByLength := make([]int, len(b.Snakes))
for i := 0; i < len(b.Snakes); i++ {
snakeIndicesByLength[i] = i
}
sort.Slice(snakeIndicesByLength, func(i int, j int) bool {
lenI := len(b.Snakes[snakeIndicesByLength[i]].Body)
lenJ := len(b.Snakes[snakeIndicesByLength[j]].Body)
return lenI > lenJ
})
// Iterate through snakes checking for eliminations.
for i := 0; i < len(b.Snakes); i++ {
snake := &b.Snakes[i]
if len(snake.Body) <= 0 {
@ -273,14 +287,15 @@ func (r *StandardRuleset) eliminateSnakes(b *BoardState) error {
}
// Always check body collisions before head-to-heads
for j := 0; j < len(b.Snakes); j++ {
other := &b.Snakes[j]
for _, otherIndex := range snakeIndicesByLength {
other := &b.Snakes[otherIndex]
if r.snakeHasBodyCollided(snake, other) {
if snake.ID == other.ID {
snake.EliminatedCause = EliminatedBySelfCollision
} else {
snake.EliminatedCause = EliminatedByCollision
}
snake.EliminatedBy = other.ID
break
}
}
@ -288,11 +303,12 @@ func (r *StandardRuleset) eliminateSnakes(b *BoardState) error {
continue
}
// Always check body collisions before head-to-heads
for j := 0; j < len(b.Snakes); j++ {
other := &b.Snakes[j]
// Always check head-to-heads after body collisions
for _, otherIndex := range snakeIndicesByLength {
other := &b.Snakes[otherIndex]
if snake.ID != other.ID && r.snakeHasLostHeadToHead(snake, other) {
snake.EliminatedCause = EliminatedByHeadToHeadCollision
snake.EliminatedBy = other.ID
break
}
}

View file

@ -763,61 +763,78 @@ func TestSnakeHasLostHeadToHead(t *testing.T) {
func TestEliminateSnakes(t *testing.T) {
tests := []struct {
Name string
Snakes []Snake
ExpectedEliminatedCauses []string
ExpectedEliminatedBy []string
Err error
}{
{
"Empty",
[]Snake{},
[]string{},
[]string{},
nil,
},
{
"Zero Snake",
[]Snake{
Snake{},
},
[]string{NotEliminated},
[]string{""},
errors.New("snake is length zero"),
},
{
"Single Starvation",
[]Snake{
Snake{Body: []Point{{1, 1}}},
Snake{ID: "1", Body: []Point{{1, 1}}},
},
[]string{EliminatedByStarvation},
[]string{""},
nil,
},
{
"Not Eliminated",
[]Snake{
Snake{Health: 1, Body: []Point{{1, 1}}},
Snake{ID: "1", Health: 1, Body: []Point{{1, 1}}},
},
[]string{NotEliminated},
[]string{""},
nil,
},
{
"Out of Bounds",
[]Snake{
Snake{Health: 1, Body: []Point{{-1, 1}}},
Snake{ID: "1", Health: 1, Body: []Point{{-1, 1}}},
},
[]string{EliminatedByOutOfBounds},
[]string{""},
nil,
},
{
"Self Collision",
[]Snake{
Snake{Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}},
Snake{ID: "1", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}},
},
[]string{EliminatedBySelfCollision},
[]string{"1"},
nil,
},
{
"Multiple Separate Deaths",
[]Snake{
Snake{Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}},
Snake{Health: 1, Body: []Point{{-1, 1}}},
Snake{ID: "1", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}},
Snake{ID: "2", Health: 1, Body: []Point{{-1, 1}}},
},
[]string{
EliminatedBySelfCollision,
EliminatedByOutOfBounds},
[]string{"1", ""},
nil,
},
{
"Other Collision",
[]Snake{
Snake{ID: "1", Health: 1, Body: []Point{{0, 2}, {0, 3}, {0, 4}}},
Snake{ID: "2", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 2}}},
@ -825,9 +842,11 @@ func TestEliminateSnakes(t *testing.T) {
[]string{
EliminatedByCollision,
NotEliminated},
[]string{"2", ""},
nil,
},
{
"All Eliminated Head 2 Head",
[]Snake{
Snake{ID: "1", Health: 1, Body: []Point{{1, 1}}},
Snake{ID: "2", Health: 1, Body: []Point{{1, 1}}},
@ -838,9 +857,11 @@ func TestEliminateSnakes(t *testing.T) {
EliminatedByHeadToHeadCollision,
EliminatedByHeadToHeadCollision,
},
[]string{"2", "1", "1"},
nil,
},
{
"One Snake wins Head 2 Head",
[]Snake{
Snake{ID: "1", Health: 1, Body: []Point{{1, 1}, {0, 1}}},
Snake{ID: "2", Health: 1, Body: []Point{{1, 1}, {1, 2}, {1, 3}}},
@ -851,9 +872,11 @@ func TestEliminateSnakes(t *testing.T) {
NotEliminated,
EliminatedByHeadToHeadCollision,
},
[]string{"2", "", "2"},
nil,
},
{
"All Snakes Body Eliminated",
[]Snake{
Snake{ID: "1", Health: 1, Body: []Point{{4, 4}, {3, 3}}},
Snake{ID: "2", Health: 1, Body: []Point{{3, 3}, {2, 2}}},
@ -868,9 +891,11 @@ func TestEliminateSnakes(t *testing.T) {
EliminatedByCollision,
EliminatedByCollision,
},
[]string{"4", "1", "2", "3", "4"},
nil,
},
{
"All Snakes Eliminated Head 2 Head",
[]Snake{
Snake{ID: "1", Health: 1, Body: []Point{{4, 4}, {4, 5}}},
Snake{ID: "2", Health: 1, Body: []Point{{4, 4}, {4, 3}}},
@ -883,9 +908,11 @@ func TestEliminateSnakes(t *testing.T) {
EliminatedByHeadToHeadCollision,
EliminatedByHeadToHeadCollision,
},
[]string{"2", "1", "1", "1"},
nil,
},
{
"4 Snakes Head 2 Head",
[]Snake{
Snake{ID: "1", Health: 1, Body: []Point{{4, 4}, {4, 5}}},
Snake{ID: "2", Health: 1, Body: []Point{{4, 4}, {4, 3}}},
@ -898,12 +925,14 @@ func TestEliminateSnakes(t *testing.T) {
NotEliminated,
EliminatedByHeadToHeadCollision,
},
[]string{"3", "3", "", "3"},
nil,
},
}
r := StandardRuleset{}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
b := &BoardState{
Width: 10,
Height: 10,
@ -911,9 +940,11 @@ func TestEliminateSnakes(t *testing.T) {
}
err := r.eliminateSnakes(b)
require.Equal(t, test.Err, err)
for i := 0; i < len(b.Snakes); i++ {
require.Equal(t, test.ExpectedEliminatedCauses[i], b.Snakes[i].EliminatedCause)
for i, snake := range b.Snakes {
require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause)
require.Equal(t, test.ExpectedEliminatedBy[i], snake.EliminatedBy)
}
})
}
}