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/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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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/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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

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

View file

@ -3,6 +3,7 @@ package rules
import ( import (
"errors" "errors"
"math/rand" "math/rand"
"sort"
) )
type StandardRuleset struct{} type StandardRuleset struct{}
@ -256,6 +257,19 @@ func (r *StandardRuleset) reduceSnakeHealth(b *BoardState) error {
} }
func (r *StandardRuleset) eliminateSnakes(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++ { for i := 0; i < len(b.Snakes); i++ {
snake := &b.Snakes[i] snake := &b.Snakes[i]
if len(snake.Body) <= 0 { if len(snake.Body) <= 0 {
@ -273,14 +287,15 @@ func (r *StandardRuleset) eliminateSnakes(b *BoardState) error {
} }
// Always check body collisions before head-to-heads // Always check body collisions before head-to-heads
for j := 0; j < len(b.Snakes); j++ { for _, otherIndex := range snakeIndicesByLength {
other := &b.Snakes[j] other := &b.Snakes[otherIndex]
if r.snakeHasBodyCollided(snake, other) { if r.snakeHasBodyCollided(snake, other) {
if snake.ID == other.ID { if snake.ID == other.ID {
snake.EliminatedCause = EliminatedBySelfCollision snake.EliminatedCause = EliminatedBySelfCollision
} else { } else {
snake.EliminatedCause = EliminatedByCollision snake.EliminatedCause = EliminatedByCollision
} }
snake.EliminatedBy = other.ID
break break
} }
} }
@ -288,11 +303,12 @@ func (r *StandardRuleset) eliminateSnakes(b *BoardState) error {
continue continue
} }
// Always check body collisions before head-to-heads // Always check head-to-heads after body collisions
for j := 0; j < len(b.Snakes); j++ { for _, otherIndex := range snakeIndicesByLength {
other := &b.Snakes[j] other := &b.Snakes[otherIndex]
if snake.ID != other.ID && r.snakeHasLostHeadToHead(snake, other) { if snake.ID != other.ID && r.snakeHasLostHeadToHead(snake, other) {
snake.EliminatedCause = EliminatedByHeadToHeadCollision snake.EliminatedCause = EliminatedByHeadToHeadCollision
snake.EliminatedBy = other.ID
break break
} }
} }

View file

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