From dab9178a5506bcb9813325195eab26bd2b222866 Mon Sep 17 00:00:00 2001 From: Rob O'Dwyer Date: Wed, 11 May 2022 08:26:28 -0700 Subject: [PATCH] DEV 1247: Add a new map generator interface (#71) * reorganize code * first draft of map generator interfaces * add explicit random interface to board helpers * implement standard map * rename Generator to GameMap * allow initializing snakes separately from placing them * add random number generator to Settings * updates to GameMap interface * add helpers for creating and updating BoardState with maps --- board.go | 78 +++++++--- board_test.go | 30 ++-- cli/commands/play.go | 2 +- constants.go | 60 ++++++++ constrictor_test.go | 2 +- maps/game_map.go | 115 +++++++++++++++ maps/game_map_test.go | 64 +++++++++ maps/helpers.go | 42 ++++++ maps/registry.go | 35 +++++ maps/standard.go | 80 +++++++++++ maps/standard_test.go | 322 ++++++++++++++++++++++++++++++++++++++++++ pipeline.go | 14 +- rand.go | 56 ++++++++ ruleset.go | 170 ++++++++-------------- standard.go | 4 +- standard_test.go | 2 +- 16 files changed, 916 insertions(+), 160 deletions(-) create mode 100644 constants.go create mode 100644 maps/game_map.go create mode 100644 maps/game_map_test.go create mode 100644 maps/helpers.go create mode 100644 maps/registry.go create mode 100644 maps/standard.go create mode 100644 maps/standard_test.go create mode 100644 rand.go diff --git a/board.go b/board.go index ec86c0c..22a3faa 100644 --- a/board.go +++ b/board.go @@ -1,9 +1,5 @@ package rules -import ( - "math/rand" -) - type BoardState struct { Turn int32 Height int32 @@ -13,6 +9,20 @@ type BoardState struct { Hazards []Point } +type Point struct { + X int32 + Y int32 +} + +type Snake struct { + ID string + Body []Point + Health int32 + EliminatedCause string + EliminatedOnTurn int32 + EliminatedBy string +} + // NewBoardState returns an empty but fully initialized BoardState func NewBoardState(width, height int32) *BoardState { return &BoardState{ @@ -49,15 +59,15 @@ func (prevState *BoardState) Clone() *BoardState { // "default" board state with snakes and food. // In a real game, the engine may generate the board without calling this // function, or customize the results based on game-specific settings. -func CreateDefaultBoardState(width int32, height int32, snakeIDs []string) (*BoardState, error) { +func CreateDefaultBoardState(rand Rand, width int32, height int32, snakeIDs []string) (*BoardState, error) { initialBoardState := NewBoardState(width, height) - err := PlaceSnakesAutomatically(initialBoardState, snakeIDs) + err := PlaceSnakesAutomatically(rand, initialBoardState, snakeIDs) if err != nil { return nil, err } - err = PlaceFoodAutomatically(initialBoardState) + err = PlaceFoodAutomatically(rand, initialBoardState) if err != nil { return nil, err } @@ -66,14 +76,14 @@ func CreateDefaultBoardState(width int32, height int32, snakeIDs []string) (*Boa } // PlaceSnakesAutomatically initializes the array of snakes based on the provided snake IDs and the size of the board. -func PlaceSnakesAutomatically(b *BoardState, snakeIDs []string) error { +func PlaceSnakesAutomatically(rand Rand, b *BoardState, snakeIDs []string) error { if isKnownBoardSize(b) { - return PlaceSnakesFixed(b, snakeIDs) + return PlaceSnakesFixed(rand, b, snakeIDs) } - return PlaceSnakesRandomly(b, snakeIDs) + return PlaceSnakesRandomly(rand, b, snakeIDs) } -func PlaceSnakesFixed(b *BoardState, snakeIDs []string) error { +func PlaceSnakesFixed(rand Rand, b *BoardState, snakeIDs []string) error { b.Snakes = make([]Snake, len(snakeIDs)) for i := 0; i < len(snakeIDs); i++ { @@ -116,7 +126,7 @@ func PlaceSnakesFixed(b *BoardState, snakeIDs []string) error { return nil } -func PlaceSnakesRandomly(b *BoardState, snakeIDs []string) error { +func PlaceSnakesRandomly(rand Rand, b *BoardState, snakeIDs []string) error { b.Snakes = make([]Snake, len(snakeIDs)) for i := 0; i < len(snakeIDs); i++ { @@ -127,7 +137,7 @@ func PlaceSnakesRandomly(b *BoardState, snakeIDs []string) error { } for i := 0; i < len(b.Snakes); i++ { - unoccupiedPoints := getEvenUnoccupiedPoints(b) + unoccupiedPoints := GetEvenUnoccupiedPoints(b) if len(unoccupiedPoints) <= 0 { return ErrorNoRoomForSnake } @@ -139,8 +149,30 @@ func PlaceSnakesRandomly(b *BoardState, snakeIDs []string) error { return nil } +// Adds all snakes without body coordinates to the board. +// This allows GameMaps to access the list of snakes and perform initial placement. +func InitializeSnakes(b *BoardState, snakeIDs []string) { + b.Snakes = make([]Snake, len(snakeIDs)) + + for i := 0; i < len(snakeIDs); i++ { + b.Snakes[i] = Snake{ + ID: snakeIDs[i], + Health: SnakeMaxHealth, + Body: []Point{}, + } + } +} + // PlaceSnake adds a snake to the board with the given ID and body coordinates. func PlaceSnake(b *BoardState, snakeID string, body []Point) error { + // Update an existing snake that already has a body + for index, snake := range b.Snakes { + if snake.ID == snakeID { + b.Snakes[index].Body = body + return nil + } + } + // Add a new snake b.Snakes = append(b.Snakes, Snake{ ID: snakeID, Health: SnakeMaxHealth, @@ -150,14 +182,14 @@ func PlaceSnake(b *BoardState, snakeID string, body []Point) error { } // PlaceFoodAutomatically initializes the array of food based on the size of the board and the number of snakes. -func PlaceFoodAutomatically(b *BoardState) error { +func PlaceFoodAutomatically(rand Rand, b *BoardState) error { if isKnownBoardSize(b) { - return PlaceFoodFixed(b) + return PlaceFoodFixed(rand, b) } - return PlaceFoodRandomly(b, int32(len(b.Snakes))) + return PlaceFoodRandomly(rand, b, int32(len(b.Snakes))) } -func PlaceFoodFixed(b *BoardState) error { +func PlaceFoodFixed(rand Rand, b *BoardState) error { centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2} // Place 1 food within exactly 2 moves of each snake, but never towards the center or in a corner @@ -220,7 +252,7 @@ func PlaceFoodFixed(b *BoardState) error { // Finally, always place 1 food in center of board for dramatic purposes isCenterOccupied := true - unoccupiedPoints := getUnoccupiedPoints(b, true) + unoccupiedPoints := GetUnoccupiedPoints(b, true) for _, point := range unoccupiedPoints { if point == centerCoord { isCenterOccupied = false @@ -236,9 +268,9 @@ func PlaceFoodFixed(b *BoardState) error { } // PlaceFoodRandomly adds up to n new food to the board in random unoccupied squares -func PlaceFoodRandomly(b *BoardState, n int32) error { +func PlaceFoodRandomly(rand Rand, b *BoardState, n int32) error { for i := int32(0); i < n; i++ { - unoccupiedPoints := getUnoccupiedPoints(b, false) + unoccupiedPoints := GetUnoccupiedPoints(b, false) if len(unoccupiedPoints) > 0 { newFood := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))] b.Food = append(b.Food, newFood) @@ -254,9 +286,9 @@ func absInt32(n int32) int32 { return n } -func getEvenUnoccupiedPoints(b *BoardState) []Point { +func GetEvenUnoccupiedPoints(b *BoardState) []Point { // Start by getting unoccupied points - unoccupiedPoints := getUnoccupiedPoints(b, true) + unoccupiedPoints := GetUnoccupiedPoints(b, true) // Create a new array to hold points that are even evenUnoccupiedPoints := []Point{} @@ -269,7 +301,7 @@ func getEvenUnoccupiedPoints(b *BoardState) []Point { return evenUnoccupiedPoints } -func getUnoccupiedPoints(b *BoardState, includePossibleMoves bool) []Point { +func GetUnoccupiedPoints(b *BoardState, includePossibleMoves bool) []Point { pointIsOccupied := map[int32]map[int32]bool{} for _, p := range b.Food { if _, xExists := pointIsOccupied[p.X]; !xExists { diff --git a/board_test.go b/board_test.go index 9e58536..e120742 100644 --- a/board_test.go +++ b/board_test.go @@ -37,7 +37,7 @@ func TestCreateDefaultBoardState(t *testing.T) { } for testNum, test := range tests { - state, err := CreateDefaultBoardState(test.Width, test.Height, test.IDs) + state, err := CreateDefaultBoardState(MaxRand, test.Width, test.Height, test.IDs) require.Equal(t, test.Err, err) if err != nil { require.Nil(t, state) @@ -196,8 +196,8 @@ func TestPlaceSnakesDefault(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprint(test.BoardState.Width, test.BoardState.Height, len(test.SnakeIDs)), func(t *testing.T) { - require.Equal(t, test.BoardState.Width*test.BoardState.Height, int32(len(getUnoccupiedPoints(test.BoardState, true)))) - err := PlaceSnakesAutomatically(test.BoardState, test.SnakeIDs) + require.Equal(t, test.BoardState.Width*test.BoardState.Height, int32(len(GetUnoccupiedPoints(test.BoardState, true)))) + err := PlaceSnakesAutomatically(MaxRand, test.BoardState, test.SnakeIDs) require.Equal(t, test.Err, err, "Snakes: %d", len(test.BoardState.Snakes)) if err == nil { for i := 0; i < len(test.BoardState.Snakes); i++ { @@ -338,7 +338,7 @@ func TestPlaceFood(t *testing.T) { for _, test := range tests { require.Len(t, test.BoardState.Food, 0) - err := PlaceFoodAutomatically(test.BoardState) + err := PlaceFoodAutomatically(MaxRand, test.BoardState) require.NoError(t, err) require.Equal(t, test.ExpectedFood, len(test.BoardState.Food)) for _, point := range test.BoardState.Food { @@ -396,7 +396,7 @@ func TestPlaceFoodFixed(t *testing.T) { for _, test := range tests { require.Len(t, test.BoardState.Food, 0) - err := PlaceFoodFixed(test.BoardState) + err := PlaceFoodFixed(MaxRand, test.BoardState) require.NoError(t, err) require.Equal(t, len(test.BoardState.Snakes)+1, len(test.BoardState.Food)) @@ -444,7 +444,7 @@ func TestPlaceFoodFixedNoRoom(t *testing.T) { }, Food: []Point{}, } - err := PlaceFoodFixed(boardState) + err := PlaceFoodFixed(MaxRand, boardState) require.Error(t, err) } @@ -463,18 +463,18 @@ func TestPlaceFoodFixedNoRoom_Corners(t *testing.T) { // There are only two possible food spawn locations for each snake, // so repeat calls to place food should fail after 2 successes - err := PlaceFoodFixed(boardState) + err := PlaceFoodFixed(MaxRand, boardState) require.NoError(t, err) boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food require.Equal(t, 4, len(boardState.Food)) - err = PlaceFoodFixed(boardState) + err = PlaceFoodFixed(MaxRand, boardState) require.NoError(t, err) boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food require.Equal(t, 8, len(boardState.Food)) // And now there should be no more room. - err = PlaceFoodFixed(boardState) + err = PlaceFoodFixed(MaxRand, boardState) require.Error(t, err) expectedFood := []Point{ @@ -503,18 +503,18 @@ func TestPlaceFoodFixedNoRoom_Cardinal(t *testing.T) { // There are only two possible spawn locations for each snake, // so repeat calls to place food should fail after 2 successes - err := PlaceFoodFixed(boardState) + err := PlaceFoodFixed(MaxRand, boardState) require.NoError(t, err) boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food require.Equal(t, 4, len(boardState.Food)) - err = PlaceFoodFixed(boardState) + err = PlaceFoodFixed(MaxRand, boardState) require.NoError(t, err) boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food require.Equal(t, 8, len(boardState.Food)) // And now there should be no more room. - err = PlaceFoodFixed(boardState) + err = PlaceFoodFixed(MaxRand, boardState) require.Error(t, err) expectedFood := []Point{ @@ -653,7 +653,7 @@ func TestGetUnoccupiedPoints(t *testing.T) { } for _, test := range tests { - unoccupiedPoints := getUnoccupiedPoints(test.Board, true) + unoccupiedPoints := GetUnoccupiedPoints(test.Board, true) require.Equal(t, len(test.Expected), len(unoccupiedPoints)) for i, e := range test.Expected { require.Equal(t, e, unoccupiedPoints[i]) @@ -739,7 +739,7 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) { } for _, test := range tests { - evenUnoccupiedPoints := getEvenUnoccupiedPoints(test.Board) + evenUnoccupiedPoints := GetEvenUnoccupiedPoints(test.Board) require.Equal(t, len(test.Expected), len(evenUnoccupiedPoints)) for i, e := range test.Expected { require.Equal(t, e, evenUnoccupiedPoints[i]) @@ -756,7 +756,7 @@ func TestPlaceFoodRandomly(t *testing.T) { }, } // Food should never spawn, no room - err := PlaceFoodRandomly(b, 99) + err := PlaceFoodRandomly(MaxRand, b, 99) require.NoError(t, err) require.Equal(t, len(b.Food), 0) } diff --git a/cli/commands/play.go b/cli/commands/play.go index fdc25b7..5116e98 100644 --- a/cli/commands/play.go +++ b/cli/commands/play.go @@ -231,7 +231,7 @@ func initializeBoardFromArgs(ruleset rules.Ruleset, snakeStates map[string]Snake for _, snakeState := range snakeStates { snakeIds = append(snakeIds, snakeState.ID) } - state, err := rules.CreateDefaultBoardState(Width, Height, snakeIds) + state, err := rules.CreateDefaultBoardState(rules.GlobalRand, Width, Height, snakeIds) if err != nil { log.Panic("[PANIC]: Error Initializing Board State") } diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..3116d88 --- /dev/null +++ b/constants.go @@ -0,0 +1,60 @@ +package rules + +type RulesetError string + +func (err RulesetError) Error() string { return string(err) } + +const ( + MoveUp = "up" + MoveDown = "down" + MoveRight = "right" + MoveLeft = "left" + + BoardSizeSmall = 7 + BoardSizeMedium = 11 + BoardSizeLarge = 19 + + SnakeMaxHealth = 100 + SnakeStartSize = 3 + + // Snake state constants + NotEliminated = "" + EliminatedByCollision = "snake-collision" + EliminatedBySelfCollision = "snake-self-collision" + EliminatedByOutOfHealth = "out-of-health" + EliminatedByHeadToHeadCollision = "head-collision" + EliminatedByOutOfBounds = "wall-collision" + EliminatedBySquad = "squad-eliminated" + + // Error constants + ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions") + ErrorNoRoomForSnake = RulesetError("not enough space to place snake") + ErrorNoRoomForFood = RulesetError("not enough space to place food") + ErrorNoMoveFound = RulesetError("move not provided for snake") + ErrorZeroLengthSnake = RulesetError("snake is length zero") + ErrorEmptyRegistry = RulesetError("empty registry") + ErrorNoStages = RulesetError("no stages") + ErrorStageNotFound = RulesetError("stage not found") + ErrorMapNotFound = RulesetError("map not found") + + // Ruleset / game type names + GameTypeConstrictor = "constrictor" + GameTypeRoyale = "royale" + GameTypeSolo = "solo" + GameTypeSquad = "squad" + GameTypeStandard = "standard" + GameTypeWrapped = "wrapped" + + // Game creation parameter names + ParamGameType = "name" + ParamFoodSpawnChance = "foodSpawnChance" + ParamMinimumFood = "minimumFood" + ParamHazardDamagePerTurn = "damagePerTurn" + ParamHazardMap = "hazardMap" + ParamHazardMapAuthor = "hazardMapAuthor" + ParamShrinkEveryNTurns = "shrinkEveryNTurns" + ParamAllowBodyCollisions = "allowBodyCollisions" + ParamSharedElimination = "sharedElimination" + ParamSharedHealth = "sharedHealth" + ParamSharedLength = "sharedLength" +) diff --git a/constrictor_test.go b/constrictor_test.go index 84a964e..535401e 100644 --- a/constrictor_test.go +++ b/constrictor_test.go @@ -26,7 +26,7 @@ func TestConstrictorModifyInitialBoardState(t *testing.T) { } r := ConstrictorRuleset{} for testNum, test := range tests { - state, err := CreateDefaultBoardState(test.Width, test.Height, test.IDs) + state, err := CreateDefaultBoardState(MaxRand, test.Width, test.Height, test.IDs) require.NoError(t, err) require.NotNil(t, state) state, err = r.ModifyInitialBoardState(state) diff --git a/maps/game_map.go b/maps/game_map.go new file mode 100644 index 0000000..5c80427 --- /dev/null +++ b/maps/game_map.go @@ -0,0 +1,115 @@ +package maps + +import "github.com/BattlesnakeOfficial/rules" + +type GameMap interface { + // Return a unique identifier for this map. + ID() string + + // Return non-functional metadata about this map. + Meta() Metadata + + // 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 +} + +type Metadata struct { + Name string + Author string + Description string +} + +// Editor is used by GameMap implementations to modify the board state. +type Editor interface { + // Returns a random number generator. This MUST be used for any non-deterministic behavior in a GameMap. + Random() rules.Rand + + // Clears all food from the board. + ClearFood() + + // Clears all hazards from the board. + ClearHazards() + + // Adds a food to the board. Does not check for duplicates. + AddFood(rules.Point) + + // Adds a hazard to the board. Does not check for duplicates. + AddHazard(rules.Point) + + // Removes all food from a specific tile on the board. + RemoveFood(rules.Point) + + // Removes all hazards from a specific tile on the board. + RemoveHazard(rules.Point) + + // Updates the body and health of a snake. + PlaceSnake(id string, body []rules.Point, health int32) +} + +// An Editor backed by a BoardState. +type BoardStateEditor struct { + *rules.BoardState + rand rules.Rand +} + +func NewBoardStateEditor(boardState *rules.BoardState, rand rules.Rand) *BoardStateEditor { + return &BoardStateEditor{ + BoardState: boardState, + rand: rand, + } +} + +func (editor *BoardStateEditor) Random() rules.Rand { return editor.rand } + +func (editor *BoardStateEditor) ClearFood() { + editor.Food = []rules.Point{} +} + +func (editor *BoardStateEditor) ClearHazards() { + editor.Hazards = []rules.Point{} +} + +func (editor *BoardStateEditor) AddFood(p rules.Point) { + editor.Food = append(editor.Food, rules.Point{X: p.X, Y: p.Y}) +} + +func (editor *BoardStateEditor) AddHazard(p rules.Point) { + editor.Hazards = append(editor.Hazards, rules.Point{X: p.X, Y: p.Y}) +} + +func (editor *BoardStateEditor) RemoveFood(p rules.Point) { + for index, food := range editor.Food { + if food.X == p.X && food.Y == p.Y { + editor.Food[index] = editor.Food[len(editor.Food)-1] + editor.Food = editor.Food[:len(editor.Food)-1] + } + } +} + +func (editor *BoardStateEditor) RemoveHazard(p rules.Point) { + for index, food := range editor.Hazards { + if food.X == p.X && food.Y == p.Y { + editor.Hazards[index] = editor.Hazards[len(editor.Hazards)-1] + editor.Hazards = editor.Hazards[:len(editor.Hazards)-1] + } + } +} + +func (editor *BoardStateEditor) PlaceSnake(id string, body []rules.Point, health int32) { + for index, snake := range editor.Snakes { + if snake.ID == id { + editor.Snakes[index].Body = body + editor.Snakes[index].Health = health + return + } + } + + editor.Snakes = append(editor.Snakes, rules.Snake{ + ID: id, + Health: health, + Body: body, + }) +} diff --git a/maps/game_map_test.go b/maps/game_map_test.go new file mode 100644 index 0000000..176fafe --- /dev/null +++ b/maps/game_map_test.go @@ -0,0 +1,64 @@ +package maps + +import ( + "testing" + + "github.com/BattlesnakeOfficial/rules" + "github.com/stretchr/testify/require" +) + +func TestBoardStateEditorInterface(t *testing.T) { + var _ Editor = (*BoardStateEditor)(nil) +} + +func TestBoardStateEditor(t *testing.T) { + boardState := rules.NewBoardState(11, 11) + boardState.Snakes = append(boardState.Snakes, rules.Snake{ + ID: "existing_snake", + Health: 100, + }) + + editor := BoardStateEditor{BoardState: boardState} + + editor.AddFood(rules.Point{X: 1, Y: 3}) + editor.AddFood(rules.Point{X: 3, Y: 6}) + editor.AddFood(rules.Point{X: 3, Y: 7}) + editor.RemoveFood(rules.Point{X: 3, Y: 6}) + editor.AddHazard(rules.Point{X: 1, Y: 3}) + editor.AddHazard(rules.Point{X: 3, Y: 6}) + editor.AddHazard(rules.Point{X: 3, Y: 7}) + editor.RemoveHazard(rules.Point{X: 3, Y: 6}) + 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{ + {X: 1, Y: 3}, + {X: 3, Y: 7}, + }, + Hazards: []rules.Point{ + {X: 1, Y: 3}, + {X: 3, Y: 7}, + }, + Snakes: []rules.Snake{ + { + ID: "existing_snake", + Health: 99, + Body: []rules.Point{{X: 5, Y: 2}, {X: 5, Y: 1}, {X: 5, Y: 0}}, + }, + { + ID: "new_snake", + Health: 98, + Body: []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}, + }, + }, + }, boardState) + + editor.ClearFood() + require.Equal(t, []rules.Point{}, boardState.Food) + + editor.ClearHazards() + require.Equal(t, []rules.Point{}, boardState.Hazards) +} diff --git a/maps/helpers.go b/maps/helpers.go new file mode 100644 index 0000000..8b220b0 --- /dev/null +++ b/maps/helpers.go @@ -0,0 +1,42 @@ +package maps + +import "github.com/BattlesnakeOfficial/rules" + +// SetupBoard is a shortcut for looking up a map by ID and initializing a new board state with it. +func SetupBoard(mapID string, settings rules.Settings, width, height int, snakeIDs []string) (*rules.BoardState, error) { + boardState := rules.NewBoardState(int32(width), int32(height)) + + rules.InitializeSnakes(boardState, snakeIDs) + + gameMap, err := GetMap(mapID) + if err != nil { + return nil, err + } + + editor := NewBoardStateEditor(boardState, settings.Rand()) + + err = gameMap.SetupBoard(boardState, settings, editor) + if err != nil { + return nil, err + } + + 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) + if err != nil { + return nil, err + } + + nextBoardState := previousBoardState.Clone() + editor := NewBoardStateEditor(nextBoardState, settings.Rand()) + + err = gameMap.SetupBoard(previousBoardState, settings, editor) + if err != nil { + return nil, err + } + + return nextBoardState, nil +} diff --git a/maps/registry.go b/maps/registry.go new file mode 100644 index 0000000..6a0b9c2 --- /dev/null +++ b/maps/registry.go @@ -0,0 +1,35 @@ +package maps + +import ( + "fmt" + + "github.com/BattlesnakeOfficial/rules" +) + +// MapRegistry is a mapping of map names to game maps. +type MapRegistry map[string]GameMap + +var globalRegistry = MapRegistry{} + +// RegisterMap adds a stage to the registry. +// If a map has already been registered this will panic. +func (registry MapRegistry) RegisterMap(id string, m GameMap) { + if _, ok := registry[id]; ok { + panic(fmt.Sprintf("map '%s' has already been registered", id)) + } + + registry[id] = m +} + +// GetMap returns the map associated with the given ID. +func (registry MapRegistry) GetMap(id string) (GameMap, error) { + if m, ok := registry[id]; ok { + return m, nil + } + return nil, rules.ErrorMapNotFound +} + +// GetMap returns the map associated with the given ID from the global registry. +func GetMap(id string) (GameMap, error) { + return globalRegistry.GetMap(id) +} diff --git a/maps/standard.go b/maps/standard.go new file mode 100644 index 0000000..059607d --- /dev/null +++ b/maps/standard.go @@ -0,0 +1,80 @@ +package maps + +import ( + "github.com/BattlesnakeOfficial/rules" +) + +type StandardMap struct{} + +func init() { + globalRegistry.RegisterMap("standard", StandardMap{}) +} + +func (m StandardMap) ID() string { + return "standard" +} + +func (m StandardMap) Meta() Metadata { + return Metadata{ + Name: "Standard", + Description: "Standard snake placement and food spawning", + Author: "Battlesnake", + } +} + +func (m StandardMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + snakeIDs := make([]string, 0, len(initialBoardState.Snakes)) + for _, snake := range initialBoardState.Snakes { + snakeIDs = append(snakeIDs, snake.ID) + } + + tempBoardState, err := rules.CreateDefaultBoardState(editor.Random(), initialBoardState.Width, initialBoardState.Height, snakeIDs) + if err != nil { + return err + } + + // Copy food from temp board state + for _, food := range tempBoardState.Food { + editor.AddFood(food) + } + + // Copy snakes from temp board state + for _, snake := range tempBoardState.Snakes { + editor.PlaceSnake(snake.ID, snake.Body, snake.Health) + } + + return nil +} + +func (m StandardMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { + minFood := int(settings.MinimumFood) + foodSpawnChance := int(settings.FoodSpawnChance) + numCurrentFood := len(lastBoardState.Food) + + if numCurrentFood < minFood { + placeFoodRandomly(lastBoardState, editor, minFood-numCurrentFood) + return nil + } + if foodSpawnChance > 0 && (100-editor.Random().Intn(100)) < foodSpawnChance { + placeFoodRandomly(lastBoardState, editor, 1) + return nil + } + + return nil +} + +func placeFoodRandomly(b *rules.BoardState, editor Editor, n int) { + unoccupiedPoints := rules.GetUnoccupiedPoints(b, false) + + if len(unoccupiedPoints) < n { + n = len(unoccupiedPoints) + } + + editor.Random().Shuffle(len(unoccupiedPoints), func(i int, j int) { + unoccupiedPoints[i], unoccupiedPoints[j] = unoccupiedPoints[j], unoccupiedPoints[i] + }) + + for i := 0; i < n; i++ { + editor.AddFood(unoccupiedPoints[i]) + } +} diff --git a/maps/standard_test.go b/maps/standard_test.go new file mode 100644 index 0000000..d80efb9 --- /dev/null +++ b/maps/standard_test.go @@ -0,0 +1,322 @@ +package maps + +import ( + "fmt" + "testing" + + "github.com/BattlesnakeOfficial/rules" + "github.com/stretchr/testify/require" +) + +func TestStandardMapInterface(t *testing.T) { + var _ GameMap = StandardMap{} +} + +func TestStandardMapSetupBoard(t *testing.T) { + m := StandardMap{} + settings := rules.Settings{} + + tests := []struct { + name string + initialBoardState *rules.BoardState + rand rules.Rand + + expected *rules.BoardState + err error + }{ + { + "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{}, + }, + nil, + }, + { + "not enough room for snakes 7x7", + &rules.BoardState{ + Width: 7, + Height: 7, + Snakes: generateSnakes(9), + Food: []rules.Point{}, + Hazards: []rules.Point{}, + }, + 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.MinRand, + nil, + rules.ErrorNoRoomForSnake, + }, + { + "full 11x11 min", + &rules.BoardState{ + Width: 11, + Height: 11, + Snakes: generateSnakes(8), + Food: []rules.Point{}, + Hazards: []rules.Point{}, + }, + 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: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100}, + {ID: "3", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100}, + {ID: "4", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, + {ID: "5", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, 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: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100}, + {ID: "8", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100}, + }, + Food: []rules.Point{ + {X: 0, Y: 2}, + {X: 0, Y: 4}, + {X: 0, Y: 8}, + {X: 4, Y: 0}, + {X: 4, Y: 10}, + {X: 8, Y: 0}, + {X: 10, Y: 4}, + {X: 8, Y: 10}, + {X: 5, Y: 5}, + }, + Hazards: []rules.Point{}, + }, + nil, + }, + { + "full 11x11 max", + &rules.BoardState{ + Width: 11, + Height: 11, + Snakes: generateSnakes(8), + Food: []rules.Point{}, + Hazards: []rules.Point{}, + }, + rules.MaxRand, + &rules.BoardState{ + Width: 11, + Height: 11, + Snakes: []rules.Snake{ + {ID: "1", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, 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: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100}, + {ID: "4", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100}, + {ID: "5", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100}, + {ID: "6", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, 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{ + {X: 0, Y: 6}, + {X: 2, Y: 10}, + {X: 6, Y: 0}, + {X: 6, Y: 10}, + {X: 10, Y: 2}, + {X: 10, Y: 6}, + {X: 10, Y: 8}, + {X: 2, Y: 0}, + {X: 5, Y: 5}, + }, + Hazards: []rules.Point{}, + }, + nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + nextBoardState := rules.NewBoardState(test.initialBoardState.Width, test.initialBoardState.Height) + editor := NewBoardStateEditor(nextBoardState, test.rand) + + err := m.SetupBoard(test.initialBoardState, settings, editor) + + if test.err != nil { + require.Equal(t, test.err, err) + } else { + require.Equal(t, test.expected, nextBoardState) + } + }) + } +} + +func TestStandardMapUpdateBoard(t *testing.T) { + m := StandardMap{} + + tests := []struct { + name string + initialBoardState *rules.BoardState + settings rules.Settings + rand rules.Rand + + expected *rules.BoardState + }{ + { + "empty no food", + rules.NewBoardState(2, 2), + rules.Settings{ + FoodSpawnChance: 0, + MinimumFood: 0, + }, + rules.MinRand, + &rules.BoardState{ + Width: 2, + Height: 2, + Snakes: []rules.Snake{}, + Food: []rules.Point{}, + Hazards: []rules.Point{}, + }, + }, + { + "empty MinimumFood", + rules.NewBoardState(2, 2), + rules.Settings{ + FoodSpawnChance: 0, + MinimumFood: 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{}, + }, + }, + { + "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.MinRand, + &rules.BoardState{ + Width: 2, + Height: 2, + Snakes: []rules.Snake{}, + Food: []rules.Point{{X: 0, Y: 1}, {X: 0, Y: 0}}, + Hazards: []rules.Point{}, + }, + }, + { + "empty FoodSpawnChance inactive", + rules.NewBoardState(2, 2), + rules.Settings{ + FoodSpawnChance: 50, + MinimumFood: 0, + }, + rules.MinRand, + &rules.BoardState{ + Width: 2, + Height: 2, + Snakes: []rules.Snake{}, + Food: []rules.Point{}, + Hazards: []rules.Point{}, + }, + }, + { + "empty FoodSpawnChance active", + rules.NewBoardState(2, 2), + rules.Settings{ + FoodSpawnChance: 50, + MinimumFood: 0, + }, + rules.MaxRand, + &rules.BoardState{ + Width: 2, + Height: 2, + Snakes: []rules.Snake{}, + Food: []rules.Point{{X: 0, Y: 1}}, + Hazards: []rules.Point{}, + }, + }, + { + "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.MaxRand, + &rules.BoardState{ + Width: 2, + Height: 2, + Snakes: []rules.Snake{}, + Food: []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}}, + Hazards: []rules.Point{}, + }, + }, + { + "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.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{}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + nextBoardState := test.initialBoardState.Clone() + editor := NewBoardStateEditor(nextBoardState, test.rand) + + err := m.UpdateBoard(test.initialBoardState.Clone(), test.settings, editor) + + require.NoError(t, err) + require.Equal(t, test.expected, nextBoardState) + }) + } +} + +func generateSnakes(n int) []rules.Snake { + var snakes []rules.Snake + for i := 0; i < n; i++ { + snakes = append(snakes, rules.Snake{ + ID: fmt.Sprint(i + 1), + }) + } + return snakes +} diff --git a/pipeline.go b/pipeline.go index 2086d21..711772b 100644 --- a/pipeline.go +++ b/pipeline.go @@ -2,9 +2,6 @@ package rules import "fmt" -// StageRegistry is a mapping of stage names to stage functions -type StageRegistry map[string]StageFunc - const ( StageSpawnFoodStandard = "spawn_food.standard" StageGameOverStandard = "game_over.standard" @@ -46,6 +43,17 @@ var globalRegistry = StageRegistry{ StageModifySnakesShareAttributes: ShareAttributesSquad, } +// 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) +// and an error if any errors occurred during the stage. +// +// Errors should be treated as meaning the stage failed and the board state is now invalid. +type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error) + +// StageRegistry is a mapping of stage names to stage functions +type StageRegistry map[string]StageFunc + // RegisterPipelineStage adds a stage to the registry. // If a stage has already been mapped it will be overwritten by the newly // registered function. diff --git a/rand.go b/rand.go new file mode 100644 index 0000000..9d8eb02 --- /dev/null +++ b/rand.go @@ -0,0 +1,56 @@ +package rules + +import "math/rand" + +type Rand interface { + Intn(n int) int + Shuffle(n int, swap func(i, j int)) +} + +// A Rand implementation that just uses the global math/rand generator. +var GlobalRand globalRand + +type globalRand struct{} + +func (globalRand) Intn(n int) int { + return rand.Intn(n) +} + +func (globalRand) Shuffle(n int, swap func(i, j int)) { + rand.Shuffle(n, swap) +} + +// For testing purposes + +// A Rand implementation that always returns the minimum value for any method. +var MinRand minRand + +type minRand struct{} + +func (minRand) Intn(n int) int { + return 0 +} + +func (minRand) Shuffle(n int, swap func(i, j int)) { + // no shuffling +} + +// A Rand implementation that always returns the maximum value for any method. +var MaxRand maxRand + +type maxRand struct{} + +func (maxRand) Intn(n int) int { + return n - 1 +} + +func (maxRand) Shuffle(n int, swap func(i, j int)) { + // rotate by one element so every element is moved + if n < 2 { + return + } + for i := 0; i < n-2; i++ { + swap(i, i+1) + } + swap(n-2, n-1) +} diff --git a/ruleset.go b/ruleset.go index a9af863..3f28f49 100644 --- a/ruleset.go +++ b/ruleset.go @@ -4,67 +4,66 @@ import ( "strconv" ) -type RulesetError string +type Ruleset interface { + 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. + Settings() Settings +} -func (err RulesetError) Error() string { return string(err) } +type SnakeMove struct { + ID string + Move string +} -const ( - MoveUp = "up" - MoveDown = "down" - MoveRight = "right" - MoveLeft = "left" +// 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 int32 `json:"foodSpawnChance"` + MinimumFood int32 `json:"minimumFood"` + HazardDamagePerTurn int32 `json:"hazardDamagePerTurn"` + HazardMap string `json:"hazardMap"` + HazardMapAuthor string `json:"hazardMapAuthor"` + RoyaleSettings RoyaleSettings `json:"royale"` + SquadSettings SquadSettings `json:"squad"` - BoardSizeSmall = 7 - BoardSizeMedium = 11 - BoardSizeLarge = 19 + rand Rand +} - SnakeMaxHealth = 100 - SnakeStartSize = 3 +func (settings Settings) Rand() Rand { + // Default to global random number generator if none is set. + if settings.rand == nil { + return GlobalRand + } + return settings.rand +} - // bvanvugt - TODO: Just return formatted strings instead of codes? - NotEliminated = "" - EliminatedByCollision = "snake-collision" - EliminatedBySelfCollision = "snake-self-collision" - EliminatedByOutOfHealth = "out-of-health" - EliminatedByHeadToHeadCollision = "head-collision" - EliminatedByOutOfBounds = "wall-collision" - EliminatedBySquad = "squad-eliminated" +func (settings Settings) WithRand(rand Rand) Settings { + settings.rand = rand + return settings +} - // TODO - Error consts - ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions") - ErrorNoRoomForSnake = RulesetError("not enough space to place snake") - ErrorNoRoomForFood = RulesetError("not enough space to place food") - ErrorNoMoveFound = RulesetError("move not provided for snake") - ErrorZeroLengthSnake = RulesetError("snake is length zero") - ErrorEmptyRegistry = RulesetError("empty registry") - ErrorNoStages = RulesetError("no stages") - ErrorStageNotFound = RulesetError("stage not found") +// RoyaleSettings contains settings that are specific to the "royale" game mode +type RoyaleSettings struct { + seed int64 + ShrinkEveryNTurns int32 `json:"shrinkEveryNTurns"` +} - // Ruleset / game type names - GameTypeConstrictor = "constrictor" - GameTypeRoyale = "royale" - GameTypeSolo = "solo" - GameTypeSquad = "squad" - GameTypeStandard = "standard" - GameTypeWrapped = "wrapped" - - // Game creation parameter names - ParamGameType = "name" - ParamFoodSpawnChance = "foodSpawnChance" - ParamMinimumFood = "minimumFood" - ParamHazardDamagePerTurn = "damagePerTurn" - ParamHazardMap = "hazardMap" - ParamHazardMapAuthor = "hazardMapAuthor" - ParamShrinkEveryNTurns = "shrinkEveryNTurns" - ParamAllowBodyCollisions = "allowBodyCollisions" - ParamSharedElimination = "sharedElimination" - ParamSharedHealth = "sharedHealth" - ParamSharedLength = "sharedLength" -) +// SquadSettings contains settings that are specific to the "squad" game mode +type SquadSettings struct { + squadMap map[string]string + 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 squads map[string]string // Snake ID -> Squad Name } @@ -93,12 +92,17 @@ func (rb *rulesetBuilder) WithParams(params map[string]string) *rulesetBuilder { return rb } -// WithSeed sets the seed used for randomisation by certain game modes. +// Deprecated: WithSeed sets the seed used for randomisation by certain game modes. func (rb *rulesetBuilder) WithSeed(seed int64) *rulesetBuilder { rb.seed = seed return rb } +func (rb *rulesetBuilder) WithRand(rand Rand) *rulesetBuilder { + rb.rand = rand + return rb +} + // AddSnakeToSquad adds the specified snake (by ID) to a squad with the given name. // This configuration may be ignored by game modes if they do not support squads. func (rb *rulesetBuilder) AddSnakeToSquad(snakeID, squadName string) *rulesetBuilder { @@ -185,6 +189,7 @@ func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRulese SharedHealth: paramsBool(rb.params, ParamSharedHealth, false), SharedLength: paramsBool(rb.params, ParamSharedLength, false), }, + rand: rb.rand, }, } } @@ -212,69 +217,6 @@ func paramsInt32(params map[string]string, paramName string, defaultValue int32) return defaultValue } -type Point struct { - X int32 - Y int32 -} - -type Snake struct { - ID string - Body []Point - Health int32 - EliminatedCause string - EliminatedOnTurn int32 - EliminatedBy string -} - -type SnakeMove struct { - ID string - Move string -} - -type Ruleset interface { - 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. - Settings() Settings -} - -// 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 int32 `json:"foodSpawnChance"` - MinimumFood int32 `json:"minimumFood"` - HazardDamagePerTurn int32 `json:"hazardDamagePerTurn"` - HazardMap string `json:"hazardMap"` - HazardMapAuthor string `json:"hazardMapAuthor"` - RoyaleSettings RoyaleSettings `json:"royale"` - SquadSettings SquadSettings `json:"squad"` -} - -// RoyaleSettings contains settings that are specific to the "royale" game mode -type RoyaleSettings struct { - seed int64 - ShrinkEveryNTurns int32 `json:"shrinkEveryNTurns"` -} - -// SquadSettings contains settings that are specific to the "squad" game mode -type SquadSettings struct { - squadMap map[string]string - AllowBodyCollisions bool `json:"allowBodyCollisions"` - SharedElimination bool `json:"sharedElimination"` - SharedHealth bool `json:"sharedHealth"` - SharedLength bool `json:"sharedLength"` -} - -// 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) -// and an error if any errors occurred during the stage. -// -// Errors should be treated as meaning the stage failed and the board state is now invalid. -type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error) - // PipelineRuleset groups the Pipeline and Ruleset methods. // It is intended to facilitate a transition from Ruleset legacy code to Pipeline code. type PipelineRuleset interface { diff --git a/standard.go b/standard.go index fa55b96..54ceb18 100644 --- a/standard.go +++ b/standard.go @@ -393,10 +393,10 @@ func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (boo } numCurrentFood := int32(len(b.Food)) if numCurrentFood < settings.MinimumFood { - return false, PlaceFoodRandomly(b, settings.MinimumFood-numCurrentFood) + return false, PlaceFoodRandomly(GlobalRand, b, settings.MinimumFood-numCurrentFood) } if settings.FoodSpawnChance > 0 && int32(rand.Intn(100)) < settings.FoodSpawnChance { - return false, PlaceFoodRandomly(b, 1) + return false, PlaceFoodRandomly(GlobalRand, b, 1) } return false, nil } diff --git a/standard_test.go b/standard_test.go index 52bc1ce..1c9b83e 100644 --- a/standard_test.go +++ b/standard_test.go @@ -15,7 +15,7 @@ func TestStandardRulesetInterface(t *testing.T) { func TestSanity(t *testing.T) { r := StandardRuleset{} - state, err := CreateDefaultBoardState(0, 0, []string{}) + state, err := CreateDefaultBoardState(MaxRand, 0, 0, []string{}) require.NoError(t, err) require.NotNil(t, state)