diff --git a/ruleset.go b/ruleset.go index 9765fd0..052f127 100644 --- a/ruleset.go +++ b/ruleset.go @@ -35,4 +35,5 @@ type SnakeMove struct { type Ruleset interface { CreateInitialBoardState(width int32, height int32, snakeIDs []string) (*BoardState, error) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) + IsGameFinished(state *BoardState) (bool, error) } diff --git a/standard.go b/standard.go index ebc32ca..d6a6a55 100644 --- a/standard.go +++ b/standard.go @@ -439,3 +439,13 @@ func (r *StandardRuleset) getUnoccupiedPoints(b *BoardState) []Point { } return unoccupiedPoints } + +func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) { + numSnakesRemaining := 0 + for i := 0; i < len(b.Snakes); i++ { + if b.Snakes[i].EliminatedCause == NotEliminated { + numSnakesRemaining++ + } + } + return numSnakesRemaining <= 1, nil +} diff --git a/standard_test.go b/standard_test.go index 4f4d269..94d69bd 100644 --- a/standard_test.go +++ b/standard_test.go @@ -1155,3 +1155,72 @@ func TestMaybeSpawnFood(t *testing.T) { } } } + +func TestIsGameOver(t *testing.T) { + tests := []struct { + Snakes []Snake + Expected bool + }{ + {[]Snake{}, true}, + {[]Snake{{}}, true}, + {[]Snake{{}, {}}, false}, + {[]Snake{{}, {}, {}, {}, {}}, false}, + { + []Snake{ + {EliminatedCause: EliminatedByCollision}, + {EliminatedCause: NotEliminated}, + }, + true, + }, + { + []Snake{ + {EliminatedCause: NotEliminated}, + {EliminatedCause: EliminatedByCollision}, + {EliminatedCause: NotEliminated}, + {EliminatedCause: NotEliminated}, + }, + false, + }, + { + []Snake{ + {EliminatedCause: EliminatedByOutOfBounds}, + {EliminatedCause: EliminatedByOutOfBounds}, + {EliminatedCause: EliminatedByOutOfBounds}, + {EliminatedCause: EliminatedByOutOfBounds}, + }, + true, + }, + { + []Snake{ + {EliminatedCause: EliminatedByOutOfBounds}, + {EliminatedCause: EliminatedByOutOfBounds}, + {EliminatedCause: EliminatedByOutOfBounds}, + {EliminatedCause: NotEliminated}, + }, + true, + }, + { + []Snake{ + {EliminatedCause: EliminatedByOutOfBounds}, + {EliminatedCause: EliminatedByOutOfBounds}, + {EliminatedCause: NotEliminated}, + {EliminatedCause: NotEliminated}, + }, + false, + }, + } + + r := StandardRuleset{} + for _, test := range tests { + b := &BoardState{ + Height: 11, + Width: 11, + Snakes: test.Snakes, + Food: []Point{}, + } + + actual, err := r.IsGameOver(b) + require.NoError(t, err) + require.Equal(t, test.Expected, actual) + } +} diff --git a/team.go b/team.go index fc45bc7..71146c8 100644 --- a/team.go +++ b/team.go @@ -44,7 +44,7 @@ func (r *TeamRuleset) areSnakesOnSameTeam(snake *Snake, other *Snake) bool { } func (r *TeamRuleset) areSnakeIDsOnSameTeam(snakeID string, otherID string) bool { - return snakeID != otherID && r.TeamMap[snakeID] == r.TeamMap[otherID] + return r.TeamMap[snakeID] == r.TeamMap[otherID] } func (r *TeamRuleset) resurrectTeamBodyCollisions(b *BoardState) error { @@ -58,7 +58,7 @@ func (r *TeamRuleset) resurrectTeamBodyCollisions(b *BoardState) error { if snake.EliminatedBy == "" { return errors.New("snake eliminated by collision and eliminatedby is not set") } - if r.areSnakeIDsOnSameTeam(snake.ID, snake.EliminatedBy) { + if snake.ID != snake.EliminatedBy && r.areSnakeIDsOnSameTeam(snake.ID, snake.EliminatedBy) { snake.EliminatedCause = NotEliminated snake.EliminatedBy = "" } @@ -108,3 +108,21 @@ func (r *TeamRuleset) shareTeamAttributes(b *BoardState) error { 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 index 76e9d46..473417c 100644 --- a/team_test.go +++ b/team_test.go @@ -284,3 +284,53 @@ func TestSharedAttributesErrorLengthZero(t *testing.T) { 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) + } +}