diff --git a/cli/commands/play.go b/cli/commands/play.go index ace4ead..a322ffa 100644 --- a/cli/commands/play.go +++ b/cli/commands/play.go @@ -135,7 +135,6 @@ var run = func(cmd *cobra.Command, args []string) { snakes := buildSnakesFromOptions() var ruleset rules.Ruleset - var outOfBounds []rules.Point ruleset = getRuleset(Seed, Turn, snakes) state := initializeBoardFromArgs(ruleset, snakes) for _, snake := range snakes { @@ -145,18 +144,12 @@ var run = func(cmd *cobra.Command, args []string) { for v := false; !v; v, _ = ruleset.IsGameOver(state) { Turn++ ruleset = getRuleset(Seed, Turn, snakes) - state = createNextBoardState(ruleset, state, outOfBounds, snakes) - - // This is a massive hack to make Battle Royale rules work... - royaleRuleset, ok := ruleset.(*rules.RoyaleRuleset) - if ok { - outOfBounds = append([]rules.Point{}, royaleRuleset.OutOfBounds...) - } + state = createNextBoardState(ruleset, state, snakes) if ViewMap { - printMap(state, outOfBounds, Turn) + printMap(state, Turn) } else { - log.Printf("[%v]: State: %v OutOfBounds: %v\n", Turn, state, outOfBounds) + log.Printf("[%v]: State: %v\n", Turn, state) } if TurnDelay > 0 { @@ -196,12 +189,12 @@ func getRuleset(seed int64, gameTurn int32, snakes []Battlesnake) rules.Ruleset switch GameType { case "royale": + standard.HazardDamagePerTurn = 15 royale = rules.RoyaleRuleset{ StandardRuleset: standard, Seed: seed, Turn: gameTurn, ShrinkEveryNTurns: 10, - DamagePerTurn: 1, } ruleset = &royale case "squad": @@ -249,7 +242,7 @@ func initializeBoardFromArgs(ruleset rules.Ruleset, snakes []Battlesnake) *rules panic(err) } for _, snake := range snakes { - requestBody := getIndividualBoardStateForSnake(state, snake, nil, ruleset) + requestBody := getIndividualBoardStateForSnake(state, snake, ruleset) u, _ := url.ParseRequestURI(snake.URL) u.Path = path.Join(u.Path, "start") _, err = HttpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody)) @@ -260,13 +253,13 @@ func initializeBoardFromArgs(ruleset rules.Ruleset, snakes []Battlesnake) *rules return state } -func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, outOfBounds []rules.Point, snakes []Battlesnake) *rules.BoardState { +func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, snakes []Battlesnake) *rules.BoardState { var moves []rules.SnakeMove if Sequential { for _, snake := range snakes { for _, stateSnake := range state.Snakes { if snake.ID == stateSnake.ID && stateSnake.EliminatedCause == rules.NotEliminated { - moves = append(moves, getMoveForSnake(ruleset, state, snake, outOfBounds)) + moves = append(moves, getMoveForSnake(ruleset, state, snake)) } } } @@ -278,7 +271,7 @@ func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, outOfB for _, stateSnake := range state.Snakes { if snake.ID == stateSnake.ID && stateSnake.EliminatedCause == rules.NotEliminated { wg.Add(1) - go getConcurrentMoveForSnake(&wg, ruleset, state, snake, outOfBounds, c) + go getConcurrentMoveForSnake(&wg, ruleset, state, snake, c) } } } @@ -303,13 +296,13 @@ func createNextBoardState(ruleset rules.Ruleset, state *rules.BoardState, outOfB return state } -func getConcurrentMoveForSnake(wg *sync.WaitGroup, ruleset rules.Ruleset, state *rules.BoardState, snake Battlesnake, outOfBounds []rules.Point, c chan rules.SnakeMove) { +func getConcurrentMoveForSnake(wg *sync.WaitGroup, ruleset rules.Ruleset, state *rules.BoardState, snake Battlesnake, c chan rules.SnakeMove) { defer wg.Done() - c <- getMoveForSnake(ruleset, state, snake, outOfBounds) + c <- getMoveForSnake(ruleset, state, snake) } -func getMoveForSnake(ruleset rules.Ruleset, state *rules.BoardState, snake Battlesnake, outOfBounds []rules.Point) rules.SnakeMove { - requestBody := getIndividualBoardStateForSnake(state, snake, outOfBounds, ruleset) +func getMoveForSnake(ruleset rules.Ruleset, state *rules.BoardState, snake Battlesnake) rules.SnakeMove { + requestBody := getIndividualBoardStateForSnake(state, snake, ruleset) u, _ := url.ParseRequestURI(snake.URL) u.Path = path.Join(u.Path, "move") res, err := HttpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody)) @@ -336,7 +329,7 @@ func getMoveForSnake(ruleset rules.Ruleset, state *rules.BoardState, snake Battl } func sendEndRequest(ruleset rules.Ruleset, state *rules.BoardState, snake Battlesnake) { - requestBody := getIndividualBoardStateForSnake(state, snake, nil, ruleset) + requestBody := getIndividualBoardStateForSnake(state, snake, ruleset) u, _ := url.ParseRequestURI(snake.URL) u.Path = path.Join(u.Path, "end") _, err := HttpClient.Post(u.String(), "application/json", bytes.NewBuffer(requestBody)) @@ -345,7 +338,7 @@ func sendEndRequest(ruleset rules.Ruleset, state *rules.BoardState, snake Battle } } -func getIndividualBoardStateForSnake(state *rules.BoardState, snake Battlesnake, outOfBounds []rules.Point, ruleset rules.Ruleset) []byte { +func getIndividualBoardStateForSnake(state *rules.BoardState, snake Battlesnake, ruleset rules.Ruleset) []byte { var youSnake rules.Snake for _, snk := range state.Snakes { if snake.ID == snk.ID { @@ -363,7 +356,7 @@ func getIndividualBoardStateForSnake(state *rules.BoardState, snake Battlesnake, Height: state.Height, Width: state.Width, Food: coordFromPointArray(state.Food), - Hazards: coordFromPointArray(outOfBounds), + Hazards: coordFromPointArray(state.Hazards), Snakes: buildSnakesResponse(state.Snakes), }, You: snakeResponseFromSnake(youSnake), @@ -490,7 +483,7 @@ func buildSnakesFromOptions() []Battlesnake { return snakes } -func printMap(state *rules.BoardState, outOfBounds []rules.Point, gameTurn int32) { +func printMap(state *rules.BoardState, gameTurn int32) { var o bytes.Buffer o.WriteString(fmt.Sprintf("Ruleset: %s, Seed: %d, Turn: %v\n", GameType, Seed, gameTurn)) board := make([][]rune, state.Width) @@ -502,10 +495,10 @@ func printMap(state *rules.BoardState, outOfBounds []rules.Point, gameTurn int32 board[x][y] = '◦' } } - for _, oob := range outOfBounds { + for _, oob := range state.Hazards { board[oob.X][oob.Y] = '░' } - o.WriteString(fmt.Sprintf("Hazards ░: %v\n", outOfBounds)) + o.WriteString(fmt.Sprintf("Hazards ░: %v\n", state.Hazards)) for _, f := range state.Food { board[f.X][f.Y] = '⚕' } diff --git a/cli/commands/play_test.go b/cli/commands/play_test.go index 5545777..26dfdc4 100644 --- a/cli/commands/play_test.go +++ b/cli/commands/play_test.go @@ -1,9 +1,10 @@ package commands import ( + "testing" + "github.com/BattlesnakeOfficial/rules" "github.com/stretchr/testify/require" - "testing" ) func TestGetIndividualBoardStateForSnake(t *testing.T) { @@ -14,8 +15,8 @@ func TestGetIndividualBoardStateForSnake(t *testing.T) { Width: 11, Snakes: []rules.Snake{s1, s2}, } - bs := Battlesnake{Name: "one", URL: "", ID: "one"} - requestBody := getIndividualBoardStateForSnake(state, bs, nil, &rules.StandardRuleset{}) + snake := Battlesnake{Name: "one", URL: "", ID: "one"} + requestBody := getIndividualBoardStateForSnake(state, snake, &rules.StandardRuleset{}) expected := "{\"game\":{\"id\":\"\",\"timeout\":500,\"ruleset\":{\"name\":\"standard\",\"version\":\"cli\"}},\"turn\":0,\"board\":{\"height\":11,\"width\":11,\"food\":[],\"hazards\":[],\"snakes\":[{\"id\":\"one\",\"name\":\"\",\"health\":0,\"body\":[{\"x\":3,\"y\":3}],\"latency\":\"0\",\"head\":{\"x\":3,\"y\":3},\"length\":1,\"shout\":\"\",\"squad\":\"\"},{\"id\":\"two\",\"name\":\"\",\"health\":0,\"body\":[{\"x\":4,\"y\":3}],\"latency\":\"0\",\"head\":{\"x\":4,\"y\":3},\"length\":1,\"shout\":\"\",\"squad\":\"\"}]},\"you\":{\"id\":\"one\",\"name\":\"\",\"health\":0,\"body\":[{\"x\":3,\"y\":3}],\"latency\":\"0\",\"head\":{\"x\":3,\"y\":3},\"length\":1,\"shout\":\"\",\"squad\":\"\"}}" require.Equal(t, expected, string(requestBody)) diff --git a/constrictor_test.go b/constrictor_test.go index cbf0506..6097ba4 100644 --- a/constrictor_test.go +++ b/constrictor_test.go @@ -124,6 +124,7 @@ func TestConstrictorCreateNextBoardState(t *testing.T) { for _, test := range tests { nextState, err := r.CreateNextBoardState(test.prevState, test.moves) require.NoError(t, err) - require.Equal(t, test.expectedState, nextState) + require.Equal(t, test.expectedState.Food, nextState.Food) + require.Equal(t, test.expectedState.Snakes, nextState.Snakes) } } diff --git a/royale.go b/royale.go index 49c75b4..ecd2c67 100644 --- a/royale.go +++ b/royale.go @@ -10,19 +10,16 @@ type RoyaleRuleset struct { Seed int64 + // TODO: move Turn into BoardState? Turn int32 ShrinkEveryNTurns int32 - DamagePerTurn int32 - - // Output - OutOfBounds []Point } func (r *RoyaleRuleset) Name() string { return "royale" } func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { - if r.ShrinkEveryNTurns < 1 { - return nil, errors.New("royale game must shrink at least every turn") + if r.StandardRuleset.HazardDamagePerTurn < 1 { + return nil, errors.New("royale damage per turn must be greater than zero") } nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves) @@ -30,26 +27,8 @@ func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []Snak return nil, err } - // Algorithm: - // - Populate OOB for last turn - // - Apply damage to snake heads that are OOB - // - Re-populate OOB for this turn - // ---> This means damage on board shrinks doesn't hit until the following turn. - - // TODO: LOG? - err = r.populateOutOfBounds(nextBoardState, r.Turn-1) - if err != nil { - return nil, err - } - - // TODO: LOG? - err = r.damageOutOfBounds(nextBoardState) - if err != nil { - return nil, err - } - - // TODO: LOG? - err = r.populateOutOfBounds(nextBoardState, r.Turn) + // Royale's only job is now to populate the hazards for next turn - StandardRuleset takes care of applying hazard damage. + err = r.populateHazards(nextBoardState, r.Turn) if err != nil { return nil, err } @@ -57,11 +36,11 @@ func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []Snak return nextBoardState, nil } -func (r *RoyaleRuleset) populateOutOfBounds(b *BoardState, turn int32) error { - r.OutOfBounds = []Point{} +func (r *RoyaleRuleset) populateHazards(b *BoardState, turn int32) error { + b.Hazards = []Point{} if r.ShrinkEveryNTurns < 1 { - return errors.New("royale game must shrink at least every turn") + return errors.New("royale game can't shrink more frequently than every turn") } if turn < r.ShrinkEveryNTurns { @@ -89,34 +68,7 @@ func (r *RoyaleRuleset) populateOutOfBounds(b *BoardState, turn int32) error { for x := int32(0); x < b.Width; x++ { for y := int32(0); y < b.Height; y++ { if x < minX || x > maxX || y < minY || y > maxY { - r.OutOfBounds = append(r.OutOfBounds, Point{x, y}) - } - } - } - - return nil -} - -func (r *RoyaleRuleset) damageOutOfBounds(b *BoardState) error { - if r.DamagePerTurn < 1 { - return errors.New("royale damage per turn must be greater than zero") - } - - for i := 0; i < len(b.Snakes); i++ { - snake := &b.Snakes[i] - if snake.EliminatedCause == NotEliminated { - head := snake.Body[0] - for _, p := range r.OutOfBounds { - if head == p { - // Snake is now out of bounds, reduce health - snake.Health = snake.Health - r.DamagePerTurn - if snake.Health < 0 { - snake.Health = 0 - } - if r.StandardRuleset.snakeIsOutOfHealth(snake) { - snake.EliminatedCause = EliminatedByOutOfHealth - } - } + b.Hazards = append(b.Hazards, Point{x, y}) } } } diff --git a/royale_test.go b/royale_test.go index 5e955bf..8dba985 100644 --- a/royale_test.go +++ b/royale_test.go @@ -13,14 +13,20 @@ func TestRoyaleRulesetInterface(t *testing.T) { func TestRoyaleDefaultSanity(t *testing.T) { boardState := &BoardState{} - r := RoyaleRuleset{} + r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 1}, ShrinkEveryNTurns: 0} _, err := r.CreateNextBoardState(boardState, []SnakeMove{}) require.Error(t, err) - require.Equal(t, errors.New("royale game must shrink at least every turn"), err) + require.Equal(t, errors.New("royale game can't shrink more frequently than every turn"), err) - r = RoyaleRuleset{ShrinkEveryNTurns: 1, DamagePerTurn: 1} + r = RoyaleRuleset{ShrinkEveryNTurns: 1} _, err = r.CreateNextBoardState(boardState, []SnakeMove{}) + require.Error(t, err) + require.Equal(t, errors.New("royale damage per turn must be greater than zero"), err) + + r = RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 1}, ShrinkEveryNTurns: 1} + boardState, err = r.CreateNextBoardState(boardState, []SnakeMove{}) require.NoError(t, err) + require.Len(t, boardState.Hazards, 0) } func TestRoyaleName(t *testing.T) { @@ -28,56 +34,56 @@ func TestRoyaleName(t *testing.T) { require.Equal(t, "royale", r.Name()) } -func TestRoyaleOutOfBounds(t *testing.T) { +func TestRoyaleHazards(t *testing.T) { seed := int64(25543234525) tests := []struct { - Width int32 - Height int32 - Turn int32 - ShrinkEveryNTurns int32 - Error error - ExpectedOutOfBounds []Point + Width int32 + Height int32 + Turn int32 + ShrinkEveryNTurns int32 + Error error + ExpectedHazards []Point }{ - {Error: errors.New("royale game must shrink at least every turn")}, - {ShrinkEveryNTurns: 1, ExpectedOutOfBounds: []Point{}}, - {Turn: 1, ShrinkEveryNTurns: 1, ExpectedOutOfBounds: []Point{}}, - {Width: 3, Height: 3, Turn: 1, ShrinkEveryNTurns: 10, ExpectedOutOfBounds: []Point{}}, - {Width: 3, Height: 3, Turn: 9, ShrinkEveryNTurns: 10, ExpectedOutOfBounds: []Point{}}, + {Error: errors.New("royale game can't shrink more frequently than every turn")}, + {ShrinkEveryNTurns: 1, ExpectedHazards: []Point{}}, + {Turn: 1, ShrinkEveryNTurns: 1, ExpectedHazards: []Point{}}, + {Width: 3, Height: 3, Turn: 1, ShrinkEveryNTurns: 10, ExpectedHazards: []Point{}}, + {Width: 3, Height: 3, Turn: 9, ShrinkEveryNTurns: 10, ExpectedHazards: []Point{}}, { Width: 3, Height: 3, Turn: 10, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}}, + ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}}, }, { Width: 3, Height: 3, Turn: 11, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}}, + ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}}, }, { Width: 3, Height: 3, Turn: 19, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}}, + ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}}, }, { Width: 3, Height: 3, Turn: 20, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 2}, {2, 2}}, + ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 2}, {2, 2}}, }, { Width: 3, Height: 3, Turn: 31, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 1}, {1, 2}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 1}, {1, 2}, {2, 1}, {2, 2}}, }, { Width: 3, Height: 3, Turn: 42, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, }, { Width: 3, Height: 3, Turn: 53, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, }, { Width: 3, Height: 3, Turn: 64, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, }, { Width: 3, Height: 3, Turn: 6987, ShrinkEveryNTurns: 10, - ExpectedOutOfBounds: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, }, } @@ -87,19 +93,22 @@ func TestRoyaleOutOfBounds(t *testing.T) { Height: test.Height, } r := RoyaleRuleset{ + StandardRuleset: StandardRuleset{ + HazardDamagePerTurn: 1, + }, Seed: seed, Turn: test.Turn, ShrinkEveryNTurns: test.ShrinkEveryNTurns, } - err := r.populateOutOfBounds(b, test.Turn) + err := r.populateHazards(b, test.Turn) require.Equal(t, test.Error, err) if err == nil { // Obstacles should match - require.Equal(t, test.ExpectedOutOfBounds, r.OutOfBounds) - for _, expectedP := range test.ExpectedOutOfBounds { + require.Equal(t, test.ExpectedHazards, b.Hazards) + for _, expectedP := range test.ExpectedHazards { wasFound := false - for _, actualP := range r.OutOfBounds { + for _, actualP := range b.Hazards { if expectedP == actualP { wasFound = true break @@ -111,134 +120,57 @@ func TestRoyaleOutOfBounds(t *testing.T) { } } -func TestRoyaleDamageOutOfBounds(t *testing.T) { - tests := []struct { - Snakes []Snake - OutOfBounds []Point - ExpectedEliminatedCauses []string - ExpectedEliminatedByIDs []string - }{ - {}, - { - Snakes: []Snake{{Body: []Point{{0, 0}}}}, - OutOfBounds: []Point{}, - ExpectedEliminatedCauses: []string{NotEliminated}, - ExpectedEliminatedByIDs: []string{""}, - }, - { - Snakes: []Snake{{Body: []Point{{0, 0}}}}, - OutOfBounds: []Point{{0, 0}}, - ExpectedEliminatedCauses: []string{EliminatedByOutOfHealth}, - ExpectedEliminatedByIDs: []string{""}, - }, - { - Snakes: []Snake{{Body: []Point{{0, 0}, {1, 0}, {2, 0}}}}, - OutOfBounds: []Point{{1, 0}, {2, 0}}, - ExpectedEliminatedCauses: []string{NotEliminated}, - ExpectedEliminatedByIDs: []string{""}, - }, - { - Snakes: []Snake{ - {Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, - {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, - }, - OutOfBounds: []Point{{1, 0}, {2, 0}, {3, 4}, {3, 5}, {3, 6}}, - ExpectedEliminatedCauses: []string{NotEliminated, NotEliminated}, - ExpectedEliminatedByIDs: []string{"", ""}, - }, - { - Snakes: []Snake{ - {Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, - {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, - }, - OutOfBounds: []Point{{3, 3}}, - ExpectedEliminatedCauses: []string{NotEliminated, EliminatedByOutOfHealth}, - ExpectedEliminatedByIDs: []string{"", ""}, - }, - } - - for _, test := range tests { - b := &BoardState{Snakes: test.Snakes} - r := RoyaleRuleset{OutOfBounds: test.OutOfBounds, DamagePerTurn: 100} - err := r.damageOutOfBounds(b) - require.NoError(t, err) - - for i, snake := range b.Snakes { - require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause) - } - - } -} - -func TestRoyaleDamagePerTurn(t *testing.T) { - tests := []struct { - Health int32 - DamagePerTurn int32 - ExpectedHealth int32 - ExpectedEliminationCause string - Error error - }{ - {100, 0, 100, NotEliminated, errors.New("royale damage per turn must be greater than zero")}, - {100, -100, 100, NotEliminated, errors.New("royale damage per turn must be greater than zero")}, - {100, 1, 99, NotEliminated, nil}, - {100, 99, 1, NotEliminated, nil}, - {100, 100, 0, EliminatedByOutOfHealth, nil}, - {100, 101, 0, EliminatedByOutOfHealth, nil}, - {100, 999, 0, EliminatedByOutOfHealth, nil}, - {2, 1, 1, NotEliminated, nil}, - {1, 1, 0, EliminatedByOutOfHealth, nil}, - {1, 999, 0, EliminatedByOutOfHealth, nil}, - {0, 1, 0, EliminatedByOutOfHealth, nil}, - {0, 999, 0, EliminatedByOutOfHealth, nil}, - } - - for _, test := range tests { - b := &BoardState{Snakes: []Snake{{Health: test.Health, Body: []Point{{0, 0}}}}} - r := RoyaleRuleset{OutOfBounds: []Point{{0, 0}}, DamagePerTurn: test.DamagePerTurn} - - err := r.damageOutOfBounds(b) - require.Equal(t, test.Error, err) - require.Equal(t, test.ExpectedHealth, b.Snakes[0].Health) - require.Equal(t, test.ExpectedEliminationCause, b.Snakes[0].EliminatedCause) - } -} - func TestRoyalDamageNextTurn(t *testing.T) { seed := int64(45897034512311) - b := &BoardState{Width: 10, Height: 10, Snakes: []Snake{{ID: "one", Health: 100, Body: []Point{{9, 1}}}}} - r := RoyaleRuleset{Seed: seed, ShrinkEveryNTurns: 10, DamagePerTurn: 30} + base := &BoardState{Width: 10, Height: 10, Snakes: []Snake{{ID: "one", Health: 100, Body: []Point{{9, 1}, {9, 1}, {9, 1}}}}} + r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 30}, Seed: seed, ShrinkEveryNTurns: 10} m := []SnakeMove{{ID: "one", Move: "down"}} r.Turn = 10 - n, err := r.CreateNextBoardState(b, m) + err := r.populateHazards(base, r.Turn-1) require.NoError(t, err) - require.Equal(t, NotEliminated, n.Snakes[0].EliminatedCause) - require.Equal(t, int32(99), n.Snakes[0].Health) - require.Equal(t, Point{9, 0}, n.Snakes[0].Body[0]) - require.Equal(t, 10, len(r.OutOfBounds)) // X = 0 + next, err := r.CreateNextBoardState(base, m) + require.NoError(t, err) + require.Equal(t, NotEliminated, next.Snakes[0].EliminatedCause) + require.Equal(t, int32(99), next.Snakes[0].Health) + require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0]) + require.Equal(t, 10, len(next.Hazards)) // X = 0 r.Turn = 20 - n, err = r.CreateNextBoardState(b, m) + err = r.populateHazards(base, r.Turn-1) require.NoError(t, err) - require.Equal(t, NotEliminated, n.Snakes[0].EliminatedCause) - require.Equal(t, int32(99), n.Snakes[0].Health) - require.Equal(t, Point{9, 0}, n.Snakes[0].Body[0]) - require.Equal(t, 20, len(r.OutOfBounds)) // X = 9 + next, err = r.CreateNextBoardState(base, m) + require.NoError(t, err) + require.Equal(t, NotEliminated, next.Snakes[0].EliminatedCause) + require.Equal(t, int32(99), next.Snakes[0].Health) + require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0]) + require.Equal(t, 20, len(next.Hazards)) // X = 9 r.Turn = 21 - n, err = r.CreateNextBoardState(b, m) + err = r.populateHazards(base, r.Turn-1) require.NoError(t, err) - require.Equal(t, NotEliminated, n.Snakes[0].EliminatedCause) - require.Equal(t, int32(69), n.Snakes[0].Health) - require.Equal(t, Point{9, 0}, n.Snakes[0].Body[0]) - require.Equal(t, 20, len(r.OutOfBounds)) + next, err = r.CreateNextBoardState(base, m) + require.NoError(t, err) + require.Equal(t, NotEliminated, next.Snakes[0].EliminatedCause) + require.Equal(t, int32(69), next.Snakes[0].Health) + require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0]) + require.Equal(t, 20, len(next.Hazards)) - b.Snakes[0].Health = 15 - n, err = r.CreateNextBoardState(b, m) + base.Snakes[0].Health = 15 + next, err = r.CreateNextBoardState(base, m) require.NoError(t, err) - require.Equal(t, EliminatedByOutOfHealth, n.Snakes[0].EliminatedCause) - require.Equal(t, int32(0), n.Snakes[0].Health) - require.Equal(t, Point{9, 0}, n.Snakes[0].Body[0]) - require.Equal(t, 20, len(r.OutOfBounds)) + require.Equal(t, EliminatedByOutOfHealth, next.Snakes[0].EliminatedCause) + require.Equal(t, int32(0), next.Snakes[0].Health) + require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0]) + require.Equal(t, 20, len(next.Hazards)) + + base.Food = append(base.Food, Point{9, 0}) + next, err = r.CreateNextBoardState(base, m) + require.NoError(t, err) + require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0]) + require.Equal(t, NotEliminated, next.Snakes[0].EliminatedCause) + require.Equal(t, int32(100), next.Snakes[0].Health) + require.Equal(t, Point{9, 0}, next.Snakes[0].Body[0]) + require.Equal(t, 20, len(next.Hazards)) } diff --git a/ruleset.go b/ruleset.go index 6f79f61..bc442a8 100644 --- a/ruleset.go +++ b/ruleset.go @@ -47,10 +47,11 @@ type Snake struct { } type BoardState struct { - Height int32 - Width int32 - Food []Point - Snakes []Snake + Height int32 + Width int32 + Food []Point + Snakes []Snake + Hazards []Point } type SnakeMove struct { diff --git a/standard.go b/standard.go index 0997c7f..19125b8 100644 --- a/standard.go +++ b/standard.go @@ -6,8 +6,9 @@ import ( ) type StandardRuleset struct { - FoodSpawnChance int32 // [0, 100] - MinimumFood int32 + FoodSpawnChance int32 // [0, 100] + MinimumFood int32 + HazardDamagePerTurn int32 } func (r *StandardRuleset) Name() string { return "standard" } @@ -50,14 +51,14 @@ func (r *StandardRuleset) placeSnakesFixed(b *BoardState) error { // Create start 8 points mn, md, mx := int32(1), (b.Width-1)/2, b.Width-2 startPoints := []Point{ - Point{mn, mn}, - Point{mn, md}, - Point{mn, mx}, - Point{md, mn}, - Point{md, mx}, - Point{mx, mn}, - Point{mx, md}, - Point{mx, mx}, + {mn, mn}, + {mn, md}, + {mn, mx}, + {md, mn}, + {md, mx}, + {mx, mn}, + {mx, md}, + {mx, mx}, } // Sanity check @@ -107,10 +108,10 @@ func (r *StandardRuleset) placeFoodFixed(b *BoardState) error { for i := 0; i < len(b.Snakes); i++ { snakeHead := b.Snakes[i].Body[0] possibleFoodLocations := []Point{ - Point{snakeHead.X - 1, snakeHead.Y - 1}, - Point{snakeHead.X - 1, snakeHead.Y + 1}, - Point{snakeHead.X + 1, snakeHead.Y - 1}, - Point{snakeHead.X + 1, snakeHead.Y + 1}, + {snakeHead.X - 1, snakeHead.Y - 1}, + {snakeHead.X - 1, snakeHead.Y + 1}, + {snakeHead.X + 1, snakeHead.Y - 1}, + {snakeHead.X + 1, snakeHead.Y + 1}, } availableFoodLocations := []Point{} @@ -175,10 +176,11 @@ func (r *StandardRuleset) isKnownBoardSize(b *BoardState) bool { func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { // We specifically want to copy prevState, so as not to alter it directly. nextState := &BoardState{ - Height: prevState.Height, - Width: prevState.Width, - Food: append([]Point{}, prevState.Food...), - Snakes: make([]Snake, len(prevState.Snakes)), + Height: prevState.Height, + Width: prevState.Width, + Food: append([]Point{}, prevState.Food...), + Snakes: make([]Snake, len(prevState.Snakes)), + Hazards: append([]Point{}, prevState.Hazards...), } for i := 0; i < len(prevState.Snakes); i++ { nextState.Snakes[i].ID = prevState.Snakes[i].ID @@ -202,6 +204,11 @@ func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []Sn return nil, err } + err = r.maybeDamageHazards(nextState) + if err != nil { + return nil, err + } + // TODO: LOG? // bvanvugt: We specifically want this to happen before elimination for two reasons: // 1) We want snakes to be able to eat on their very last turn and still survive. @@ -307,6 +314,41 @@ func (r *StandardRuleset) reduceSnakeHealth(b *BoardState) error { return nil } +func (r *StandardRuleset) maybeDamageHazards(b *BoardState) error { + for i := 0; i < len(b.Snakes); i++ { + snake := &b.Snakes[i] + if snake.EliminatedCause != NotEliminated { + continue + } + head := snake.Body[0] + for _, p := range b.Hazards { + if head == p { + // If there's a food in this square, don't reduce health + foundFood := false + for _, food := range b.Food { + if p == food { + foundFood = true + } + } + if foundFood { + continue + } + + // Snake is in a hazard, reduce health + snake.Health = snake.Health - r.HazardDamagePerTurn + if snake.Health < 0 { + snake.Health = 0 + } + if r.snakeIsOutOfHealth(snake) { + snake.EliminatedCause = EliminatedByOutOfHealth + } + } + } + } + + return nil +} + func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error { // First order snake indices by length. // In multi-collision scenarios we want to always attribute elimination to the longest snake. diff --git a/standard_test.go b/standard_test.go index c938d92..e103bb1 100644 --- a/standard_test.go +++ b/standard_test.go @@ -74,6 +74,7 @@ func TestCreateInitialBoardState(t *testing.T) { require.Equal(t, id, state.Snakes[i].ID) } require.Len(t, state.Food, test.ExpectedNumFood, testNum) + require.Len(t, state.Hazards, 0, testNum) } } @@ -489,7 +490,8 @@ func TestCreateNextBoardState(t *testing.T) { Health: 100, }, }, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{0, 0}, {1, 0}}, + Hazards: []Point{}, }, []SnakeMove{}, ErrorNoMoveFound, @@ -511,7 +513,8 @@ func TestCreateNextBoardState(t *testing.T) { Health: 100, }, }, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{0, 0}, {1, 0}}, + Hazards: []Point{}, }, []SnakeMove{ {ID: "one", Move: MoveUp}, @@ -542,7 +545,8 @@ func TestCreateNextBoardState(t *testing.T) { EliminatedCause: EliminatedByOutOfBounds, }, }, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{0, 0}, {1, 0}}, + Hazards: []Point{}, }, []SnakeMove{ {ID: "one", Move: MoveDown}, @@ -571,7 +575,8 @@ func TestCreateNextBoardState(t *testing.T) { EliminatedCause: EliminatedByOutOfBounds, }, }, - Food: []Point{{0, 0}}, + Food: []Point{{0, 0}}, + Hazards: []Point{}, }, }, } @@ -580,7 +585,13 @@ func TestCreateNextBoardState(t *testing.T) { for _, test := range tests { nextState, err := r.CreateNextBoardState(test.prevState, test.moves) require.Equal(t, test.expectedError, err) - require.Equal(t, test.expectedState, nextState) + if test.expectedState != nil { + require.Equal(t, test.expectedState.Width, nextState.Width) + require.Equal(t, test.expectedState.Height, nextState.Height) + require.Equal(t, test.expectedState.Food, nextState.Food) + require.Equal(t, test.expectedState.Snakes, nextState.Snakes) + require.Equal(t, test.expectedState.Hazards, nextState.Hazards) + } } } @@ -645,7 +656,12 @@ func TestEatingOnLastMove(t *testing.T) { for _, test := range tests { nextState, err := r.CreateNextBoardState(test.prevState, test.moves) require.Equal(t, err, test.expectedError) - require.Equal(t, nextState, test.expectedState) + if test.expectedState != nil { + require.Equal(t, test.expectedState.Width, nextState.Width) + require.Equal(t, test.expectedState.Height, nextState.Height) + require.Equal(t, test.expectedState.Food, nextState.Food) + require.Equal(t, test.expectedState.Snakes, nextState.Snakes) + } } } @@ -757,7 +773,12 @@ func TestHeadToHeadOnFood(t *testing.T) { for _, test := range tests { nextState, err := r.CreateNextBoardState(test.prevState, test.moves) require.Equal(t, test.expectedError, err) - require.Equal(t, test.expectedState, nextState) + if test.expectedState != nil { + require.Equal(t, test.expectedState.Width, nextState.Width) + require.Equal(t, test.expectedState.Height, nextState.Height) + require.Equal(t, test.expectedState.Food, nextState.Food) + require.Equal(t, test.expectedState.Snakes, nextState.Snakes) + } } } @@ -829,7 +850,12 @@ func TestRegressionIssue19(t *testing.T) { for _, test := range tests { nextState, err := r.CreateNextBoardState(test.prevState, test.moves) require.Equal(t, err, test.expectedError) - require.Equal(t, nextState, test.expectedState) + if test.expectedState != nil { + require.Equal(t, test.expectedState.Width, nextState.Width) + require.Equal(t, test.expectedState.Height, nextState.Height) + require.Equal(t, test.expectedState.Food, nextState.Food) + require.Equal(t, test.expectedState.Snakes, nextState.Snakes) + } } } @@ -1190,7 +1216,7 @@ func TestSnakeIsOutOfBounds(t *testing.T) { s := Snake{Body: []Point{test.Point}} require.Equal(t, test.Expected, r.snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Head%+v", test.Point) // Test with point as body - s = Snake{Body: []Point{Point{0, 0}, Point{0, 0}, test.Point}} + s = Snake{Body: []Point{{0, 0}, {0, 0}, test.Point}} require.Equal(t, test.Expected, r.snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Body%+v", test.Point) } } @@ -1366,7 +1392,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Zero Snake", []Snake{ - Snake{}, + {}, }, []string{NotEliminated}, []string{""}, @@ -1375,7 +1401,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Single Starvation", []Snake{ - Snake{ID: "1", Body: []Point{{1, 1}}}, + {ID: "1", Body: []Point{{1, 1}}}, }, []string{EliminatedByOutOfHealth}, []string{""}, @@ -1384,7 +1410,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Not Eliminated", []Snake{ - Snake{ID: "1", Health: 1, Body: []Point{{1, 1}}}, + {ID: "1", Health: 1, Body: []Point{{1, 1}}}, }, []string{NotEliminated}, []string{""}, @@ -1393,7 +1419,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Out of Bounds", []Snake{ - Snake{ID: "1", Health: 1, Body: []Point{{-1, 1}}}, + {ID: "1", Health: 1, Body: []Point{{-1, 1}}}, }, []string{EliminatedByOutOfBounds}, []string{""}, @@ -1402,7 +1428,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Self Collision", []Snake{ - Snake{ID: "1", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}}, + {ID: "1", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}}, }, []string{EliminatedBySelfCollision}, []string{"1"}, @@ -1411,8 +1437,8 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Multiple Separate Deaths", []Snake{ - Snake{ID: "1", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}}, - Snake{ID: "2", Health: 1, Body: []Point{{-1, 1}}}, + {ID: "1", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}}, + {ID: "2", Health: 1, Body: []Point{{-1, 1}}}, }, []string{ EliminatedBySelfCollision, @@ -1423,8 +1449,8 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "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}}}, + {ID: "1", Health: 1, Body: []Point{{0, 2}, {0, 3}, {0, 4}}}, + {ID: "2", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 2}}}, }, []string{ EliminatedByCollision, @@ -1435,9 +1461,9 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "All Eliminated Head 2 Head", []Snake{ - Snake{ID: "1", Health: 1, Body: []Point{{1, 1}}}, - Snake{ID: "2", Health: 1, Body: []Point{{1, 1}}}, - Snake{ID: "3", Health: 1, Body: []Point{{1, 1}}}, + {ID: "1", Health: 1, Body: []Point{{1, 1}}}, + {ID: "2", Health: 1, Body: []Point{{1, 1}}}, + {ID: "3", Health: 1, Body: []Point{{1, 1}}}, }, []string{ EliminatedByHeadToHeadCollision, @@ -1450,9 +1476,9 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "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}}}, - Snake{ID: "3", Health: 1, Body: []Point{{1, 1}}}, + {ID: "1", Health: 1, Body: []Point{{1, 1}, {0, 1}}}, + {ID: "2", Health: 1, Body: []Point{{1, 1}, {1, 2}, {1, 3}}}, + {ID: "3", Health: 1, Body: []Point{{1, 1}}}, }, []string{ EliminatedByHeadToHeadCollision, @@ -1465,11 +1491,11 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "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}}}, - Snake{ID: "3", Health: 1, Body: []Point{{2, 2}, {1, 1}}}, - Snake{ID: "4", Health: 1, Body: []Point{{1, 1}, {4, 4}}}, - Snake{ID: "5", Health: 1, Body: []Point{{4, 4}}}, // Body collision takes priority + {ID: "1", Health: 1, Body: []Point{{4, 4}, {3, 3}}}, + {ID: "2", Health: 1, Body: []Point{{3, 3}, {2, 2}}}, + {ID: "3", Health: 1, Body: []Point{{2, 2}, {1, 1}}}, + {ID: "4", Health: 1, Body: []Point{{1, 1}, {4, 4}}}, + {ID: "5", Health: 1, Body: []Point{{4, 4}}}, // Body collision takes priority }, []string{ EliminatedByCollision, @@ -1484,10 +1510,10 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "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}}}, - Snake{ID: "3", Health: 1, Body: []Point{{4, 4}, {5, 4}}}, - Snake{ID: "4", Health: 1, Body: []Point{{4, 4}, {3, 4}}}, + {ID: "1", Health: 1, Body: []Point{{4, 4}, {4, 5}}}, + {ID: "2", Health: 1, Body: []Point{{4, 4}, {4, 3}}}, + {ID: "3", Health: 1, Body: []Point{{4, 4}, {5, 4}}}, + {ID: "4", Health: 1, Body: []Point{{4, 4}, {3, 4}}}, }, []string{ EliminatedByHeadToHeadCollision, @@ -1501,10 +1527,10 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "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}}}, - Snake{ID: "3", Health: 1, Body: []Point{{4, 4}, {5, 4}, {6, 4}}}, - Snake{ID: "4", Health: 1, Body: []Point{{4, 4}, {3, 4}}}, + {ID: "1", Health: 1, Body: []Point{{4, 4}, {4, 5}}}, + {ID: "2", Health: 1, Body: []Point{{4, 4}, {4, 3}}}, + {ID: "3", Health: 1, Body: []Point{{4, 4}, {5, 4}, {6, 4}}}, + {ID: "4", Health: 1, Body: []Point{{4, 4}, {3, 4}}}, }, []string{ EliminatedByHeadToHeadCollision, @@ -1574,6 +1600,111 @@ func TestMaybeEliminateSnakesPriority(t *testing.T) { } } +func TestMaybeDamageHazards(t *testing.T) { + tests := []struct { + Snakes []Snake + Hazards []Point + Food []Point + ExpectedEliminatedCauses []string + ExpectedEliminatedByIDs []string + }{ + {}, + { + Snakes: []Snake{{Body: []Point{{0, 0}}}}, + Hazards: []Point{}, + ExpectedEliminatedCauses: []string{NotEliminated}, + ExpectedEliminatedByIDs: []string{""}, + }, + { + Snakes: []Snake{{Body: []Point{{0, 0}}}}, + Hazards: []Point{{0, 0}}, + ExpectedEliminatedCauses: []string{EliminatedByOutOfHealth}, + ExpectedEliminatedByIDs: []string{""}, + }, + { + Snakes: []Snake{{Body: []Point{{0, 0}}}}, + Hazards: []Point{{0, 0}}, + Food: []Point{{0, 0}}, + ExpectedEliminatedCauses: []string{NotEliminated}, + ExpectedEliminatedByIDs: []string{""}, + }, + { + Snakes: []Snake{{Body: []Point{{0, 0}, {1, 0}, {2, 0}}}}, + Hazards: []Point{{1, 0}, {2, 0}}, + ExpectedEliminatedCauses: []string{NotEliminated}, + ExpectedEliminatedByIDs: []string{""}, + }, + { + Snakes: []Snake{ + {Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, + {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, + }, + Hazards: []Point{{1, 0}, {2, 0}, {3, 4}, {3, 5}, {3, 6}}, + ExpectedEliminatedCauses: []string{NotEliminated, NotEliminated}, + ExpectedEliminatedByIDs: []string{"", ""}, + }, + { + Snakes: []Snake{ + {Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, + {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, + }, + Hazards: []Point{{3, 3}}, + ExpectedEliminatedCauses: []string{NotEliminated, EliminatedByOutOfHealth}, + ExpectedEliminatedByIDs: []string{"", ""}, + }, + } + + for _, test := range tests { + b := &BoardState{Snakes: test.Snakes, Hazards: test.Hazards, Food: test.Food} + r := StandardRuleset{HazardDamagePerTurn: 100} + err := r.maybeDamageHazards(b) + require.NoError(t, err) + + for i, snake := range b.Snakes { + require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause) + } + + } +} + +func TestHazardDamagePerTurn(t *testing.T) { + tests := []struct { + Health int32 + HazardDamagePerTurn int32 + Food bool + ExpectedHealth int32 + ExpectedEliminationCause string + Error error + }{ + {100, 1, false, 99, NotEliminated, nil}, + {100, 1, true, 100, NotEliminated, nil}, + {100, 99, false, 1, NotEliminated, nil}, + {100, 99, true, 100, NotEliminated, nil}, + {100, 100, false, 0, EliminatedByOutOfHealth, nil}, + {100, 101, false, 0, EliminatedByOutOfHealth, nil}, + {100, 999, false, 0, EliminatedByOutOfHealth, nil}, + {100, 100, true, 100, NotEliminated, nil}, + {2, 1, false, 1, NotEliminated, nil}, + {1, 1, false, 0, EliminatedByOutOfHealth, nil}, + {1, 999, false, 0, EliminatedByOutOfHealth, nil}, + {0, 1, false, 0, EliminatedByOutOfHealth, nil}, + {0, 999, false, 0, EliminatedByOutOfHealth, nil}, + } + + for _, test := range tests { + b := &BoardState{Snakes: []Snake{{Health: test.Health, Body: []Point{{0, 0}}}}, Hazards: []Point{{0, 0}}} + if test.Food { + b.Food = []Point{{0, 0}} + } + r := StandardRuleset{HazardDamagePerTurn: test.HazardDamagePerTurn} + + err := r.maybeDamageHazards(b) + require.Equal(t, test.Error, err) + require.Equal(t, test.ExpectedHealth, b.Snakes[0].Health) + require.Equal(t, test.ExpectedEliminationCause, b.Snakes[0].EliminatedCause) + } +} + func TestMaybeFeedSnakes(t *testing.T) { tests := []struct { Name string