From 82e1999126e2ddeb000ba212608eb3426003419c Mon Sep 17 00:00:00 2001 From: Rob O'Dwyer Date: Fri, 28 Oct 2022 16:49:49 -0700 Subject: [PATCH] DEV-1761: New rules API (#118) * DEV-1761: Clean up Ruleset interface (#115) * remove legacy ruleset types and simplify ruleset interface * remove unnecessary settings argument from Ruleset interface * decouple rules.Settings from client API and store settings as strings * DEV 1761: Add new BoardState and Point fields (#117) * add Point.TTL, Point.Value, GameState and PointState to BoardState * allow maps to access BoardState.GameState,PointState * add PreUpdateBoard and refactor snail_mode with it * fix bug where an extra turn was printed to the console * fix formatting * fix lint errors Co-authored-by: JonathanArns --- board.go | 116 +++++-- board_test.go | 228 +++++++------ cases_test.go | 4 +- cli/commands/play.go | 133 +++++--- cli/commands/play_test.go | 88 ++--- client/fixtures_test.go | 26 +- client/models.go | 46 ++- client/models_test.go | 3 +- client/testdata/snake_request.json | 12 +- constrictor.go | 25 -- constrictor_test.go | 26 +- maps/arcade_maze.go | 11 +- maps/castle_wall.go | 18 +- maps/empty.go | 6 +- maps/empty_test.go | 116 ++----- maps/game_map.go | 33 +- maps/game_map_test.go | 18 +- maps/hazard_pits.go | 13 +- maps/hazard_pits_test.go | 4 +- maps/hazards.go | 64 +++- maps/hazards_test.go | 10 +- maps/healing_pools.go | 11 +- maps/healing_pools_test.go | 8 +- maps/helpers.go | 21 +- maps/helpers_test.go | 15 +- maps/registry_test.go | 16 +- maps/rivers_and_bridges.go | 30 +- maps/royale.go | 15 +- maps/sinkholes.go | 13 +- maps/sinkholes_test.go | 2 +- maps/snail_mode.go | 120 +++---- maps/solo_maze.go | 6 +- maps/standard.go | 10 +- maps/standard_test.go | 217 +++--------- pipeline.go | 61 ++-- pipeline_internal_test.go | 8 +- pipeline_test.go | 12 +- royale.go | 41 +-- royale_test.go | 76 ++--- ruleset.go | 180 ++-------- ruleset_internal_test.go | 31 +- ruleset_test.go | 124 +------ settings.go | 90 +++++ settings_test.go | 31 ++ solo.go | 19 -- solo_test.go | 47 +-- standard.go | 62 +--- standard_test.go | 519 ++++++++++++++--------------- wrapped.go | 20 -- wrapped_test.go | 154 +++++---- 50 files changed, 1349 insertions(+), 1610 deletions(-) create mode 100644 settings.go create mode 100644 settings_test.go diff --git a/board.go b/board.go index d9ab796..cb8f1e1 100644 --- a/board.go +++ b/board.go @@ -2,6 +2,9 @@ package rules import "fmt" +// BoardState represents the internal state of a game board. +// NOTE: use NewBoardState to construct these to ensure fields are initialized +// correctly and that tests are resilient to changes to this type. type BoardState struct { Turn int Height int @@ -9,15 +12,26 @@ type BoardState struct { Food []Point Snakes []Snake Hazards []Point + + // Generic game-level state for maps and rules stages to persist data between turns. + GameState map[string]string + + // Numeric state keyed to specific points, also persisted between turns. + PointState map[Point]int } type Point struct { - X int - Y int + X int `json:"X"` + Y int `json:"Y"` + TTL int `json:"TTL,omitempty"` + Value int `json:"Value,omitempty"` } // Makes it easier to copy sample points out of Go logs and test failures. func (p Point) GoString() string { + if p.TTL != 0 || p.Value != 0 { + return fmt.Sprintf("{X:%d, Y:%d, TTL:%d, Value:%d}", p.X, p.Y, p.TTL, p.Value) + } return fmt.Sprintf("{X:%d, Y:%d}", p.X, p.Y) } @@ -33,24 +47,34 @@ type Snake struct { // NewBoardState returns an empty but fully initialized BoardState func NewBoardState(width, height int) *BoardState { return &BoardState{ - Turn: 0, - Height: height, - Width: width, - Food: []Point{}, - Snakes: []Snake{}, - Hazards: []Point{}, + Turn: 0, + Height: height, + Width: width, + Food: []Point{}, + Snakes: []Snake{}, + Hazards: []Point{}, + GameState: map[string]string{}, + PointState: map[Point]int{}, } } // Clone returns a deep copy of prevState that can be safely modified without affecting the original func (prevState *BoardState) Clone() *BoardState { nextState := &BoardState{ - Turn: prevState.Turn, - Height: prevState.Height, - Width: prevState.Width, - Food: append([]Point{}, prevState.Food...), - Snakes: make([]Snake, len(prevState.Snakes)), - Hazards: append([]Point{}, prevState.Hazards...), + Turn: prevState.Turn, + Height: prevState.Height, + Width: prevState.Width, + Food: append([]Point{}, prevState.Food...), + Snakes: make([]Snake, len(prevState.Snakes)), + Hazards: append([]Point{}, prevState.Hazards...), + GameState: make(map[string]string, len(prevState.GameState)), + PointState: make(map[Point]int, len(prevState.PointState)), + } + for key, value := range prevState.GameState { + nextState.GameState[key] = value + } + for key, value := range prevState.PointState { + nextState.PointState[key] = value } for i := 0; i < len(prevState.Snakes); i++ { nextState.Snakes[i].ID = prevState.Snakes[i].ID @@ -63,6 +87,42 @@ func (prevState *BoardState) Clone() *BoardState { return nextState } +// Builder method to set Turn and return the modified BoardState. +func (state *BoardState) WithTurn(turn int) *BoardState { + state.Turn = turn + return state +} + +// Builder method to set Food and return the modified BoardState. +func (state *BoardState) WithFood(food []Point) *BoardState { + state.Food = food + return state +} + +// Builder method to set Hazards and return the modified BoardState. +func (state *BoardState) WithHazards(hazards []Point) *BoardState { + state.Hazards = hazards + return state +} + +// Builder method to set Snakes and return the modified BoardState. +func (state *BoardState) WithSnakes(snakes []Snake) *BoardState { + state.Snakes = snakes + return state +} + +// Builder method to set State and return the modified BoardState. +func (state *BoardState) WithGameState(gameState map[string]string) *BoardState { + state.GameState = gameState + return state +} + +// Builder method to set PointState and return the modified BoardState. +func (state *BoardState) WithPointState(pointState map[Point]int) *BoardState { + state.PointState = pointState + return state +} + // CreateDefaultBoardState is a convenience function for fully initializing a // "default" board state with snakes and food. // In a real game, the engine may generate the board without calling this @@ -120,16 +180,16 @@ func PlaceSnakesFixed(rand Rand, b *BoardState, snakeIDs []string) error { // Create start 8 points mn, md, mx := 1, (b.Width-1)/2, b.Width-2 cornerPoints := []Point{ - {mn, mn}, - {mn, mx}, - {mx, mn}, - {mx, mx}, + {X: mn, Y: mn}, + {X: mn, Y: mx}, + {X: mx, Y: mn}, + {X: mx, Y: mx}, } cardinalPoints := []Point{ - {mn, md}, - {md, mn}, - {md, mx}, - {mx, md}, + {X: mn, Y: md}, + {X: md, Y: mn}, + {X: md, Y: mx}, + {X: mx, Y: md}, } // Sanity check @@ -325,7 +385,7 @@ func PlaceFoodAutomatically(rand Rand, b *BoardState) error { // Deprecated: will be replaced by maps.PlaceFoodFixed func PlaceFoodFixed(rand Rand, b *BoardState) error { - centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2} + centerCoord := Point{X: (b.Width - 1) / 2, Y: (b.Height - 1) / 2} isSmallBoard := b.Width*b.Height < BoardSizeMedium*BoardSizeMedium // Up to 4 snakes can be placed such that food is nearby on small boards. @@ -335,10 +395,10 @@ func PlaceFoodFixed(rand Rand, b *BoardState) error { for i := 0; i < len(b.Snakes); i++ { snakeHead := b.Snakes[i].Body[0] possibleFoodLocations := []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}, + {X: snakeHead.X - 1, Y: snakeHead.Y - 1}, + {X: snakeHead.X - 1, Y: snakeHead.Y + 1}, + {X: snakeHead.X + 1, Y: snakeHead.Y - 1}, + {X: snakeHead.X + 1, Y: snakeHead.Y + 1}, } // Remove any invalid/unwanted positions @@ -448,7 +508,7 @@ func GetEvenUnoccupiedPoints(b *BoardState) []Point { // removeCenterCoord filters out the board's center point from a list of points. func removeCenterCoord(b *BoardState, points []Point) []Point { - centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2} + centerCoord := Point{X: (b.Width - 1) / 2, Y: (b.Height - 1) / 2} var noCenterPoints []Point for _, p := range points { if p != centerCoord { diff --git a/board_test.go b/board_test.go index 730d831..827bb4f 100644 --- a/board_test.go +++ b/board_test.go @@ -8,6 +8,30 @@ import ( "github.com/stretchr/testify/require" ) +func TestBoardStateClone(t *testing.T) { + empty := &BoardState{} + require.Equal(t, NewBoardState(0, 0), empty.Clone()) + + full := NewBoardState(11, 11). + WithTurn(99). + WithFood([]Point{{X: 1, Y: 2, TTL: 10, Value: 100}}). + WithHazards([]Point{{X: 3, Y: 4, TTL: 5, Value: 50}}). + WithSnakes([]Snake{ + { + ID: "1", + Body: []Point{{X: 1, Y: 2}}, + Health: 99, + EliminatedCause: EliminatedByCollision, + EliminatedOnTurn: 45, + EliminatedBy: "2", + }, + }). + WithGameState(map[string]string{"example": "game data"}). + WithPointState(map[Point]int{{X: 1, Y: 1}: 42}) + + require.Equal(t, full, full.Clone()) +} + func TestDev1235(t *testing.T) { // Small boards should no longer error and only get 1 food when num snakes > 4 state, err := CreateDefaultBoardState(MaxRand, BoardSizeSmall, BoardSizeSmall, []string{ @@ -346,23 +370,23 @@ func TestPlaceSnake(t *testing.T) { boardState := NewBoardState(BoardSizeSmall, BoardSizeSmall) require.Empty(t, boardState.Snakes) - _ = PlaceSnake(boardState, "a", []Point{{0, 0}, {1, 0}, {1, 1}}) + _ = PlaceSnake(boardState, "a", []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}) require.Len(t, boardState.Snakes, 1) require.Equal(t, Snake{ ID: "a", - Body: []Point{{0, 0}, {1, 0}, {1, 1}}, + Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}, Health: SnakeMaxHealth, EliminatedCause: NotEliminated, EliminatedBy: "", }, boardState.Snakes[0]) - _ = PlaceSnake(boardState, "b", []Point{{0, 2}, {1, 2}, {3, 2}}) + _ = PlaceSnake(boardState, "b", []Point{{X: 0, Y: 2}, {X: 1, Y: 2}, {X: 3, Y: 2}}) require.Len(t, boardState.Snakes, 2) require.Equal(t, Snake{ ID: "b", - Body: []Point{{0, 2}, {1, 2}, {3, 2}}, + Body: []Point{{X: 0, Y: 2}, {X: 1, Y: 2}, {X: 3, Y: 2}}, Health: SnakeMaxHealth, EliminatedCause: NotEliminated, EliminatedBy: "", @@ -411,9 +435,9 @@ func TestPlaceFood(t *testing.T) { Width: BoardSizeSmall, Height: BoardSizeSmall, Snakes: []Snake{ - {Body: []Point{{5, 1}}}, - {Body: []Point{{5, 3}}}, - {Body: []Point{{5, 5}}}, + {Body: []Point{{X: 5, Y: 1}}}, + {Body: []Point{{X: 5, Y: 3}}}, + {Body: []Point{{X: 5, Y: 5}}}, }, }, 4, // +1 because of fixed spawn locations @@ -423,14 +447,14 @@ func TestPlaceFood(t *testing.T) { Width: BoardSizeMedium, Height: BoardSizeMedium, Snakes: []Snake{ - {Body: []Point{{1, 1}}}, - {Body: []Point{{1, 5}}}, - {Body: []Point{{1, 9}}}, - {Body: []Point{{5, 1}}}, - {Body: []Point{{5, 9}}}, - {Body: []Point{{9, 1}}}, - {Body: []Point{{9, 5}}}, - {Body: []Point{{9, 9}}}, + {Body: []Point{{X: 1, Y: 1}}}, + {Body: []Point{{X: 1, Y: 5}}}, + {Body: []Point{{X: 1, Y: 9}}}, + {Body: []Point{{X: 5, Y: 1}}}, + {Body: []Point{{X: 5, Y: 9}}}, + {Body: []Point{{X: 9, Y: 1}}}, + {Body: []Point{{X: 9, Y: 5}}}, + {Body: []Point{{X: 9, Y: 9}}}, }, }, 9, // +1 because of fixed spawn locations @@ -440,12 +464,12 @@ func TestPlaceFood(t *testing.T) { Width: BoardSizeLarge, Height: BoardSizeLarge, Snakes: []Snake{ - {Body: []Point{{1, 1}}}, - {Body: []Point{{1, 9}}}, - {Body: []Point{{1, 17}}}, - {Body: []Point{{17, 1}}}, - {Body: []Point{{17, 9}}}, - {Body: []Point{{17, 17}}}, + {Body: []Point{{X: 1, Y: 1}}}, + {Body: []Point{{X: 1, Y: 9}}}, + {Body: []Point{{X: 1, Y: 17}}}, + {Body: []Point{{X: 17, Y: 1}}}, + {Body: []Point{{X: 17, Y: 9}}}, + {Body: []Point{{X: 17, Y: 17}}}, }, }, 7, // +1 because of fixed spawn locations @@ -478,7 +502,7 @@ func TestPlaceFoodFixed(t *testing.T) { Width: BoardSizeSmall, Height: BoardSizeSmall, Snakes: []Snake{ - {Body: []Point{{1, 3}}}, + {Body: []Point{{X: 1, Y: 3}}}, }, }, }, @@ -487,10 +511,10 @@ func TestPlaceFoodFixed(t *testing.T) { Width: BoardSizeMedium, Height: BoardSizeMedium, Snakes: []Snake{ - {Body: []Point{{1, 1}}}, - {Body: []Point{{1, 5}}}, - {Body: []Point{{9, 5}}}, - {Body: []Point{{9, 9}}}, + {Body: []Point{{X: 1, Y: 1}}}, + {Body: []Point{{X: 1, Y: 5}}}, + {Body: []Point{{X: 9, Y: 5}}}, + {Body: []Point{{X: 9, Y: 9}}}, }, }, }, @@ -499,14 +523,14 @@ func TestPlaceFoodFixed(t *testing.T) { Width: BoardSizeLarge, Height: BoardSizeLarge, Snakes: []Snake{ - {Body: []Point{{1, 1}}}, - {Body: []Point{{1, 9}}}, - {Body: []Point{{1, 17}}}, - {Body: []Point{{9, 1}}}, - {Body: []Point{{9, 17}}}, - {Body: []Point{{17, 1}}}, - {Body: []Point{{17, 9}}}, - {Body: []Point{{17, 17}}}, + {Body: []Point{{X: 1, Y: 1}}}, + {Body: []Point{{X: 1, Y: 9}}}, + {Body: []Point{{X: 1, Y: 17}}}, + {Body: []Point{{X: 9, Y: 1}}}, + {Body: []Point{{X: 9, Y: 17}}}, + {Body: []Point{{X: 17, Y: 1}}}, + {Body: []Point{{X: 17, Y: 9}}}, + {Body: []Point{{X: 17, Y: 17}}}, }, }, }, @@ -519,16 +543,16 @@ func TestPlaceFoodFixed(t *testing.T) { require.NoError(t, err) require.Equal(t, len(test.BoardState.Snakes)+1, len(test.BoardState.Food)) - midPoint := Point{(test.BoardState.Width - 1) / 2, (test.BoardState.Height - 1) / 2} + midPoint := Point{X: (test.BoardState.Width - 1) / 2, Y: (test.BoardState.Height - 1) / 2} // Make sure every snake has food within 2 moves of it for _, snake := range test.BoardState.Snakes { head := snake.Body[0] - bottomLeft := Point{head.X - 1, head.Y - 1} - topLeft := Point{head.X - 1, head.Y + 1} - bottomRight := Point{head.X + 1, head.Y - 1} - topRight := Point{head.X + 1, head.Y + 1} + bottomLeft := Point{X: head.X - 1, Y: head.Y - 1} + topLeft := Point{X: head.X - 1, Y: head.Y + 1} + bottomRight := Point{X: head.X + 1, Y: head.Y - 1} + topRight := Point{X: head.X + 1, Y: head.Y + 1} foundFoodInTwoMoves := false for _, food := range test.BoardState.Food { @@ -559,7 +583,7 @@ func TestPlaceFoodFixedNoRoom(t *testing.T) { Width: 3, Height: 3, Snakes: []Snake{ - {Body: []Point{{1, 1}}}, + {Body: []Point{{X: 1, Y: 1}}}, }, Food: []Point{}, } @@ -572,10 +596,10 @@ func TestPlaceFoodFixedNoRoom_Corners(t *testing.T) { Width: 7, Height: 7, Snakes: []Snake{ - {Body: []Point{{1, 1}}}, - {Body: []Point{{1, 5}}}, - {Body: []Point{{5, 1}}}, - {Body: []Point{{5, 5}}}, + {Body: []Point{{X: 1, Y: 1}}}, + {Body: []Point{{X: 1, Y: 5}}}, + {Body: []Point{{X: 5, Y: 1}}}, + {Body: []Point{{X: 5, Y: 5}}}, }, Food: []Point{}, } @@ -597,10 +621,10 @@ func TestPlaceFoodFixedNoRoom_Corners(t *testing.T) { require.Error(t, err) expectedFood := []Point{ - {0, 2}, {2, 0}, // Snake @ {1, 1} - {0, 4}, {2, 6}, // Snake @ {1, 5} - {4, 0}, {6, 2}, // Snake @ {5, 1} - {4, 6}, {6, 4}, // Snake @ {5, 5} + {X: 0, Y: 2}, {X: 2, Y: 0}, // Snake @ {X: 1, Y: 1} + {X: 0, Y: 4}, {X: 2, Y: 6}, // Snake @ {X: 1, Y: 5} + {X: 4, Y: 0}, {X: 6, Y: 2}, // Snake @ {X: 5, Y: 1} + {X: 4, Y: 6}, {X: 6, Y: 4}, // Snake @ {X: 5, Y: 5} } sortPoints(expectedFood) sortPoints(boardState.Food) @@ -612,10 +636,10 @@ func TestPlaceFoodFixedNoRoom_Cardinal(t *testing.T) { Width: 11, Height: 11, Snakes: []Snake{ - {Body: []Point{{1, 5}}}, - {Body: []Point{{5, 1}}}, - {Body: []Point{{5, 9}}}, - {Body: []Point{{9, 5}}}, + {Body: []Point{{X: 1, Y: 5}}}, + {Body: []Point{{X: 5, Y: 1}}}, + {Body: []Point{{X: 5, Y: 9}}}, + {Body: []Point{{X: 9, Y: 5}}}, }, Food: []Point{}, } @@ -637,10 +661,10 @@ func TestPlaceFoodFixedNoRoom_Cardinal(t *testing.T) { require.Error(t, err) expectedFood := []Point{ - {0, 4}, {0, 6}, // Snake @ {1, 5} - {4, 0}, {6, 0}, // Snake @ {5, 1} - {4, 10}, {6, 10}, // Snake @ {5, 9} - {10, 4}, {10, 6}, // Snake @ {9, 5} + {X: 0, Y: 4}, {X: 0, Y: 6}, // Snake @ {X: 1, Y: 5} + {X: 4, Y: 0}, {X: 6, Y: 0}, // Snake @ {X: 5, Y: 1} + {X: 4, Y: 10}, {X: 6, Y: 10}, // Snake @ {X: 5, Y: 9} + {X: 10, Y: 4}, {X: 10, Y: 6}, // Snake @ {X: 9, Y: 5} } sortPoints(expectedFood) sortPoints(boardState.Food) @@ -653,15 +677,15 @@ func TestGetDistanceBetweenPoints(t *testing.T) { B Point Expected int }{ - {Point{0, 0}, Point{0, 0}, 0}, - {Point{0, 0}, Point{1, 0}, 1}, - {Point{0, 0}, Point{0, 1}, 1}, - {Point{0, 0}, Point{1, 1}, 2}, - {Point{0, 0}, Point{4, 4}, 8}, - {Point{0, 0}, Point{4, 6}, 10}, - {Point{8, 0}, Point{8, 0}, 0}, - {Point{8, 0}, Point{8, 8}, 8}, - {Point{8, 0}, Point{0, 8}, 16}, + {Point{X: 0, Y: 0}, Point{X: 0, Y: 0}, 0}, + {Point{X: 0, Y: 0}, Point{X: 1, Y: 0}, 1}, + {Point{X: 0, Y: 0}, Point{X: 0, Y: 1}, 1}, + {Point{X: 0, Y: 0}, Point{X: 1, Y: 1}, 2}, + {Point{X: 0, Y: 0}, Point{X: 4, Y: 4}, 8}, + {Point{X: 0, Y: 0}, Point{X: 4, Y: 6}, 10}, + {Point{X: 8, Y: 0}, Point{X: 8, Y: 0}, 0}, + {Point{X: 8, Y: 0}, Point{X: 8, Y: 8}, 8}, + {Point{X: 8, Y: 0}, Point{X: 0, Y: 8}, 16}, } for _, test := range tests { @@ -704,20 +728,20 @@ func TestGetUnoccupiedPoints(t *testing.T) { Height: 1, Width: 1, }, - []Point{{0, 0}}, + []Point{{X: 0, Y: 0}}, }, { &BoardState{ Height: 1, Width: 2, }, - []Point{{0, 0}, {1, 0}}, + []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, }, { &BoardState{ Height: 1, Width: 1, - Food: []Point{{0, 0}, {101, 202}, {-4, -5}}, + Food: []Point{{X: 0, Y: 0}, {X: 101, Y: 202}, {X: -4, Y: -5}}, }, []Point{}, }, @@ -725,15 +749,15 @@ func TestGetUnoccupiedPoints(t *testing.T) { &BoardState{ Height: 2, Width: 2, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, }, - []Point{{0, 1}, {1, 1}}, + []Point{{X: 0, Y: 1}, {X: 1, Y: 1}}, }, { &BoardState{ Height: 2, Width: 2, - Food: []Point{{0, 0}, {0, 1}, {1, 0}, {1, 1}}, + Food: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}}, }, []Point{}, }, @@ -742,38 +766,38 @@ func TestGetUnoccupiedPoints(t *testing.T) { Height: 4, Width: 1, Snakes: []Snake{ - {Body: []Point{{0, 0}}}, + {Body: []Point{{X: 0, Y: 0}}}, }, }, - []Point{{0, 1}, {0, 2}, {0, 3}}, + []Point{{X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}}, }, { &BoardState{ Height: 2, Width: 3, Snakes: []Snake{ - {Body: []Point{{0, 0}, {1, 0}, {1, 1}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}}, }, }, - []Point{{0, 1}, {2, 0}, {2, 1}}, + []Point{{X: 0, Y: 1}, {X: 2, Y: 0}, {X: 2, Y: 1}}, }, { &BoardState{ Height: 2, Width: 3, - Food: []Point{{0, 0}, {1, 0}, {1, 1}, {2, 0}}, + Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 2, Y: 0}}, Snakes: []Snake{ - {Body: []Point{{0, 0}, {1, 0}, {1, 1}}}, - {Body: []Point{{0, 1}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}}, + {Body: []Point{{X: 0, Y: 1}}}, }, }, - []Point{{2, 1}}, + []Point{{X: 2, Y: 1}}, }, { &BoardState{ Height: 1, Width: 1, - Hazards: []Point{{0, 0}}, + Hazards: []Point{{X: 0, Y: 0}}, }, []Point{}, }, @@ -781,22 +805,22 @@ func TestGetUnoccupiedPoints(t *testing.T) { &BoardState{ Height: 2, Width: 2, - Hazards: []Point{{1, 1}}, + Hazards: []Point{{X: 1, Y: 1}}, }, - []Point{{0, 0}, {0, 1}, {1, 0}}, + []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}}, }, { &BoardState{ Height: 2, Width: 3, - Food: []Point{{1, 1}, {2, 0}}, + Food: []Point{{X: 1, Y: 1}, {X: 2, Y: 0}}, Snakes: []Snake{ - {Body: []Point{{0, 0}, {1, 0}, {1, 1}}}, - {Body: []Point{{0, 1}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}}, + {Body: []Point{{X: 0, Y: 1}}}, }, - Hazards: []Point{{0, 0}, {1, 0}}, + Hazards: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, }, - []Point{{2, 1}}, + []Point{{X: 2, Y: 1}}, }, } @@ -819,20 +843,20 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) { Height: 1, Width: 1, }, - []Point{{0, 0}}, + []Point{{X: 0, Y: 0}}, }, { &BoardState{ Height: 2, Width: 2, }, - []Point{{0, 0}, {1, 1}}, + []Point{{X: 0, Y: 0}, {X: 1, Y: 1}}, }, { &BoardState{ Height: 1, Width: 1, - Food: []Point{{0, 0}, {101, 202}, {-4, -5}}, + Food: []Point{{X: 0, Y: 0}, {X: 101, Y: 202}, {X: -4, Y: -5}}, }, []Point{}, }, @@ -840,15 +864,15 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) { &BoardState{ Height: 2, Width: 2, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, }, - []Point{{1, 1}}, + []Point{{X: 1, Y: 1}}, }, { &BoardState{ Height: 4, Width: 4, - Food: []Point{{0, 0}, {0, 2}, {1, 1}, {1, 3}, {2, 0}, {2, 2}, {3, 1}, {3, 3}}, + Food: []Point{{X: 0, Y: 0}, {X: 0, Y: 2}, {X: 1, Y: 1}, {X: 1, Y: 3}, {X: 2, Y: 0}, {X: 2, Y: 2}, {X: 3, Y: 1}, {X: 3, Y: 3}}, }, []Point{}, }, @@ -857,32 +881,32 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) { Height: 4, Width: 1, Snakes: []Snake{ - {Body: []Point{{0, 0}}}, + {Body: []Point{{X: 0, Y: 0}}}, }, }, - []Point{{0, 2}}, + []Point{{X: 0, Y: 2}}, }, { &BoardState{ Height: 2, Width: 3, Snakes: []Snake{ - {Body: []Point{{0, 0}, {1, 0}, {1, 1}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}}, }, }, - []Point{{2, 0}}, + []Point{{X: 2, Y: 0}}, }, { &BoardState{ Height: 2, Width: 3, - Food: []Point{{0, 0}, {1, 0}, {1, 1}, {2, 1}}, + Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 2, Y: 1}}, Snakes: []Snake{ - {Body: []Point{{0, 0}, {1, 0}, {1, 1}}}, - {Body: []Point{{0, 1}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}}, + {Body: []Point{{X: 0, Y: 1}}}, }, }, - []Point{{2, 0}}, + []Point{{X: 2, Y: 0}}, }, } @@ -902,7 +926,7 @@ func TestPlaceFoodRandomly(t *testing.T) { Height: 1, Width: 3, Snakes: []Snake{ - {Body: []Point{{1, 0}}}, + {Body: []Point{{X: 1, Y: 0}}}, }, } // Food should never spawn, no room diff --git a/cases_test.go b/cases_test.go index efb58a8..3e8fdb7 100644 --- a/cases_test.go +++ b/cases_test.go @@ -29,8 +29,8 @@ func (gc *gameTestCase) requireValidNextState(t *testing.T, r Ruleset) { t.Helper() t.Run(gc.name, func(t *testing.T) { t.Helper() - prev := gc.prevState.Clone() // clone to protect against mutation (so we can ru-use test cases) - nextState, err := r.CreateNextBoardState(prev, gc.moves) + prev := gc.prevState.Clone() // clone to protect against mutation (so we can re-use test cases) + _, nextState, err := r.Execute(prev, gc.moves) require.Equal(t, gc.expectedError, err) if gc.expectedState != nil { require.Equal(t, gc.expectedState.Width, nextState.Width) diff --git a/cli/commands/play.go b/cli/commands/play.go index 3a07e76..caabe2d 100644 --- a/cli/commands/play.go +++ b/cli/commands/play.go @@ -88,7 +88,9 @@ func NewPlayCommand() *cobra.Command { if err := gameState.Initialize(); err != nil { log.ERROR.Fatalf("Error initializing game: %v", err) } - gameState.Run() + if err := gameState.Run(); err != nil { + log.ERROR.Fatalf("Error running game: %v", err) + } }, } @@ -143,7 +145,6 @@ func (gameState *GameState) Initialize() error { // Create settings object gameState.settings = map[string]string{ - rules.ParamGameType: gameState.GameType, rules.ParamFoodSpawnChance: fmt.Sprint(gameState.FoodSpawnChance), rules.ParamMinimumFood: fmt.Sprint(gameState.MinimumFood), rules.ParamHazardDamagePerTurn: fmt.Sprint(gameState.HazardDamagePerTurn), @@ -155,7 +156,7 @@ func (gameState *GameState) Initialize() error { WithSeed(gameState.Seed). WithParams(gameState.settings). WithSolo(len(gameState.URLs) < 2). - Ruleset() + NamedRuleset(gameState.GameType) gameState.ruleset = ruleset // Initialize snake states as empty until we can ping the snake URLs @@ -173,13 +174,22 @@ func (gameState *GameState) Initialize() error { } // Setup and run a full game. -func (gameState *GameState) Run() { +func (gameState *GameState) Run() error { + var gameOver bool + var err error + // Setup local state for snakes - gameState.snakeStates = gameState.buildSnakesFromOptions() + gameState.snakeStates, err = gameState.buildSnakesFromOptions() + if err != nil { + return fmt.Errorf("Error getting snake metadata: %w", err) + } rand.Seed(gameState.Seed) - boardState := gameState.initializeBoardFromArgs() + gameOver, boardState, err := gameState.initializeBoardFromArgs() + if err != nil { + return fmt.Errorf("Error initializing board: %w", err) + } gameExporter := GameExporter{ game: gameState.createClientGame(), @@ -209,7 +219,7 @@ func (gameState *GameState) Run() { if gameState.ViewInBrowser { serverURL, err := boardServer.Listen() if err != nil { - log.ERROR.Fatalf("Error starting HTTP server: %v", err) + return fmt.Errorf("Error starting HTTP server: %w", err) } defer boardServer.Shutdown() log.INFO.Printf("Board server listening on %s", serverURL) @@ -233,29 +243,37 @@ func (gameState *GameState) Run() { gameState.printState(boardState) } + // Export game first, if enabled, so that we capture the request for turn zero. + if exportGame { + // The output file was designed in a way so that (nearly) every entry is equivalent to a valid API request. + // This is meant to help unlock further development of tools such as replaying a saved game by simply copying each line and sending it as a POST request. + // There was a design choice to be made here: the difference between SnakeRequest and BoardState is the `you` key. + // We could choose to either store the SnakeRequest of each snake OR to omit the `you` key OR fill the `you` key with one of the snakes + // In all cases the API request is technically non-compliant with how the actual API request should be. + // The third option (filling the `you` key with an arbitrary snake) is the closest to the actual API request that would need the least manipulation to + // be adjusted to look like an API call for a specific snake in the game. + for _, snakeState := range gameState.snakeStates { + snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState) + gameExporter.AddSnakeRequest(snakeRequest) + break + } + } + var endTime time.Time - for v := false; !v; v, _ = gameState.ruleset.IsGameOver(boardState) { + for !gameOver { if gameState.TurnDuration > 0 { endTime = time.Now().Add(time.Duration(gameState.TurnDuration) * time.Millisecond) } - // Export game first, if enabled, so that we save the board on turn zero - if exportGame { - // The output file was designed in a way so that (nearly) every entry is equivalent to a valid API request. - // This is meant to help unlock further development of tools such as replaying a saved game by simply copying each line and sending it as a POST request. - // There was a design choice to be made here: the difference between SnakeRequest and BoardState is the `you` key. - // We could choose to either store the SnakeRequest of each snake OR to omit the `you` key OR fill the `you` key with one of the snakes - // In all cases the API request is technically non-compliant with how the actual API request should be. - // The third option (filling the `you` key with an arbitrary snake) is the closest to the actual API request that would need the least manipulation to - // be adjusted to look like an API call for a specific snake in the game. - for _, snakeState := range gameState.snakeStates { - snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState) - gameExporter.AddSnakeRequest(snakeRequest) - break - } + gameOver, boardState, err = gameState.createNextBoardState(boardState) + if err != nil { + return fmt.Errorf("Error processing game: %w", err) } - boardState = gameState.createNextBoardState(boardState) + if gameOver { + // Stop processing here - because game over is detected at the start of the pipeline, nothing will have changed. + break + } if gameState.ViewMap { gameState.printMap(boardState) @@ -274,14 +292,13 @@ func (gameState *GameState) Run() { if gameState.ViewInBrowser { boardServer.SendEvent(gameState.buildFrameEvent(boardState)) } - } - // Export final turn - if exportGame { - for _, snakeState := range gameState.snakeStates { - snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState) - gameExporter.AddSnakeRequest(snakeRequest) - break + if exportGame { + for _, snakeState := range gameState.snakeStates { + snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState) + gameExporter.AddSnakeRequest(snakeRequest) + break + } } } @@ -320,24 +337,26 @@ func (gameState *GameState) Run() { if exportGame { lines, err := gameExporter.FlushToFile(gameState.outputFile) if err != nil { - log.ERROR.Fatalf("Unable to export game. Reason: %v", err) + return fmt.Errorf("Unable to export game: %w", err) } log.INFO.Printf("Wrote %d lines to output file: %s", lines, gameState.OutputPath) } + + return nil } -func (gameState *GameState) initializeBoardFromArgs() *rules.BoardState { +func (gameState *GameState) initializeBoardFromArgs() (bool, *rules.BoardState, error) { snakeIds := []string{} for _, snakeState := range gameState.snakeStates { snakeIds = append(snakeIds, snakeState.ID) } boardState, err := maps.SetupBoard(gameState.gameMap.ID(), gameState.ruleset.Settings(), gameState.Width, gameState.Height, snakeIds) if err != nil { - log.ERROR.Fatalf("Error Initializing Board State: %v", err) + return false, nil, fmt.Errorf("Error initializing BoardState with map: %w", err) } - boardState, err = gameState.ruleset.ModifyInitialBoardState(boardState) + gameOver, boardState, err := gameState.ruleset.Execute(boardState, nil) if err != nil { - log.ERROR.Fatalf("Error Initializing Board State: %v", err) + return false, nil, fmt.Errorf("Error initializing BoardState with ruleset: %w", err) } for _, snakeState := range gameState.snakeStates { @@ -351,12 +370,18 @@ func (gameState *GameState) initializeBoardFromArgs() *rules.BoardState { log.WARN.Printf("Request to %v failed", u.String()) } } - return boardState + return gameOver, boardState, nil } -func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) *rules.BoardState { - stateUpdates := make(chan SnakeState, len(gameState.snakeStates)) +func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) (bool, *rules.BoardState, error) { + // apply PreUpdateBoard before making requests to snakes + boardState, err := maps.PreUpdateBoard(gameState.gameMap, boardState, gameState.ruleset.Settings()) + if err != nil { + return false, boardState, fmt.Errorf("Error pre-updating board with game map: %w", err) + } + // get moves from snakes + stateUpdates := make(chan SnakeState, len(gameState.snakeStates)) if gameState.Sequential { for _, snakeState := range gameState.snakeStates { for _, snake := range boardState.Snakes { @@ -393,19 +418,20 @@ func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) * moves = append(moves, rules.SnakeMove{ID: snakeState.ID, Move: snakeState.LastMove}) } - boardState, err := gameState.ruleset.CreateNextBoardState(boardState, moves) + gameOver, boardState, err := gameState.ruleset.Execute(boardState, moves) if err != nil { - log.ERROR.Fatalf("Error producing next board state: %v", err) + return false, boardState, fmt.Errorf("Error updating board state from ruleset: %w", err) } - boardState, err = maps.UpdateBoard(gameState.gameMap.ID(), boardState, gameState.ruleset.Settings()) + // apply PostUpdateBoard after ruleset operates on snake moves + boardState, err = maps.PostUpdateBoard(gameState.gameMap, boardState, gameState.ruleset.Settings()) if err != nil { - log.ERROR.Fatalf("Error updating board with game map: %v", err) + return false, boardState, fmt.Errorf("Error post-updating board with game map: %w", err) } boardState.Turn += 1 - return boardState + return gameOver, boardState, nil } func (gameState *GameState) getSnakeUpdate(boardState *rules.BoardState, snakeState SnakeState) SnakeState { @@ -522,13 +548,13 @@ func (gameState *GameState) createClientGame() client.Game { Ruleset: client.Ruleset{ Name: gameState.ruleset.Name(), Version: "cli", // TODO: Use GitHub Release Version - Settings: gameState.ruleset.Settings(), + Settings: client.ConvertRulesetSettings(gameState.ruleset.Settings()), }, Map: gameState.gameMap.ID(), } } -func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState { +func (gameState *GameState) buildSnakesFromOptions() (map[string]SnakeState, error) { bodyChars := []rune{'■', '⌀', '●', '☻', '◘', '☺', '□', '⍟'} var numSnakes int snakes := map[string]SnakeState{} @@ -560,11 +586,11 @@ func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState { if i < numURLs { u, err := url.ParseRequestURI(gameState.URLs[i]) if err != nil { - log.ERROR.Fatalf("URL %v is not valid: %v", gameState.URLs[i], err) + return nil, fmt.Errorf("URL %v is not valid: %w", gameState.URLs[i], err) } snakeURL = u.String() } else { - log.ERROR.Fatalf("URL for name %v is missing", gameState.Names[i]) + return nil, fmt.Errorf("URL for name %v is missing", gameState.Names[i]) } snakeState := SnakeState{ @@ -573,25 +599,25 @@ func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState { var snakeErr error res, _, err := gameState.httpClient.Get(snakeURL) if err != nil { - log.ERROR.Fatalf("Snake metadata request to %v failed: %v", snakeURL, err) + return nil, fmt.Errorf("Snake metadata request to %v failed: %w", snakeURL, err) } snakeState.StatusCode = res.StatusCode if res.Body == nil { - log.ERROR.Fatalf("Empty response body from snake metadata URL: %v", snakeURL) + return nil, fmt.Errorf("Empty response body from snake metadata URL: %v", snakeURL) } defer res.Body.Close() body, readErr := ioutil.ReadAll(res.Body) if readErr != nil { - log.ERROR.Fatalf("Error reading from snake metadata URL %v: %v", snakeURL, readErr) + return nil, fmt.Errorf("Error reading from snake metadata URL %v: %w", snakeURL, readErr) } pingResponse := client.SnakeMetadataResponse{} jsonErr := json.Unmarshal(body, &pingResponse) if jsonErr != nil { - log.ERROR.Fatalf("Failed to parse response from %v: %v", snakeURL, jsonErr) + return nil, fmt.Errorf("Failed to parse response from %v: %w", snakeURL, jsonErr) } snakeState.Head = pingResponse.Head @@ -608,7 +634,7 @@ func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState { log.INFO.Printf("Snake ID: %v URL: %v, Name: \"%v\"", snakeState.ID, snakeURL, snakeState.Name) } - return snakes + return snakes, nil } func (gameState *GameState) printState(boardState *rules.BoardState) { @@ -762,7 +788,8 @@ func (gameState *GameState) buildFrameEvent(boardState *rules.BoardState) board. func serialiseSnakeRequest(snakeRequest client.SnakeRequest) []byte { requestJSON, err := json.Marshal(snakeRequest) if err != nil { - log.ERROR.Fatalf("Error marshalling JSON from State: %v", err) + // This is likely to be a programming error like a unsupported type or cyclical reference + log.ERROR.Panicf("Error marshalling JSON from State: %v", err) } return requestJSON } diff --git a/cli/commands/play_test.go b/cli/commands/play_test.go index e1c358a..44d0b35 100644 --- a/cli/commands/play_test.go +++ b/cli/commands/play_test.go @@ -45,11 +45,10 @@ func buildDefaultGameState() *GameState { func TestGetIndividualBoardStateForSnake(t *testing.T) { s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}} s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}} - state := &rules.BoardState{ - Height: 11, - Width: 11, - Snakes: []rules.Snake{s1, s2}, - } + state := rules.NewBoardState(11, 11). + WithSnakes( + []rules.Snake{s1, s2}, + ) s1State := SnakeState{ ID: "one", Name: "ONE", @@ -85,11 +84,8 @@ func TestGetIndividualBoardStateForSnake(t *testing.T) { func TestSettingsRequestSerialization(t *testing.T) { s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}} s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}} - state := &rules.BoardState{ - Height: 11, - Width: 11, - Snakes: []rules.Snake{s1, s2}, - } + state := rules.NewBoardState(11, 11). + WithSnakes([]rules.Snake{s1, s2}) s1State := SnakeState{ ID: "one", Name: "ONE", @@ -255,12 +251,11 @@ func TestBuildFrameEvent(t *testing.T) { }, { name: "snake fields", - boardState: &rules.BoardState{ - Turn: 99, - Height: 19, - Width: 25, - Food: []rules.Point{{X: 9, Y: 4}}, - Snakes: []rules.Snake{ + boardState: rules.NewBoardState(19, 25). + WithTurn(99). + WithFood([]rules.Point{{X: 9, Y: 4}}). + WithHazards([]rules.Point{{X: 8, Y: 6}}). + WithSnakes([]rules.Snake{ { ID: "1", Body: []rules.Point{ @@ -273,9 +268,7 @@ func TestBuildFrameEvent(t *testing.T) { EliminatedOnTurn: 45, EliminatedBy: "1", }, - }, - Hazards: []rules.Point{{X: 8, Y: 6}}, - }, + }), snakeStates: map[string]SnakeState{ "1": { URL: "http://example.com", @@ -326,18 +319,15 @@ func TestBuildFrameEvent(t *testing.T) { }, { name: "snake errors", - boardState: &rules.BoardState{ - Height: 19, - Width: 25, - Snakes: []rules.Snake{ + boardState: rules.NewBoardState(19, 25). + WithSnakes([]rules.Snake{ { ID: "bad_status", }, { ID: "connection_error", }, - }, - }, + }), snakeStates: map[string]SnakeState{ "bad_status": { StatusCode: 504, @@ -366,6 +356,8 @@ func TestBuildFrameEvent(t *testing.T) { Error: "0:Error communicating with server", }, }, + Food: []rules.Point{}, + Hazards: []rules.Point{}, }, }, }, @@ -384,11 +376,7 @@ func TestBuildFrameEvent(t *testing.T) { func TestGetMoveForSnake(t *testing.T) { s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}} s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}} - boardState := &rules.BoardState{ - Height: 11, - Width: 11, - Snakes: []rules.Snake{s1, s2}, - } + boardState := rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{s1, s2}) tests := []struct { name string @@ -530,11 +518,7 @@ func TestGetMoveForSnake(t *testing.T) { func TestCreateNextBoardState(t *testing.T) { s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}} - boardState := &rules.BoardState{ - Height: 11, - Width: 11, - Snakes: []rules.Snake{s1}, - } + boardState := rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{s1}) snakeState := SnakeState{ ID: s1.ID, URL: "http://example.com", @@ -549,7 +533,9 @@ func TestCreateNextBoardState(t *testing.T) { gameState.snakeStates = map[string]SnakeState{s1.ID: snakeState} gameState.httpClient = stubHTTPClient{nil, 200, func(_ string) string { return `{"move": "right"}` }, 54 * time.Millisecond} - nextBoardState := gameState.createNextBoardState(boardState) + gameOver, nextBoardState, err := gameState.createNextBoardState(boardState) + require.NoError(t, err) + require.False(t, gameOver) snakeState = gameState.snakeStates[s1.ID] require.NotNil(t, nextBoardState) @@ -593,16 +579,18 @@ func TestOutputFile(t *testing.T) { outputFile := new(closableBuffer) gameState.outputFile = outputFile - gameState.ruleset = StubRuleset{maxTurns: 1, settings: rules.Settings{ - FoodSpawnChance: 1, - MinimumFood: 2, - HazardDamagePerTurn: 3, - RoyaleSettings: rules.RoyaleSettings{ - ShrinkEveryNTurns: 4, - }, - }} + gameState.ruleset = StubRuleset{ + maxTurns: 1, + settings: rules.NewSettings(map[string]string{ + rules.ParamFoodSpawnChance: "1", + rules.ParamMinimumFood: "2", + rules.ParamHazardDamagePerTurn: "3", + rules.ParamShrinkEveryNTurns: "4", + }), + } - gameState.Run() + err = gameState.Run() + require.NoError(t, err) lines := strings.Split(outputFile.String(), "\n") require.Len(t, lines, 5) @@ -626,14 +614,8 @@ type StubRuleset struct { func (ruleset StubRuleset) Name() string { return "standard" } func (ruleset StubRuleset) Settings() rules.Settings { return ruleset.settings } -func (ruleset StubRuleset) ModifyInitialBoardState(initialState *rules.BoardState) (*rules.BoardState, error) { - return initialState, nil -} -func (ruleset StubRuleset) CreateNextBoardState(prevState *rules.BoardState, moves []rules.SnakeMove) (*rules.BoardState, error) { - return prevState, nil -} -func (ruleset StubRuleset) IsGameOver(state *rules.BoardState) (bool, error) { - return state.Turn >= ruleset.maxTurns, nil +func (ruleset StubRuleset) Execute(prevState *rules.BoardState, moves []rules.SnakeMove) (bool, *rules.BoardState, error) { + return prevState.Turn >= ruleset.maxTurns, prevState, nil } type stubHTTPClient struct { diff --git a/client/fixtures_test.go b/client/fixtures_test.go index 1678423..d01e4bc 100644 --- a/client/fixtures_test.go +++ b/client/fixtures_test.go @@ -9,7 +9,7 @@ func exampleSnakeRequest() SnakeRequest { Ruleset: Ruleset{ Name: "test-ruleset-name", Version: "cli", - Settings: exampleRulesetSettings, + Settings: ConvertRulesetSettings(exampleRulesetSettings), }, Timeout: 33, Source: "league", @@ -75,21 +75,9 @@ func exampleSnakeRequest() SnakeRequest { } } -var exampleRulesetSettings = rules.Settings{ - FoodSpawnChance: 10, - MinimumFood: 20, - HazardDamagePerTurn: 30, - HazardMap: "hz_spiral", - HazardMapAuthor: "altersaddle", - - RoyaleSettings: rules.RoyaleSettings{ - ShrinkEveryNTurns: 40, - }, - - SquadSettings: rules.SquadSettings{ - AllowBodyCollisions: true, - SharedElimination: true, - SharedHealth: true, - SharedLength: true, - }, -} +var exampleRulesetSettings = rules.NewSettings(map[string]string{ + rules.ParamFoodSpawnChance: "10", + rules.ParamMinimumFood: "20", + rules.ParamHazardDamagePerTurn: "30", + rules.ParamShrinkEveryNTurns: "40", +}) diff --git a/client/models.go b/client/models.go index 0d63c7d..544b066 100644 --- a/client/models.go +++ b/client/models.go @@ -49,19 +49,47 @@ type Customizations struct { } type Ruleset struct { - Name string `json:"name"` - Version string `json:"version"` - Settings rules.Settings `json:"settings"` + Name string `json:"name"` + Version string `json:"version"` + Settings RulesetSettings `json:"settings"` } -// RulesetSettings is deprecated: use rules.Settings instead -type RulesetSettings rules.Settings +// RulesetSettings contains a static collection of a few settings that are exposed through the API. +type RulesetSettings struct { + FoodSpawnChance int `json:"foodSpawnChance"` + MinimumFood int `json:"minimumFood"` + HazardDamagePerTurn int `json:"hazardDamagePerTurn"` + HazardMap string `json:"hazardMap"` // Deprecated, replaced by Game.Map + HazardMapAuthor string `json:"hazardMapAuthor"` // Deprecated, no planned replacement + RoyaleSettings RoyaleSettings `json:"royale"` + SquadSettings SquadSettings `json:"squad"` // Deprecated, provided with default fields for API compatibility +} + +// RoyaleSettings contains settings that are specific to the "royale" game mode +type RoyaleSettings struct { + ShrinkEveryNTurns int `json:"shrinkEveryNTurns"` +} -// RoyaleSettings is deprecated: use rules.RoyaleSettings instead -type RoyaleSettings rules.RoyaleSettings +// SquadSettings contains settings that are specific to the "squad" game mode +type SquadSettings struct { + AllowBodyCollisions bool `json:"allowBodyCollisions"` + SharedElimination bool `json:"sharedElimination"` + SharedHealth bool `json:"sharedHealth"` + SharedLength bool `json:"sharedLength"` +} -// SquadSettings is deprecated: use rules.SquadSettings instead -type SquadSettings rules.SquadSettings +// Converts a rules.Settings (which can contain arbitrary settings) into the static RulesetSettings used in the client API. +func ConvertRulesetSettings(settings rules.Settings) RulesetSettings { + return RulesetSettings{ + FoodSpawnChance: settings.Int(rules.ParamFoodSpawnChance, 0), + MinimumFood: settings.Int(rules.ParamMinimumFood, 0), + HazardDamagePerTurn: settings.Int(rules.ParamHazardDamagePerTurn, 0), + RoyaleSettings: RoyaleSettings{ + ShrinkEveryNTurns: settings.Int(rules.ParamShrinkEveryNTurns, 0), + }, + SquadSettings: SquadSettings{}, + } +} // Coord represents a point on the board type Coord struct { diff --git a/client/models_test.go b/client/models_test.go index a00e5a5..5253867 100644 --- a/client/models_test.go +++ b/client/models_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "testing" - "github.com/BattlesnakeOfficial/rules" "github.com/BattlesnakeOfficial/rules/test" "github.com/stretchr/testify/require" ) @@ -19,7 +18,7 @@ func TestBuildSnakeRequestJSON(t *testing.T) { func TestBuildSnakeRequestJSONEmptyRulesetSettings(t *testing.T) { snakeRequest := exampleSnakeRequest() - snakeRequest.Game.Ruleset.Settings = rules.Settings{} + snakeRequest.Game.Ruleset.Settings = RulesetSettings{} data, err := json.MarshalIndent(snakeRequest, "", " ") require.NoError(t, err) diff --git a/client/testdata/snake_request.json b/client/testdata/snake_request.json index cd4b1a3..5cf8fd9 100644 --- a/client/testdata/snake_request.json +++ b/client/testdata/snake_request.json @@ -8,16 +8,16 @@ "foodSpawnChance": 10, "minimumFood": 20, "hazardDamagePerTurn": 30, - "hazardMap": "hz_spiral", - "hazardMapAuthor": "altersaddle", + "hazardMap": "", + "hazardMapAuthor": "", "royale": { "shrinkEveryNTurns": 40 }, "squad": { - "allowBodyCollisions": true, - "sharedElimination": true, - "sharedHealth": true, - "sharedLength": true + "allowBodyCollisions": false, + "sharedElimination": false, + "sharedHealth": false, + "sharedLength": false } } }, diff --git a/constrictor.go b/constrictor.go index 3fb5eea..c7b7ef4 100644 --- a/constrictor.go +++ b/constrictor.go @@ -22,31 +22,6 @@ var wrappedConstrictorRulesetStages = []string{ StageModifySnakesAlwaysGrow, } -type ConstrictorRuleset struct { - StandardRuleset -} - -func (r *ConstrictorRuleset) Name() string { return GameTypeConstrictor } - -func (r ConstrictorRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) { - return NewPipeline(constrictorRulesetStages...).Execute(bs, s, sm) -} - -func (r *ConstrictorRuleset) ModifyInitialBoardState(initialBoardState *BoardState) (*BoardState, error) { - _, nextState, err := r.Execute(initialBoardState, r.Settings(), nil) - return nextState, err -} - -func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { - _, nextState, err := r.Execute(prevState, r.Settings(), moves) - - return nextState, err -} - -func (r *ConstrictorRuleset) IsGameOver(b *BoardState) (bool, error) { - return GameOverStandard(b, r.Settings(), nil) -} - func RemoveFoodConstrictor(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) { // Remove all food from the board b.Food = []Point{} diff --git a/constrictor_test.go b/constrictor_test.go index 68df8ff..722a408 100644 --- a/constrictor_test.go +++ b/constrictor_test.go @@ -4,10 +4,6 @@ import ( "testing" ) -func TestConstrictorRulesetInterface(t *testing.T) { - var _ Ruleset = (*ConstrictorRuleset)(nil) -} - // Test that two equal snakes collide and both get eliminated // also checks: // - food removed @@ -21,16 +17,16 @@ var constrictorMoveAndCollideMAD = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}, {2, 1}}, + Body: []Point{{X: 1, Y: 1}, {X: 2, Y: 1}}, Health: 99, }, { ID: "two", - Body: []Point{{1, 2}, {2, 2}}, + Body: []Point{{X: 1, Y: 2}, {X: 2, Y: 2}}, Health: 99, }, }, - Food: []Point{{10, 10}, {9, 9}, {8, 8}}, + Food: []Point{{X: 10, Y: 10}, {X: 9, Y: 9}, {X: 8, Y: 8}}, Hazards: []Point{}, }, []SnakeMove{ @@ -44,7 +40,7 @@ var constrictorMoveAndCollideMAD = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 2}, {1, 1}, {1, 1}}, + Body: []Point{{X: 1, Y: 2}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100, EliminatedCause: EliminatedByCollision, EliminatedBy: "two", @@ -52,7 +48,7 @@ var constrictorMoveAndCollideMAD = gameTestCase{ }, { ID: "two", - Body: []Point{{1, 1}, {1, 2}, {1, 2}}, + Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}, {X: 1, Y: 2}}, Health: 100, EliminatedCause: EliminatedByCollision, EliminatedBy: "one", @@ -70,15 +66,11 @@ func TestConstrictorCreateNextBoardState(t *testing.T) { standardCaseErrZeroLengthSnake, constrictorMoveAndCollideMAD, } - rb := NewRulesetBuilder().WithParams(map[string]string{ - ParamGameType: GameTypeConstrictor, - }) - r := ConstrictorRuleset{} + r := NewRulesetBuilder().NamedRuleset(GameTypeConstrictor) for _, gc := range cases { - gc.requireValidNextState(t, &r) - // also test a RulesBuilder constructed instance - gc.requireValidNextState(t, rb.Ruleset()) + // test a RulesBuilder constructed instance + gc.requireValidNextState(t, r) // also test a pipeline with the same settings - gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeConstrictor, NewPipeline(constrictorRulesetStages...))) + gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeConstrictor, NewPipeline(constrictorRulesetStages...))) } } diff --git a/maps/arcade_maze.go b/maps/arcade_maze.go index 6e0885a..a813de9 100644 --- a/maps/arcade_maze.go +++ b/maps/arcade_maze.go @@ -63,7 +63,7 @@ func (m ArcadeMazeMap) SetupBoard(initialBoardState *rules.BoardState, settings editor.AddHazard(hazard) } - if settings.MinimumFood > 0 { + if settings.Int(rules.ParamMinimumFood, 0) > 0 { // Add food in center editor.AddFood(rules.Point{X: 9, Y: 11}) } @@ -71,11 +71,16 @@ func (m ArcadeMazeMap) SetupBoard(initialBoardState *rules.BoardState, settings return nil } -func (m ArcadeMazeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m ArcadeMazeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m ArcadeMazeMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { rand := settings.GetRand(lastBoardState.Turn) // Respect FoodSpawnChance setting - if settings.FoodSpawnChance == 0 || rand.Intn(100) > settings.FoodSpawnChance { + foodSpawnChance := settings.Int(rules.ParamFoodSpawnChance, 0) + if foodSpawnChance == 0 || rand.Intn(100) > foodSpawnChance { return nil } diff --git a/maps/castle_wall.go b/maps/castle_wall.go index 9f30f83..9e239c9 100644 --- a/maps/castle_wall.go +++ b/maps/castle_wall.go @@ -136,7 +136,11 @@ func (m CastleWallMediumHazardsMap) SetupBoard(initialBoardState *rules.BoardSta return setupCastleWallBoard(m.Meta().MaxPlayers, startPositions, castleWallMediumHazards, initialBoardState, settings, editor) } -func (m CastleWallMediumHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m CastleWallMediumHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m CastleWallMediumHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { maxFood := 2 return updateCastleWallBoard(maxFood, castleWallMediumFood, lastBoardState, settings, editor) } @@ -228,7 +232,11 @@ func (m CastleWallLargeHazardsMap) SetupBoard(initialBoardState *rules.BoardStat return setupCastleWallBoard(m.Meta().MaxPlayers, startPositions, castleWallLargeHazards, initialBoardState, settings, editor) } -func (m CastleWallLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m CastleWallLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m CastleWallLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { maxFood := 2 return updateCastleWallBoard(maxFood, castleWallLargeFood, lastBoardState, settings, editor) } @@ -420,7 +428,11 @@ func (m CastleWallExtraLargeHazardsMap) SetupBoard(initialBoardState *rules.Boar return setupCastleWallBoard(m.Meta().MaxPlayers, startPositions, castleWallExtraLargeHazards, initialBoardState, settings, editor) } -func (m CastleWallExtraLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m CastleWallExtraLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m CastleWallExtraLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { maxFood := 4 return updateCastleWallBoard(maxFood, castleWallExtraLargeFood, lastBoardState, settings, editor) } diff --git a/maps/empty.go b/maps/empty.go index 40699f9..1bff1f2 100644 --- a/maps/empty.go +++ b/maps/empty.go @@ -53,6 +53,10 @@ func (m EmptyMap) SetupBoard(initialBoardState *rules.BoardState, settings rules return nil } -func (m EmptyMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m EmptyMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m EmptyMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { return nil } diff --git a/maps/empty_test.go b/maps/empty_test.go index 9bfa3ed..4fa413d 100644 --- a/maps/empty_test.go +++ b/maps/empty_test.go @@ -28,95 +28,53 @@ func TestEmptyMapSetupBoard(t *testing.T) { "empty 7x7", rules.NewBoardState(7, 7), rules.MinRand, - &rules.BoardState{ - Width: 7, - Height: 7, - Snakes: []rules.Snake{}, - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(7, 7), nil, }, { "not enough room for snakes 7x7", - &rules.BoardState{ - Width: 7, - Height: 7, - Snakes: generateSnakes(17), - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(7, 7).WithSnakes(generateSnakes(17)), rules.MinRand, nil, rules.ErrorTooManySnakes, }, { "not enough room for snakes 5x5", - &rules.BoardState{ - Width: 5, - Height: 5, - Snakes: generateSnakes(14), - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(5, 5).WithSnakes(generateSnakes(14)), rules.MinRand, nil, rules.ErrorTooManySnakes, }, { "full 11x11 min", - &rules.BoardState{ - Width: 11, - Height: 11, - Snakes: generateSnakes(8), - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)), rules.MinRand, - &rules.BoardState{ - Width: 11, - Height: 11, - Snakes: []rules.Snake{ - {ID: "1", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100}, - {ID: "2", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100}, - {ID: "3", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100}, - {ID: "4", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100}, - {ID: "5", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100}, - {ID: "6", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, - {ID: "7", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100}, - {ID: "8", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100}, - }, - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{ + {ID: "1", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100}, + {ID: "2", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100}, + {ID: "3", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100}, + {ID: "4", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100}, + {ID: "5", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100}, + {ID: "6", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, + {ID: "7", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100}, + {ID: "8", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100}, + }), nil, }, { "full 11x11 max", - &rules.BoardState{ - Width: 11, - Height: 11, - Snakes: generateSnakes(8), - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)), rules.MaxRand, - &rules.BoardState{ - Width: 11, - Height: 11, - Snakes: []rules.Snake{ - {ID: "1", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, - {ID: "2", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100}, - {ID: "3", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100}, - {ID: "4", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100}, - {ID: "5", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100}, - {ID: "6", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100}, - {ID: "7", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100}, - {ID: "8", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100}, - }, - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{ + {ID: "1", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, + {ID: "2", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100}, + {ID: "3", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100}, + {ID: "4", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100}, + {ID: "5", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100}, + {ID: "6", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100}, + {ID: "7", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100}, + {ID: "8", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100}, + }), nil, }, } @@ -139,27 +97,13 @@ func TestEmptyMapSetupBoard(t *testing.T) { func TestEmptyMapUpdateBoard(t *testing.T) { m := maps.EmptyMap{} - initialBoardState := &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 0}}, - Hazards: []rules.Point{}, - } - settings := rules.Settings{ - FoodSpawnChance: 50, - MinimumFood: 2, - }.WithRand(rules.MaxRand) + initialBoardState := rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}}) + settings := rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "2").WithRand(rules.MaxRand) nextBoardState := initialBoardState.Clone() - err := m.UpdateBoard(initialBoardState.Clone(), settings, maps.NewBoardStateEditor(nextBoardState)) + err := m.PostUpdateBoard(initialBoardState.Clone(), settings, maps.NewBoardStateEditor(nextBoardState)) require.NoError(t, err) - require.Equal(t, &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 0}}, - Hazards: []rules.Point{}, - }, nextBoardState) + expectedBoardState := rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}}) + require.Equal(t, expectedBoardState, nextBoardState) } diff --git a/maps/game_map.go b/maps/game_map.go index 5ab76b4..5990b5e 100644 --- a/maps/game_map.go +++ b/maps/game_map.go @@ -24,8 +24,21 @@ type GameMap interface { // Called to generate a new board. The map is responsible for placing all snakes, food, and hazards. SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error - // Called every turn to optionally update the board. - UpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error + // Called every turn to optionally update the board before the board is sent to snakes to get their moves. + // Changes made here will be seen by snakes before before making their moves, but users in the + // browser will see the changes at the same time as the snakes' moves. + // + // State that is stored in the map by this method will be visible to the PostUpdateBoard method + // later in the same turn, but will not nessecarily be available when processing later turns. + // + // Disclaimer: Unless you have a specific usecase like moving hazards or storing intermediate state, + // PostUpdateBoard is probably the better function to use. + PreUpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error + + // Called every turn to optionally update the board after all other rules have been applied. + // Changes made here will be seen by both snakes and users in the browser, before before snakes + // make their next moves. + PostUpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error } type Metadata struct { @@ -166,6 +179,12 @@ type Editor interface { // Note: the body values in the return value are a copy and modifying them won't affect the board. SnakeBodies() map[string][]rules.Point + // Get an editable reference to the BoardState's GameState field + GameState() map[string]string + + // Get an editable reference to the BoardState's PointState field + PointState() map[rules.Point]int + // Given a list of Snakes and a list of head coordinates, randomly place // the snakes on those coordinates, or return an error if placement of all // Snakes is impossible. @@ -270,6 +289,16 @@ func (editor *BoardStateEditor) SnakeBodies() map[string][]rules.Point { return result } +// Get an editable reference to the BoardState's GameState field +func (editor *BoardStateEditor) GameState() map[string]string { + return editor.boardState.GameState +} + +// Get an editable reference to the BoardState's PointState field +func (editor *BoardStateEditor) PointState() map[rules.Point]int { + return editor.boardState.PointState +} + // Given a list of Snakes and a list of head coordinates, randomly place // the snakes on those coordinates, or return an error if placement of all // Snakes is impossible. diff --git a/maps/game_map_test.go b/maps/game_map_test.go index 7ce3865..3224042 100644 --- a/maps/game_map_test.go +++ b/maps/game_map_test.go @@ -135,18 +135,16 @@ func TestBoardStateEditor(t *testing.T) { editor.PlaceSnake("existing_snake", []rules.Point{{X: 5, Y: 2}, {X: 5, Y: 1}, {X: 5, Y: 0}}, 99) editor.PlaceSnake("new_snake", []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}, 98) - require.Equal(t, &rules.BoardState{ - Width: 11, - Height: 11, - Food: []rules.Point{ + expected := rules.NewBoardState(11, 11). + WithFood([]rules.Point{ {X: 1, Y: 3}, {X: 3, Y: 7}, - }, - Hazards: []rules.Point{ + }). + WithHazards([]rules.Point{ {X: 1, Y: 3}, {X: 3, Y: 7}, - }, - Snakes: []rules.Snake{ + }). + WithSnakes([]rules.Snake{ { ID: "existing_snake", Health: 99, @@ -157,8 +155,8 @@ func TestBoardStateEditor(t *testing.T) { Health: 98, Body: []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}, }, - }, - }, boardState) + }) + require.Equal(t, expected, boardState) require.Equal(t, []rules.Point{ {X: 1, Y: 3}, diff --git a/maps/hazard_pits.go b/maps/hazard_pits.go index f37f9b2..fa56337 100644 --- a/maps/hazard_pits.go +++ b/maps/hazard_pits.go @@ -97,8 +97,12 @@ func (m HazardPitsMap) SetupBoard(initialBoardState *rules.BoardState, settings return nil } -func (m HazardPitsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m HazardPitsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m HazardPitsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) if err != nil { return err } @@ -109,9 +113,10 @@ func (m HazardPitsMap) UpdateBoard(lastBoardState *rules.BoardState, settings ru // Cycle 3 - 3 layers // Cycle 4-6 - 4 layers of hazards - if lastBoardState.Turn%settings.RoyaleSettings.ShrinkEveryNTurns == 0 { + shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0) + if lastBoardState.Turn%shrinkEveryNTurns == 0 { // Is it time to update the hazards - layers := (lastBoardState.Turn / settings.RoyaleSettings.ShrinkEveryNTurns) % 7 + layers := (lastBoardState.Turn / shrinkEveryNTurns) % 7 if layers > 4 { layers = 4 } diff --git a/maps/hazard_pits_test.go b/maps/hazard_pits_test.go index 498d742..dba6a9a 100644 --- a/maps/hazard_pits_test.go +++ b/maps/hazard_pits_test.go @@ -38,7 +38,7 @@ func TestHazardPitsMap(t *testing.T) { state = rules.NewBoardState(int(11), int(11)) m = maps.HazardPitsMap{} - settings.RoyaleSettings.ShrinkEveryNTurns = 1 + settings = rules.NewSettingsWithParams(rules.ParamShrinkEveryNTurns, "1") editor = maps.NewBoardStateEditor(state) require.Empty(t, state.Hazards) err = m.SetupBoard(state, settings, editor) @@ -47,7 +47,7 @@ func TestHazardPitsMap(t *testing.T) { // Verify the hazard progression through the turns for i := 0; i < 16; i++ { state.Turn = i - err = m.UpdateBoard(state, settings, editor) + err = m.PostUpdateBoard(state, settings, editor) require.NoError(t, err) if i == 1 { require.Len(t, state.Hazards, 21) diff --git a/maps/hazards.go b/maps/hazards.go index 53b1ff9..f4db547 100644 --- a/maps/hazards.go +++ b/maps/hazards.go @@ -54,8 +54,12 @@ func (m InnerBorderHazardsMap) SetupBoard(lastBoardState *rules.BoardState, sett return nil } -func (m InnerBorderHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - return StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m InnerBorderHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m InnerBorderHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) } type ConcentricRingsHazardsMap struct{} @@ -96,8 +100,12 @@ func (m ConcentricRingsHazardsMap) SetupBoard(lastBoardState *rules.BoardState, return nil } -func (m ConcentricRingsHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - return StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m ConcentricRingsHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m ConcentricRingsHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) } type ColumnsHazardsMap struct{} @@ -135,8 +143,12 @@ func (m ColumnsHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings return nil } -func (m ColumnsHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - return StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m ColumnsHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m ColumnsHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) } type SpiralHazardsMap struct{} @@ -163,8 +175,12 @@ func (m SpiralHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings return (StandardMap{}).SetupBoard(lastBoardState, settings, editor) } -func (m SpiralHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m SpiralHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m SpiralHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) if err != nil { return err } @@ -256,8 +272,12 @@ func (m ScatterFillMap) SetupBoard(lastBoardState *rules.BoardState, settings ru return (StandardMap{}).SetupBoard(lastBoardState, settings, editor) } -func (m ScatterFillMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m ScatterFillMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m ScatterFillMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) if err != nil { return err } @@ -308,8 +328,12 @@ func (m DirectionalExpandingBoxMap) SetupBoard(lastBoardState *rules.BoardState, return (StandardMap{}).SetupBoard(lastBoardState, settings, editor) } -func (m DirectionalExpandingBoxMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m DirectionalExpandingBoxMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m DirectionalExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) if err != nil { return err } @@ -423,8 +447,12 @@ func (m ExpandingBoxMap) SetupBoard(lastBoardState *rules.BoardState, settings r return (StandardMap{}).SetupBoard(lastBoardState, settings, editor) } -func (m ExpandingBoxMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m ExpandingBoxMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m ExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) if err != nil { return err } @@ -499,8 +527,12 @@ func (m ExpandingScatterMap) SetupBoard(lastBoardState *rules.BoardState, settin return (StandardMap{}).SetupBoard(lastBoardState, settings, editor) } -func (m ExpandingScatterMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m ExpandingScatterMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return (StandardMap{}).PreUpdateBoard(lastBoardState, settings, editor) +} + +func (m ExpandingScatterMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) if err != nil { return err } diff --git a/maps/hazards_test.go b/maps/hazards_test.go index 80229e9..7350e97 100644 --- a/maps/hazards_test.go +++ b/maps/hazards_test.go @@ -102,7 +102,7 @@ func TestSpiralHazardsMap(t *testing.T) { for i := 0; i < 1000; i++ { state.Turn = i - err = m.UpdateBoard(state, settings, editor) + err = m.PostUpdateBoard(state, settings, editor) require.NoError(t, err) } require.NotEmpty(t, state.Hazards) @@ -123,7 +123,7 @@ func TestScatterFillMap(t *testing.T) { totalTurns := 11 * 11 * 2 for i := 0; i < totalTurns; i++ { state.Turn = i - err = m.UpdateBoard(state, settings, editor) + err = m.PostUpdateBoard(state, settings, editor) require.NoError(t, err) } require.NotEmpty(t, state.Hazards) @@ -144,7 +144,7 @@ func TestDirectionalExpandingBoxMap(t *testing.T) { totalTurns := 1000 for i := 0; i < totalTurns; i++ { state.Turn = i - err = m.UpdateBoard(state, settings, editor) + err = m.PostUpdateBoard(state, settings, editor) require.NoError(t, err) } require.NotEmpty(t, state.Hazards) @@ -165,7 +165,7 @@ func TestExpandingBoxMap(t *testing.T) { totalTurns := 1000 for i := 0; i < totalTurns; i++ { state.Turn = i - err = m.UpdateBoard(state, settings, editor) + err = m.PostUpdateBoard(state, settings, editor) require.NoError(t, err) } require.NotEmpty(t, state.Hazards) @@ -186,7 +186,7 @@ func TestExpandingScatterMap(t *testing.T) { totalTurns := 1000 for i := 0; i < totalTurns; i++ { state.Turn = i - err = m.UpdateBoard(state, settings, editor) + err = m.PostUpdateBoard(state, settings, editor) require.NoError(t, err) } require.NotEmpty(t, state.Hazards) diff --git a/maps/healing_pools.go b/maps/healing_pools.go index 1a67a3f..49f277c 100644 --- a/maps/healing_pools.go +++ b/maps/healing_pools.go @@ -50,12 +50,17 @@ func (m HealingPoolsMap) SetupBoard(initialBoardState *rules.BoardState, setting return nil } -func (m HealingPoolsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - if err := (StandardMap{}).UpdateBoard(lastBoardState, settings, editor); err != nil { +func (m HealingPoolsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m HealingPoolsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + if err := (StandardMap{}).PostUpdateBoard(lastBoardState, settings, editor); err != nil { return err } - if lastBoardState.Turn > 0 && settings.RoyaleSettings.ShrinkEveryNTurns > 0 && len(lastBoardState.Hazards) > 0 && lastBoardState.Turn%settings.RoyaleSettings.ShrinkEveryNTurns == 0 { + shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0) + if lastBoardState.Turn > 0 && shrinkEveryNTurns > 0 && len(lastBoardState.Hazards) > 0 && lastBoardState.Turn%shrinkEveryNTurns == 0 { // Attempt to remove a healing pool every ShrinkEveryNTurns until there are none remaining i := rand.Intn(len(lastBoardState.Hazards)) editor.RemoveHazard(lastBoardState.Hazards[i]) diff --git a/maps/healing_pools_test.go b/maps/healing_pools_test.go index d807c32..1203fa3 100644 --- a/maps/healing_pools_test.go +++ b/maps/healing_pools_test.go @@ -40,8 +40,8 @@ func TestHealingPoolsMap(t *testing.T) { t.Run(fmt.Sprintf("%dx%d", tc.boardSize, tc.boardSize), func(t *testing.T) { m := maps.HealingPoolsMap{} state := rules.NewBoardState(tc.boardSize, tc.boardSize) - settings := rules.Settings{} - settings.RoyaleSettings.ShrinkEveryNTurns = 10 + shrinkEveryNTurns := 10 + settings := rules.NewSettingsWithParams(rules.ParamShrinkEveryNTurns, fmt.Sprint(shrinkEveryNTurns)) // ensure the hazards are added to the board at setup editor := maps.NewBoardStateEditor(state) @@ -56,10 +56,10 @@ func TestHealingPoolsMap(t *testing.T) { } // ensure the hazards are removed - totalTurns := settings.RoyaleSettings.ShrinkEveryNTurns*tc.expectedHazards + 1 + totalTurns := shrinkEveryNTurns*tc.expectedHazards + 1 for i := 0; i < totalTurns; i++ { state.Turn = i - err = m.UpdateBoard(state, settings, editor) + err = m.PostUpdateBoard(state, settings, editor) require.NoError(t, err) } diff --git a/maps/helpers.go b/maps/helpers.go index 68b190c..9bc76f3 100644 --- a/maps/helpers.go +++ b/maps/helpers.go @@ -25,17 +25,24 @@ func SetupBoard(mapID string, settings rules.Settings, width, height int, snakeI return boardState, nil } -// UpdateBoard is a shortcut for looking up a map by ID and updating an existing board state with it. -func UpdateBoard(mapID string, previousBoardState *rules.BoardState, settings rules.Settings) (*rules.BoardState, error) { - gameMap, err := GetMap(mapID) +// PreUpdateBoard updates a board state with a map. +func PreUpdateBoard(gameMap GameMap, previousBoardState *rules.BoardState, settings rules.Settings) (*rules.BoardState, error) { + nextBoardState := previousBoardState.Clone() + editor := NewBoardStateEditor(nextBoardState) + + err := gameMap.PreUpdateBoard(previousBoardState, settings, editor) if err != nil { return nil, err } + return nextBoardState, nil +} + +func PostUpdateBoard(gameMap GameMap, previousBoardState *rules.BoardState, settings rules.Settings) (*rules.BoardState, error) { nextBoardState := previousBoardState.Clone() editor := NewBoardStateEditor(nextBoardState) - err = gameMap.UpdateBoard(previousBoardState, settings, editor) + err := gameMap.PostUpdateBoard(previousBoardState, settings, editor) if err != nil { return nil, err } @@ -77,7 +84,11 @@ func (m StubMap) SetupBoard(initialBoardState *rules.BoardState, settings rules. return nil } -func (m StubMap) UpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m StubMap) PreUpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m StubMap) PostUpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { if m.Error != nil { return m.Error } diff --git a/maps/helpers_test.go b/maps/helpers_test.go index fdf7e5b..be554bc 100644 --- a/maps/helpers_test.go +++ b/maps/helpers_test.go @@ -82,11 +82,10 @@ func TestUpdateBoard(t *testing.T) { }, } - previousBoardState := &rules.BoardState{ - Turn: 0, - Food: []rules.Point{{X: 0, Y: 1}}, - Hazards: []rules.Point{{X: 3, Y: 4}}, - Snakes: []rules.Snake{ + previousBoardState := rules.NewBoardState(5, 5). + WithFood([]rules.Point{{X: 0, Y: 1}}). + WithHazards([]rules.Point{{X: 3, Y: 4}}). + WithSnakes([]rules.Snake{ { ID: "1", Health: 100, @@ -96,11 +95,9 @@ func TestUpdateBoard(t *testing.T) { {X: 6, Y: 2}, }, }, - }, - } - + }) maps.TestMap(testMap.ID(), testMap, func() { - boardState, err := maps.UpdateBoard(testMap.ID(), previousBoardState, rules.Settings{}) + boardState, err := maps.PostUpdateBoard(testMap, previousBoardState, rules.Settings{}) require.NoError(t, err) diff --git a/maps/registry_test.go b/maps/registry_test.go index e70999a..4f5d43a 100644 --- a/maps/registry_test.go +++ b/maps/registry_test.go @@ -10,14 +10,12 @@ import ( const maxBoardWidth, maxBoardHeight = 25, 25 -var testSettings rules.Settings = rules.Settings{ - FoodSpawnChance: 25, - MinimumFood: 1, - HazardDamagePerTurn: 14, - RoyaleSettings: rules.RoyaleSettings{ - ShrinkEveryNTurns: 1, - }, -} +var testSettings rules.Settings = rules.NewSettings(map[string]string{ + rules.ParamFoodSpawnChance: "25", + rules.ParamMinimumFood: "1", + rules.ParamHazardDamagePerTurn: "14", + rules.ParamShrinkEveryNTurns: "1", +}) func TestRegisteredMaps(t *testing.T) { for mapName, gameMap := range globalRegistry { @@ -96,7 +94,7 @@ func TestRegisteredMaps(t *testing.T) { passedBoardState := previousBoardState.Clone() tempBoardState := previousBoardState.Clone() - err := gameMap.UpdateBoard(passedBoardState, testSettings, NewBoardStateEditor(tempBoardState)) + err := gameMap.PostUpdateBoard(passedBoardState, testSettings, NewBoardStateEditor(tempBoardState)) require.NoError(t, err, "GameMap.UpdateBoard returned an error") require.Equal(t, previousBoardState, passedBoardState, "BoardState should not be modified directly by GameMap.UpdateBoard") }) diff --git a/maps/rivers_and_bridges.go b/maps/rivers_and_bridges.go index a27b119..3c14d3c 100644 --- a/maps/rivers_and_bridges.go +++ b/maps/rivers_and_bridges.go @@ -71,7 +71,11 @@ func (m RiverAndBridgesMediumHazardsMap) SetupBoard(initialBoardState *rules.Boa return setupRiverAndBridgesBoard(riversAndBridgesMediumStartPositions, riversAndBridgesMediumHazards, initialBoardState, settings, editor) } -func (m RiverAndBridgesMediumHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m RiverAndBridgesMediumHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m RiverAndBridgesMediumHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { return placeRiverAndBridgesFood(lastBoardState, settings, editor) } @@ -142,7 +146,11 @@ func (m RiverAndBridgesLargeHazardsMap) SetupBoard(initialBoardState *rules.Boar return setupRiverAndBridgesBoard(riversAndBridgesLargeStartPositions, riversAndBridgesLargeHazards, initialBoardState, settings, editor) } -func (m RiverAndBridgesLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m RiverAndBridgesLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m RiverAndBridgesLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { return placeRiverAndBridgesFood(lastBoardState, settings, editor) } @@ -241,7 +249,11 @@ func (m RiverAndBridgesExtraLargeHazardsMap) SetupBoard(initialBoardState *rules return setupRiverAndBridgesBoard(riversAndBridgesExtraLargeStartPositions, riversAndBridgesExtraLargeHazards, initialBoardState, settings, editor) } -func (m RiverAndBridgesExtraLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m RiverAndBridgesExtraLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m RiverAndBridgesExtraLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { return placeRiverAndBridgesFood(lastBoardState, settings, editor) } @@ -355,7 +367,11 @@ func (m IslandsAndBridgesMediumHazardsMap) SetupBoard(initialBoardState *rules.B return setupRiverAndBridgesBoard(islandsAndBridgesMediumStartPositions, islandsAndBridgesMediumHazards, initialBoardState, settings, editor) } -func (m IslandsAndBridgesMediumHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m IslandsAndBridgesMediumHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m IslandsAndBridgesMediumHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { return placeRiverAndBridgesFood(lastBoardState, settings, editor) } @@ -441,7 +457,11 @@ func (m IslandsAndBridgesLargeHazardsMap) SetupBoard(initialBoardState *rules.Bo return setupRiverAndBridgesBoard(islandsAndBridgesLargeStartPositions, islandsAndBridgesLargeHazards, initialBoardState, settings, editor) } -func (m IslandsAndBridgesLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m IslandsAndBridgesLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m IslandsAndBridgesLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { return placeRiverAndBridgesFood(lastBoardState, settings, editor) } diff --git a/maps/royale.go b/maps/royale.go index c04f94b..69380d2 100644 --- a/maps/royale.go +++ b/maps/royale.go @@ -33,20 +33,25 @@ func (m RoyaleHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings return StandardMap{}.SetupBoard(lastBoardState, settings, editor) } -func (m RoyaleHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m RoyaleHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m RoyaleHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { // Use StandardMap to populate food - if err := (StandardMap{}).UpdateBoard(lastBoardState, settings, editor); err != nil { + if err := (StandardMap{}).PostUpdateBoard(lastBoardState, settings, editor); err != nil { return err } // Royale uses the current turn to generate hazards, not the previous turn that's in the board state turn := lastBoardState.Turn + 1 - if settings.RoyaleSettings.ShrinkEveryNTurns < 1 { + shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0) + if shrinkEveryNTurns < 1 { return errors.New("royale game can't shrink more frequently than every turn") } - if turn < settings.RoyaleSettings.ShrinkEveryNTurns { + if turn < shrinkEveryNTurns { return nil } @@ -56,7 +61,7 @@ func (m RoyaleHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings // Get random generator for turn zero, because we're regenerating all hazards every time. randGenerator := settings.GetRand(0) - numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns + numShrinks := turn / shrinkEveryNTurns minX, maxX := 0, lastBoardState.Width-1 minY, maxY := 0, lastBoardState.Height-1 for i := 0; i < numShrinks; i++ { diff --git a/maps/sinkholes.go b/maps/sinkholes.go index 849645c..25990d9 100644 --- a/maps/sinkholes.go +++ b/maps/sinkholes.go @@ -33,8 +33,12 @@ func (m SinkholesMap) SetupBoard(initialBoardState *rules.BoardState, settings r return (StandardMap{}).SetupBoard(initialBoardState, settings, editor) } -func (m SinkholesMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +func (m SinkholesMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m SinkholesMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) if err != nil { return err } @@ -42,8 +46,9 @@ func (m SinkholesMap) UpdateBoard(lastBoardState *rules.BoardState, settings rul currentTurn := lastBoardState.Turn startTurn := 1 spawnEveryNTurns := 10 - if settings.RoyaleSettings.ShrinkEveryNTurns > 0 { - spawnEveryNTurns = settings.RoyaleSettings.ShrinkEveryNTurns + shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0) + if shrinkEveryNTurns > 0 { + spawnEveryNTurns = shrinkEveryNTurns } maxRings := 5 if lastBoardState.Width == 7 { diff --git a/maps/sinkholes_test.go b/maps/sinkholes_test.go index 4fa06a7..b05705f 100644 --- a/maps/sinkholes_test.go +++ b/maps/sinkholes_test.go @@ -38,7 +38,7 @@ func TestSinkholesMap(t *testing.T) { totalTurns := 100 for i := 0; i < totalTurns; i++ { state.Turn = i - err = m.UpdateBoard(state, settings, editor) + err = m.PostUpdateBoard(state, settings, editor) require.NoError(t, err) } require.NotEmpty(t, state.Hazards) diff --git a/maps/snail_mode.go b/maps/snail_mode.go index 5a843ae..882be05 100644 --- a/maps/snail_mode.go +++ b/maps/snail_mode.go @@ -4,20 +4,22 @@ import ( "github.com/BattlesnakeOfficial/rules" ) -type SnailModeMap struct{} +type SnailModeMap struct { + lastTailPositions map[rules.Point]int // local state is preserved during the turn +} // init registers this map in the global registry. func init() { - globalRegistry.RegisterMap("snail_mode", SnailModeMap{}) + globalRegistry.RegisterMap("snail_mode", &SnailModeMap{lastTailPositions: nil}) } // ID returns a unique identifier for this map. -func (m SnailModeMap) ID() string { +func (m *SnailModeMap) ID() string { return "snail_mode" } // Meta returns the non-functional metadata about this map. -func (m SnailModeMap) Meta() Metadata { +func (m *SnailModeMap) Meta() Metadata { return Metadata{ Name: "Snail Mode", Description: "Snakes leave behind a trail of hazards", @@ -31,7 +33,7 @@ func (m SnailModeMap) Meta() Metadata { } // SetupBoard here is pretty 'standard' and doesn't do any special setup for this game mode -func (m SnailModeMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m *SnailModeMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { rand := settings.GetRand(0) if len(initialBoardState.Snakes) > int(m.Meta().MaxPlayers) { @@ -57,23 +59,6 @@ func (m SnailModeMap) SetupBoard(initialBoardState *rules.BoardState, settings r return nil } -// storeTailLocation returns an offboard point that corresponds to the given point. -// This is useful for storing state that can be accessed next turn. -func storeTailLocation(point rules.Point, height int) rules.Point { - return rules.Point{X: point.X, Y: point.Y + height} -} - -// getPrevTailLocation returns the onboard point that corresponds to an offboard point. -// This is useful for restoring state that was stored last turn. -func getPrevTailLocation(point rules.Point, height int) rules.Point { - return rules.Point{X: point.X, Y: point.Y - height} -} - -// outOfBounds determines if the given point is out of bounds for the current board size -func outOfBounds(p rules.Point, w, h int) bool { - return p.X < 0 || p.Y < 0 || p.X >= w || p.Y >= h -} - // doubleTail determine if the snake has a double stacked tail currently func doubleTail(snake *rules.Snake) bool { almostTail := snake.Body[len(snake.Body)-2] @@ -86,12 +71,28 @@ func isEliminated(s *rules.Snake) bool { return s.EliminatedCause != rules.NotEliminated } -// UpdateBoard does the work of placing the hazards along the 'snail tail' of snakes -// This is responsible for saving the current tail location off the board -// and restoring the previous tail position. This also handles removing one hazards from -// the current stacks so the hazards tails fade as the snake moves away. -func (m SnailModeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { - err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor) +// PreUpdateBoard stores the tail position of each snake in memory, to be +// able to place hazards there after the snakes move. +func (m *SnailModeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + m.lastTailPositions = make(map[rules.Point]int) + for _, snake := range lastBoardState.Snakes { + if isEliminated(&snake) { + continue + } + // Double tail means that the tail will stay on the same square for more + // than one turn, so we don't want to spawn hazards + if doubleTail(&snake) { + continue + } + m.lastTailPositions[snake.Body[len(snake.Body)-1]] = len(snake.Body) + } + return nil +} + +// PostUpdateBoard does the work of placing the hazards along the 'snail tail' of snakes +// This also handles removing one hazards from the current stacks so the hazards tails fade as the snake moves away. +func (m *SnailModeMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor) if err != nil { return err } @@ -100,79 +101,38 @@ func (m SnailModeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rul // need to be cleared first. editor.ClearHazards() - // This is a list of all the hazards we want to add for the previous tails - // These were stored off board in the previous turn as a way to save state - // When we add the locations to this list we have already converted the off-board - // points to on-board points - tailLocations := make([]rules.Point, 0, len(lastBoardState.Snakes)) - // Count the number of hazards for a given position - // Add non-double tail locations to a slice hazardCounts := map[rules.Point]int{} for _, hazard := range lastBoardState.Hazards { - - // discard out of bound - if outOfBounds(hazard, lastBoardState.Width, lastBoardState.Height) { - onBoardTail := getPrevTailLocation(hazard, lastBoardState.Height) - tailLocations = append(tailLocations, onBoardTail) - } else { - hazardCounts[hazard]++ - } + hazardCounts[hazard]++ } // Add back existing hazards, but with a stack of 1 less than before. // This has the effect of making the snail-trail disappear over time. for hazard, count := range hazardCounts { - for i := 0; i < count-1; i++ { editor.AddHazard(hazard) } } - // Store a stack of hazards for the tail of each snake. This is stored out - // of bounds and then applied on the next turn. The stack count is equal - // the lenght of the snake. - for _, snake := range lastBoardState.Snakes { - if isEliminated(&snake) { - continue - } - - // Double tail means that the tail will stay on the same square for more - // than one turn, so we don't want to spawn hazards - if doubleTail(&snake) { - continue - } - - tail := snake.Body[len(snake.Body)-1] - offBoardTail := storeTailLocation(tail, lastBoardState.Height) - for i := 0; i < len(snake.Body); i++ { - editor.AddHazard(offBoardTail) - } - } - - // Read offboard tails and move them to the board. The offboard tails are - // stacked based on the length of the snake - for _, p := range tailLocations { - - // Skip position if a snakes head occupies it. - // Otherwise hazard shows up in the viewer on top of a snake head, but - // does not damage the snake, which is visually confusing. - isHead := false + // Place a new stack of hazards where each snake's tail used to be +NewHazardLoop: + for location, count := range m.lastTailPositions { for _, snake := range lastBoardState.Snakes { if isEliminated(&snake) { continue } head := snake.Body[0] - if p.X == head.X && p.Y == head.Y { - isHead = true - break + if location.X == head.X && location.Y == head.Y { + // Skip position if a snakes head occupies it. + // Otherwise hazard shows up in the viewer on top of a snake head, but + // does not damage the snake, which is visually confusing. + continue NewHazardLoop } } - if isHead { - continue + for i := 0; i < count; i++ { + editor.AddHazard(location) } - - editor.AddHazard(p) } return nil diff --git a/maps/solo_maze.go b/maps/solo_maze.go index d752594..5d5afb7 100644 --- a/maps/solo_maze.go +++ b/maps/solo_maze.go @@ -176,7 +176,11 @@ func (m SoloMazeMap) PlaceFood(boardState *rules.BoardState, settings rules.Sett } } -func (m SoloMazeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m SoloMazeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m SoloMazeMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { currentLevel, e := m.ReadBitState(lastBoardState) if e != nil { return e diff --git a/maps/standard.go b/maps/standard.go index ee1eba1..1c912d9 100644 --- a/maps/standard.go +++ b/maps/standard.go @@ -57,7 +57,11 @@ func (m StandardMap) SetupBoard(initialBoardState *rules.BoardState, settings ru return nil } -func (m StandardMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { +func (m StandardMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + return nil +} + +func (m StandardMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { rand := settings.GetRand(lastBoardState.Turn) foodNeeded := checkFoodNeedingPlacement(rand, settings, lastBoardState) @@ -69,8 +73,8 @@ func (m StandardMap) UpdateBoard(lastBoardState *rules.BoardState, settings rule } func checkFoodNeedingPlacement(rand rules.Rand, settings rules.Settings, state *rules.BoardState) int { - minFood := int(settings.MinimumFood) - foodSpawnChance := int(settings.FoodSpawnChance) + minFood := settings.Int(rules.ParamMinimumFood, 0) + foodSpawnChance := settings.Int(rules.ParamFoodSpawnChance, 0) numCurrentFood := len(state.Food) if numCurrentFood < minFood { diff --git a/maps/standard_test.go b/maps/standard_test.go index b567a7c..6b1a002 100644 --- a/maps/standard_test.go +++ b/maps/standard_test.go @@ -29,65 +29,29 @@ func TestStandardMapSetupBoard(t *testing.T) { "empty 7x7", rules.NewBoardState(7, 7), rules.MinRand, - &rules.BoardState{ - Width: 7, - Height: 7, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 3, Y: 3}}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(7, 7).WithFood([]rules.Point{{X: 3, Y: 3}}), nil, }, { "not enough room for snakes 7x7", - &rules.BoardState{ - Width: 7, - Height: 7, - Snakes: generateSnakes(17), - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(7, 7).WithSnakes(generateSnakes(17)), rules.MinRand, nil, rules.ErrorTooManySnakes, }, { "not enough room for snakes 5x5", - &rules.BoardState{ - Width: 5, - Height: 5, - Snakes: generateSnakes(14), - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(5, 5).WithSnakes(generateSnakes(14)), rules.MinRand, nil, rules.ErrorTooManySnakes, }, { "full 11x11 min", - &rules.BoardState{ - Width: 11, - Height: 11, - Snakes: generateSnakes(8), - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)), rules.MinRand, - &rules.BoardState{ - Width: 11, - Height: 11, - Snakes: []rules.Snake{ - {ID: "1", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100}, - {ID: "2", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100}, - {ID: "3", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100}, - {ID: "4", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100}, - {ID: "5", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100}, - {ID: "6", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, - {ID: "7", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100}, - {ID: "8", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100}, - }, - Food: []rules.Point{ + rules.NewBoardState(11, 11). + WithFood([]rules.Point{ {X: 0, Y: 2}, {X: 0, Y: 8}, {X: 8, Y: 0}, @@ -97,35 +61,25 @@ func TestStandardMapSetupBoard(t *testing.T) { {X: 4, Y: 10}, {X: 10, Y: 4}, {X: 5, Y: 5}, - }, - Hazards: []rules.Point{}, - }, + }). + WithSnakes([]rules.Snake{ + {ID: "1", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100}, + {ID: "2", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100}, + {ID: "3", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100}, + {ID: "4", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100}, + {ID: "5", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100}, + {ID: "6", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, + {ID: "7", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100}, + {ID: "8", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100}, + }), nil, }, { "full 11x11 max", - &rules.BoardState{ - Width: 11, - Height: 11, - Snakes: generateSnakes(8), - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)), rules.MaxRand, - &rules.BoardState{ - Width: 11, - Height: 11, - Snakes: []rules.Snake{ - {ID: "1", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, - {ID: "2", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100}, - {ID: "3", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100}, - {ID: "4", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100}, - {ID: "5", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100}, - {ID: "6", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100}, - {ID: "7", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100}, - {ID: "8", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100}, - }, - Food: []rules.Point{ + rules.NewBoardState(11, 11). + WithFood([]rules.Point{ {X: 6, Y: 0}, {X: 6, Y: 10}, {X: 10, Y: 6}, @@ -135,9 +89,17 @@ func TestStandardMapSetupBoard(t *testing.T) { {X: 10, Y: 8}, {X: 2, Y: 0}, {X: 5, Y: 5}, - }, - Hazards: []rules.Point{}, - }, + }). + WithSnakes([]rules.Snake{ + {ID: "1", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, + {ID: "2", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100}, + {ID: "3", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100}, + {ID: "4", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100}, + {ID: "5", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100}, + {ID: "6", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100}, + {ID: "7", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100}, + {ID: "8", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100}, + }), nil, }, } @@ -172,132 +134,51 @@ func TestStandardMapUpdateBoard(t *testing.T) { { "empty no food", rules.NewBoardState(2, 2), - rules.Settings{ - FoodSpawnChance: 0, - MinimumFood: 0, - }, + rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "0", rules.ParamMinimumFood, "0"), rules.MinRand, - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(2, 2), }, { "empty MinimumFood", rules.NewBoardState(2, 2), - rules.Settings{ - FoodSpawnChance: 0, - MinimumFood: 2, - }, + rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "0", rules.ParamMinimumFood, "2"), rules.MinRand, - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}}), }, { "not empty MinimumFood", - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 1}}, - Hazards: []rules.Point{}, - }, - rules.Settings{ - FoodSpawnChance: 0, - MinimumFood: 2, - }, + rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 1}}), + rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "0", rules.ParamMinimumFood, "2"), rules.MinRand, - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 1}, {X: 0, Y: 0}}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 1}, {X: 0, Y: 0}}), }, { "empty FoodSpawnChance inactive", rules.NewBoardState(2, 2), - rules.Settings{ - FoodSpawnChance: 50, - MinimumFood: 0, - }, + rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"), rules.MinRand, - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(2, 2), }, { "empty FoodSpawnChance active", rules.NewBoardState(2, 2), - rules.Settings{ - FoodSpawnChance: 50, - MinimumFood: 0, - }, + rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"), rules.MaxRand, - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 1}}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 1}}), }, { "not empty FoodSpawnChance active", - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 0}}, - Hazards: []rules.Point{}, - }, - rules.Settings{ - FoodSpawnChance: 50, - MinimumFood: 0, - }, + rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}}), + rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"), rules.MaxRand, - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}}), }, { "not empty FoodSpawnChance no room", - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}}, - Hazards: []rules.Point{}, - }, - rules.Settings{ - FoodSpawnChance: 50, - MinimumFood: 0, - }, + rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}}), + rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"), rules.MaxRand, - &rules.BoardState{ - Width: 2, - Height: 2, - Snakes: []rules.Snake{}, - Food: []rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}}, - Hazards: []rules.Point{}, - }, + rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}}), }, } for _, test := range tests { @@ -306,7 +187,7 @@ func TestStandardMapUpdateBoard(t *testing.T) { settings := test.settings.WithRand(test.rand) editor := maps.NewBoardStateEditor(nextBoardState) - err := m.UpdateBoard(test.initialBoardState.Clone(), settings, editor) + err := m.PostUpdateBoard(test.initialBoardState.Clone(), settings, editor) require.NoError(t, err) require.Equal(t, test.expected, nextBoardState) diff --git a/pipeline.go b/pipeline.go index b11e6fd..9e3be27 100644 --- a/pipeline.go +++ b/pipeline.go @@ -38,6 +38,33 @@ var globalRegistry = StageRegistry{ StageMovementWrapBoundaries: MoveSnakesWrapped, } +// Pipeline is an ordered sequences of game stages which are executed to produce the +// next game state. +// +// If a stage produces an error or an ended game state, the pipeline is halted at that stage. +type Pipeline interface { + // Execute runs the pipeline stages and produces a next game state. + // + // If any stage produces an error or an ended game state, the pipeline + // immediately stops at that stage. + // + // Errors should be checked and the other results ignored if error is non-nil. + // + // If the pipeline is already in an error state (this can be checked by calling Err()), + // this error will be immediately returned and the pipeline will not run. + // + // After the pipeline runs, the results will be the result of the last stage that was executed. + Execute(*BoardState, Settings, []SnakeMove) (bool, *BoardState, error) + + // Err provides a way to check for errors before/without calling Execute. + // Err returns an error if the Pipeline is in an error state. + // If this error is not nil, this error will also be returned from Execute, so it is + // optional to call Err. + // The idea is to reduce error-checking verbosity for the majority of cases where a + // Pipeline is immediately executed after construction (i.e. NewPipeline(...).Execute(...)). + Err() error +} + // StageFunc represents a single stage of an ordered pipeline and applies custom logic to the board state each turn. // It is expected to modify the boardState directly. // The return values are a boolean (to indicate whether the game has ended as a result of the stage) @@ -46,6 +73,14 @@ var globalRegistry = StageRegistry{ // Errors should be treated as meaning the stage failed and the board state is now invalid. type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error) +// IsInitialization checks whether the current state means the game is initialising (turn zero). +// Useful for StageFuncs that need to apply different behaviour on initialisation. +func IsInitialization(b *BoardState, settings Settings, moves []SnakeMove) bool { + // We can safely assume that the game state is in the initialisation phase when + // the turn hasn't advanced and the moves are empty + return b.Turn <= 0 && len(moves) == 0 +} + // StageRegistry is a mapping of stage names to stage functions type StageRegistry map[string]StageFunc @@ -76,32 +111,6 @@ func RegisterPipelineStage(s string, fn StageFunc) { } } -// Pipeline is an ordered sequences of game stages which are executed to produce the -// next game state. -// -// If a stage produces an error or an ended game state, the pipeline is halted at that stage. -type Pipeline interface { - // Execute runs the pipeline stages and produces a next game state. - // - // If any stage produces an error or an ended game state, the pipeline - // immediately stops at that stage. - // - // Errors should be checked and the other results ignored if error is non-nil. - // - // If the pipeline is already in an error state (this can be checked by calling Err()), - // this error will be immediately returned and the pipeline will not run. - // - // After the pipeline runs, the results will be the result of the last stage that was executed. - Execute(*BoardState, Settings, []SnakeMove) (bool, *BoardState, error) - // Err provides a way to check for errors before/without calling Execute. - // Err returns an error if the Pipeline is in an error state. - // If this error is not nil, this error will also be returned from Execute, so it is - // optional to call Err. - // The idea is to reduce error-checking verbosity for the majority of cases where a - // Pipeline is immediately executed after construction (i.e. NewPipeline(...).Execute(...)). - Err() error -} - // pipeline is an implementation of Pipeline type pipeline struct { // stages is a list of stages that should be executed from slice start to end diff --git a/pipeline_internal_test.go b/pipeline_internal_test.go index 861faf4..fb9fda0 100644 --- a/pipeline_internal_test.go +++ b/pipeline_internal_test.go @@ -27,7 +27,7 @@ func TestPipelineRuleset(t *testing.T) { name: "test", pipeline: p, } - ended, err := pr.IsGameOver(&BoardState{}) + ended, _, err := pr.Execute(&BoardState{}, nil) require.NoError(t, err) require.True(t, ended) @@ -37,7 +37,7 @@ func TestPipelineRuleset(t *testing.T) { name: "test", pipeline: p, } - ended, err = pr.IsGameOver(&BoardState{}) + ended, _, err = pr.Execute(&BoardState{}, nil) require.NoError(t, err) require.False(t, ended) @@ -56,10 +56,10 @@ func TestPipelineRuleset(t *testing.T) { pipeline: p, } require.Empty(t, b.Food) - b, err = pr.ModifyInitialBoardState(b) + _, b, err = pr.Execute(b, nil) require.NoError(t, err) require.Empty(t, b.Food, "food should not be added on initialisation phase") - b, err = pr.CreateNextBoardState(b, mockSnakeMoves()) + _, b, err = pr.Execute(b, mockSnakeMoves()) require.NoError(t, err) require.NotEmpty(t, b.Food, "fodo should be added now") } diff --git a/pipeline_test.go b/pipeline_test.go index ccd1c29..085a3e9 100644 --- a/pipeline_test.go +++ b/pipeline_test.go @@ -21,17 +21,17 @@ func TestPipeline(t *testing.T) { r.RegisterPipelineStage("astage", mockStageFn(false, nil)) p = rules.NewPipelineFromRegistry(r) require.Equal(t, rules.ErrorNoStages, p.Err()) - _, _, err = p.Execute(&rules.BoardState{}, rules.Settings{}, nil) + _, _, err = p.Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil) require.Equal(t, rules.ErrorNoStages, err) // test that an unregistered stage name errors p = rules.NewPipelineFromRegistry(r, "doesntexist") - _, _, err = p.Execute(&rules.BoardState{}, rules.Settings{}, nil) + _, _, err = p.Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil) require.Equal(t, rules.ErrorStageNotFound, p.Err()) require.Equal(t, rules.ErrorStageNotFound, err) // simplest case - one stage - ended, next, err := rules.NewPipelineFromRegistry(r, "astage").Execute(&rules.BoardState{}, rules.Settings{}, nil) + ended, next, err := rules.NewPipelineFromRegistry(r, "astage").Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil) require.NoError(t, err) require.NoError(t, err) require.NotNil(t, next) @@ -39,20 +39,20 @@ func TestPipeline(t *testing.T) { // test that the pipeline short-circuits for a stage that errors r.RegisterPipelineStage("errors", mockStageFn(false, errors.New(""))) - ended, next, err = rules.NewPipelineFromRegistry(r, "errors", "astage").Execute(&rules.BoardState{}, rules.Settings{}, nil) + ended, next, err = rules.NewPipelineFromRegistry(r, "errors", "astage").Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil) require.Error(t, err) require.NotNil(t, next) require.False(t, ended) // test that the pipeline short-circuits for a stage that ends r.RegisterPipelineStage("ends", mockStageFn(true, nil)) - ended, next, err = rules.NewPipelineFromRegistry(r, "ends", "astage").Execute(&rules.BoardState{}, rules.Settings{}, nil) + ended, next, err = rules.NewPipelineFromRegistry(r, "ends", "astage").Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil) require.NoError(t, err) require.NotNil(t, next) require.True(t, ended) // test that the pipeline runs normally for multiple stages - ended, next, err = rules.NewPipelineFromRegistry(r, "astage", "ends").Execute(&rules.BoardState{}, rules.Settings{}, nil) + ended, next, err = rules.NewPipelineFromRegistry(r, "astage", "ends").Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil) require.NoError(t, err) require.NotNil(t, next) require.True(t, ended) diff --git a/royale.go b/royale.go index 806f7b6..88a1fcb 100644 --- a/royale.go +++ b/royale.go @@ -14,26 +14,6 @@ var royaleRulesetStages = []string{ StageSpawnHazardsShrinkMap, } -type RoyaleRuleset struct { - StandardRuleset - - ShrinkEveryNTurns int -} - -func (r *RoyaleRuleset) Name() string { return GameTypeRoyale } - -func (r RoyaleRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) { - return NewPipeline(royaleRulesetStages...).Execute(bs, s, sm) -} - -func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { - if r.StandardRuleset.HazardDamagePerTurn < 1 { - return nil, errors.New("royale damage per turn must be greater than zero") - } - _, nextState, err := r.Execute(prevState, r.Settings(), moves) - return nextState, err -} - func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) { if IsInitialization(b, settings, moves) { return false, nil @@ -43,17 +23,18 @@ func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) // Royale uses the current turn to generate hazards, not the previous turn that's in the board state turn := b.Turn + 1 - if settings.RoyaleSettings.ShrinkEveryNTurns < 1 { + shrinkEveryNTurns := settings.Int(ParamShrinkEveryNTurns, 0) + if shrinkEveryNTurns < 1 { return false, errors.New("royale game can't shrink more frequently than every turn") } - if turn < settings.RoyaleSettings.ShrinkEveryNTurns { + if turn < shrinkEveryNTurns { return false, nil } randGenerator := settings.GetRand(0) - numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns + numShrinks := turn / shrinkEveryNTurns minX, maxX := 0, b.Width-1 minY, maxY := 0, b.Height-1 for i := 0; i < numShrinks; i++ { @@ -72,22 +53,10 @@ func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) for x := 0; x < b.Width; x++ { for y := 0; y < b.Height; y++ { if x < minX || x > maxX || y < minY || y > maxY { - b.Hazards = append(b.Hazards, Point{x, y}) + b.Hazards = append(b.Hazards, Point{X: x, Y: y}) } } } return false, nil } - -func (r *RoyaleRuleset) IsGameOver(b *BoardState) (bool, error) { - return GameOverStandard(b, r.Settings(), nil) -} - -func (r RoyaleRuleset) Settings() Settings { - s := r.StandardRuleset.Settings() - s.RoyaleSettings = RoyaleSettings{ - ShrinkEveryNTurns: r.ShrinkEveryNTurns, - } - return s -} diff --git a/royale_test.go b/royale_test.go index d3e28e1..c08fe5b 100644 --- a/royale_test.go +++ b/royale_test.go @@ -2,14 +2,19 @@ package rules import ( "errors" + "fmt" "math/rand" "testing" "github.com/stretchr/testify/require" ) -func TestRoyaleRulesetInterface(t *testing.T) { - var _ Ruleset = (*RoyaleRuleset)(nil) +func getRoyaleRuleset(hazardDamagePerTurn, shrinkEveryNTurns int) Ruleset { + settings := NewSettingsWithParams( + ParamHazardDamagePerTurn, fmt.Sprint(hazardDamagePerTurn), + ParamShrinkEveryNTurns, fmt.Sprint(shrinkEveryNTurns), + ) + return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeRoyale) } func TestRoyaleDefaultSanity(t *testing.T) { @@ -19,24 +24,19 @@ func TestRoyaleDefaultSanity(t *testing.T) { {ID: "2", Body: []Point{{X: 0, Y: 1}}}, }, } - r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 1}, ShrinkEveryNTurns: 0} - _, err := r.CreateNextBoardState(boardState, []SnakeMove{{"1", "right"}, {"2", "right"}}) + r := getRoyaleRuleset(1, 0) + _, _, err := r.Execute(boardState, []SnakeMove{{"1", "right"}, {"2", "right"}}) require.Error(t, err) require.Equal(t, errors.New("royale game can't shrink more frequently than every turn"), err) - 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{}) + r = getRoyaleRuleset(1, 1) + _, boardState, err = r.Execute(boardState, []SnakeMove{}) require.NoError(t, err) require.Len(t, boardState.Hazards, 0) } func TestRoyaleName(t *testing.T) { - r := RoyaleRuleset{} + r := getRoyaleRuleset(0, 0) require.Equal(t, "royale", r.Name()) } @@ -57,39 +57,39 @@ func TestRoyaleHazards(t *testing.T) { {Width: 3, Height: 3, Turn: 9, ShrinkEveryNTurns: 10, ExpectedHazards: []Point{}}, { Width: 3, Height: 3, Turn: 10, ShrinkEveryNTurns: 10, - ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}}, + ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}, }, { Width: 3, Height: 3, Turn: 11, ShrinkEveryNTurns: 10, - ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}}, + ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}, }, { Width: 3, Height: 3, Turn: 19, ShrinkEveryNTurns: 10, - ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}}, + ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}, }, { Width: 3, Height: 3, Turn: 20, ShrinkEveryNTurns: 10, - ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 2}, {2, 2}}, + ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 2}, {X: 2, Y: 2}}, }, { Width: 3, Height: 3, Turn: 31, ShrinkEveryNTurns: 10, - ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 1}, {1, 2}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 1}, {X: 2, Y: 2}}, }, { Width: 3, Height: 3, Turn: 42, ShrinkEveryNTurns: 10, - ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}}, }, { Width: 3, Height: 3, Turn: 53, ShrinkEveryNTurns: 10, - ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}}, }, { Width: 3, Height: 3, Turn: 64, ShrinkEveryNTurns: 10, - ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}}, }, { Width: 3, Height: 3, Turn: 6987, ShrinkEveryNTurns: 10, - ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}}, + ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}}, }, } @@ -99,12 +99,10 @@ func TestRoyaleHazards(t *testing.T) { Width: test.Width, Height: test.Height, } - settings := Settings{ - HazardDamagePerTurn: 1, - RoyaleSettings: RoyaleSettings{ - ShrinkEveryNTurns: test.ShrinkEveryNTurns, - }, - }.WithSeed(seed) + settings := NewSettingsWithParams( + ParamHazardDamagePerTurn, "1", + ParamShrinkEveryNTurns, fmt.Sprint(test.ShrinkEveryNTurns), + ).WithSeed(seed) _, err := PopulateHazardsRoyale(b, settings, mockSnakeMoves()) require.Equal(t, test.Error, err) @@ -139,12 +137,12 @@ var royaleCaseHazardsPlaced = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}, {1, 2}}, + Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}}, Health: 100, }, { ID: "two", - Body: []Point{{3, 4}, {3, 3}}, + Body: []Point{{X: 3, Y: 4}, {X: 3, Y: 3}}, Health: 100, }, { @@ -154,7 +152,7 @@ var royaleCaseHazardsPlaced = gameTestCase{ EliminatedCause: EliminatedByOutOfBounds, }, }, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, Hazards: []Point{}, }, []SnakeMove{ @@ -169,12 +167,12 @@ var royaleCaseHazardsPlaced = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 0}, {1, 1}, {1, 1}}, + Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100, }, { ID: "two", - Body: []Point{{3, 5}, {3, 4}}, + Body: []Point{{X: 3, Y: 5}, {X: 3, Y: 4}}, Health: 99, }, { @@ -184,7 +182,7 @@ var royaleCaseHazardsPlaced = gameTestCase{ EliminatedCause: EliminatedByOutOfBounds, }, }, - Food: []Point{{0, 0}}, + Food: []Point{{X: 0, Y: 0}}, Hazards: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}, {X: 3, Y: 0}, {X: 4, Y: 0}, {X: 5, Y: 0}, {X: 6, Y: 0}, {X: 7, Y: 0}, {X: 8, Y: 0}, {X: 9, Y: 0}}, }, } @@ -204,22 +202,14 @@ func TestRoyaleCreateNextBoardState(t *testing.T) { *s2, royaleCaseHazardsPlaced, } - r := RoyaleRuleset{ - StandardRuleset: StandardRuleset{ - HazardDamagePerTurn: 1, - }, - ShrinkEveryNTurns: 1, - } rb := NewRulesetBuilder().WithParams(map[string]string{ - ParamGameType: GameTypeRoyale, ParamHazardDamagePerTurn: "1", ParamShrinkEveryNTurns: "1", }).WithSeed(1234) for _, gc := range cases { rand.Seed(1234) - gc.requireValidNextState(t, &r) - // also test a RulesBuilder constructed instance - gc.requireValidNextState(t, rb.Ruleset()) + // test a RulesBuilder constructed instance + gc.requireValidNextState(t, rb.NamedRuleset(GameTypeRoyale)) // also test a pipeline with the same settings gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeRoyale, NewPipeline(royaleRulesetStages...))) } diff --git a/ruleset.go b/ruleset.go index c615012..9f83c08 100644 --- a/ruleset.go +++ b/ruleset.go @@ -1,16 +1,15 @@ package rules -import ( - "strconv" -) - type Ruleset interface { + // Returns the name of the ruleset, if applicable. Name() string - ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) - CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) - IsGameOver(state *BoardState) (bool, error) - // Settings provides the game settings that are relevant to the ruleset. + + // Returns the settings used by the ruleset. Settings() Settings + + // Processes the next turn of the ruleset, returning whether the game has ended, the next BoardState, or an error. + // For turn zero (initialization), moves will be left empty. + Execute(prevState *BoardState, moves []SnakeMove) (gameOver bool, nextState *BoardState, err error) } type SnakeMove struct { @@ -18,68 +17,12 @@ type SnakeMove struct { Move string } -// Settings contains all settings relevant to a game. -// It is used by game logic to take a previous game state and produce a next game state. -type Settings struct { - FoodSpawnChance int `json:"foodSpawnChance"` - MinimumFood int `json:"minimumFood"` - HazardDamagePerTurn int `json:"hazardDamagePerTurn"` - HazardMap string `json:"hazardMap"` - HazardMapAuthor string `json:"hazardMapAuthor"` - RoyaleSettings RoyaleSettings `json:"royale"` - SquadSettings SquadSettings `json:"squad"` // Deprecated, provided with default fields for API compatibility - - rand Rand - seed int64 -} - -// Get a random number generator initialized based on the seed and current turn. -func (settings Settings) GetRand(turn int) Rand { - // Allow overriding the random generator for testing - if settings.rand != nil { - return settings.rand - } - - if settings.seed != 0 { - return NewSeedRand(settings.seed + int64(turn)) - } - - // Default to global random number generator if neither seed or rand are set. - return GlobalRand -} - -func (settings Settings) WithRand(rand Rand) Settings { - settings.rand = rand - return settings -} - -func (settings Settings) Seed() int64 { - return settings.seed -} - -func (settings Settings) WithSeed(seed int64) Settings { - settings.seed = seed - return settings -} - -// RoyaleSettings contains settings that are specific to the "royale" game mode -type RoyaleSettings struct { - ShrinkEveryNTurns int `json:"shrinkEveryNTurns"` -} - -// SquadSettings contains settings that are specific to the "squad" game mode -type SquadSettings struct { - AllowBodyCollisions bool `json:"allowBodyCollisions"` - SharedElimination bool `json:"sharedElimination"` - SharedHealth bool `json:"sharedHealth"` - SharedLength bool `json:"sharedLength"` -} - type rulesetBuilder struct { - params map[string]string // game customisation parameters - seed int64 // used for random events in games - rand Rand // used for random number generation - solo bool // if true, only 1 alive snake is required to keep the game from ending + params map[string]string // game customisation parameters + seed int64 // used for random events in games + rand Rand // used for random number generation + solo bool // if true, only 1 alive snake is required to keep the game from ending + settings *Settings // used to set settings directly instead of via string params } // NewRulesetBuilder returns an instance of a builder for the Ruleset types. @@ -89,7 +32,7 @@ func NewRulesetBuilder() *rulesetBuilder { } } -// WithParams accepts a map of game parameters for customizing games. +// WithParams accepts a map of string parameters for customizing games. // // Parameters are copied. If called multiple times, parameters are merged such that: // - existing keys in both maps get overwritten by the new ones @@ -125,13 +68,14 @@ func (rb *rulesetBuilder) WithSolo(value bool) *rulesetBuilder { return rb } -// Ruleset constructs a customised ruleset using the parameters passed to the builder. -func (rb rulesetBuilder) Ruleset() PipelineRuleset { - name, ok := rb.params[ParamGameType] - if !ok { - name = GameTypeStandard - } +// WithSettings sets the settings object for the ruleset directly. +func (rb *rulesetBuilder) WithSettings(settings Settings) *rulesetBuilder { + rb.settings = &settings + return rb +} +// NamedRuleset constructs a known ruleset by using name to look up a standard pipeline. +func (rb rulesetBuilder) NamedRuleset(name string) Ruleset { var stages []string if rb.solo { stages = append(stages, StageGameOverSoloSnake) @@ -153,61 +97,26 @@ func (rb rulesetBuilder) Ruleset() PipelineRuleset { case GameTypeWrapped: stages = append(stages, wrappedRulesetStages[1:]...) default: + name = GameTypeStandard stages = append(stages, standardRulesetStages[1:]...) } return rb.PipelineRuleset(name, NewPipeline(stages...)) } -// PipelineRuleset provides an implementation of the Ruleset using a pipeline with a name. -// It is intended to facilitate transitioning away from legacy Ruleset implementations to Pipeline -// implementations. -func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRuleset { +// PipelineRuleset constructs a ruleset with the given name and pipeline using the parameters passed to the builder. +// This can be used to create custom rulesets. +func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) Ruleset { + var settings Settings + if rb.settings != nil { + settings = *rb.settings + } else { + settings = NewSettings(rb.params).WithRand(rb.rand).WithSeed(rb.seed) + } return &pipelineRuleset{ name: name, pipeline: p, - settings: Settings{ - FoodSpawnChance: paramsInt(rb.params, ParamFoodSpawnChance, 0), - MinimumFood: paramsInt(rb.params, ParamMinimumFood, 0), - HazardDamagePerTurn: paramsInt(rb.params, ParamHazardDamagePerTurn, 0), - HazardMap: rb.params[ParamHazardMap], - HazardMapAuthor: rb.params[ParamHazardMapAuthor], - RoyaleSettings: RoyaleSettings{ - ShrinkEveryNTurns: paramsInt(rb.params, ParamShrinkEveryNTurns, 0), - }, - rand: rb.rand, - seed: rb.seed, - }, - } -} - -// paramsBool returns the boolean value for the specified parameter. -// If the parameter doesn't exist, the default value will be returned. -// If the parameter does exist, but is not "true", false will be returned. -func paramsBool(params map[string]string, paramName string, defaultValue bool) bool { - if val, ok := params[paramName]; ok { - return val == "true" - } - return defaultValue -} - -// paramsInt returns the int value for the specified parameter. -// If the parameter doesn't exist, the default value will be returned. -// If the parameter does exist, but is not a valid int, the default value will be returned. -func paramsInt(params map[string]string, paramName string, defaultValue int) int { - if val, ok := params[paramName]; ok { - i, err := strconv.Atoi(val) - if err == nil { - return i - } + settings: settings, } - return defaultValue -} - -// PipelineRuleset groups the Pipeline and Ruleset methods. -// It is intended to facilitate a transition from Ruleset legacy code to Pipeline code. -type PipelineRuleset interface { - Ruleset - Pipeline } type pipelineRuleset struct { @@ -225,33 +134,10 @@ func (r pipelineRuleset) Settings() Settings { func (r pipelineRuleset) Name() string { return r.name } // impl Ruleset -// IMPORTANT: this implementation of IsGameOver deviates from the previous Ruleset implementations -// in that it checks if the *NEXT* state results in game over, not the previous state. -// This is due to the design of pipelines / stage functions not having a distinction between -// checking for game over and producing a next state. -func (r *pipelineRuleset) IsGameOver(b *BoardState) (bool, error) { - gameover, _, err := r.Execute(b, r.Settings(), nil) // checks if next state is game over - return gameover, err -} - -// impl Ruleset -func (r pipelineRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) { - _, nextState, err := r.Execute(initialState, r.Settings(), nil) - return nextState, err -} - -// impl Pipeline -func (r pipelineRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) { - return r.pipeline.Execute(bs, s, sm) -} - -// impl Ruleset -func (r pipelineRuleset) CreateNextBoardState(bs *BoardState, sm []SnakeMove) (*BoardState, error) { - _, nextState, err := r.Execute(bs, r.Settings(), sm) - return nextState, err +func (r pipelineRuleset) Execute(bs *BoardState, sm []SnakeMove) (bool, *BoardState, error) { + return r.pipeline.Execute(bs, r.Settings(), sm) } -// impl Pipeline func (r pipelineRuleset) Err() error { return r.pipeline.Err() } diff --git a/ruleset_internal_test.go b/ruleset_internal_test.go index bbd47e7..2ab035f 100644 --- a/ruleset_internal_test.go +++ b/ruleset_internal_test.go @@ -10,31 +10,6 @@ import ( _ "github.com/BattlesnakeOfficial/rules/test" ) -func TestParamInt(t *testing.T) { - require.Equal(t, 5, paramsInt(nil, "test", 5), "nil map") - require.Equal(t, 10, paramsInt(map[string]string{}, "foo", 10), "empty map") - require.Equal(t, 10, paramsInt(map[string]string{"hullo": "there"}, "hullo", 10), "invalid value") - require.Equal(t, 20, paramsInt(map[string]string{"bonjour": "20"}, "bonjour", 20), "valid value") -} - -func TestParamBool(t *testing.T) { - // missing values default to specified value - require.Equal(t, true, paramsBool(nil, "test", true), "nil map true") - require.Equal(t, false, paramsBool(nil, "test", false), "nil map false") - - // missing values default to specified value - require.Equal(t, true, paramsBool(map[string]string{}, "foo", true), "empty map true") - require.Equal(t, false, paramsBool(map[string]string{}, "foo", false), "empty map false") - - // invalid values (exist but not booL) default to false - require.Equal(t, false, paramsBool(map[string]string{"hullo": "there"}, "hullo", true), "invalid value default true") - require.Equal(t, false, paramsBool(map[string]string{"hullo": "there"}, "hullo", false), "invalid value default false") - - // valid values ignore defaults - require.Equal(t, false, paramsBool(map[string]string{"bonjour": "false"}, "bonjour", false), "valid value false") - require.Equal(t, true, paramsBool(map[string]string{"bonjour": "true"}, "bonjour", false), "valid value true") -} - func TestRulesetError(t *testing.T) { err := (error)(RulesetError("test error string")) require.Equal(t, "test error string", err.Error()) @@ -42,10 +17,10 @@ func TestRulesetError(t *testing.T) { func TestRulesetBuilderInternals(t *testing.T) { // test Royale with seed - rsb := NewRulesetBuilder().WithSeed(3).WithParams(map[string]string{ParamGameType: GameTypeRoyale}) + rsb := NewRulesetBuilder().WithSeed(3) require.Equal(t, int64(3), rsb.seed) - require.Equal(t, GameTypeRoyale, rsb.Ruleset().Name()) - require.Equal(t, int64(3), rsb.Ruleset().Settings().Seed()) + require.Equal(t, GameTypeRoyale, rsb.NamedRuleset(GameTypeRoyale).Name()) + require.Equal(t, int64(3), rsb.NamedRuleset(GameTypeRoyale).Settings().Seed()) // test parameter merging rsb = NewRulesetBuilder(). diff --git a/ruleset_test.go b/ruleset_test.go index 16ce402..ec2d958 100644 --- a/ruleset_test.go +++ b/ruleset_test.go @@ -5,102 +5,13 @@ import ( "testing" "github.com/BattlesnakeOfficial/rules" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestStandardRulesetSettings(t *testing.T) { - ruleset := rules.StandardRuleset{ - MinimumFood: 5, - FoodSpawnChance: 10, - HazardDamagePerTurn: 10, - HazardMap: "hz_spiral", - HazardMapAuthor: "altersaddle", - } - assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood) - assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance) - assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn) - assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap) - assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor) -} - -func TestWrappedRulesetSettings(t *testing.T) { - ruleset := rules.WrappedRuleset{ - StandardRuleset: rules.StandardRuleset{ - MinimumFood: 5, - FoodSpawnChance: 10, - HazardDamagePerTurn: 10, - HazardMap: "hz_spiral", - HazardMapAuthor: "altersaddle", - }, - } - assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood) - assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance) - assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn) - assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap) - assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor) -} - -func TestSoloRulesetSettings(t *testing.T) { - ruleset := rules.SoloRuleset{ - StandardRuleset: rules.StandardRuleset{ - MinimumFood: 5, - FoodSpawnChance: 10, - HazardDamagePerTurn: 10, - HazardMap: "hz_spiral", - HazardMapAuthor: "altersaddle", - }, - } - assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood) - assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance) - assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn) - assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap) - assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor) -} - -func TestRoyaleRulesetSettings(t *testing.T) { - ruleset := rules.RoyaleRuleset{ - ShrinkEveryNTurns: 12, - StandardRuleset: rules.StandardRuleset{ - MinimumFood: 5, - FoodSpawnChance: 10, - HazardDamagePerTurn: 10, - HazardMap: "hz_spiral", - HazardMapAuthor: "altersaddle", - }, - } - assert.Equal(t, ruleset.ShrinkEveryNTurns, ruleset.Settings().RoyaleSettings.ShrinkEveryNTurns) - assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood) - assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance) - assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn) - assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap) - assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor) -} - -func TestConstrictorRulesetSettings(t *testing.T) { - ruleset := rules.ConstrictorRuleset{ - StandardRuleset: rules.StandardRuleset{ - MinimumFood: 5, - FoodSpawnChance: 10, - HazardDamagePerTurn: 10, - HazardMap: "hz_spiral", - HazardMapAuthor: "altersaddle", - }, - } - assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood) - assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance) - assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn) - assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap) - assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor) -} - func TestRulesetBuilder(t *testing.T) { // Test that a fresh instance can produce a Ruleset - require.NotNil(t, rules.NewRulesetBuilder().Ruleset()) - require.Equal(t, rules.GameTypeStandard, rules.NewRulesetBuilder().Ruleset().Name(), "should default to standard game") - - // test nil safety / defaults - require.NotNil(t, rules.NewRulesetBuilder().Ruleset()) + require.NotNil(t, rules.NewRulesetBuilder().NamedRuleset("")) + require.Equal(t, rules.GameTypeStandard, rules.NewRulesetBuilder().NamedRuleset("").Name(), "should default to standard game") // make sure it works okay for lots of game types expectedResults := []struct { @@ -120,32 +31,23 @@ func TestRulesetBuilder(t *testing.T) { rsb.WithParams(map[string]string{ // apply the standard rule params - rules.ParamGameType: expected.GameType, rules.ParamFoodSpawnChance: "10", rules.ParamMinimumFood: "5", rules.ParamHazardDamagePerTurn: "12", - rules.ParamHazardMap: "test", - rules.ParamHazardMapAuthor: "tester", }) - require.NotNil(t, rsb.Ruleset()) - require.Equal(t, expected.GameType, rsb.Ruleset().Name()) + require.NotNil(t, rsb.NamedRuleset(expected.GameType)) + require.Equal(t, expected.GameType, rsb.NamedRuleset(expected.GameType).Name()) // All the standard settings should always be copied over - require.Equal(t, 10, rsb.Ruleset().Settings().FoodSpawnChance) - require.Equal(t, 12, rsb.Ruleset().Settings().HazardDamagePerTurn) - require.Equal(t, 5, rsb.Ruleset().Settings().MinimumFood) - require.Equal(t, "test", rsb.Ruleset().Settings().HazardMap) - require.Equal(t, "tester", rsb.Ruleset().Settings().HazardMapAuthor) + require.Equal(t, 10, rsb.NamedRuleset(expected.GameType).Settings().Int(rules.ParamFoodSpawnChance, 0)) + require.Equal(t, 12, rsb.NamedRuleset(expected.GameType).Settings().Int(rules.ParamHazardDamagePerTurn, 0)) + require.Equal(t, 5, rsb.NamedRuleset(expected.GameType).Settings().Int(rules.ParamMinimumFood, 0)) }) } } func TestRulesetBuilderGameOver(t *testing.T) { - settings := rules.Settings{ - RoyaleSettings: rules.RoyaleSettings{ - ShrinkEveryNTurns: 12, - }, - } + settings := rules.NewSettingsWithParams(rules.ParamShrinkEveryNTurns, "12") moves := []rules.SnakeMove{ {ID: "1", Move: "up"}, } @@ -214,13 +116,11 @@ func TestRulesetBuilderGameOver(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprintf("%v_%v", test.gameType, test.solo), func(t *testing.T) { - rsb := rules.NewRulesetBuilder().WithParams(map[string]string{ - rules.ParamGameType: test.gameType, - }).WithSolo(test.solo) + rsb := rules.NewRulesetBuilder().WithSettings(settings).WithSolo(test.solo) - ruleset := rsb.Ruleset() + ruleset := rsb.NamedRuleset(test.gameType) - gameOver, _, err := ruleset.Execute(boardState, settings, moves) + gameOver, _, err := ruleset.Execute(boardState, moves) require.NoError(t, err) require.Equal(t, test.gameOver, gameOver) @@ -234,7 +134,7 @@ func TestStageFuncContract(t *testing.T) { stage = func(bs *rules.BoardState, s rules.Settings, sm []rules.SnakeMove) (bool, error) { return true, nil } - ended, err := stage(nil, rules.NewRulesetBuilder().Ruleset().Settings(), nil) + ended, err := stage(nil, rules.NewRulesetBuilder().NamedRuleset("").Settings(), nil) require.NoError(t, err) require.True(t, ended) } diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..a15317b --- /dev/null +++ b/settings.go @@ -0,0 +1,90 @@ +package rules + +import "strconv" + +// Settings contains all settings relevant to a game. +// The settings are stored as raw string values, which should not be accessed +// directly. Calling code should instead use the Int/Bool methods to parse them. +type Settings struct { + rawValues map[string]string + + rand Rand + seed int64 +} + +func NewSettings(params map[string]string) Settings { + rawValues := make(map[string]string, len(params)) + + // Copy incoming params into a new map + for key, value := range params { + rawValues[key] = value + } + + return Settings{ + rawValues: rawValues, + } +} + +func NewSettingsWithParams(params ...string) Settings { + rawValues := map[string]string{} + + for index := 1; index < len(params); index += 2 { + rawValues[params[index-1]] = params[index] + } + + return Settings{ + rawValues: rawValues, + } +} + +// Get a random number generator initialized based on the seed and current turn. +func (settings Settings) GetRand(turn int) Rand { + // Allow overriding the random generator for testing + if settings.rand != nil { + return settings.rand + } + + if settings.seed != 0 { + return NewSeedRand(settings.seed + int64(turn)) + } + + // Default to global random number generator if neither seed or rand are set. + return GlobalRand +} + +func (settings Settings) WithRand(rand Rand) Settings { + settings.rand = rand + return settings +} + +func (settings Settings) Seed() int64 { + return settings.seed +} + +func (settings Settings) WithSeed(seed int64) Settings { + settings.seed = seed + return settings +} + +// Bool returns the boolean value for the specified parameter. +// If the parameter doesn't exist, the default value will be returned. +// If the parameter does exist, but is not "true", false will be returned. +func (settings Settings) Bool(paramName string, defaultValue bool) bool { + if val, ok := settings.rawValues[paramName]; ok { + return val == "true" + } + return defaultValue +} + +// Int returns the int value for the specified parameter. +// If the parameter doesn't exist, the default value will be returned. +// If the parameter does exist, but is not a valid int, the default value will be returned. +func (settings Settings) Int(paramName string, defaultValue int) int { + if val, ok := settings.rawValues[paramName]; ok { + i, err := strconv.Atoi(val) + if err == nil { + return i + } + } + return defaultValue +} diff --git a/settings_test.go b/settings_test.go new file mode 100644 index 0000000..652d7d7 --- /dev/null +++ b/settings_test.go @@ -0,0 +1,31 @@ +package rules_test + +import ( + "testing" + + "github.com/BattlesnakeOfficial/rules" + "github.com/stretchr/testify/assert" +) + +func TestSettings(t *testing.T) { + params := map[string]string{ + "invalidSetting": "abcd", + "intSetting": "1234", + "boolSetting": "true", + } + + settings := rules.NewSettings(params) + + assert.Equal(t, 4567, settings.Int("missingIntSetting", 4567)) + assert.Equal(t, 4567, settings.Int("invalidSetting", 4567)) + assert.Equal(t, 1234, settings.Int("intSetting", 4567)) + + assert.Equal(t, false, settings.Bool("missingBoolSetting", false)) + assert.Equal(t, true, settings.Bool("missingBoolSetting", true)) + assert.Equal(t, false, settings.Bool("invalidSetting", true)) + assert.Equal(t, true, settings.Bool("boolSetting", true)) + + assert.Equal(t, 4567, rules.NewSettingsWithParams("newIntSetting").Int("newIntSetting", 4567)) + assert.Equal(t, 1234, rules.NewSettingsWithParams("newIntSetting", "1234").Int("newIntSetting", 4567)) + assert.Equal(t, 4567, rules.NewSettingsWithParams("x", "y", "newIntSetting").Int("newIntSetting", 4567)) +} diff --git a/solo.go b/solo.go index 7ddbaea..2acb864 100644 --- a/solo.go +++ b/solo.go @@ -9,25 +9,6 @@ var soloRulesetStages = []string{ StageEliminationStandard, } -type SoloRuleset struct { - StandardRuleset -} - -func (r *SoloRuleset) Name() string { return GameTypeSolo } - -func (r SoloRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) { - return NewPipeline(soloRulesetStages...).Execute(bs, s, sm) -} - -func (r *SoloRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { - _, nextState, err := r.Execute(prevState, r.Settings(), moves) - return nextState, err -} - -func (r *SoloRuleset) IsGameOver(b *BoardState) (bool, error) { - return GameOverSolo(b, r.Settings(), nil) -} - func GameOverSolo(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) { for i := 0; i < len(b.Snakes); i++ { if b.Snakes[i].EliminatedCause == NotEliminated { diff --git a/solo_test.go b/solo_test.go index 96555f0..599d3a4 100644 --- a/solo_test.go +++ b/solo_test.go @@ -6,20 +6,21 @@ import ( "github.com/stretchr/testify/require" ) -func TestSoloRulesetInterface(t *testing.T) { - var _ Ruleset = (*SoloRuleset)(nil) +func getSoloRuleset(settings Settings) Ruleset { + return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeSolo) } func TestSoloName(t *testing.T) { - r := SoloRuleset{} + r := getSoloRuleset(Settings{}) require.Equal(t, "solo", r.Name()) } func TestSoloCreateNextBoardStateSanity(t *testing.T) { boardState := &BoardState{} - r := SoloRuleset{} - _, err := r.CreateNextBoardState(boardState, []SnakeMove{}) + r := getSoloRuleset(Settings{}) + gameOver, _, err := r.Execute(boardState, []SnakeMove{}) require.NoError(t, err) + require.True(t, gameOver) } func TestSoloIsGameOver(t *testing.T) { @@ -41,7 +42,7 @@ func TestSoloIsGameOver(t *testing.T) { }, } - r := SoloRuleset{} + r := getSoloRuleset(Settings{}) for _, test := range tests { b := &BoardState{ Height: 11, @@ -50,7 +51,7 @@ func TestSoloIsGameOver(t *testing.T) { Food: []Point{}, } - actual, err := r.IsGameOver(b) + actual, _, err := r.Execute(b, nil) require.NoError(t, err) require.Equal(t, test.Expected, actual) } @@ -69,11 +70,11 @@ var soloCaseNotOver = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}, {1, 2}}, + Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}}, Health: 100, }, }, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, Hazards: []Point{}, }, []SnakeMove{ @@ -86,11 +87,11 @@ var soloCaseNotOver = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 0}, {1, 1}, {1, 1}}, + Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100, }, }, - Food: []Point{{0, 0}}, + Food: []Point{{X: 0, Y: 0}}, Hazards: []Point{}, }, } @@ -104,14 +105,10 @@ func TestSoloCreateNextBoardState(t *testing.T) { standardMoveAndCollideMAD, soloCaseNotOver, } - r := SoloRuleset{} - rb := NewRulesetBuilder().WithParams(map[string]string{ - ParamGameType: GameTypeSolo, - }) + r := getSoloRuleset(Settings{}) for _, gc := range cases { - gc.requireValidNextState(t, &r) - // also test a RulesBuilder constructed instance - gc.requireValidNextState(t, rb.Ruleset()) + // test a RulesBuilder constructed instance + gc.requireValidNextState(t, r) // also test a pipeline with the same settings gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeSolo, NewPipeline(soloRulesetStages...))) } @@ -119,26 +116,18 @@ func TestSoloCreateNextBoardState(t *testing.T) { // Test a snake running right into the wall is properly eliminated func TestSoloEliminationOutOfBounds(t *testing.T) { - r := SoloRuleset{} + r := getSoloRuleset(Settings{}) // Using MaxRand is important because it ensures that the snakes are consistently placed in a way this test will work. // Actually random placement could result in the assumptions made by this test being incorrect. initialState, err := CreateDefaultBoardState(MaxRand, 2, 2, []string{"one"}) require.NoError(t, err) - _, next, err := r.Execute( - initialState, - r.Settings(), - []SnakeMove{{ID: "one", Move: "right"}}, - ) + _, next, err := r.Execute(initialState, []SnakeMove{{ID: "one", Move: "right"}}) require.NoError(t, err) require.NotNil(t, initialState) - ended, next, err := r.Execute( - next, - r.Settings(), - []SnakeMove{{ID: "one", Move: "right"}}, - ) + ended, next, err := r.Execute(next, []SnakeMove{{ID: "one", Move: "right"}}) require.NoError(t, err) require.NotNil(t, initialState) diff --git a/standard.go b/standard.go index 191c739..3312ac5 100644 --- a/standard.go +++ b/standard.go @@ -5,14 +5,6 @@ import ( "sort" ) -type StandardRuleset struct { - FoodSpawnChance int // [0, 100] - MinimumFood int - HazardDamagePerTurn int - HazardMap string // optional - HazardMapAuthor string // optional -} - var standardRulesetStages = []string{ StageGameOverStandard, StageMovementStandard, @@ -22,23 +14,6 @@ var standardRulesetStages = []string{ StageEliminationStandard, } -func (r *StandardRuleset) Name() string { return GameTypeStandard } - -func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) { - // No-op - return initialState, nil -} - -// impl Pipeline -func (r StandardRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) { - return NewPipeline(standardRulesetStages...).Execute(bs, s, sm) -} - -func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { - _, nextState, err := r.Execute(prevState, r.Settings(), moves) - return nextState, err -} - func MoveSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) { if IsInitialization(b, settings, moves) { return false, nil @@ -156,6 +131,7 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove) if IsInitialization(b, settings, moves) { return false, nil } + hazardDamage := settings.Int(ParamHazardDamagePerTurn, 0) for i := 0; i < len(b.Snakes); i++ { snake := &b.Snakes[i] if snake.EliminatedCause != NotEliminated { @@ -176,7 +152,7 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove) } // Snake is in a hazard, reduce health - snake.Health = snake.Health - settings.HazardDamagePerTurn + snake.Health = snake.Health - hazardDamage if snake.Health < 0 { snake.Health = 0 } @@ -393,20 +369,18 @@ func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (boo if IsInitialization(b, settings, moves) { return false, nil } + minimumFood := settings.Int(ParamMinimumFood, 0) + foodSpawnChance := settings.Int(ParamFoodSpawnChance, 0) numCurrentFood := int(len(b.Food)) - if numCurrentFood < settings.MinimumFood { - return false, PlaceFoodRandomly(GlobalRand, b, settings.MinimumFood-numCurrentFood) + if numCurrentFood < minimumFood { + return false, PlaceFoodRandomly(GlobalRand, b, minimumFood-numCurrentFood) } - if settings.FoodSpawnChance > 0 && int(rand.Intn(100)) < settings.FoodSpawnChance { + if foodSpawnChance > 0 && int(rand.Intn(100)) < foodSpawnChance { return false, PlaceFoodRandomly(GlobalRand, b, 1) } return false, nil } -func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) { - return GameOverStandard(b, r.Settings(), nil) -} - func GameOverStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) { numSnakesRemaining := 0 for i := 0; i < len(b.Snakes); i++ { @@ -416,25 +390,3 @@ func GameOverStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool } return numSnakesRemaining <= 1, nil } - -func (r StandardRuleset) Settings() Settings { - return Settings{ - FoodSpawnChance: r.FoodSpawnChance, - MinimumFood: r.MinimumFood, - HazardDamagePerTurn: r.HazardDamagePerTurn, - HazardMap: r.HazardMap, - HazardMapAuthor: r.HazardMapAuthor, - } -} - -// impl Pipeline -func (r StandardRuleset) Err() error { - return nil -} - -// IsInitialization checks whether the current state means the game is initialising. -func IsInitialization(b *BoardState, settings Settings, moves []SnakeMove) bool { - // We can safely assume that the game state is in the initialisation phase when - // the turn hasn't advanced and the moves are empty - return b.Turn <= 0 && len(moves) == 0 -} diff --git a/standard_test.go b/standard_test.go index 5ccd0da..188e176 100644 --- a/standard_test.go +++ b/standard_test.go @@ -1,6 +1,7 @@ package rules import ( + "fmt" "math" "math/rand" "testing" @@ -8,38 +9,29 @@ import ( "github.com/stretchr/testify/require" ) -func TestStandardRulesetInterface(t *testing.T) { - var _ Ruleset = (*StandardRuleset)(nil) +func getStandardRuleset(settings Settings) Ruleset { + return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeStandard) } func TestSanity(t *testing.T) { - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) state, err := CreateDefaultBoardState(MaxRand, 0, 0, []string{}) require.NoError(t, err) require.NotNil(t, state) - state, err = r.ModifyInitialBoardState(state) + gameOver, state, err := r.Execute(state, []SnakeMove{}) require.NoError(t, err) + require.True(t, gameOver) require.NotNil(t, state) require.Equal(t, 0, state.Width) require.Equal(t, 0, state.Height) require.Len(t, state.Food, 0) require.Len(t, state.Snakes, 0) - - next, err := r.CreateNextBoardState( - &BoardState{}, - []SnakeMove{}, - ) - require.NoError(t, err) - require.NotNil(t, next) - require.Equal(t, 0, state.Width) - require.Equal(t, 0, state.Height) - require.Len(t, state.Snakes, 0) } func TestStandardName(t *testing.T) { - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) require.Equal(t, "standard", r.Name()) } @@ -52,16 +44,16 @@ var standardCaseErrNoMoveFound = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}, {1, 2}}, + Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}}, Health: 100, }, { ID: "two", - Body: []Point{{3, 4}, {3, 3}}, + Body: []Point{{X: 3, Y: 4}, {X: 3, Y: 3}}, Health: 100, }, }, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, Hazards: []Point{}, }, []SnakeMove{ @@ -80,7 +72,7 @@ var standardCaseErrZeroLengthSnake = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}, {1, 2}}, + Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}}, Health: 100, }, { @@ -89,7 +81,7 @@ var standardCaseErrZeroLengthSnake = gameTestCase{ Health: 100, }, }, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, Hazards: []Point{}, }, []SnakeMove{ @@ -109,12 +101,12 @@ var standardCaseMoveEatAndGrow = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}, {1, 2}}, + Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}}, Health: 100, }, { ID: "two", - Body: []Point{{3, 4}, {3, 3}}, + Body: []Point{{X: 3, Y: 4}, {X: 3, Y: 3}}, Health: 100, }, { @@ -124,7 +116,7 @@ var standardCaseMoveEatAndGrow = gameTestCase{ EliminatedCause: EliminatedByOutOfBounds, }, }, - Food: []Point{{0, 0}, {1, 0}}, + Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, Hazards: []Point{}, }, []SnakeMove{ @@ -139,12 +131,12 @@ var standardCaseMoveEatAndGrow = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 0}, {1, 1}, {1, 1}}, + Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100, }, { ID: "two", - Body: []Point{{3, 5}, {3, 4}}, + Body: []Point{{X: 3, Y: 5}, {X: 3, Y: 4}}, Health: 99, }, { @@ -154,7 +146,7 @@ var standardCaseMoveEatAndGrow = gameTestCase{ EliminatedCause: EliminatedByOutOfBounds, }, }, - Food: []Point{{0, 0}}, + Food: []Point{{X: 0, Y: 0}}, Hazards: []Point{}, }, } @@ -170,12 +162,12 @@ var standardMoveAndCollideMAD = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}, {2, 1}}, + Body: []Point{{X: 1, Y: 1}, {X: 2, Y: 1}}, Health: 99, }, { ID: "two", - Body: []Point{{1, 2}, {2, 2}}, + Body: []Point{{X: 1, Y: 2}, {X: 2, Y: 2}}, Health: 99, }, }, @@ -193,7 +185,7 @@ var standardMoveAndCollideMAD = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 2}, {1, 1}}, + Body: []Point{{X: 1, Y: 2}, {X: 1, Y: 1}}, Health: 98, EliminatedCause: EliminatedByCollision, EliminatedBy: "two", @@ -201,7 +193,7 @@ var standardMoveAndCollideMAD = gameTestCase{ }, { ID: "two", - Body: []Point{{1, 1}, {1, 2}}, + Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}}, Health: 98, EliminatedCause: EliminatedByCollision, EliminatedBy: "one", @@ -220,14 +212,10 @@ func TestStandardCreateNextBoardState(t *testing.T) { standardCaseMoveEatAndGrow, standardMoveAndCollideMAD, } - r := StandardRuleset{} - rb := NewRulesetBuilder().WithParams(map[string]string{ - ParamGameType: GameTypeStandard, - }) + r := getStandardRuleset(Settings{}) for _, gc := range cases { - gc.requireValidNextState(t, &r) - // also test a RulesBuilder constructed instance - gc.requireValidNextState(t, rb.Ruleset()) + // test a RulesBuilder constructed instance + gc.requireValidNextState(t, r) // also test a pipeline with the same settings gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeStandard, NewPipeline(standardRulesetStages...))) } @@ -252,16 +240,16 @@ func TestEatingOnLastMove(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{0, 2}, {0, 1}, {0, 0}}, + Body: []Point{{X: 0, Y: 2}, {X: 0, Y: 1}, {X: 0, Y: 0}}, Health: 1, }, { ID: "two", - Body: []Point{{3, 2}, {3, 3}, {3, 4}}, + Body: []Point{{X: 3, Y: 2}, {X: 3, Y: 3}, {X: 3, Y: 4}}, Health: 1, }, }, - Food: []Point{{0, 3}, {9, 9}}, + Food: []Point{{X: 0, Y: 3}, {X: 9, Y: 9}}, }, []SnakeMove{ {ID: "one", Move: MoveUp}, @@ -274,26 +262,26 @@ func TestEatingOnLastMove(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}}, + Body: []Point{{X: 0, Y: 3}, {X: 0, Y: 2}, {X: 0, Y: 1}, {X: 0, Y: 1}}, Health: 100, }, { ID: "two", - Body: []Point{{3, 1}, {3, 2}, {3, 3}}, + Body: []Point{{X: 3, Y: 1}, {X: 3, Y: 2}, {X: 3, Y: 3}}, Health: 0, EliminatedCause: EliminatedByOutOfHealth, EliminatedOnTurn: 1, }, }, - Food: []Point{{9, 9}}, + Food: []Point{{X: 9, Y: 9}}, }, }, } rand.Seed(0) // Seed with a value that will reliably not spawn food - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) for _, test := range tests { - nextState, err := r.CreateNextBoardState(test.prevState, test.moves) + _, nextState, err := r.Execute(test.prevState, test.moves) require.Equal(t, err, test.expectedError) if test.expectedState != nil { require.Equal(t, test.expectedState.Width, nextState.Width) @@ -325,16 +313,16 @@ func TestHeadToHeadOnFood(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{0, 2}, {0, 1}, {0, 0}}, + Body: []Point{{X: 0, Y: 2}, {X: 0, Y: 1}, {X: 0, Y: 0}}, Health: 10, }, { ID: "two", - Body: []Point{{0, 4}, {0, 5}, {0, 6}}, + Body: []Point{{X: 0, Y: 4}, {X: 0, Y: 5}, {X: 0, Y: 6}}, Health: 10, }, }, - Food: []Point{{0, 3}, {9, 9}}, + Food: []Point{{X: 0, Y: 3}, {X: 9, Y: 9}}, }, []SnakeMove{ {ID: "one", Move: MoveUp}, @@ -347,7 +335,7 @@ func TestHeadToHeadOnFood(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}}, + Body: []Point{{X: 0, Y: 3}, {X: 0, Y: 2}, {X: 0, Y: 1}, {X: 0, Y: 1}}, Health: 100, EliminatedCause: EliminatedByHeadToHeadCollision, EliminatedBy: "two", @@ -355,14 +343,14 @@ func TestHeadToHeadOnFood(t *testing.T) { }, { ID: "two", - Body: []Point{{0, 3}, {0, 4}, {0, 5}, {0, 5}}, + Body: []Point{{X: 0, Y: 3}, {X: 0, Y: 4}, {X: 0, Y: 5}, {X: 0, Y: 5}}, Health: 100, EliminatedCause: EliminatedByHeadToHeadCollision, EliminatedBy: "one", EliminatedOnTurn: 42, }, }, - Food: []Point{{9, 9}}, + Food: []Point{{X: 9, Y: 9}}, }, }, { @@ -373,16 +361,16 @@ func TestHeadToHeadOnFood(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{0, 2}, {0, 1}, {0, 0}}, + Body: []Point{{X: 0, Y: 2}, {X: 0, Y: 1}, {X: 0, Y: 0}}, Health: 10, }, { ID: "two", - Body: []Point{{0, 4}, {0, 5}, {0, 6}, {0, 7}}, + Body: []Point{{X: 0, Y: 4}, {X: 0, Y: 5}, {X: 0, Y: 6}, {X: 0, Y: 7}}, Health: 10, }, }, - Food: []Point{{0, 3}, {9, 9}}, + Food: []Point{{X: 0, Y: 3}, {X: 9, Y: 9}}, }, []SnakeMove{ {ID: "one", Move: MoveUp}, @@ -395,7 +383,7 @@ func TestHeadToHeadOnFood(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{0, 3}, {0, 2}, {0, 1}, {0, 1}}, + Body: []Point{{X: 0, Y: 3}, {X: 0, Y: 2}, {X: 0, Y: 1}, {X: 0, Y: 1}}, Health: 100, EliminatedCause: EliminatedByHeadToHeadCollision, EliminatedBy: "two", @@ -403,19 +391,19 @@ func TestHeadToHeadOnFood(t *testing.T) { }, { ID: "two", - Body: []Point{{0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 6}}, + Body: []Point{{X: 0, Y: 3}, {X: 0, Y: 4}, {X: 0, Y: 5}, {X: 0, Y: 6}, {X: 0, Y: 6}}, Health: 100, }, }, - Food: []Point{{9, 9}}, + Food: []Point{{X: 9, Y: 9}}, }, }, } rand.Seed(0) // Seed with a value that will reliably not spawn food - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) for _, test := range tests { - nextState, err := r.CreateNextBoardState(test.prevState, test.moves) + _, nextState, err := r.Execute(test.prevState, test.moves) require.Equal(t, test.expectedError, err) if test.expectedState != nil { require.Equal(t, test.expectedState.Width, nextState.Width) @@ -441,22 +429,22 @@ func TestRegressionIssue19(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{0, 2}, {0, 1}, {0, 0}}, + Body: []Point{{X: 0, Y: 2}, {X: 0, Y: 1}, {X: 0, Y: 0}}, Health: 100, }, { ID: "two", - Body: []Point{{0, 5}, {0, 6}, {0, 7}}, + Body: []Point{{X: 0, Y: 5}, {X: 0, Y: 6}, {X: 0, Y: 7}}, Health: 100, }, { ID: "eliminated", - Body: []Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}}, + Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}, {X: 0, Y: 4}, {X: 0, Y: 5}, {X: 0, Y: 6}}, Health: 0, EliminatedCause: EliminatedByOutOfHealth, }, }, - Food: []Point{{9, 9}}, + Food: []Point{{X: 9, Y: 9}}, }, []SnakeMove{ {ID: "one", Move: MoveUp}, @@ -469,30 +457,30 @@ func TestRegressionIssue19(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{0, 3}, {0, 2}, {0, 1}}, + Body: []Point{{X: 0, Y: 3}, {X: 0, Y: 2}, {X: 0, Y: 1}}, Health: 99, }, { ID: "two", - Body: []Point{{0, 4}, {0, 5}, {0, 6}}, + Body: []Point{{X: 0, Y: 4}, {X: 0, Y: 5}, {X: 0, Y: 6}}, Health: 99, }, { ID: "eliminated", - Body: []Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}}, + Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}, {X: 0, Y: 4}, {X: 0, Y: 5}, {X: 0, Y: 6}}, Health: 0, EliminatedCause: EliminatedByOutOfHealth, }, }, - Food: []Point{{9, 9}}, + Food: []Point{{X: 9, Y: 9}}, }, }, } rand.Seed(0) // Seed with a value that will reliably not spawn food - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) for _, test := range tests { - nextState, err := r.CreateNextBoardState(test.prevState, test.moves) + _, nextState, err := r.Execute(test.prevState, test.moves) require.Equal(t, err, test.expectedError) if test.expectedState != nil { require.Equal(t, test.expectedState.Width, nextState.Width) @@ -509,17 +497,17 @@ func TestMoveSnakes(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{10, 110}, {11, 110}}, + Body: []Point{{X: 10, Y: 110}, {X: 11, Y: 110}}, Health: 111111, }, { ID: "two", - Body: []Point{{23, 220}, {22, 220}, {21, 220}, {20, 220}}, + Body: []Point{{X: 23, Y: 220}, {X: 22, Y: 220}, {X: 21, Y: 220}, {X: 20, Y: 220}}, Health: 222222, }, { ID: "three", - Body: []Point{{0, 0}}, + Body: []Point{{X: 0, Y: 0}}, Health: 1, EliminatedCause: EliminatedByOutOfBounds, }, @@ -535,33 +523,33 @@ func TestMoveSnakes(t *testing.T) { ExpectedThree []Point }{ { - MoveDown, []Point{{10, 109}, {10, 110}}, - MoveUp, []Point{{23, 221}, {23, 220}, {22, 220}, {21, 220}}, - MoveDown, []Point{{0, 0}}, + MoveDown, []Point{{X: 10, Y: 109}, {X: 10, Y: 110}}, + MoveUp, []Point{{X: 23, Y: 221}, {X: 23, Y: 220}, {X: 22, Y: 220}, {X: 21, Y: 220}}, + MoveDown, []Point{{X: 0, Y: 0}}, }, { - MoveRight, []Point{{11, 109}, {10, 109}}, - MoveLeft, []Point{{22, 221}, {23, 221}, {23, 220}, {22, 220}}, - MoveDown, []Point{{0, 0}}, + MoveRight, []Point{{X: 11, Y: 109}, {X: 10, Y: 109}}, + MoveLeft, []Point{{X: 22, Y: 221}, {X: 23, Y: 221}, {X: 23, Y: 220}, {X: 22, Y: 220}}, + MoveDown, []Point{{X: 0, Y: 0}}, }, { - MoveRight, []Point{{12, 109}, {11, 109}}, - MoveLeft, []Point{{21, 221}, {22, 221}, {23, 221}, {23, 220}}, - MoveDown, []Point{{0, 0}}, + MoveRight, []Point{{X: 12, Y: 109}, {X: 11, Y: 109}}, + MoveLeft, []Point{{X: 21, Y: 221}, {X: 22, Y: 221}, {X: 23, Y: 221}, {X: 23, Y: 220}}, + MoveDown, []Point{{X: 0, Y: 0}}, }, { - MoveRight, []Point{{13, 109}, {12, 109}}, - MoveLeft, []Point{{20, 221}, {21, 221}, {22, 221}, {23, 221}}, - MoveDown, []Point{{0, 0}}, + MoveRight, []Point{{X: 13, Y: 109}, {X: 12, Y: 109}}, + MoveLeft, []Point{{X: 20, Y: 221}, {X: 21, Y: 221}, {X: 22, Y: 221}, {X: 23, Y: 221}}, + MoveDown, []Point{{X: 0, Y: 0}}, }, { - MoveDown, []Point{{13, 108}, {13, 109}}, - MoveUp, []Point{{20, 222}, {20, 221}, {21, 221}, {22, 221}}, - MoveDown, []Point{{0, 0}}, + MoveDown, []Point{{X: 13, Y: 108}, {X: 13, Y: 109}}, + MoveUp, []Point{{X: 20, Y: 222}, {X: 20, Y: 221}, {X: 21, Y: 221}, {X: 22, Y: 221}}, + MoveDown, []Point{{X: 0, Y: 0}}, }, } - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) for _, test := range tests { moves := []SnakeMove{ {ID: "one", Move: test.MoveOne}, @@ -601,7 +589,7 @@ func TestMoveSnakesWrongID(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}}, + Body: []Point{{X: 1, Y: 1}}, }, }, } @@ -612,7 +600,7 @@ func TestMoveSnakesWrongID(t *testing.T) { }, } - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) _, err := MoveSnakesStandard(b, r.Settings(), moves) require.Equal(t, ErrorNoMoveFound, err) } @@ -622,11 +610,11 @@ func TestMoveSnakesNotEnoughMoves(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}}, + Body: []Point{{X: 1, Y: 1}}, }, { ID: "two", - Body: []Point{{2, 2}}, + Body: []Point{{X: 2, Y: 2}}, }, }, } @@ -637,7 +625,7 @@ func TestMoveSnakesNotEnoughMoves(t *testing.T) { }, } - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) _, err := MoveSnakesStandard(b, r.Settings(), moves) require.Equal(t, ErrorNoMoveFound, err) } @@ -647,7 +635,7 @@ func TestMoveSnakesExtraMovesIgnored(t *testing.T) { Snakes: []Snake{ { ID: "one", - Body: []Point{{1, 1}}, + Body: []Point{{X: 1, Y: 1}}, }, }, } @@ -662,10 +650,10 @@ func TestMoveSnakesExtraMovesIgnored(t *testing.T) { }, } - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) _, err := MoveSnakesStandard(b, r.Settings(), moves) require.NoError(t, err) - require.Equal(t, []Point{{1, 0}}, b.Snakes[0].Body) + require.Equal(t, []Point{{X: 1, Y: 0}}, b.Snakes[0].Body) } func TestMoveSnakesDefault(t *testing.T) { @@ -675,38 +663,38 @@ func TestMoveSnakesDefault(t *testing.T) { Expected []Point }{ { - Body: []Point{{0, 0}}, + Body: []Point{{X: 0, Y: 0}}, Move: "invalid", - Expected: []Point{{0, 1}}, + Expected: []Point{{X: 0, Y: 1}}, }, { - Body: []Point{{5, 5}, {5, 5}}, + Body: []Point{{X: 5, Y: 5}, {X: 5, Y: 5}}, Move: "", - Expected: []Point{{5, 6}, {5, 5}}, + Expected: []Point{{X: 5, Y: 6}, {X: 5, Y: 5}}, }, { - Body: []Point{{5, 5}, {5, 4}}, - Expected: []Point{{5, 6}, {5, 5}}, + Body: []Point{{X: 5, Y: 5}, {X: 5, Y: 4}}, + Expected: []Point{{X: 5, Y: 6}, {X: 5, Y: 5}}, }, { - Body: []Point{{5, 4}, {5, 5}}, - Expected: []Point{{5, 3}, {5, 4}}, + Body: []Point{{X: 5, Y: 4}, {X: 5, Y: 5}}, + Expected: []Point{{X: 5, Y: 3}, {X: 5, Y: 4}}, }, { - Body: []Point{{5, 4}, {5, 5}}, - Expected: []Point{{5, 3}, {5, 4}}, + Body: []Point{{X: 5, Y: 4}, {X: 5, Y: 5}}, + Expected: []Point{{X: 5, Y: 3}, {X: 5, Y: 4}}, }, { - Body: []Point{{4, 5}, {5, 5}}, - Expected: []Point{{3, 5}, {4, 5}}, + Body: []Point{{X: 4, Y: 5}, {X: 5, Y: 5}}, + Expected: []Point{{X: 3, Y: 5}, {X: 4, Y: 5}}, }, { - Body: []Point{{5, 5}, {4, 5}}, - Expected: []Point{{6, 5}, {5, 5}}, + Body: []Point{{X: 5, Y: 5}, {X: 4, Y: 5}}, + Expected: []Point{{X: 6, Y: 5}, {X: 5, Y: 5}}, }, } - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) for _, test := range tests { b := &BoardState{ Snakes: []Snake{ @@ -737,50 +725,50 @@ func TestGetDefaultMove(t *testing.T) { ExpectedMove: MoveUp, }, { - SnakeBody: []Point{{0, 0}}, + SnakeBody: []Point{{X: 0, Y: 0}}, ExpectedMove: MoveUp, }, { - SnakeBody: []Point{{-1, -1}}, + SnakeBody: []Point{{X: -1, Y: -1}}, ExpectedMove: MoveUp, }, // Stacked (fallback to default) { - SnakeBody: []Point{{2, 2}, {2, 2}}, + SnakeBody: []Point{{X: 2, Y: 2}, {X: 2, Y: 2}}, ExpectedMove: MoveUp, }, // Neck next to head { - SnakeBody: []Point{{2, 2}, {2, 1}}, + SnakeBody: []Point{{X: 2, Y: 2}, {X: 2, Y: 1}}, ExpectedMove: MoveUp, }, { - SnakeBody: []Point{{2, 2}, {2, 3}}, + SnakeBody: []Point{{X: 2, Y: 2}, {X: 2, Y: 3}}, ExpectedMove: MoveDown, }, { - SnakeBody: []Point{{2, 2}, {1, 2}}, + SnakeBody: []Point{{X: 2, Y: 2}, {X: 1, Y: 2}}, ExpectedMove: MoveRight, }, { - SnakeBody: []Point{{2, 2}, {3, 2}}, + SnakeBody: []Point{{X: 2, Y: 2}, {X: 3, Y: 2}}, ExpectedMove: MoveLeft, }, // Board wrap cases { - SnakeBody: []Point{{0, 0}, {0, 2}}, + SnakeBody: []Point{{X: 0, Y: 0}, {X: 0, Y: 2}}, ExpectedMove: MoveUp, }, { - SnakeBody: []Point{{0, 0}, {2, 0}}, + SnakeBody: []Point{{X: 0, Y: 0}, {X: 2, Y: 0}}, ExpectedMove: MoveRight, }, { - SnakeBody: []Point{{0, 2}, {0, 0}}, + SnakeBody: []Point{{X: 0, Y: 2}, {X: 0, Y: 0}}, ExpectedMove: MoveDown, }, { - SnakeBody: []Point{{2, 0}, {0, 0}}, + SnakeBody: []Point{{X: 2, Y: 0}, {X: 0, Y: 0}}, ExpectedMove: MoveLeft, }, } @@ -795,22 +783,22 @@ func TestReduceSnakeHealth(t *testing.T) { b := &BoardState{ Snakes: []Snake{ { - Body: []Point{{0, 0}, {0, 1}}, + Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}}, Health: 99, }, { - Body: []Point{{5, 8}, {6, 8}, {7, 8}}, + Body: []Point{{X: 5, Y: 8}, {X: 6, Y: 8}, {X: 7, Y: 8}}, Health: 2, }, { - Body: []Point{{0, 0}, {0, 1}}, + Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}}, Health: 50, EliminatedCause: EliminatedByCollision, }, }, } - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) _, err := ReduceSnakeHealthStandard(b, r.Settings(), mockSnakeMoves()) require.NoError(t, err) require.Equal(t, b.Snakes[0].Health, 98) @@ -898,7 +886,7 @@ func TestSnakeIsOutOfBounds(t *testing.T) { s := Snake{Body: []Point{test.Point}} require.Equal(t, test.Expected, snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Head%+v", test.Point) // Test with point as body - s = Snake{Body: []Point{{0, 0}, {0, 0}, test.Point}} + s = Snake{Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 0}, test.Point}} require.Equal(t, test.Expected, snakeIsOutOfBounds(&s, boardWidth, boardHeight), "Body%+v", test.Point) } } @@ -908,26 +896,26 @@ func TestSnakeHasBodyCollidedSelf(t *testing.T) { Body []Point Expected bool }{ - {[]Point{{1, 1}}, false}, + {[]Point{{X: 1, Y: 1}}, false}, // Self stacks should self collide // (we rely on snakes moving before we check self-collision on turn one) - {[]Point{{2, 2}, {2, 2}}, true}, - {[]Point{{3, 3}, {3, 3}, {3, 3}}, true}, - {[]Point{{5, 5}, {5, 5}, {5, 5}, {5, 5}, {5, 5}}, true}, + {[]Point{{X: 2, Y: 2}, {X: 2, Y: 2}}, true}, + {[]Point{{X: 3, Y: 3}, {X: 3, Y: 3}, {X: 3, Y: 3}}, true}, + {[]Point{{X: 5, Y: 5}, {X: 5, Y: 5}, {X: 5, Y: 5}, {X: 5, Y: 5}, {X: 5, Y: 5}}, true}, // Non-collision cases - {[]Point{{0, 0}, {1, 0}, {1, 0}}, false}, - {[]Point{{0, 0}, {1, 0}, {2, 0}}, false}, - {[]Point{{0, 0}, {1, 0}, {2, 0}, {2, 0}, {2, 0}}, false}, - {[]Point{{0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}}, false}, - {[]Point{{0, 0}, {0, 1}, {0, 2}}, false}, - {[]Point{{0, 0}, {0, 1}, {0, 2}, {0, 2}, {0, 2}}, false}, - {[]Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}}, false}, + {[]Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 0}}, false}, + {[]Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}}, false}, + {[]Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}, {X: 2, Y: 0}, {X: 2, Y: 0}}, false}, + {[]Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}, {X: 3, Y: 0}, {X: 4, Y: 0}}, false}, + {[]Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}, false}, + {[]Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 2}, {X: 0, Y: 2}}, false}, + {[]Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}, {X: 0, Y: 4}}, false}, // Collision cases - {[]Point{{0, 0}, {1, 0}, {0, 0}}, true}, - {[]Point{{0, 0}, {0, 0}, {1, 0}}, true}, - {[]Point{{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}}, true}, - {[]Point{{4, 4}, {3, 4}, {3, 3}, {4, 4}, {4, 4}}, true}, - {[]Point{{3, 3}, {3, 4}, {3, 3}, {4, 4}, {4, 5}}, true}, + {[]Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 0, Y: 0}}, true}, + {[]Point{{X: 0, Y: 0}, {X: 0, Y: 0}, {X: 1, Y: 0}}, true}, + {[]Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 0, Y: 1}, {X: 0, Y: 0}}, true}, + {[]Point{{X: 4, Y: 4}, {X: 3, Y: 4}, {X: 3, Y: 3}, {X: 4, Y: 4}, {X: 4, Y: 4}}, true}, + {[]Point{{X: 3, Y: 3}, {X: 3, Y: 4}, {X: 3, Y: 3}, {X: 4, Y: 4}, {X: 4, Y: 5}}, true}, } for _, test := range tests { @@ -944,38 +932,38 @@ func TestSnakeHasBodyCollidedOther(t *testing.T) { }{ { // Just heads - []Point{{0, 0}}, - []Point{{1, 1}}, + []Point{{X: 0, Y: 0}}, + []Point{{X: 1, Y: 1}}, false, }, { // Head-to-heads are not considered in body collisions - []Point{{0, 0}}, - []Point{{0, 0}}, + []Point{{X: 0, Y: 0}}, + []Point{{X: 0, Y: 0}}, false, }, { // Stacked bodies - []Point{{0, 0}}, - []Point{{0, 0}, {0, 0}}, + []Point{{X: 0, Y: 0}}, + []Point{{X: 0, Y: 0}, {X: 0, Y: 0}}, true, }, { // Separate stacked bodies - []Point{{0, 0}, {0, 0}, {0, 0}}, - []Point{{1, 1}, {1, 1}, {1, 1}}, + []Point{{X: 0, Y: 0}, {X: 0, Y: 0}, {X: 0, Y: 0}}, + []Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, false, }, { // Stacked bodies, separated heads - []Point{{0, 0}, {1, 0}, {1, 0}}, - []Point{{2, 0}, {1, 0}, {1, 0}}, + []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 0}}, + []Point{{X: 2, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 0}}, false, }, { // Mid-snake collision - []Point{{1, 1}}, - []Point{{0, 1}, {1, 1}, {2, 1}}, + []Point{{X: 1, Y: 1}}, + []Point{{X: 0, Y: 1}, {X: 1, Y: 1}, {X: 2, Y: 1}}, true, }, } @@ -996,50 +984,50 @@ func TestSnakeHasLostHeadToHead(t *testing.T) { }{ { // Just heads - []Point{{0, 0}}, - []Point{{1, 1}}, + []Point{{X: 0, Y: 0}}, + []Point{{X: 1, Y: 1}}, false, false, }, { // Just heads colliding - []Point{{0, 0}}, - []Point{{0, 0}}, + []Point{{X: 0, Y: 0}}, + []Point{{X: 0, Y: 0}}, true, true, }, { // One snake larger - []Point{{0, 0}, {1, 0}, {2, 0}}, - []Point{{0, 0}}, + []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}}, + []Point{{X: 0, Y: 0}}, false, true, }, { // Other snake equal - []Point{{0, 0}, {1, 0}, {2, 0}}, - []Point{{0, 0}, {0, 1}, {0, 2}}, + []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}}, + []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}, true, true, }, { // Other snake longer - []Point{{0, 0}, {1, 0}, {2, 0}}, - []Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}}, + []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}}, + []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}}, true, false, }, { // Body collision - []Point{{0, 1}, {1, 1}, {2, 1}}, - []Point{{0, 0}, {0, 1}, {0, 2}, {0, 3}}, + []Point{{X: 0, Y: 1}, {X: 1, Y: 1}, {X: 2, Y: 1}}, + []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}}, false, false, }, { // Separate stacked bodies, head collision - []Point{{3, 10}, {2, 10}, {2, 10}}, - []Point{{3, 10}, {4, 10}, {4, 10}}, + []Point{{X: 3, Y: 10}, {X: 2, Y: 10}, {X: 2, Y: 10}}, + []Point{{X: 3, Y: 10}, {X: 4, Y: 10}, {X: 4, Y: 10}}, true, true, }, { // Separate stacked bodies, head collision - []Point{{10, 3}, {10, 2}, {10, 1}, {10, 0}}, - []Point{{10, 3}, {10, 4}, {10, 5}}, + []Point{{X: 10, Y: 3}, {X: 10, Y: 2}, {X: 10, Y: 1}, {X: 10, Y: 0}}, + []Point{{X: 10, Y: 3}, {X: 10, Y: 4}, {X: 10, Y: 5}}, false, true, }, } @@ -1080,7 +1068,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Single Starvation", []Snake{ - {ID: "1", Body: []Point{{1, 1}}}, + {ID: "1", Body: []Point{{X: 1, Y: 1}}}, }, []string{EliminatedByOutOfHealth}, []string{""}, @@ -1089,7 +1077,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Not Eliminated", []Snake{ - {ID: "1", Health: 1, Body: []Point{{1, 1}}}, + {ID: "1", Health: 1, Body: []Point{{X: 1, Y: 1}}}, }, []string{NotEliminated}, []string{""}, @@ -1098,7 +1086,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Out of Bounds", []Snake{ - {ID: "1", Health: 1, Body: []Point{{-1, 1}}}, + {ID: "1", Health: 1, Body: []Point{{X: -1, Y: 1}}}, }, []string{EliminatedByOutOfBounds}, []string{""}, @@ -1107,7 +1095,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Self Collision", []Snake{ - {ID: "1", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}}, + {ID: "1", Health: 1, Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 0}}}, }, []string{EliminatedBySelfCollision}, []string{"1"}, @@ -1116,8 +1104,8 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Multiple Separate Deaths", []Snake{ - {ID: "1", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 0}}}, - {ID: "2", Health: 1, Body: []Point{{-1, 1}}}, + {ID: "1", Health: 1, Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 0}}}, + {ID: "2", Health: 1, Body: []Point{{X: -1, Y: 1}}}, }, []string{ EliminatedBySelfCollision, @@ -1128,8 +1116,8 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "Other Collision", []Snake{ - {ID: "1", Health: 1, Body: []Point{{0, 2}, {0, 3}, {0, 4}}}, - {ID: "2", Health: 1, Body: []Point{{0, 0}, {0, 1}, {0, 2}}}, + {ID: "1", Health: 1, Body: []Point{{X: 0, Y: 2}, {X: 0, Y: 3}, {X: 0, Y: 4}}}, + {ID: "2", Health: 1, Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}}, }, []string{ EliminatedByCollision, @@ -1140,9 +1128,9 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "All Eliminated Head 2 Head", []Snake{ - {ID: "1", Health: 1, Body: []Point{{1, 1}}}, - {ID: "2", Health: 1, Body: []Point{{1, 1}}}, - {ID: "3", Health: 1, Body: []Point{{1, 1}}}, + {ID: "1", Health: 1, Body: []Point{{X: 1, Y: 1}}}, + {ID: "2", Health: 1, Body: []Point{{X: 1, Y: 1}}}, + {ID: "3", Health: 1, Body: []Point{{X: 1, Y: 1}}}, }, []string{ EliminatedByHeadToHeadCollision, @@ -1155,9 +1143,9 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "One Snake wins Head 2 Head", []Snake{ - {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}}}, + {ID: "1", Health: 1, Body: []Point{{X: 1, Y: 1}, {X: 0, Y: 1}}}, + {ID: "2", Health: 1, Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}, {X: 1, Y: 3}}}, + {ID: "3", Health: 1, Body: []Point{{X: 1, Y: 1}}}, }, []string{ EliminatedByHeadToHeadCollision, @@ -1170,11 +1158,11 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "All Snakes Body Eliminated", []Snake{ - {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 + {ID: "1", Health: 1, Body: []Point{{X: 4, Y: 4}, {X: 3, Y: 3}}}, + {ID: "2", Health: 1, Body: []Point{{X: 3, Y: 3}, {X: 2, Y: 2}}}, + {ID: "3", Health: 1, Body: []Point{{X: 2, Y: 2}, {X: 1, Y: 1}}}, + {ID: "4", Health: 1, Body: []Point{{X: 1, Y: 1}, {X: 4, Y: 4}}}, + {ID: "5", Health: 1, Body: []Point{{X: 4, Y: 4}}}, // Body collision takes priority }, []string{ EliminatedByCollision, @@ -1189,10 +1177,10 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "All Snakes Eliminated Head 2 Head", []Snake{ - {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}}}, + {ID: "1", Health: 1, Body: []Point{{X: 4, Y: 4}, {X: 4, Y: 5}}}, + {ID: "2", Health: 1, Body: []Point{{X: 4, Y: 4}, {X: 4, Y: 3}}}, + {ID: "3", Health: 1, Body: []Point{{X: 4, Y: 4}, {X: 5, Y: 4}}}, + {ID: "4", Health: 1, Body: []Point{{X: 4, Y: 4}, {X: 3, Y: 4}}}, }, []string{ EliminatedByHeadToHeadCollision, @@ -1206,10 +1194,10 @@ func TestMaybeEliminateSnakes(t *testing.T) { { "4 Snakes Head 2 Head", []Snake{ - {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}}}, + {ID: "1", Health: 1, Body: []Point{{X: 4, Y: 4}, {X: 4, Y: 5}}}, + {ID: "2", Health: 1, Body: []Point{{X: 4, Y: 4}, {X: 4, Y: 3}}}, + {ID: "3", Health: 1, Body: []Point{{X: 4, Y: 4}, {X: 5, Y: 4}, {X: 6, Y: 4}}}, + {ID: "4", Health: 1, Body: []Point{{X: 4, Y: 4}, {X: 3, Y: 4}}}, }, []string{ EliminatedByHeadToHeadCollision, @@ -1222,7 +1210,6 @@ func TestMaybeEliminateSnakes(t *testing.T) { }, } - r := StandardRuleset{} for _, test := range tests { t.Run(test.Name, func(t *testing.T) { b := &BoardState{ @@ -1230,7 +1217,7 @@ func TestMaybeEliminateSnakes(t *testing.T) { Height: 10, Snakes: test.Snakes, } - _, err := EliminateSnakesStandard(b, r.Settings(), mockSnakeMoves()) + _, err := EliminateSnakesStandard(b, Settings{}, mockSnakeMoves()) require.Equal(t, test.Err, err) for i, snake := range b.Snakes { require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause) @@ -1248,12 +1235,12 @@ func TestMaybeEliminateSnakesPriority(t *testing.T) { }{ { []Snake{ - {ID: "1", Health: 0, Body: []Point{{-1, 0}, {0, 0}, {1, 0}}}, - {ID: "2", Health: 1, Body: []Point{{-1, 0}, {0, 0}, {1, 0}}}, - {ID: "3", Health: 1, Body: []Point{{1, 0}, {0, 0}, {1, 0}}}, - {ID: "4", Health: 1, Body: []Point{{1, 0}, {1, 1}, {1, 2}}}, - {ID: "5", Health: 1, Body: []Point{{2, 2}, {2, 1}, {2, 0}}}, - {ID: "6", Health: 1, Body: []Point{{2, 2}, {2, 3}, {2, 4}, {2, 5}}}, + {ID: "1", Health: 0, Body: []Point{{X: -1, Y: 0}, {X: 0, Y: 0}, {X: 1, Y: 0}}}, + {ID: "2", Health: 1, Body: []Point{{X: -1, Y: 0}, {X: 0, Y: 0}, {X: 1, Y: 0}}}, + {ID: "3", Health: 1, Body: []Point{{X: 1, Y: 0}, {X: 0, Y: 0}, {X: 1, Y: 0}}}, + {ID: "4", Health: 1, Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}}}, + {ID: "5", Health: 1, Body: []Point{{X: 2, Y: 2}, {X: 2, Y: 1}, {X: 2, Y: 0}}}, + {ID: "6", Health: 1, Body: []Point{{X: 2, Y: 2}, {X: 2, Y: 3}, {X: 2, Y: 4}, {X: 2, Y: 5}}}, }, []string{ EliminatedByOutOfHealth, @@ -1267,7 +1254,7 @@ func TestMaybeEliminateSnakesPriority(t *testing.T) { }, } - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) for _, test := range tests { b := &BoardState{Width: 10, Height: 10, Snakes: test.Snakes} _, err := EliminateSnakesStandard(b, r.Settings(), mockSnakeMoves()) @@ -1290,50 +1277,50 @@ func TestMaybeDamageHazards(t *testing.T) { }{ {}, { - Snakes: []Snake{{Body: []Point{{0, 0}}}}, + Snakes: []Snake{{Body: []Point{{X: 0, Y: 0}}}}, Hazards: []Point{}, ExpectedEliminatedCauses: []string{NotEliminated}, ExpectedEliminatedByIDs: []string{""}, ExpectedEliminatedOnTurns: []int{0}, }, { - Snakes: []Snake{{Body: []Point{{0, 0}}}}, - Hazards: []Point{{0, 0}}, + Snakes: []Snake{{Body: []Point{{X: 0, Y: 0}}}}, + Hazards: []Point{{X: 0, Y: 0}}, ExpectedEliminatedCauses: []string{EliminatedByHazard}, ExpectedEliminatedByIDs: []string{""}, ExpectedEliminatedOnTurns: []int{42}, }, { - Snakes: []Snake{{Body: []Point{{0, 0}}}}, - Hazards: []Point{{0, 0}}, - Food: []Point{{0, 0}}, + Snakes: []Snake{{Body: []Point{{X: 0, Y: 0}}}}, + Hazards: []Point{{X: 0, Y: 0}}, + Food: []Point{{X: 0, Y: 0}}, ExpectedEliminatedCauses: []string{NotEliminated}, ExpectedEliminatedByIDs: []string{""}, ExpectedEliminatedOnTurns: []int{0}, }, { - Snakes: []Snake{{Body: []Point{{0, 0}, {1, 0}, {2, 0}}}}, - Hazards: []Point{{1, 0}, {2, 0}}, + Snakes: []Snake{{Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}}}}, + Hazards: []Point{{X: 1, Y: 0}, {X: 2, Y: 0}}, ExpectedEliminatedCauses: []string{NotEliminated}, ExpectedEliminatedByIDs: []string{""}, ExpectedEliminatedOnTurns: []int{0}, }, { Snakes: []Snake{ - {Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, - {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}}}, + {Body: []Point{{X: 3, Y: 3}, {X: 3, Y: 4}, {X: 3, Y: 5}, {X: 3, Y: 6}}}, }, - Hazards: []Point{{1, 0}, {2, 0}, {3, 4}, {3, 5}, {3, 6}}, + Hazards: []Point{{X: 1, Y: 0}, {X: 2, Y: 0}, {X: 3, Y: 4}, {X: 3, Y: 5}, {X: 3, Y: 6}}, ExpectedEliminatedCauses: []string{NotEliminated, NotEliminated}, ExpectedEliminatedByIDs: []string{"", ""}, ExpectedEliminatedOnTurns: []int{0, 0}, }, { Snakes: []Snake{ - {Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, - {Body: []Point{{3, 3}, {3, 4}, {3, 5}, {3, 6}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}}}, + {Body: []Point{{X: 3, Y: 3}, {X: 3, Y: 4}, {X: 3, Y: 5}, {X: 3, Y: 6}}}, }, - Hazards: []Point{{3, 3}}, + Hazards: []Point{{X: 3, Y: 3}}, ExpectedEliminatedCauses: []string{NotEliminated, EliminatedByHazard}, ExpectedEliminatedByIDs: []string{"", ""}, ExpectedEliminatedOnTurns: []int{0, 42}, @@ -1342,7 +1329,7 @@ func TestMaybeDamageHazards(t *testing.T) { for _, test := range tests { b := &BoardState{Turn: 41, Snakes: test.Snakes, Hazards: test.Hazards, Food: test.Food} - r := StandardRuleset{HazardDamagePerTurn: 100} + r := getStandardRuleset(NewSettingsWithParams(ParamHazardDamagePerTurn, "100")) _, err := DamageHazardsStandard(b, r.Settings(), mockSnakeMoves()) require.NoError(t, err) @@ -1382,11 +1369,11 @@ func TestHazardDamagePerTurn(t *testing.T) { } for _, test := range tests { - b := &BoardState{Snakes: []Snake{{Health: test.Health, Body: []Point{{0, 0}}}}, Hazards: []Point{{0, 0}}} + b := &BoardState{Snakes: []Snake{{Health: test.Health, Body: []Point{{X: 0, Y: 0}}}}, Hazards: []Point{{X: 0, Y: 0}}} if test.Food { - b.Food = []Point{{0, 0}} + b.Food = []Point{{X: 0, Y: 0}} } - r := StandardRuleset{HazardDamagePerTurn: test.HazardDamagePerTurn} + r := getStandardRuleset(NewSettingsWithParams(ParamHazardDamagePerTurn, fmt.Sprint(test.HazardDamagePerTurn))) _, err := DamageHazardsStandard(b, r.Settings(), mockSnakeMoves()) require.Equal(t, test.Error, err) @@ -1406,63 +1393,63 @@ func TestMaybeFeedSnakes(t *testing.T) { { Name: "snake not on food", Snakes: []Snake{ - {Health: 5, Body: []Point{{0, 0}, {0, 1}, {0, 2}}}, + {Health: 5, Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}}, }, - Food: []Point{{3, 3}}, + Food: []Point{{X: 3, Y: 3}}, ExpectedSnakes: []Snake{ - {Health: 5, Body: []Point{{0, 0}, {0, 1}, {0, 2}}}, + {Health: 5, Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}}, }, - ExpectedFood: []Point{{3, 3}}, + ExpectedFood: []Point{{X: 3, Y: 3}}, }, { Name: "snake on food", Snakes: []Snake{ - {Health: SnakeMaxHealth - 1, Body: []Point{{2, 1}, {1, 1}, {1, 2}, {2, 2}}}, + {Health: SnakeMaxHealth - 1, Body: []Point{{X: 2, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 2}}}, }, - Food: []Point{{2, 1}}, + Food: []Point{{X: 2, Y: 1}}, ExpectedSnakes: []Snake{ - {Health: SnakeMaxHealth, Body: []Point{{2, 1}, {1, 1}, {1, 2}, {2, 2}, {2, 2}}}, + {Health: SnakeMaxHealth, Body: []Point{{X: 2, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 2}, {X: 2, Y: 2}}}, }, ExpectedFood: []Point{}, }, { Name: "food under body", Snakes: []Snake{ - {Body: []Point{{0, 0}, {0, 1}, {0, 2}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}}, }, - Food: []Point{{0, 1}}, + Food: []Point{{X: 0, Y: 1}}, ExpectedSnakes: []Snake{ - {Body: []Point{{0, 0}, {0, 1}, {0, 2}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}}, }, - ExpectedFood: []Point{{0, 1}}, + ExpectedFood: []Point{{X: 0, Y: 1}}, }, { Name: "snake on food but already eliminated", Snakes: []Snake{ - {Body: []Point{{0, 0}, {0, 1}, {0, 2}}, EliminatedCause: "EliminatedByOutOfBounds"}, + {Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}, EliminatedCause: "EliminatedByOutOfBounds"}, }, - Food: []Point{{0, 0}}, + Food: []Point{{X: 0, Y: 0}}, ExpectedSnakes: []Snake{ - {Body: []Point{{0, 0}, {0, 1}, {0, 2}}}, + {Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}}, }, - ExpectedFood: []Point{{0, 0}}, + ExpectedFood: []Point{{X: 0, Y: 0}}, }, { Name: "multiple snakes on same food", Snakes: []Snake{ - {Health: SnakeMaxHealth, Body: []Point{{0, 0}, {0, 1}, {0, 2}}}, - {Health: SnakeMaxHealth - 9, Body: []Point{{0, 0}, {1, 0}, {2, 0}}}, + {Health: SnakeMaxHealth, Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}}}, + {Health: SnakeMaxHealth - 9, Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}}}, }, - Food: []Point{{0, 0}, {4, 4}}, + Food: []Point{{X: 0, Y: 0}, {X: 4, Y: 4}}, ExpectedSnakes: []Snake{ - {Health: SnakeMaxHealth, Body: []Point{{0, 0}, {0, 1}, {0, 2}, {0, 2}}}, - {Health: SnakeMaxHealth, Body: []Point{{0, 0}, {1, 0}, {2, 0}, {2, 0}}}, + {Health: SnakeMaxHealth, Body: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 2}}}, + {Health: SnakeMaxHealth, Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}, {X: 2, Y: 0}}}, }, - ExpectedFood: []Point{{4, 4}}, + ExpectedFood: []Point{{X: 4, Y: 4}}, }, } - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) for _, test := range tests { b := &BoardState{ Snakes: test.Snakes, @@ -1489,17 +1476,17 @@ func TestMaybeSpawnFoodMinimum(t *testing.T) { {0, []Point{}, 0}, {1, []Point{}, 1}, {9, []Point{}, 9}, - {7, []Point{{4, 5}, {4, 4}, {4, 1}}, 7}, + {7, []Point{{X: 4, Y: 5}, {X: 4, Y: 4}, {X: 4, Y: 1}}, 7}, } for _, test := range tests { - r := StandardRuleset{MinimumFood: test.MinimumFood} + r := getStandardRuleset(NewSettingsWithParams(ParamMinimumFood, fmt.Sprint(test.MinimumFood))) b := &BoardState{ Height: 11, Width: 11, Snakes: []Snake{ - {Body: []Point{{1, 0}, {1, 1}}}, - {Body: []Point{{0, 1}, {0, 2}, {0, 3}}}, + {Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}}}, + {Body: []Point{{X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}}}, }, Food: test.Food, } @@ -1511,13 +1498,13 @@ func TestMaybeSpawnFoodMinimum(t *testing.T) { } func TestMaybeSpawnFoodZeroChance(t *testing.T) { - r := StandardRuleset{FoodSpawnChance: 0} + r := getStandardRuleset(NewSettingsWithParams(ParamFoodSpawnChance, "0")) b := &BoardState{ Height: 11, Width: 11, Snakes: []Snake{ - {Body: []Point{{1, 0}, {1, 1}}}, - {Body: []Point{{0, 1}, {0, 2}, {0, 3}}}, + {Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}}}, + {Body: []Point{{X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}}}, }, Food: []Point{}, } @@ -1529,13 +1516,13 @@ func TestMaybeSpawnFoodZeroChance(t *testing.T) { } func TestMaybeSpawnFoodHundredChance(t *testing.T) { - r := StandardRuleset{FoodSpawnChance: 100} + r := getStandardRuleset(NewSettingsWithParams(ParamFoodSpawnChance, "100")) b := &BoardState{ Height: 11, Width: 11, Snakes: []Snake{ - {Body: []Point{{1, 0}, {1, 1}}}, - {Body: []Point{{0, 1}, {0, 2}, {0, 3}}}, + {Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}}}, + {Body: []Point{{X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}}}, }, Food: []Point{}, } @@ -1555,20 +1542,20 @@ func TestMaybeSpawnFoodHalfChance(t *testing.T) { // Use pre-tested seeds and results {123, []Point{}, 1}, {12345, []Point{}, 0}, - {456, []Point{{4, 4}}, 1}, - {789, []Point{{4, 4}}, 2}, - {511, []Point{{4, 4}}, 1}, - {165, []Point{{4, 4}}, 2}, + {456, []Point{{X: 4, Y: 4}}, 1}, + {789, []Point{{X: 4, Y: 4}}, 2}, + {511, []Point{{X: 4, Y: 4}}, 1}, + {165, []Point{{X: 4, Y: 4}}, 2}, } - r := StandardRuleset{FoodSpawnChance: 50} + r := getStandardRuleset(NewSettingsWithParams(ParamFoodSpawnChance, "50")) for _, test := range tests { b := &BoardState{ Height: 4, Width: 5, Snakes: []Snake{ - {Body: []Point{{1, 0}, {1, 1}}}, - {Body: []Point{{0, 1}, {0, 2}, {0, 3}}}, + {Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}}}, + {Body: []Point{{X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}}}, }, Food: test.Food, } @@ -1634,7 +1621,7 @@ func TestIsGameOver(t *testing.T) { }, } - r := StandardRuleset{} + r := getStandardRuleset(Settings{}) for _, test := range tests { b := &BoardState{ Height: 11, @@ -1643,7 +1630,7 @@ func TestIsGameOver(t *testing.T) { Food: []Point{}, } - actual, err := r.IsGameOver(b) + actual, _, err := r.Execute(b, nil) require.NoError(t, err) require.Equal(t, test.Expected, actual) } diff --git a/wrapped.go b/wrapped.go index 0b5a4c1..7ab484f 100644 --- a/wrapped.go +++ b/wrapped.go @@ -9,22 +9,6 @@ var wrappedRulesetStages = []string{ StageEliminationStandard, } -type WrappedRuleset struct { - StandardRuleset -} - -func (r *WrappedRuleset) Name() string { return GameTypeWrapped } - -func (r WrappedRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) { - return NewPipeline(wrappedRulesetStages...).Execute(bs, s, sm) -} - -func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) { - _, nextState, err := r.Execute(prevState, r.Settings(), moves) - - return nextState, err -} - func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) { if IsInitialization(b, settings, moves) { return false, nil @@ -47,10 +31,6 @@ func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (boo return false, nil } -func (r *WrappedRuleset) IsGameOver(b *BoardState) (bool, error) { - return GameOverStandard(b, r.Settings(), nil) -} - func wrap(value, min, max int) int { if value < min { return max diff --git a/wrapped_test.go b/wrapped_test.go index a5bbafa..8eb3b31 100644 --- a/wrapped_test.go +++ b/wrapped_test.go @@ -7,15 +7,19 @@ import ( "github.com/stretchr/testify/require" ) +func getWrappedRuleset(settings Settings) Ruleset { + return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeWrapped) +} + 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}}}, + {ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}}, }, } @@ -26,17 +30,18 @@ func TestLeft(t *testing.T) { {ID: "topRight", Move: "left"}, } - r := WrappedRuleset{} + r := getWrappedRuleset(Settings{}) - nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves) require.NoError(t, err) + require.False(t, gameOver) 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}}}, + {ID: "bottomLeft", Health: 10, Body: []Point{{X: 10, Y: 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{X: 9, Y: 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{X: 10, Y: 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{X: 9, Y: 10}}}, } for i, snake := range nextBoardState.Snakes { require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) @@ -51,10 +56,10 @@ func TestRight(t *testing.T) { 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}}}, + {ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}}, }, } @@ -65,17 +70,18 @@ func TestRight(t *testing.T) { {ID: "topRight", Move: "right"}, } - r := WrappedRuleset{} + r := getWrappedRuleset(Settings{}) - nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves) require.NoError(t, err) + require.False(t, gameOver) 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}}}, + {ID: "bottomLeft", Health: 10, Body: []Point{{X: 1, Y: 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{X: 0, Y: 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{X: 1, Y: 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{X: 0, Y: 10}}}, } for i, snake := range nextBoardState.Snakes { require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) @@ -90,10 +96,10 @@ func TestUp(t *testing.T) { 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}}}, + {ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}}, }, } @@ -104,17 +110,18 @@ func TestUp(t *testing.T) { {ID: "topRight", Move: "up"}, } - r := WrappedRuleset{} + r := getWrappedRuleset(Settings{}) - nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves) require.NoError(t, err) + require.False(t, gameOver) 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}}}, + {ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 1}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 1}}}, + {ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}}, + {ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 0}}}, } for i, snake := range nextBoardState.Snakes { require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) @@ -129,10 +136,10 @@ func TestDown(t *testing.T) { 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}}}, + {ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}}, + {ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}}, + {ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}}, }, } @@ -143,17 +150,18 @@ func TestDown(t *testing.T) { {ID: "topRight", Move: "down"}, } - r := WrappedRuleset{} + r := getWrappedRuleset(Settings{}) - nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves) require.NoError(t, err) + require.False(t, gameOver) 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}}}, + {ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}}, + {ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 10}}}, + {ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 9}}}, + {ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 9}}}, } for i, snake := range nextBoardState.Snakes { require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) @@ -168,14 +176,14 @@ func TestEdgeCrossingCollision(t *testing.T) { Width: 11, Height: 11, Snakes: []Snake{ - {ID: "left", Health: 10, Body: []Point{{0, 5}}}, + {ID: "left", Health: 10, Body: []Point{{X: 0, Y: 5}}}, {ID: "rightEdge", Health: 10, Body: []Point{ - {10, 1}, - {10, 2}, - {10, 3}, - {10, 4}, - {10, 5}, - {10, 6}, + {X: 10, Y: 1}, + {X: 10, Y: 2}, + {X: 10, Y: 3}, + {X: 10, Y: 4}, + {X: 10, Y: 5}, + {X: 10, Y: 6}, }}, }, } @@ -185,21 +193,22 @@ func TestEdgeCrossingCollision(t *testing.T) { {ID: "rightEdge", Move: "down"}, } - r := WrappedRuleset{} + r := getWrappedRuleset(Settings{}) - nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves) require.NoError(t, err) + require.False(t, gameOver) require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes)) expectedSnakes := []Snake{ - {ID: "left", Health: 0, Body: []Point{{10, 5}}, EliminatedCause: EliminatedByCollision, EliminatedBy: "rightEdge"}, + {ID: "left", Health: 0, Body: []Point{{X: 10, Y: 5}}, EliminatedCause: EliminatedByCollision, EliminatedBy: "rightEdge"}, {ID: "rightEdge", Health: 10, Body: []Point{ - {10, 0}, - {10, 1}, - {10, 2}, - {10, 3}, - {10, 4}, - {10, 5}, + {X: 10, Y: 0}, + {X: 10, Y: 1}, + {X: 10, Y: 2}, + {X: 10, Y: 3}, + {X: 10, Y: 4}, + {X: 10, Y: 5}, }}, } for i, snake := range nextBoardState.Snakes { @@ -215,11 +224,11 @@ func TestEdgeCrossingEating(t *testing.T) { Width: 11, Height: 11, Snakes: []Snake{ - {ID: "left", Health: 10, Body: []Point{{0, 5}, {1, 5}}}, - {ID: "other", Health: 10, Body: []Point{{5, 5}}}, + {ID: "left", Health: 10, Body: []Point{{X: 0, Y: 5}, {X: 1, Y: 5}}}, + {ID: "other", Health: 10, Body: []Point{{X: 5, Y: 5}}}, }, Food: []Point{ - {10, 5}, + {X: 10, Y: 5}, }, } @@ -228,15 +237,16 @@ func TestEdgeCrossingEating(t *testing.T) { {ID: "other", Move: "left"}, } - r := WrappedRuleset{} + r := getWrappedRuleset(Settings{}) - nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves) + gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves) require.NoError(t, err) + require.False(t, gameOver) 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}}}, + {ID: "left", Health: 100, Body: []Point{{X: 10, Y: 5}, {X: 0, Y: 5}, {X: 0, Y: 5}}}, + {ID: "other", Health: 9, Body: []Point{{X: 4, Y: 5}}}, } for i, snake := range nextBoardState.Snakes { require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID) @@ -271,12 +281,12 @@ var wrappedCaseMoveAndWrap = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{0, 0}, {1, 0}}, + Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, Health: 100, }, { ID: "two", - Body: []Point{{3, 4}, {3, 3}}, + Body: []Point{{X: 3, Y: 4}, {X: 3, Y: 3}}, Health: 100, }, { @@ -301,12 +311,12 @@ var wrappedCaseMoveAndWrap = gameTestCase{ Snakes: []Snake{ { ID: "one", - Body: []Point{{9, 0}, {0, 0}}, + Body: []Point{{X: 9, Y: 0}, {X: 0, Y: 0}}, Health: 99, }, { ID: "two", - Body: []Point{{3, 5}, {3, 4}}, + Body: []Point{{X: 3, Y: 5}, {X: 3, Y: 4}}, Health: 99, }, { @@ -330,14 +340,10 @@ func TestWrappedCreateNextBoardState(t *testing.T) { standardMoveAndCollideMAD, wrappedCaseMoveAndWrap, } - r := WrappedRuleset{} - rb := NewRulesetBuilder().WithParams(map[string]string{ - ParamGameType: GameTypeWrapped, - }) + r := getWrappedRuleset(Settings{}) for _, gc := range cases { - gc.requireValidNextState(t, &r) - // also test a RulesBuilder constructed instance - gc.requireValidNextState(t, rb.Ruleset()) + // test a RulesBuilder constructed instance + gc.requireValidNextState(t, r) // also test a pipeline with the same settings gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeWrapped, NewPipeline(wrappedRulesetStages...))) }