diff --git a/cli/commands/play.go b/cli/commands/play.go index ac57ae1..27d5e28 100644 --- a/cli/commands/play.go +++ b/cli/commands/play.go @@ -214,6 +214,10 @@ func getRuleset(seed int64, gameTurn int32, snakes []Battlesnake) rules.Ruleset ruleset = &rules.SoloRuleset{ StandardRuleset: standard, } + case "wrapped": + ruleset = &rules.WrappedRuleset{ + StandardRuleset: standard, + } case "constrictor": ruleset = &rules.ConstrictorRuleset{ StandardRuleset: standard, diff --git a/wrapped.go b/wrapped.go new file mode 100644 index 0000000..bb0a27b --- /dev/null +++ b/wrapped.go @@ -0,0 +1,66 @@ +package rules + +type WrappedRuleset struct { + StandardRuleset +} + +func replace(value, min, max int32) int32 { + if value < min { + return max + } + if value > max { + return min + } + return value +} + +func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { + nextState := prevState.Clone() + + err := r.moveSnakes(nextState, moves) + if err != nil { + return nil, err + } + + err = r.reduceSnakeHealth(nextState) + if err != nil { + return nil, err + } + + err = r.maybeDamageHazards(nextState) + if err != nil { + return nil, err + } + + err = r.maybeFeedSnakes(nextState) + if err != nil { + return nil, err + } + + err = r.maybeSpawnFood(nextState) + if err != nil { + return nil, err + } + + err = r.maybeEliminateSnakes(nextState) + if err != nil { + return nil, err + } + + return nextState, nil +} + +func (r *WrappedRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error { + err := r.StandardRuleset.moveSnakes(b, moves) + if err != nil { + return err + } + + for i := 0; i < len(b.Snakes); i++ { + snake := &b.Snakes[i] + snake.Body[0].X = replace(snake.Body[0].X, 0, b.Width-1) + snake.Body[0].Y = replace(snake.Body[0].Y, 0, b.Height-1) + } + + return nil +} diff --git a/wrapped_test.go b/wrapped_test.go new file mode 100644 index 0000000..1ba3472 --- /dev/null +++ b/wrapped_test.go @@ -0,0 +1,248 @@ +package rules + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLeft(t *testing.T) { + boardState := &BoardState{ + Width: 11, + Height: 11, + Snakes: []Snake{ + {ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{0, 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{10, 10}}}, + }, + } + + snakeMoves := []SnakeMove{ + {ID: "bottomLeft", Move: "left"}, + {ID: "bottomRight", Move: "left"}, + {ID: "topLeft", Move: "left"}, + {ID: "topRight", Move: "left"}, + } + + r := WrappedRuleset{} + + nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + require.NoError(t, err) + require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes)) + + expectedSnakes := []Snake{ + {ID: "bottomLeft", Health: 10, Body: []Point{{10, 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{9, 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{10, 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{9, 10}}}, + } + for i, snake := range nextBoardState.Snakes { + require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedCause, snake.EliminatedCause, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedBy, snake.EliminatedBy, snake.ID) + require.Equal(t, expectedSnakes[i].Body, snake.Body, snake.ID) + } +} + +func TestRight(t *testing.T) { + boardState := &BoardState{ + Width: 11, + Height: 11, + Snakes: []Snake{ + {ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{0, 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{10, 10}}}, + }, + } + + snakeMoves := []SnakeMove{ + {ID: "bottomLeft", Move: "right"}, + {ID: "bottomRight", Move: "right"}, + {ID: "topLeft", Move: "right"}, + {ID: "topRight", Move: "right"}, + } + + r := WrappedRuleset{} + + nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + require.NoError(t, err) + require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes)) + + expectedSnakes := []Snake{ + {ID: "bottomLeft", Health: 10, Body: []Point{{1, 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{0, 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{1, 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{0, 10}}}, + } + for i, snake := range nextBoardState.Snakes { + require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedCause, snake.EliminatedCause, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedBy, snake.EliminatedBy, snake.ID) + require.Equal(t, expectedSnakes[i].Body, snake.Body, snake.ID) + } +} + +func TestUp(t *testing.T) { + boardState := &BoardState{ + Width: 11, + Height: 11, + Snakes: []Snake{ + {ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{0, 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{10, 10}}}, + }, + } + + snakeMoves := []SnakeMove{ + {ID: "bottomLeft", Move: "up"}, + {ID: "bottomRight", Move: "up"}, + {ID: "topLeft", Move: "up"}, + {ID: "topRight", Move: "up"}, + } + + r := WrappedRuleset{} + + nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + require.NoError(t, err) + require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes)) + + expectedSnakes := []Snake{ + {ID: "bottomLeft", Health: 10, Body: []Point{{0, 1}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{10, 1}}}, + {ID: "topLeft", Health: 10, Body: []Point{{0, 0}}}, + {ID: "topRight", Health: 10, Body: []Point{{10, 0}}}, + } + for i, snake := range nextBoardState.Snakes { + require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) + require.Equal(t, expectedSnakes[i].Body, snake.Body, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedCause, snake.EliminatedCause, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedBy, snake.EliminatedBy, snake.ID) + } +} + +func TestDown(t *testing.T) { + boardState := &BoardState{ + Width: 11, + Height: 11, + Snakes: []Snake{ + {ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{0, 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{10, 10}}}, + }, + } + + snakeMoves := []SnakeMove{ + {ID: "bottomLeft", Move: "down"}, + {ID: "bottomRight", Move: "down"}, + {ID: "topLeft", Move: "down"}, + {ID: "topRight", Move: "down"}, + } + + r := WrappedRuleset{} + + nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + require.NoError(t, err) + require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes)) + + expectedSnakes := []Snake{ + {ID: "bottomLeft", Health: 10, Body: []Point{{0, 10}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{10, 10}}}, + {ID: "topLeft", Health: 10, Body: []Point{{0, 9}}}, + {ID: "topRight", Health: 10, Body: []Point{{10, 9}}}, + } + for i, snake := range nextBoardState.Snakes { + require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) + require.Equal(t, expectedSnakes[i].Body, snake.Body, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedCause, snake.EliminatedCause, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedBy, snake.EliminatedBy, snake.ID) + } +} + +func TestEdgeCrossingCollision(t *testing.T) { + boardState := &BoardState{ + Width: 11, + Height: 11, + Snakes: []Snake{ + {ID: "left", Health: 10, Body: []Point{{0, 5}}}, + {ID: "rightEdge", Health: 10, Body: []Point{ + {10, 1}, + {10, 2}, + {10, 3}, + {10, 4}, + {10, 5}, + {10, 6}, + }}, + }, + } + + snakeMoves := []SnakeMove{ + {ID: "left", Move: "left"}, + {ID: "rightEdge", Move: "down"}, + } + + r := WrappedRuleset{} + + nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + require.NoError(t, err) + require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes)) + + expectedSnakes := []Snake{ + {ID: "left", Health: 0, Body: []Point{{10, 5}}, EliminatedCause: EliminatedByCollision, EliminatedBy: "rightEdge"}, + {ID: "rightEdge", Health: 10, Body: []Point{ + {10, 0}, + {10, 1}, + {10, 2}, + {10, 3}, + {10, 4}, + {10, 5}, + }}, + } + for i, snake := range nextBoardState.Snakes { + require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) + require.Equal(t, expectedSnakes[i].Body, snake.Body, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedCause, snake.EliminatedCause, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedBy, snake.EliminatedBy, snake.ID) + } +} + +func TestEdgeCrossingEating(t *testing.T) { + boardState := &BoardState{ + Width: 11, + Height: 11, + Snakes: []Snake{ + {ID: "left", Health: 10, Body: []Point{{0, 5}, {1, 5}}}, + {ID: "other", Health: 10, Body: []Point{{5, 5}}}, + }, + Food: []Point{ + {10, 5}, + }, + } + + snakeMoves := []SnakeMove{ + {ID: "left", Move: "left"}, + {ID: "other", Move: "left"}, + } + + r := WrappedRuleset{} + + nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + require.NoError(t, err) + require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes)) + + expectedSnakes := []Snake{ + {ID: "left", Health: 100, Body: []Point{{10, 5}, {0, 5}, {0, 5}}}, + {ID: "other", Health: 9, Body: []Point{{4, 5}}}, + } + for i, snake := range nextBoardState.Snakes { + require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedCause, snake.EliminatedCause, snake.ID) + require.Equal(t, expectedSnakes[i].EliminatedBy, snake.EliminatedBy, snake.ID) + require.Equal(t, expectedSnakes[i].Body, snake.Body, snake.ID) + require.Equal(t, expectedSnakes[i].Health, snake.Health, snake.ID) + + } +}