diff --git a/team.go b/team.go deleted file mode 100644 index 71146c8..0000000 --- a/team.go +++ /dev/null @@ -1,128 +0,0 @@ -package rules - -import ( - "errors" -) - -type TeamRuleset struct { - StandardRuleset - - TeamMap map[string]string - - // These are intentionally designed so that they default to a standard game. - AllowBodyCollisions bool - SharedElimination bool - SharedHealth bool - SharedLength bool -} - -const EliminatedByTeam = "team-eliminated" - -func (r *TeamRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { - nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves) - if err != nil { - return nil, err - } - - // TODO: LOG? - err = r.resurrectTeamBodyCollisions(nextBoardState) - if err != nil { - return nil, err - } - - // TODO: LOG? - err = r.shareTeamAttributes(nextBoardState) - if err != nil { - return nil, err - } - - return nextBoardState, nil -} - -func (r *TeamRuleset) areSnakesOnSameTeam(snake *Snake, other *Snake) bool { - return r.areSnakeIDsOnSameTeam(snake.ID, other.ID) -} - -func (r *TeamRuleset) areSnakeIDsOnSameTeam(snakeID string, otherID string) bool { - return r.TeamMap[snakeID] == r.TeamMap[otherID] -} - -func (r *TeamRuleset) resurrectTeamBodyCollisions(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.areSnakeIDsOnSameTeam(snake.ID, snake.EliminatedBy) { - snake.EliminatedCause = NotEliminated - snake.EliminatedBy = "" - } - } - } - - return nil -} - -func (r *TeamRuleset) shareTeamAttributes(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.areSnakesOnSameTeam(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 = EliminatedByTeam - // We intentionally do not set snake.EliminatedBy because there might be multiple culprits. - snake.EliminatedBy = "" - } - } - } - } - } - - return nil -} - -func (r *TeamRuleset) 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.areSnakesOnSameTeam(snakesRemaining[i], snakesRemaining[0]) { - // There are multiple teams remaining - return false, nil - } - } - // no snakes or single team remaining - return true, nil -} diff --git a/team_test.go b/team_test.go deleted file mode 100644 index 101fa83..0000000 --- a/team_test.go +++ /dev/null @@ -1,340 +0,0 @@ -package rules - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestTeamRulesetInterface(t *testing.T) { - var _ Ruleset = (*TeamRuleset)(nil) -} - -func TestCreateNextBoardStateSanity(t *testing.T) { - boardState := &BoardState{} - r := TeamRuleset{} - _, err := r.CreateNextBoardState(boardState, []SnakeMove{}) - require.NoError(t, err) -} - -func TestResurrectTeamBodyCollisionsSanity(t *testing.T) { - boardState := &BoardState{} - r := TeamRuleset{} - err := r.resurrectTeamBodyCollisions(boardState) - require.NoError(t, err) -} - -func TestSharedAttributesSanity(t *testing.T) { - boardState := &BoardState{} - r := TeamRuleset{} - err := r.shareTeamAttributes(boardState) - require.NoError(t, err) -} - -func TestAllowBodyCollisions(t *testing.T) { - testSnakes := []struct { - SnakeID string - TeamID string - EliminatedCause string - EliminatedBy string - ExpectedCause string - ExpectedBy string - }{ - // Team Red - {"R1", "red", NotEliminated, "", NotEliminated, ""}, - {"R2", "red", EliminatedByCollision, "R1", NotEliminated, ""}, - // Team Blue - {"B1", "blue", EliminatedByCollision, "R1", EliminatedByCollision, "R1"}, - {"B2", "blue", EliminatedBySelfCollision, "B1", EliminatedBySelfCollision, "B1"}, - {"B4", "blue", EliminatedByOutOfBounds, "", EliminatedByOutOfBounds, ""}, - {"B3", "blue", NotEliminated, "", NotEliminated, ""}, - // More Team Red - {"R3", "red", NotEliminated, "", NotEliminated, ""}, - {"R4", "red", EliminatedByCollision, "R4", EliminatedByCollision, "R4"}, // this is an error case but worth testing - {"R5", "red", EliminatedByCollision, "R4", NotEliminated, ""}, - // // Team Green - {"G1", "green", EliminatedByStarvation, "x", EliminatedByStarvation, "x"}, - // // Team Yellow - {"Y1", "yellow", EliminatedByCollision, "B4", EliminatedByCollision, "B4"}, - } - - boardState := &BoardState{} - teamMap := make(map[string]string) - for _, testSnake := range testSnakes { - boardState.Snakes = append(boardState.Snakes, Snake{ - ID: testSnake.SnakeID, - EliminatedCause: testSnake.EliminatedCause, - EliminatedBy: testSnake.EliminatedBy, - }) - teamMap[testSnake.SnakeID] = testSnake.TeamID - } - require.Equal(t, len(teamMap), len(boardState.Snakes), "team map is wrong size, error in test setup") - - r := TeamRuleset{TeamMap: teamMap, AllowBodyCollisions: true} - err := r.resurrectTeamBodyCollisions(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 := TeamRuleset{ - AllowBodyCollisions: true, - TeamMap: map[string]string{ - "1": "red", - "2": "red", - }, - } - err := r.resurrectTeamBodyCollisions(boardState) - require.Error(t, err) -} - -func TestShareTeamHealth(t *testing.T) { - testSnakes := []struct { - SnakeID string - TeamID string - Health int32 - ExpectedHealth int32 - }{ - // Team Red - {"R1", "red", 11, 88}, - {"R2", "red", 22, 88}, - // Team Blue - {"B1", "blue", 33, 333}, - {"B2", "blue", 333, 333}, - {"B3", "blue", 3, 333}, - // More Team Red - {"R3", "red", 77, 88}, - {"R4", "red", 88, 88}, - // Team Green - {"G1", "green", 100, 100}, - // Team Yellow - {"Y1", "yellow", 1, 1}, - } - - boardState := &BoardState{} - teamMap := make(map[string]string) - for _, testSnake := range testSnakes { - boardState.Snakes = append(boardState.Snakes, Snake{ - ID: testSnake.SnakeID, - Health: testSnake.Health, - }) - teamMap[testSnake.SnakeID] = testSnake.TeamID - } - require.Equal(t, len(teamMap), len(boardState.Snakes), "team map is wrong size, error in test setup") - - r := TeamRuleset{SharedHealth: true, TeamMap: teamMap} - err := r.shareTeamAttributes(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 - TeamID string - Body []Point - ExpectedBody []Point - }{ - // Team Red - {"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}}}, - // Team Blue - {"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 Team Red - {"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}}}, - // Team Green - {"G1", "green", []Point{{1, 1}}, []Point{{1, 1}}}, - // Team Yellow - {"Y1", "yellow", []Point{{1, 3}, {1, 4}, {1, 5}, {1, 6}}, []Point{{1, 3}, {1, 4}, {1, 5}, {1, 6}}}, - } - - boardState := &BoardState{} - teamMap := make(map[string]string) - for _, testSnake := range testSnakes { - boardState.Snakes = append(boardState.Snakes, Snake{ - ID: testSnake.SnakeID, - Body: testSnake.Body, - }) - teamMap[testSnake.SnakeID] = testSnake.TeamID - } - require.Equal(t, len(teamMap), len(boardState.Snakes), "team map is wrong size, error in test setup") - - r := TeamRuleset{SharedLength: true, TeamMap: teamMap} - err := r.shareTeamAttributes(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 - TeamID string - EliminatedCause string - EliminatedBy string - ExpectedCause string - ExpectedBy string - }{ - // Team Red - {"R1", "red", NotEliminated, "", EliminatedByTeam, ""}, - {"R2", "red", EliminatedByHeadToHeadCollision, "y", EliminatedByHeadToHeadCollision, "y"}, - // Team Blue - {"B1", "blue", EliminatedByOutOfBounds, "z", EliminatedByOutOfBounds, "z"}, - {"B2", "blue", NotEliminated, "", EliminatedByTeam, ""}, - {"B3", "blue", NotEliminated, "", EliminatedByTeam, ""}, - // More Team Red - {"R3", "red", NotEliminated, "", EliminatedByTeam, ""}, - {"R4", "red", EliminatedByCollision, "B1", EliminatedByCollision, "B1"}, - // Team Green - {"G1", "green", EliminatedByStarvation, "x", EliminatedByStarvation, "x"}, - // Team Yellow - {"Y1", "yellow", NotEliminated, "", NotEliminated, ""}, - } - - boardState := &BoardState{} - teamMap := make(map[string]string) - for _, testSnake := range testSnakes { - boardState.Snakes = append(boardState.Snakes, Snake{ - ID: testSnake.SnakeID, - EliminatedCause: testSnake.EliminatedCause, - EliminatedBy: testSnake.EliminatedBy, - }) - teamMap[testSnake.SnakeID] = testSnake.TeamID - } - require.Equal(t, len(teamMap), len(boardState.Snakes), "team map is wrong size, error in test setup") - - r := TeamRuleset{SharedElimination: true, TeamMap: teamMap} - err := r.shareTeamAttributes(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 := TeamRuleset{ - SharedLength: true, - TeamMap: map[string]string{ - "1": "red", - "2": "red", - }, - } - err := r.shareTeamAttributes(boardState) - require.Error(t, err) -} - -func TestTeamIsGameOver(t *testing.T) { - tests := []struct { - Snakes []Snake - TeamMap 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 := TeamRuleset{TeamMap: test.TeamMap} - - actual, err := r.IsGameOver(b) - require.NoError(t, err) - require.Equal(t, test.Expected, actual) - } -}