Rename TeamRuleset -> SquadRuleset

This commit is contained in:
Brad Van Vugt 2020-07-21 14:58:56 -07:00
parent d0400fcb18
commit 01eaf6267d
2 changed files with 468 additions and 0 deletions

128
squad.go Normal file
View file

@ -0,0 +1,128 @@
package rules
import (
"errors"
)
type SquadRuleset struct {
StandardRuleset
SquadMap map[string]string
// These are intentionally designed so that they default to a standard game.
AllowBodyCollisions bool
SharedElimination bool
SharedHealth bool
SharedLength bool
}
const EliminatedBySquad = "squad-eliminated"
func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
if err != nil {
return nil, err
}
// TODO: LOG?
err = r.resurrectSquadBodyCollisions(nextBoardState)
if err != nil {
return nil, err
}
// TODO: LOG?
err = r.shareSquadAttributes(nextBoardState)
if err != nil {
return nil, err
}
return nextBoardState, nil
}
func (r *SquadRuleset) areSnakesOnSameSquad(snake *Snake, other *Snake) bool {
return r.areSnakeIDsOnSameSquad(snake.ID, other.ID)
}
func (r *SquadRuleset) areSnakeIDsOnSameSquad(snakeID string, otherID string) bool {
return r.SquadMap[snakeID] == r.SquadMap[otherID]
}
func (r *SquadRuleset) resurrectSquadBodyCollisions(b *BoardState) error {
if !r.AllowBodyCollisions {
return nil
}
for i := 0; i < len(b.Snakes); i++ {
snake := &b.Snakes[i]
if snake.EliminatedCause == EliminatedByCollision {
if snake.EliminatedBy == "" {
return errors.New("snake eliminated by collision and eliminatedby is not set")
}
if snake.ID != snake.EliminatedBy && r.areSnakeIDsOnSameSquad(snake.ID, snake.EliminatedBy) {
snake.EliminatedCause = NotEliminated
snake.EliminatedBy = ""
}
}
}
return nil
}
func (r *SquadRuleset) shareSquadAttributes(b *BoardState) error {
if !(r.SharedElimination || r.SharedLength || r.SharedHealth) {
return nil
}
for i := 0; i < len(b.Snakes); i++ {
snake := &b.Snakes[i]
if snake.EliminatedCause != NotEliminated {
continue
}
for j := 0; j < len(b.Snakes); j++ {
other := &b.Snakes[j]
if r.areSnakesOnSameSquad(snake, other) {
if r.SharedHealth {
if snake.Health < other.Health {
snake.Health = other.Health
}
}
if r.SharedLength {
if len(snake.Body) == 0 || len(other.Body) == 0 {
return errors.New("found snake of zero length")
}
for len(snake.Body) < len(other.Body) {
r.growSnake(snake)
}
}
if r.SharedElimination {
if snake.EliminatedCause == NotEliminated && other.EliminatedCause != NotEliminated {
snake.EliminatedCause = EliminatedBySquad
// We intentionally do not set snake.EliminatedBy because there might be multiple culprits.
snake.EliminatedBy = ""
}
}
}
}
}
return nil
}
func (r *SquadRuleset) IsGameOver(b *BoardState) (bool, error) {
snakesRemaining := []*Snake{}
for i := 0; i < len(b.Snakes); i++ {
if b.Snakes[i].EliminatedCause == NotEliminated {
snakesRemaining = append(snakesRemaining, &b.Snakes[i])
}
}
for i := 0; i < len(snakesRemaining); i++ {
if !r.areSnakesOnSameSquad(snakesRemaining[i], snakesRemaining[0]) {
// There are multiple squads remaining
return false, nil
}
}
// no snakes or single squad remaining
return true, nil
}

340
squad_test.go Normal file
View file

