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
This commit is contained in:
parent
1c3f434841
commit
dab9178a55
16 changed files with 916 additions and 160 deletions
78
board.go
78
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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
60
constants.go
Normal file
60
constants.go
Normal file
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
115
maps/game_map.go
Normal file
115
maps/game_map.go
Normal file
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
64
maps/game_map_test.go
Normal file
64
maps/game_map_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
42
maps/helpers.go
Normal file
42
maps/helpers.go
Normal file
|
|
@ -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
|
||||
}
|
||||
35
maps/registry.go
Normal file
35
maps/registry.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
80
maps/standard.go
Normal file
80
maps/standard.go
Normal file
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
322
maps/standard_test.go
Normal file
322
maps/standard_test.go
Normal file
|
|
@ -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
|
||||
}
|
||||
14
pipeline.go
14
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.
|
||||
|
|
|
|||
56
rand.go
Normal file
56
rand.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
170
ruleset.go
170
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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue