DEV 1303: Add empty and royale maps and update game map interface (#72)
* move random generator into Settings * add empty and royale maps * place snakes on either cardinal or corner positions first
This commit is contained in:
parent
6fa2da2f01
commit
e94d758a9b
12 changed files with 479 additions and 52 deletions
35
board.go
35
board.go
|
|
@ -1,5 +1,7 @@
|
||||||
package rules
|
package rules
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
type BoardState struct {
|
type BoardState struct {
|
||||||
Turn int32
|
Turn int32
|
||||||
Height int32
|
Height int32
|
||||||
|
|
@ -14,6 +16,11 @@ type Point struct {
|
||||||
Y int32
|
Y int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Makes it easier to copy sample points out of Go logs and test failures.
|
||||||
|
func (p Point) GoString() string {
|
||||||
|
return fmt.Sprintf("{X:%d, Y:%d}", p.X, p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
type Snake struct {
|
type Snake struct {
|
||||||
ID string
|
ID string
|
||||||
Body []Point
|
Body []Point
|
||||||
|
|
@ -95,26 +102,40 @@ func PlaceSnakesFixed(rand Rand, b *BoardState, snakeIDs []string) error {
|
||||||
|
|
||||||
// Create start 8 points
|
// Create start 8 points
|
||||||
mn, md, mx := int32(1), (b.Width-1)/2, b.Width-2
|
mn, md, mx := int32(1), (b.Width-1)/2, b.Width-2
|
||||||
startPoints := []Point{
|
cornerPoints := []Point{
|
||||||
{mn, mn},
|
{mn, mn},
|
||||||
{mn, md},
|
|
||||||
{mn, mx},
|
{mn, mx},
|
||||||
|
{mx, mn},
|
||||||
|
{mx, mx},
|
||||||
|
}
|
||||||
|
cardinalPoints := []Point{
|
||||||
|
{mn, md},
|
||||||
{md, mn},
|
{md, mn},
|
||||||
{md, mx},
|
{md, mx},
|
||||||
{mx, mn},
|
|
||||||
{mx, md},
|
{mx, md},
|
||||||
{mx, mx},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if len(b.Snakes) > len(startPoints) {
|
if len(b.Snakes) > (len(cornerPoints) + len(cardinalPoints)) {
|
||||||
return ErrorTooManySnakes
|
return ErrorTooManySnakes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Randomly order them
|
// Randomly order them
|
||||||
rand.Shuffle(len(startPoints), func(i int, j int) {
|
rand.Shuffle(len(cornerPoints), func(i int, j int) {
|
||||||
startPoints[i], startPoints[j] = startPoints[j], startPoints[i]
|
cornerPoints[i], cornerPoints[j] = cornerPoints[j], cornerPoints[i]
|
||||||
})
|
})
|
||||||
|
rand.Shuffle(len(cardinalPoints), func(i int, j int) {
|
||||||
|
cardinalPoints[i], cardinalPoints[j] = cardinalPoints[j], cardinalPoints[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
var startPoints []Point
|
||||||
|
if rand.Intn(2) == 0 {
|
||||||
|
startPoints = append(startPoints, cornerPoints...)
|
||||||
|
startPoints = append(startPoints, cardinalPoints...)
|
||||||
|
} else {
|
||||||
|
startPoints = append(startPoints, cardinalPoints...)
|
||||||
|
startPoints = append(startPoints, cornerPoints...)
|
||||||
|
}
|
||||||
|
|
||||||
// Assign to snakes in order given
|
// Assign to snakes in order given
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,64 @@ func TestPlaceSnakesDefault(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlaceSnakesFixed(t *testing.T) {
|
||||||
|
snakeIDs := make([]string, 8)
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
label string
|
||||||
|
rand Rand
|
||||||
|
expectedSnakeHeads []Point
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
label: "corners before cardinal directions",
|
||||||
|
rand: MinRand,
|
||||||
|
expectedSnakeHeads: []Point{
|
||||||
|
{X: 1, Y: 1},
|
||||||
|
{X: 1, Y: 9},
|
||||||
|
{X: 9, Y: 1},
|
||||||
|
{X: 9, Y: 9},
|
||||||
|
|
||||||
|
{X: 1, Y: 5},
|
||||||
|
{X: 5, Y: 1},
|
||||||
|
{X: 5, Y: 9},
|
||||||
|
{X: 9, Y: 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "cardinal directions before corners",
|
||||||
|
rand: MaxRand,
|
||||||
|
expectedSnakeHeads: []Point{
|
||||||
|
{X: 5, Y: 1},
|
||||||
|
{X: 5, Y: 9},
|
||||||
|
{X: 9, Y: 5},
|
||||||
|
{X: 1, Y: 5},
|
||||||
|
|
||||||
|
{X: 1, Y: 9},
|
||||||
|
{X: 9, Y: 1},
|
||||||
|
{X: 9, Y: 9},
|
||||||
|
{X: 1, Y: 1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(test.label, func(t *testing.T) {
|
||||||
|
boardState := &BoardState{
|
||||||
|
Width: BoardSizeMedium,
|
||||||
|
Height: BoardSizeMedium,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := PlaceSnakesAutomatically(test.rand, boardState, snakeIDs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var snakeHeads []Point
|
||||||
|
for _, snake := range boardState.Snakes {
|
||||||
|
require.Len(t, snake.Body, 3)
|
||||||
|
snakeHeads = append(snakeHeads, snake.Body[0])
|
||||||
|
}
|
||||||
|
require.Equalf(t, test.expectedSnakeHeads, snakeHeads, "%#v", snakeHeads)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlaceSnake(t *testing.T) {
|
func TestPlaceSnake(t *testing.T) {
|
||||||
// TODO: Should PlaceSnake check for boundaries?
|
// TODO: Should PlaceSnake check for boundaries?
|
||||||
boardState := NewBoardState(BoardSizeSmall, BoardSizeSmall)
|
boardState := NewBoardState(BoardSizeSmall, BoardSizeSmall)
|
||||||
|
|
|
||||||
49
maps/empty.go
Normal file
49
maps/empty.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
package maps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmptyMap struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
globalRegistry.RegisterMap("empty", EmptyMap{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m EmptyMap) ID() string {
|
||||||
|
return "empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m EmptyMap) Meta() Metadata {
|
||||||
|
return Metadata{
|
||||||
|
Name: "Empty",
|
||||||
|
Description: "Default snake placement with no food",
|
||||||
|
Author: "Battlesnake",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m EmptyMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
rand := settings.GetRand(0)
|
||||||
|
|
||||||
|
snakeIDs := make([]string, 0, len(initialBoardState.Snakes))
|
||||||
|
for _, snake := range initialBoardState.Snakes {
|
||||||
|
snakeIDs = append(snakeIDs, snake.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempBoardState := rules.NewBoardState(initialBoardState.Width, initialBoardState.Height)
|
||||||
|
err := rules.PlaceSnakesAutomatically(rand, tempBoardState, snakeIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy snakes from temp board state
|
||||||
|
for _, snake := range tempBoardState.Snakes {
|
||||||
|
editor.PlaceSnake(snake.ID, snake.Body, snake.Health)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m EmptyMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
164
maps/empty_test.go
Normal file
164
maps/empty_test.go
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
package maps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmptyMapInterface(t *testing.T) {
|
||||||
|
var _ GameMap = EmptyMap{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyMapSetupBoard(t *testing.T) {
|
||||||
|
m := EmptyMap{}
|
||||||
|
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{},
|
||||||
|
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: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
||||||
|
{ID: "3", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
||||||
|
{ID: "4", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
||||||
|
{ID: "5", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100},
|
||||||
|
{ID: "6", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
||||||
|
{ID: "7", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
||||||
|
{ID: "8", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||||
|
},
|
||||||
|
Food: []rules.Point{},
|
||||||
|
Hazards: []rules.Point{},
|
||||||
|
},
|
||||||
|
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: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
||||||
|
{ID: "2", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
||||||
|
{ID: "3", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||||
|
{ID: "4", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100},
|
||||||
|
{ID: "5", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
||||||
|
{ID: "6", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
||||||
|
{ID: "7", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
||||||
|
{ID: "8", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100},
|
||||||
|
},
|
||||||
|
Food: []rules.Point{},
|
||||||
|
Hazards: []rules.Point{},
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
settings := settings.WithRand(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 TestEmptyMapUpdateBoard(t *testing.T) {
|
||||||
|
m := EmptyMap{}
|
||||||
|
initialBoardState := &rules.BoardState{
|
||||||
|
Width: 2,
|
||||||
|
Height: 2,
|
||||||
|
Snakes: []rules.Snake{},
|
||||||
|
Food: []rules.Point{{X: 0, Y: 0}},
|
||||||
|
Hazards: []rules.Point{},
|
||||||
|
}
|
||||||
|
settings := rules.Settings{
|
||||||
|
FoodSpawnChance: 50,
|
||||||
|
MinimumFood: 2,
|
||||||
|
}.WithRand(rules.MaxRand)
|
||||||
|
nextBoardState := initialBoardState.Clone()
|
||||||
|
|
||||||
|
err := m.UpdateBoard(initialBoardState.Clone(), settings, NewBoardStateEditor(nextBoardState))
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, &rules.BoardState{
|
||||||
|
Width: 2,
|
||||||
|
Height: 2,
|
||||||
|
Snakes: []rules.Snake{},
|
||||||
|
Food: []rules.Point{{X: 0, Y: 0}},
|
||||||
|
Hazards: []rules.Point{},
|
||||||
|
}, nextBoardState)
|
||||||
|
}
|
||||||
|
|
@ -24,9 +24,6 @@ type Metadata struct {
|
||||||
|
|
||||||
// Editor is used by GameMap implementations to modify the board state.
|
// Editor is used by GameMap implementations to modify the board state.
|
||||||
type Editor interface {
|
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.
|
// Clears all food from the board.
|
||||||
ClearFood()
|
ClearFood()
|
||||||
|
|
||||||
|
|
@ -52,18 +49,14 @@ type Editor interface {
|
||||||
// An Editor backed by a BoardState.
|
// An Editor backed by a BoardState.
|
||||||
type BoardStateEditor struct {
|
type BoardStateEditor struct {
|
||||||
*rules.BoardState
|
*rules.BoardState
|
||||||
rand rules.Rand
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBoardStateEditor(boardState *rules.BoardState, rand rules.Rand) *BoardStateEditor {
|
func NewBoardStateEditor(boardState *rules.BoardState) *BoardStateEditor {
|
||||||
return &BoardStateEditor{
|
return &BoardStateEditor{
|
||||||
BoardState: boardState,
|
BoardState: boardState,
|
||||||
rand: rand,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (editor *BoardStateEditor) Random() rules.Rand { return editor.rand }
|
|
||||||
|
|
||||||
func (editor *BoardStateEditor) ClearFood() {
|
func (editor *BoardStateEditor) ClearFood() {
|
||||||
editor.Food = []rules.Point{}
|
editor.Food = []rules.Point{}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ func SetupBoard(mapID string, settings rules.Settings, width, height int, snakeI
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
editor := NewBoardStateEditor(boardState, settings.Rand())
|
editor := NewBoardStateEditor(boardState)
|
||||||
|
|
||||||
err = gameMap.SetupBoard(boardState, settings, editor)
|
err = gameMap.SetupBoard(boardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -31,7 +31,7 @@ func UpdateBoard(mapID string, previousBoardState *rules.BoardState, settings ru
|
||||||
}
|
}
|
||||||
|
|
||||||
nextBoardState := previousBoardState.Clone()
|
nextBoardState := previousBoardState.Clone()
|
||||||
editor := NewBoardStateEditor(nextBoardState, settings.Rand())
|
editor := NewBoardStateEditor(nextBoardState)
|
||||||
|
|
||||||
err = gameMap.UpdateBoard(previousBoardState, settings, editor)
|
err = gameMap.UpdateBoard(previousBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
74
maps/royale.go
Normal file
74
maps/royale.go
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
package maps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoyaleHazardsMap struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
globalRegistry.RegisterMap("royale", RoyaleHazardsMap{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m RoyaleHazardsMap) ID() string {
|
||||||
|
return "royale"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m RoyaleHazardsMap) Meta() Metadata {
|
||||||
|
return Metadata{
|
||||||
|
Name: "Royale",
|
||||||
|
Description: "A map where hazards are generated every N turns",
|
||||||
|
Author: "Battlesnake",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m RoyaleHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return StandardMap{}.SetupBoard(lastBoardState, settings, editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m RoyaleHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
||||||
|
turn := lastBoardState.Turn + 1
|
||||||
|
|
||||||
|
if settings.RoyaleSettings.ShrinkEveryNTurns < 1 {
|
||||||
|
return errors.New("royale game can't shrink more frequently than every turn")
|
||||||
|
}
|
||||||
|
|
||||||
|
if turn < settings.RoyaleSettings.ShrinkEveryNTurns {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset hazards every turn and re-generate them
|
||||||
|
editor.ClearHazards()
|
||||||
|
|
||||||
|
// Get random generator for turn zero, because we're regenerating all hazards every time.
|
||||||
|
randGenerator := settings.GetRand(0)
|
||||||
|
|
||||||
|
numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns
|
||||||
|
minX, maxX := int32(0), lastBoardState.Width-1
|
||||||
|
minY, maxY := int32(0), lastBoardState.Height-1
|
||||||
|
for i := int32(0); i < numShrinks; i++ {
|
||||||
|
switch randGenerator.Intn(4) {
|
||||||
|
case 0:
|
||||||
|
minX += 1
|
||||||
|
case 1:
|
||||||
|
maxX -= 1
|
||||||
|
case 2:
|
||||||
|
minY += 1
|
||||||
|
case 3:
|
||||||
|
maxY -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := int32(0); x < lastBoardState.Width; x++ {
|
||||||
|
for y := int32(0); y < lastBoardState.Height; y++ {
|
||||||
|
if x < minX || x > maxX || y < minY || y > maxY {
|
||||||
|
editor.AddHazard(rules.Point{X: x, Y: y})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -23,12 +23,14 @@ func (m StandardMap) Meta() Metadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m StandardMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m StandardMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
rand := settings.GetRand(0)
|
||||||
|
|
||||||
snakeIDs := make([]string, 0, len(initialBoardState.Snakes))
|
snakeIDs := make([]string, 0, len(initialBoardState.Snakes))
|
||||||
for _, snake := range initialBoardState.Snakes {
|
for _, snake := range initialBoardState.Snakes {
|
||||||
snakeIDs = append(snakeIDs, snake.ID)
|
snakeIDs = append(snakeIDs, snake.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
tempBoardState, err := rules.CreateDefaultBoardState(editor.Random(), initialBoardState.Width, initialBoardState.Height, snakeIDs)
|
tempBoardState, err := rules.CreateDefaultBoardState(rand, initialBoardState.Width, initialBoardState.Height, snakeIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -47,30 +49,31 @@ func (m StandardMap) SetupBoard(initialBoardState *rules.BoardState, settings ru
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m StandardMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m StandardMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
rand := settings.GetRand(lastBoardState.Turn)
|
||||||
minFood := int(settings.MinimumFood)
|
minFood := int(settings.MinimumFood)
|
||||||
foodSpawnChance := int(settings.FoodSpawnChance)
|
foodSpawnChance := int(settings.FoodSpawnChance)
|
||||||
numCurrentFood := len(lastBoardState.Food)
|
numCurrentFood := len(lastBoardState.Food)
|
||||||
|
|
||||||
if numCurrentFood < minFood {
|
if numCurrentFood < minFood {
|
||||||
placeFoodRandomly(lastBoardState, editor, minFood-numCurrentFood)
|
placeFoodRandomly(rand, lastBoardState, editor, minFood-numCurrentFood)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if foodSpawnChance > 0 && (100-editor.Random().Intn(100)) < foodSpawnChance {
|
if foodSpawnChance > 0 && (100-rand.Intn(100)) < foodSpawnChance {
|
||||||
placeFoodRandomly(lastBoardState, editor, 1)
|
placeFoodRandomly(rand, lastBoardState, editor, 1)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func placeFoodRandomly(b *rules.BoardState, editor Editor, n int) {
|
func placeFoodRandomly(rand rules.Rand, b *rules.BoardState, editor Editor, n int) {
|
||||||
unoccupiedPoints := rules.GetUnoccupiedPoints(b, false)
|
unoccupiedPoints := rules.GetUnoccupiedPoints(b, false)
|
||||||
|
|
||||||
if len(unoccupiedPoints) < n {
|
if len(unoccupiedPoints) < n {
|
||||||
n = len(unoccupiedPoints)
|
n = len(unoccupiedPoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.Random().Shuffle(len(unoccupiedPoints), func(i int, j int) {
|
rand.Shuffle(len(unoccupiedPoints), func(i int, j int) {
|
||||||
unoccupiedPoints[i], unoccupiedPoints[j] = unoccupiedPoints[j], unoccupiedPoints[i]
|
unoccupiedPoints[i], unoccupiedPoints[j] = unoccupiedPoints[j], unoccupiedPoints[i]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,23 +78,23 @@ func TestStandardMapSetupBoard(t *testing.T) {
|
||||||
Height: 11,
|
Height: 11,
|
||||||
Snakes: []rules.Snake{
|
Snakes: []rules.Snake{
|
||||||
{ID: "1", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100},
|
{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: "2", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
||||||
{ID: "3", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
{ID: "3", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
||||||
{ID: "4", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
{ID: "4", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
||||||
{ID: "5", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
{ID: "5", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100},
|
||||||
{ID: "6", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
{ID: "6", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
||||||
{ID: "7", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
{ID: "7", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
||||||
{ID: "8", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
{ID: "8", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||||
},
|
},
|
||||||
Food: []rules.Point{
|
Food: []rules.Point{
|
||||||
{X: 0, Y: 2},
|
{X: 0, Y: 2},
|
||||||
{X: 0, Y: 4},
|
|
||||||
{X: 0, Y: 8},
|
{X: 0, Y: 8},
|
||||||
|
{X: 8, Y: 0},
|
||||||
|
{X: 8, Y: 10},
|
||||||
|
{X: 0, Y: 4},
|
||||||
{X: 4, Y: 0},
|
{X: 4, Y: 0},
|
||||||
{X: 4, Y: 10},
|
{X: 4, Y: 10},
|
||||||
{X: 8, Y: 0},
|
|
||||||
{X: 10, Y: 4},
|
{X: 10, Y: 4},
|
||||||
{X: 8, Y: 10},
|
|
||||||
{X: 5, Y: 5},
|
{X: 5, Y: 5},
|
||||||
},
|
},
|
||||||
Hazards: []rules.Point{},
|
Hazards: []rules.Point{},
|
||||||
|
|
@ -115,22 +115,22 @@ func TestStandardMapSetupBoard(t *testing.T) {
|
||||||
Width: 11,
|
Width: 11,
|
||||||
Height: 11,
|
Height: 11,
|
||||||
Snakes: []rules.Snake{
|
Snakes: []rules.Snake{
|
||||||
{ID: "1", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100},
|
{ID: "1", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
||||||
{ID: "2", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
{ID: "2", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
||||||
{ID: "3", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
{ID: "3", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||||
{ID: "4", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
{ID: "4", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100},
|
||||||
{ID: "5", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
{ID: "5", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
||||||
{ID: "6", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
{ID: "6", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
||||||
{ID: "7", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
{ID: "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},
|
{ID: "8", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100},
|
||||||
},
|
},
|
||||||
Food: []rules.Point{
|
Food: []rules.Point{
|
||||||
{X: 0, Y: 6},
|
|
||||||
{X: 2, Y: 10},
|
|
||||||
{X: 6, Y: 0},
|
{X: 6, Y: 0},
|
||||||
{X: 6, Y: 10},
|
{X: 6, Y: 10},
|
||||||
{X: 10, Y: 2},
|
|
||||||
{X: 10, Y: 6},
|
{X: 10, Y: 6},
|
||||||
|
{X: 0, Y: 6},
|
||||||
|
{X: 2, Y: 10},
|
||||||
|
{X: 10, Y: 2},
|
||||||
{X: 10, Y: 8},
|
{X: 10, Y: 8},
|
||||||
{X: 2, Y: 0},
|
{X: 2, Y: 0},
|
||||||
{X: 5, Y: 5},
|
{X: 5, Y: 5},
|
||||||
|
|
@ -143,14 +143,15 @@ func TestStandardMapSetupBoard(t *testing.T) {
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
nextBoardState := rules.NewBoardState(test.initialBoardState.Width, test.initialBoardState.Height)
|
nextBoardState := rules.NewBoardState(test.initialBoardState.Width, test.initialBoardState.Height)
|
||||||
editor := NewBoardStateEditor(nextBoardState, test.rand)
|
editor := NewBoardStateEditor(nextBoardState)
|
||||||
|
settings := settings.WithRand(test.rand)
|
||||||
|
|
||||||
err := m.SetupBoard(test.initialBoardState, settings, editor)
|
err := m.SetupBoard(test.initialBoardState, settings, editor)
|
||||||
|
|
||||||
if test.err != nil {
|
if test.err != nil {
|
||||||
require.Equal(t, test.err, err)
|
require.Equal(t, test.err, err)
|
||||||
} else {
|
} else {
|
||||||
require.Equal(t, test.expected, nextBoardState)
|
require.Equalf(t, test.expected, nextBoardState, "%#v", nextBoardState.Food)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -301,9 +302,10 @@ func TestStandardMapUpdateBoard(t *testing.T) {
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
nextBoardState := test.initialBoardState.Clone()
|
nextBoardState := test.initialBoardState.Clone()
|
||||||
editor := NewBoardStateEditor(nextBoardState, test.rand)
|
settings := test.settings.WithRand(test.rand)
|
||||||
|
editor := NewBoardStateEditor(nextBoardState)
|
||||||
|
|
||||||
err := m.UpdateBoard(test.initialBoardState.Clone(), test.settings, editor)
|
err := m.UpdateBoard(test.initialBoardState.Clone(), settings, editor)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.expected, nextBoardState)
|
require.Equal(t, test.expected, nextBoardState)
|
||||||
|
|
|
||||||
20
rand.go
20
rand.go
|
|
@ -20,6 +20,26 @@ func (globalRand) Shuffle(n int, swap func(i, j int)) {
|
||||||
rand.Shuffle(n, swap)
|
rand.Shuffle(n, swap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type seedRand struct {
|
||||||
|
seed int64
|
||||||
|
rand *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSeedRand(seed int64) *seedRand {
|
||||||
|
return &seedRand{
|
||||||
|
seed: seed,
|
||||||
|
rand: rand.New(rand.NewSource(seed)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s seedRand) Intn(n int) int {
|
||||||
|
return s.rand.Intn(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s seedRand) Shuffle(n int, swap func(i, j int)) {
|
||||||
|
s.rand.Shuffle(n, swap)
|
||||||
|
}
|
||||||
|
|
||||||
// For testing purposes
|
// For testing purposes
|
||||||
|
|
||||||
// A Rand implementation that always returns the minimum value for any method.
|
// A Rand implementation that always returns the minimum value for any method.
|
||||||
|
|
|
||||||
32
ruleset.go
32
ruleset.go
|
|
@ -30,21 +30,38 @@ type Settings struct {
|
||||||
SquadSettings SquadSettings `json:"squad"`
|
SquadSettings SquadSettings `json:"squad"`
|
||||||
|
|
||||||
rand Rand
|
rand Rand
|
||||||
|
seed int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (settings Settings) Rand() Rand {
|
// Get a random number generator initialized based on the seed and current turn.
|
||||||
// Default to global random number generator if none is set.
|
func (settings Settings) GetRand(turn int32) Rand {
|
||||||
if settings.rand == nil {
|
// Allow overriding the random generator for testing
|
||||||
return GlobalRand
|
if settings.rand != nil {
|
||||||
}
|
|
||||||
return settings.rand
|
return settings.rand
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings.seed != 0 {
|
||||||
|
return NewSeedRand(settings.seed + int64(turn+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to global random number generator if neither seed or rand are set.
|
||||||
|
return GlobalRand
|
||||||
|
}
|
||||||
|
|
||||||
func (settings Settings) WithRand(rand Rand) Settings {
|
func (settings Settings) WithRand(rand Rand) Settings {
|
||||||
settings.rand = rand
|
settings.rand = rand
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (settings Settings) Seed() int64 {
|
||||||
|
return settings.seed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings Settings) WithSeed(seed int64) Settings {
|
||||||
|
settings.seed = seed
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
// RoyaleSettings contains settings that are specific to the "royale" game mode
|
// RoyaleSettings contains settings that are specific to the "royale" game mode
|
||||||
type RoyaleSettings struct {
|
type RoyaleSettings struct {
|
||||||
seed int64
|
seed int64
|
||||||
|
|
@ -92,12 +109,14 @@ func (rb *rulesetBuilder) WithParams(params map[string]string) *rulesetBuilder {
|
||||||
return rb
|
return rb
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: WithSeed sets the seed used for randomisation by certain game modes.
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithRandom overrides the random number generator with a specific instance
|
||||||
|
// instead of a Rand initialized from the seed.
|
||||||
func (rb *rulesetBuilder) WithRand(rand Rand) *rulesetBuilder {
|
func (rb *rulesetBuilder) WithRand(rand Rand) *rulesetBuilder {
|
||||||
rb.rand = rand
|
rb.rand = rand
|
||||||
return rb
|
return rb
|
||||||
|
|
@ -190,6 +209,7 @@ func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRulese
|
||||||
SharedLength: paramsBool(rb.params, ParamSharedLength, false),
|
SharedLength: paramsBool(rb.params, ParamSharedLength, false),
|
||||||
},
|
},
|
||||||
rand: rb.rand,
|
rand: rb.rand,
|
||||||
|
seed: rb.seed,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,3 +191,26 @@ func TestStageFuncContract(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, ended)
|
require.True(t, ended)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRulesetBuilderGetRand(t *testing.T) {
|
||||||
|
var seed int64 = 12345
|
||||||
|
var turn int32 = 5
|
||||||
|
ruleset := rules.NewRulesetBuilder().WithSeed(seed).PipelineRuleset("example", rules.NewPipeline(rules.StageGameOverStandard))
|
||||||
|
|
||||||
|
rand1 := ruleset.Settings().GetRand(turn)
|
||||||
|
|
||||||
|
// Should produce a predictable series of numbers based on a seed
|
||||||
|
require.Equal(t, 80, rand1.Intn(100))
|
||||||
|
require.Equal(t, 94, rand1.Intn(100))
|
||||||
|
|
||||||
|
// Should produce the same number if re-initialized
|
||||||
|
require.Equal(
|
||||||
|
t,
|
||||||
|
ruleset.Settings().GetRand(turn).Intn(100),
|
||||||
|
ruleset.Settings().GetRand(turn).Intn(100),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Should produce a different series of numbers for another turn
|
||||||
|
require.Equal(t, 22, rand1.Intn(100))
|
||||||
|
require.Equal(t, 16, rand1.Intn(100))
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue