diff --git a/go.sum b/go.sum index e863f51..8fdee58 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/ruleset.go b/ruleset.go index e1fa9bb..62587df 100644 --- a/ruleset.go +++ b/ruleset.go @@ -17,6 +17,7 @@ type Snake struct { Body []Point Health int32 EliminatedCause string + EliminatedBy string } type BoardState struct { diff --git a/standard.go b/standard.go index e405667..ddf29bb 100644 --- a/standard.go +++ b/standard.go @@ -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 } } diff --git a/standard_test.go b/standard_test.go index e25fe29..5f3e7b9 100644 --- a/standard_test.go +++ b/standard_test.go @@ -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,22 +925,26 @@ func TestEliminateSnakes(t *testing.T) { NotEliminated, EliminatedByHeadToHeadCollision, }, + []string{"3", "3", "", "3"}, nil, }, } r := StandardRuleset{} for _, test := range tests { - b := &BoardState{ - Width: 10, - Height: 10, - Snakes: test.Snakes, - } - 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) - } + t.Run(test.Name, func(t *testing.T) { + b := &BoardState{ + Width: 10, + Height: 10, + Snakes: test.Snakes, + } + err := r.eliminateSnakes(b) + require.Equal(t, test.Err, err) + for i, snake := range b.Snakes { + require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause) + require.Equal(t, test.ExpectedEliminatedBy[i], snake.EliminatedBy) + } + }) } }