@ -0,0 +1,340 @@
package rules
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestSquadRulesetInterface(t *testing.T) {
var _ Ruleset = (*SquadRuleset)(nil)
}
func TestCreateNextBoardStateSanity(t *testing.T) {
boardState := &BoardState{}
r := SquadRuleset{}
_, err := r.CreateNextBoardState(boardState, []SnakeMove{})
require.NoError(t, err)
}
func TestResurrectSquadBodyCollisionsSanity(t *testing.T) {
boardState := &BoardState{}
r := SquadRuleset{}
err := r.resurrectSquadBodyCollisions(boardState)
require.NoError(t, err)
}
func TestSharedAttributesSanity(t *testing.T) {
boardState := &BoardState{}
r := SquadRuleset{}
err := r.shareSquadAttributes(boardState)
require.NoError(t, err)
}
func TestAllowBodyCollisions(t *testing.T) {
testSnakes := []struct {
SnakeID string
SquadID string
EliminatedCause string
EliminatedBy string
ExpectedCause string
ExpectedBy string
}{
// Red Squad
{"R1", "red", NotEliminated, "", NotEliminated, ""},
{"R2", "red", EliminatedByCollision, "R1", NotEliminated, ""},
// Blue Squad
{"B1", "blue", EliminatedByCollision, "R1", EliminatedByCollision, "R1"},
{"B2", "blue", EliminatedBySelfCollision, "B1", EliminatedBySelfCollision, "B1"},
{"B4", "blue", EliminatedByOutOfBounds, "", EliminatedByOutOfBounds, ""},
{"B3", "blue", NotEliminated, "", NotEliminated, ""},
// More Red Squad
{"R3", "red", NotEliminated, "", NotEliminated, ""},
{"R4", "red", EliminatedByCollision, "R4", EliminatedByCollision, "R4"}, // this is an error case but worth testing
{"R5", "red", EliminatedByCollision, "R4", NotEliminated, ""},
// Green Squad
{"G1", "green", EliminatedByStarvation, "x", EliminatedByStarvation, "x"},
// Yellow Squad
{"Y1", "yellow", EliminatedByCollision, "B4", EliminatedByCollision, "B4"},
}
boardState := &BoardState{}
squadMap := make(map[string]string)
for _, testSnake := range testSnakes {
boardState.Snakes = append(boardState.Snakes, Snake{
ID: testSnake.SnakeID,
EliminatedCause: testSnake.EliminatedCause,
EliminatedBy: testSnake.EliminatedBy,
})
squadMap[testSnake.SnakeID] = testSnake.SquadID
}
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
r := SquadRuleset{SquadMap: squadMap, AllowBodyCollisions: true}
err := r.resurrectSquadBodyCollisions(boardState)
require.NoError(t, err)
require.Equal(t, len(boardState.Snakes), len(testSnakes))
for i := 0; i < len(boardState.Snakes); i++ {
require.Equal(
t,
testSnakes[i].ExpectedCause,
boardState.Snakes[i].EliminatedCause,
"snake %s failed shared eliminated cause",
testSnakes[i].SnakeID,
)
require.Equal(
t,
testSnakes[i].ExpectedBy,
boardState.Snakes[i].EliminatedBy,
"snake %s failed shared eliminated by",
testSnakes[i].SnakeID,
)
}
}
func TestAllowBodyCollisionsEliminatedByNotSet(t *testing.T) {
boardState := &BoardState{
Snakes: []Snake{
Snake{ID: "1", EliminatedCause: EliminatedByCollision},
Snake{ID: "2"},
},
}
r := SquadRuleset{
AllowBodyCollisions: true,
SquadMap: map[string]string{
"1": "red",
"2": "red",
},
}
err := r.resurrectSquadBodyCollisions(boardState)
require.Error(t, err)
}
func TestShareSquadHealth(t *testing.T) {
testSnakes := []struct {
SnakeID string
SquadID string
Health int32
ExpectedHealth int32
}{
// Red Squad
{"R1", "red", 11, 88},
{"R2", "red", 22, 88},
// Blue Squad
{"B1", "blue", 33, 333},
{"B2", "blue", 333, 333},
{"B3", "blue", 3, 333},
// More Red Squad
{"R3", "red", 77, 88},
{"R4", "red", 88, 88},
// Green Squad
{"G1", "green", 100, 100},
// Yellow Squad
{"Y1", "yellow", 1, 1},
}
boardState := &BoardState{}
squadMap := make(map[string]string)
for _, testSnake := range testSnakes {
boardState.Snakes = append(boardState.Snakes, Snake{
ID: testSnake.SnakeID,
Health: testSnake.Health,
})
squadMap[testSnake.SnakeID] = testSnake.SquadID
}
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
r := SquadRuleset{SharedHealth: true, SquadMap: squadMap}
err := r.shareSquadAttributes(boardState)
require.NoError(t, err)
require.Equal(t, len(boardState.Snakes), len(testSnakes))
for i := 0; i < len(boardState.Snakes); i++ {
require.Equal(
t,
testSnakes[i].ExpectedHealth,
boardState.Snakes[i].Health,
"snake %s failed shared health",
testSnakes[i].SnakeID,
)
}
}
func TestSharedLength(t *testing.T) {
testSnakes := []struct {
SnakeID string
SquadID string
Body []Point
ExpectedBody []Point
}{
// Red Squad
{"R1", "red", []Point{{1, 1}}, []Point{{1, 1}, {1, 1}, {1, 1}, {1, 1}, {1, 1}}},
{"R2", "red", []Point{{2, 2}, {2, 2}}, []Point{{2, 2}, {2, 2}, {2, 2}, {2, 2}, {2, 2}}},
// Blue Squad
{"B1", "blue", []Point{{1, 1}, {1, 2}}, []Point{{1, 1}, {1, 2}}},
{"B2", "blue", []Point{{2, 1}}, []Point{{2, 1}, {2, 1}}},
{"B3", "blue", []Point{{3, 3}}, []Point{{3, 3}, {3, 3}}},
// More Red Squad
{"R3", "red", []Point{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}, []Point{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {5, 5}}},
{"R4", "red", []Point{{4, 4}}, []Point{{4, 4}, {4, 4}, {4, 4}, {4, 4}, {4, 4}}},
// Green Squad
{"G1", "green", []Point{{1, 1}}, []Point{{1, 1}}},
// Yellow Squad
{"Y1", "yellow", []Point{{1, 3}, {1, 4}, {1, 5}, {1, 6}}, []Point{{1, 3}, {1, 4}, {1, 5}, {1, 6}}},
}
boardState := &BoardState{}
squadMap := make(map[string]string)
for _, testSnake := range testSnakes {
boardState.Snakes = append(boardState.Snakes, Snake{
ID: testSnake.SnakeID,
Body: testSnake.Body,
})
squadMap[testSnake.SnakeID] = testSnake.SquadID
}
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
r := SquadRuleset{SharedLength: true, SquadMap: squadMap}
err := r.shareSquadAttributes(boardState)
require.NoError(t, err)
require.Equal(t, len(boardState.Snakes), len(testSnakes))
for i := 0; i < len(boardState.Snakes); i++ {
require.Equal(
t,
testSnakes[i].ExpectedBody,
boardState.Snakes[i].Body,
"snake %s failed shared length",
testSnakes[i].SnakeID,
)
}
}
func TestSharedElimination(t *testing.T) {
testSnakes := []struct {
SnakeID string
SquadID string
EliminatedCause string
EliminatedBy string
ExpectedCause string
ExpectedBy string
}{
// Red Squad
{"R1", "red", NotEliminated, "", EliminatedBySquad, ""},
{"R2", "red", EliminatedByHeadToHeadCollision, "y", EliminatedByHeadToHeadCollision, "y"},
// Blue Squad
{"B1", "blue", EliminatedByOutOfBounds, "z", EliminatedByOutOfBounds, "z"},
{"B2", "blue", NotEliminated, "", EliminatedBySquad, ""},
{"B3", "blue", NotEliminated, "", EliminatedBySquad, ""},
// More Red Squad
{"R3", "red", NotEliminated, "", EliminatedBySquad, ""},
{"R4", "red", EliminatedByCollision, "B1", EliminatedByCollision, "B1"},
// Green Squad
{"G1", "green", EliminatedByStarvation, "x", EliminatedByStarvation, "x"},
// Yellow Squad
{"Y1", "yellow", NotEliminated, "", NotEliminated, ""},
}
boardState := &BoardState{}
squadMap := make(map[string]string)
for _, testSnake := range testSnakes {
boardState.Snakes = append(boardState.Snakes, Snake{
ID: testSnake.SnakeID,
EliminatedCause: testSnake.EliminatedCause,
EliminatedBy: testSnake.EliminatedBy,
})
squadMap[testSnake.SnakeID] = testSnake.SquadID
}
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
r := SquadRuleset{SharedElimination: true, SquadMap: squadMap}
err := r.shareSquadAttributes(boardState)
require.NoError(t, err)
require.Equal(t, len(boardState.Snakes), len(testSnakes))
for i := 0; i < len(boardState.Snakes); i++ {
require.Equal(
t,
testSnakes[i].ExpectedCause,
boardState.Snakes[i].EliminatedCause,
"snake %s failed shared eliminated cause",
testSnakes[i].SnakeID,
)
require.Equal(
t,
testSnakes[i].ExpectedBy,
boardState.Snakes[i].EliminatedBy,
"snake %s failed shared eliminated by",
testSnakes[i].SnakeID,
)
}
}
func TestSharedAttributesErrorLengthZero(t *testing.T) {
boardState := &BoardState{
Snakes: []Snake{
Snake{ID: "1"},
Snake{ID: "2"},
},
}
r := SquadRuleset{
SharedLength: true,
SquadMap: map[string]string{
"1": "red",
"2": "red",
},
}
err := r.shareSquadAttributes(boardState)
require.Error(t, err)
}
func TestSquadIsGameOver(t *testing.T) {
tests := []struct {
Snakes []Snake
SquadMap map[string]string
Expected bool
}{
{[]Snake{}, map[string]string{}, true},
{[]Snake{{ID: "R1"}}, map[string]string{"R1": "red"}, true},
{
[]Snake{{ID: "R1"}, {ID: "R2"}, {ID: "R3"}},
map[string]string{"R1": "red", "R2": "red", "R3": "red"},
true,
},
{
[]Snake{{ID: "R1"}, {ID: "B1"}},
map[string]string{"R1": "red", "B1": "blue"},
false,
},
{
[]Snake{{ID: "R1"}, {ID: "B1"}, {ID: "B2"}, {ID: "G1"}},
map[string]string{"R1": "red", "B1": "blue", "B2": "blue", "G1": "green"},
false,
},
{
[]Snake{
{ID: "R1", EliminatedCause: EliminatedByOutOfBounds},
{ID: "B1", EliminatedCause: EliminatedBySelfCollision, EliminatedBy: "B1"},
{ID: "B2", EliminatedCause: EliminatedByCollision, EliminatedBy: "B2"},
{ID: "G1"},
},
map[string]string{"R1": "red", "B1": "blue", "B2": "blue", "G1": "green"},
true,
},
}
for _, test := range tests {
b := &BoardState{
Height: 11,
Width: 11,
Snakes: test.Snakes,
Food: []Point{},
}
r := SquadRuleset{SquadMap: test.SquadMap}
actual, err := r.IsGameOver(b)
require.NoError(t, err)
require.Equal(t, test.Expected, actual)
}
}