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
|
package rules
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BoardState struct {
|
type BoardState struct {
|
||||||
Turn int32
|
Turn int32
|
||||||
Height int32
|
Height int32
|
||||||
|
|
@ -13,6 +9,20 @@ type BoardState struct {
|
||||||
Hazards []Point
|
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
|
// NewBoardState returns an empty but fully initialized BoardState
|
||||||
func NewBoardState(width, height int32) *BoardState {
|
func NewBoardState(width, height int32) *BoardState {
|
||||||
return &BoardState{
|
return &BoardState{
|
||||||
|
|
@ -49,15 +59,15 @@ func (prevState *BoardState) Clone() *BoardState {
|
||||||
// "default" board state with snakes and food.
|
// "default" board state with snakes and food.
|
||||||
// In a real game, the engine may generate the board without calling this
|
// In a real game, the engine may generate the board without calling this
|
||||||
// function, or customize the results based on game-specific settings.
|
// 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)
|
initialBoardState := NewBoardState(width, height)
|
||||||
|
|
||||||
err := PlaceSnakesAutomatically(initialBoardState, snakeIDs)
|
err := PlaceSnakesAutomatically(rand, initialBoardState, snakeIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = PlaceFoodAutomatically(initialBoardState)
|
err = PlaceFoodAutomatically(rand, initialBoardState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// 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) {
|
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))
|
b.Snakes = make([]Snake, len(snakeIDs))
|
||||||
|
|
||||||
for i := 0; i < len(snakeIDs); i++ {
|
for i := 0; i < len(snakeIDs); i++ {
|
||||||
|
|
@ -116,7 +126,7 @@ func PlaceSnakesFixed(b *BoardState, snakeIDs []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PlaceSnakesRandomly(b *BoardState, snakeIDs []string) error {
|
func PlaceSnakesRandomly(rand Rand, b *BoardState, snakeIDs []string) error {
|
||||||
b.Snakes = make([]Snake, len(snakeIDs))
|
b.Snakes = make([]Snake, len(snakeIDs))
|
||||||
|
|
||||||
for i := 0; i < len(snakeIDs); i++ {
|
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++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
unoccupiedPoints := getEvenUnoccupiedPoints(b)
|
unoccupiedPoints := GetEvenUnoccupiedPoints(b)
|
||||||
if len(unoccupiedPoints) <= 0 {
|
if len(unoccupiedPoints) <= 0 {
|
||||||
return ErrorNoRoomForSnake
|
return ErrorNoRoomForSnake
|
||||||
}
|
}
|
||||||
|
|
@ -139,8 +149,30 @@ func PlaceSnakesRandomly(b *BoardState, snakeIDs []string) error {
|
||||||
return nil
|
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.
|
// PlaceSnake adds a snake to the board with the given ID and body coordinates.
|
||||||
func PlaceSnake(b *BoardState, snakeID string, body []Point) error {
|
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{
|
b.Snakes = append(b.Snakes, Snake{
|
||||||
ID: snakeID,
|
ID: snakeID,
|
||||||
Health: SnakeMaxHealth,
|
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.
|
// 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) {
|
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}
|
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
|
// 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
|
// Finally, always place 1 food in center of board for dramatic purposes
|
||||||
isCenterOccupied := true
|
isCenterOccupied := true
|
||||||
unoccupiedPoints := getUnoccupiedPoints(b, true)
|
unoccupiedPoints := GetUnoccupiedPoints(b, true)
|
||||||
for _, point := range unoccupiedPoints {
|
for _, point := range unoccupiedPoints {
|
||||||
if point == centerCoord {
|
if point == centerCoord {
|
||||||
isCenterOccupied = false
|
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
|
// 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++ {
|
for i := int32(0); i < n; i++ {
|
||||||
unoccupiedPoints := getUnoccupiedPoints(b, false)
|
unoccupiedPoints := GetUnoccupiedPoints(b, false)
|
||||||
if len(unoccupiedPoints) > 0 {
|
if len(unoccupiedPoints) > 0 {
|
||||||
newFood := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
|
newFood := unoccupiedPoints[rand.Intn(len(unoccupiedPoints))]
|
||||||
b.Food = append(b.Food, newFood)
|
b.Food = append(b.Food, newFood)
|
||||||
|
|
@ -254,9 +286,9 @@ func absInt32(n int32) int32 {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEvenUnoccupiedPoints(b *BoardState) []Point {
|
func GetEvenUnoccupiedPoints(b *BoardState) []Point {
|
||||||
// Start by getting unoccupied points
|
// Start by getting unoccupied points
|
||||||
unoccupiedPoints := getUnoccupiedPoints(b, true)
|
unoccupiedPoints := GetUnoccupiedPoints(b, true)
|
||||||
|
|
||||||
// Create a new array to hold points that are even
|
// Create a new array to hold points that are even
|
||||||
evenUnoccupiedPoints := []Point{}
|
evenUnoccupiedPoints := []Point{}
|
||||||
|
|
@ -269,7 +301,7 @@ func getEvenUnoccupiedPoints(b *BoardState) []Point {
|
||||||
return evenUnoccupiedPoints
|
return evenUnoccupiedPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUnoccupiedPoints(b *BoardState, includePossibleMoves bool) []Point {
|
func GetUnoccupiedPoints(b *BoardState, includePossibleMoves bool) []Point {
|
||||||
pointIsOccupied := map[int32]map[int32]bool{}
|
pointIsOccupied := map[int32]map[int32]bool{}
|
||||||
for _, p := range b.Food {
|
for _, p := range b.Food {
|
||||||
if _, xExists := pointIsOccupied[p.X]; !xExists {
|
if _, xExists := pointIsOccupied[p.X]; !xExists {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ func TestCreateDefaultBoardState(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for testNum, test := range tests {
|
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)
|
require.Equal(t, test.Err, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
require.Nil(t, state)
|
require.Nil(t, state)
|
||||||
|
|
@ -196,8 +196,8 @@ func TestPlaceSnakesDefault(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(fmt.Sprint(test.BoardState.Width, test.BoardState.Height, len(test.SnakeIDs)), func(t *testing.T) {
|
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))))
|
require.Equal(t, test.BoardState.Width*test.BoardState.Height, int32(len(GetUnoccupiedPoints(test.BoardState, true))))
|
||||||
err := PlaceSnakesAutomatically(test.BoardState, test.SnakeIDs)
|
err := PlaceSnakesAutomatically(MaxRand, test.BoardState, test.SnakeIDs)
|
||||||
require.Equal(t, test.Err, err, "Snakes: %d", len(test.BoardState.Snakes))
|
require.Equal(t, test.Err, err, "Snakes: %d", len(test.BoardState.Snakes))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for i := 0; i < len(test.BoardState.Snakes); i++ {
|
for i := 0; i < len(test.BoardState.Snakes); i++ {
|
||||||
|
|
@ -338,7 +338,7 @@ func TestPlaceFood(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
require.Len(t, test.BoardState.Food, 0)
|
require.Len(t, test.BoardState.Food, 0)
|
||||||
err := PlaceFoodAutomatically(test.BoardState)
|
err := PlaceFoodAutomatically(MaxRand, test.BoardState)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.ExpectedFood, len(test.BoardState.Food))
|
require.Equal(t, test.ExpectedFood, len(test.BoardState.Food))
|
||||||
for _, point := range test.BoardState.Food {
|
for _, point := range test.BoardState.Food {
|
||||||
|
|
@ -396,7 +396,7 @@ func TestPlaceFoodFixed(t *testing.T) {
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
require.Len(t, test.BoardState.Food, 0)
|
require.Len(t, test.BoardState.Food, 0)
|
||||||
|
|
||||||
err := PlaceFoodFixed(test.BoardState)
|
err := PlaceFoodFixed(MaxRand, test.BoardState)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(test.BoardState.Snakes)+1, len(test.BoardState.Food))
|
require.Equal(t, len(test.BoardState.Snakes)+1, len(test.BoardState.Food))
|
||||||
|
|
||||||
|
|
@ -444,7 +444,7 @@ func TestPlaceFoodFixedNoRoom(t *testing.T) {
|
||||||
},
|
},
|
||||||
Food: []Point{},
|
Food: []Point{},
|
||||||
}
|
}
|
||||||
err := PlaceFoodFixed(boardState)
|
err := PlaceFoodFixed(MaxRand, boardState)
|
||||||
require.Error(t, err)
|
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,
|
// There are only two possible food spawn locations for each snake,
|
||||||
// so repeat calls to place food should fail after 2 successes
|
// so repeat calls to place food should fail after 2 successes
|
||||||
err := PlaceFoodFixed(boardState)
|
err := PlaceFoodFixed(MaxRand, boardState)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
|
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
|
||||||
require.Equal(t, 4, len(boardState.Food))
|
require.Equal(t, 4, len(boardState.Food))
|
||||||
|
|
||||||
err = PlaceFoodFixed(boardState)
|
err = PlaceFoodFixed(MaxRand, boardState)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
|
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
|
||||||
require.Equal(t, 8, len(boardState.Food))
|
require.Equal(t, 8, len(boardState.Food))
|
||||||
|
|
||||||
// And now there should be no more room.
|
// And now there should be no more room.
|
||||||
err = PlaceFoodFixed(boardState)
|
err = PlaceFoodFixed(MaxRand, boardState)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
expectedFood := []Point{
|
expectedFood := []Point{
|
||||||
|
|
@ -503,18 +503,18 @@ func TestPlaceFoodFixedNoRoom_Cardinal(t *testing.T) {
|
||||||
|
|
||||||
// There are only two possible spawn locations for each snake,
|
// There are only two possible spawn locations for each snake,
|
||||||
// so repeat calls to place food should fail after 2 successes
|
// so repeat calls to place food should fail after 2 successes
|
||||||
err := PlaceFoodFixed(boardState)
|
err := PlaceFoodFixed(MaxRand, boardState)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
|
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
|
||||||
require.Equal(t, 4, len(boardState.Food))
|
require.Equal(t, 4, len(boardState.Food))
|
||||||
|
|
||||||
err = PlaceFoodFixed(boardState)
|
err = PlaceFoodFixed(MaxRand, boardState)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
|
boardState.Food = boardState.Food[:len(boardState.Food)-1] // Center food
|
||||||
require.Equal(t, 8, len(boardState.Food))
|
require.Equal(t, 8, len(boardState.Food))
|
||||||
|
|
||||||
// And now there should be no more room.
|
// And now there should be no more room.
|
||||||
err = PlaceFoodFixed(boardState)
|
err = PlaceFoodFixed(MaxRand, boardState)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
expectedFood := []Point{
|
expectedFood := []Point{
|
||||||
|
|
@ -653,7 +653,7 @@ func TestGetUnoccupiedPoints(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
unoccupiedPoints := getUnoccupiedPoints(test.Board, true)
|
unoccupiedPoints := GetUnoccupiedPoints(test.Board, true)
|
||||||
require.Equal(t, len(test.Expected), len(unoccupiedPoints))
|
require.Equal(t, len(test.Expected), len(unoccupiedPoints))
|
||||||
for i, e := range test.Expected {
|
for i, e := range test.Expected {
|
||||||
require.Equal(t, e, unoccupiedPoints[i])
|
require.Equal(t, e, unoccupiedPoints[i])
|
||||||
|
|
@ -739,7 +739,7 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
evenUnoccupiedPoints := getEvenUnoccupiedPoints(test.Board)
|
evenUnoccupiedPoints := GetEvenUnoccupiedPoints(test.Board)
|
||||||
require.Equal(t, len(test.Expected), len(evenUnoccupiedPoints))
|
require.Equal(t, len(test.Expected), len(evenUnoccupiedPoints))
|
||||||
for i, e := range test.Expected {
|
for i, e := range test.Expected {
|
||||||
require.Equal(t, e, evenUnoccupiedPoints[i])
|
require.Equal(t, e, evenUnoccupiedPoints[i])
|
||||||
|
|
@ -756,7 +756,7 @@ func TestPlaceFoodRandomly(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Food should never spawn, no room
|
// Food should never spawn, no room
|
||||||
err := PlaceFoodRandomly(b, 99)
|
err := PlaceFoodRandomly(MaxRand, b, 99)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(b.Food), 0)
|
require.Equal(t, len(b.Food), 0)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ func initializeBoardFromArgs(ruleset rules.Ruleset, snakeStates map[string]Snake
|
||||||
for _, snakeState := range snakeStates {
|
for _, snakeState := range snakeStates {
|
||||||
snakeIds = append(snakeIds, snakeState.ID)
|
snakeIds = append(snakeIds, snakeState.ID)
|
||||||
}
|
}
|
||||||
state, err := rules.CreateDefaultBoardState(Width, Height, snakeIds)
|
state, err := rules.CreateDefaultBoardState(rules.GlobalRand, Width, Height, snakeIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic("[PANIC]: Error Initializing Board State")
|
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{}
|
r := ConstrictorRuleset{}
|
||||||
for testNum, test := range tests {
|
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.NoError(t, err)
|
||||||
require.NotNil(t, state)
|
require.NotNil(t, state)
|
||||||
state, err = r.ModifyInitialBoardState(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"
|
import "fmt"
|
||||||
|
|
||||||
// StageRegistry is a mapping of stage names to stage functions
|
|
||||||
type StageRegistry map[string]StageFunc
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StageSpawnFoodStandard = "spawn_food.standard"
|
StageSpawnFoodStandard = "spawn_food.standard"
|
||||||
StageGameOverStandard = "game_over.standard"
|
StageGameOverStandard = "game_over.standard"
|
||||||
|
|
@ -46,6 +43,17 @@ var globalRegistry = StageRegistry{
|
||||||
StageModifySnakesShareAttributes: ShareAttributesSquad,
|
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.
|
// RegisterPipelineStage adds a stage to the registry.
|
||||||
// If a stage has already been mapped it will be overwritten by the newly
|
// If a stage has already been mapped it will be overwritten by the newly
|
||||||
// registered function.
|
// 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"
|
"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 (
|
// Settings contains all settings relevant to a game.
|
||||||
MoveUp = "up"
|
// It is used by game logic to take a previous game state and produce a next game state.
|
||||||
MoveDown = "down"
|
type Settings struct {
|
||||||
MoveRight = "right"
|
FoodSpawnChance int32 `json:"foodSpawnChance"`
|
||||||
MoveLeft = "left"
|
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
|
rand Rand
|
||||||
BoardSizeMedium = 11
|
}
|
||||||
BoardSizeLarge = 19
|
|
||||||
|
|
||||||
SnakeMaxHealth = 100
|
func (settings Settings) Rand() Rand {
|
||||||
SnakeStartSize = 3
|
// 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?
|
func (settings Settings) WithRand(rand Rand) Settings {
|
||||||
NotEliminated = ""
|
settings.rand = rand
|
||||||
EliminatedByCollision = "snake-collision"
|
return settings
|
||||||
EliminatedBySelfCollision = "snake-self-collision"
|
}
|
||||||
EliminatedByOutOfHealth = "out-of-health"
|
|
||||||
EliminatedByHeadToHeadCollision = "head-collision"
|
|
||||||
EliminatedByOutOfBounds = "wall-collision"
|
|
||||||
EliminatedBySquad = "squad-eliminated"
|
|
||||||
|
|
||||||
// TODO - Error consts
|
// RoyaleSettings contains settings that are specific to the "royale" game mode
|
||||||
ErrorTooManySnakes = RulesetError("too many snakes for fixed start positions")
|
type RoyaleSettings struct {
|
||||||
ErrorNoRoomForSnake = RulesetError("not enough space to place snake")
|
seed int64
|
||||||
ErrorNoRoomForFood = RulesetError("not enough space to place food")
|
ShrinkEveryNTurns int32 `json:"shrinkEveryNTurns"`
|
||||||
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")
|
|
||||||
|
|
||||||
// Ruleset / game type names
|
// SquadSettings contains settings that are specific to the "squad" game mode
|
||||||
GameTypeConstrictor = "constrictor"
|
type SquadSettings struct {
|
||||||
GameTypeRoyale = "royale"
|
squadMap map[string]string
|
||||||
GameTypeSolo = "solo"
|
AllowBodyCollisions bool `json:"allowBodyCollisions"`
|
||||||
GameTypeSquad = "squad"
|
SharedElimination bool `json:"sharedElimination"`
|
||||||
GameTypeStandard = "standard"
|
SharedHealth bool `json:"sharedHealth"`
|
||||||
GameTypeWrapped = "wrapped"
|
SharedLength bool `json:"sharedLength"`
|
||||||
|
}
|
||||||
// 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
type rulesetBuilder struct {
|
type rulesetBuilder struct {
|
||||||
params map[string]string // game customisation parameters
|
params map[string]string // game customisation parameters
|
||||||
seed int64 // used for random events in games
|
seed int64 // used for random events in games
|
||||||
|
rand Rand // used for random number generation
|
||||||
squads map[string]string // Snake ID -> Squad Name
|
squads map[string]string // Snake ID -> Squad Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,12 +92,17 @@ func (rb *rulesetBuilder) WithParams(params map[string]string) *rulesetBuilder {
|
||||||
return rb
|
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 {
|
func (rb *rulesetBuilder) WithSeed(seed int64) *rulesetBuilder {
|
||||||
rb.seed = seed
|
rb.seed = seed
|
||||||
return rb
|
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.
|
// 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.
|
// This configuration may be ignored by game modes if they do not support squads.
|
||||||
func (rb *rulesetBuilder) AddSnakeToSquad(snakeID, squadName string) *rulesetBuilder {
|
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),
|
SharedHealth: paramsBool(rb.params, ParamSharedHealth, false),
|
||||||
SharedLength: paramsBool(rb.params, ParamSharedLength, 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
|
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.
|
// PipelineRuleset groups the Pipeline and Ruleset methods.
|
||||||
// It is intended to facilitate a transition from Ruleset legacy code to Pipeline code.
|
// It is intended to facilitate a transition from Ruleset legacy code to Pipeline code.
|
||||||
type PipelineRuleset interface {
|
type PipelineRuleset interface {
|
||||||
|
|
|
||||||
|
|
@ -393,10 +393,10 @@ func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (boo
|
||||||
}
|
}
|
||||||
numCurrentFood := int32(len(b.Food))
|
numCurrentFood := int32(len(b.Food))
|
||||||
if numCurrentFood < settings.MinimumFood {
|
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 {
|
if settings.FoodSpawnChance > 0 && int32(rand.Intn(100)) < settings.FoodSpawnChance {
|
||||||
return false, PlaceFoodRandomly(b, 1)
|
return false, PlaceFoodRandomly(GlobalRand, b, 1)
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ func TestStandardRulesetInterface(t *testing.T) {
|
||||||
func TestSanity(t *testing.T) {
|
func TestSanity(t *testing.T) {
|
||||||
r := StandardRuleset{}
|
r := StandardRuleset{}
|
||||||
|
|
||||||
state, err := CreateDefaultBoardState(0, 0, []string{})
|
state, err := CreateDefaultBoardState(MaxRand, 0, 0, []string{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, state)
|
require.NotNil(t, state)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue