DEV-1761: New rules API (#118)
* DEV-1761: Clean up Ruleset interface (#115) * remove legacy ruleset types and simplify ruleset interface * remove unnecessary settings argument from Ruleset interface * decouple rules.Settings from client API and store settings as strings * DEV 1761: Add new BoardState and Point fields (#117) * add Point.TTL, Point.Value, GameState and PointState to BoardState * allow maps to access BoardState.GameState,PointState * add PreUpdateBoard and refactor snail_mode with it * fix bug where an extra turn was printed to the console * fix formatting * fix lint errors Co-authored-by: JonathanArns <jonathan.arns@googlemail.com>
This commit is contained in:
parent
639362ef46
commit
82e1999126
50 changed files with 1349 additions and 1610 deletions
92
board.go
92
board.go
|
|
@ -2,6 +2,9 @@ package rules
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
|
// BoardState represents the internal state of a game board.
|
||||||
|
// NOTE: use NewBoardState to construct these to ensure fields are initialized
|
||||||
|
// correctly and that tests are resilient to changes to this type.
|
||||||
type BoardState struct {
|
type BoardState struct {
|
||||||
Turn int
|
Turn int
|
||||||
Height int
|
Height int
|
||||||
|
|
@ -9,15 +12,26 @@ type BoardState struct {
|
||||||
Food []Point
|
Food []Point
|
||||||
Snakes []Snake
|
Snakes []Snake
|
||||||
Hazards []Point
|
Hazards []Point
|
||||||
|
|
||||||
|
// Generic game-level state for maps and rules stages to persist data between turns.
|
||||||
|
GameState map[string]string
|
||||||
|
|
||||||
|
// Numeric state keyed to specific points, also persisted between turns.
|
||||||
|
PointState map[Point]int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Point struct {
|
type Point struct {
|
||||||
X int
|
X int `json:"X"`
|
||||||
Y int
|
Y int `json:"Y"`
|
||||||
|
TTL int `json:"TTL,omitempty"`
|
||||||
|
Value int `json:"Value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Makes it easier to copy sample points out of Go logs and test failures.
|
// Makes it easier to copy sample points out of Go logs and test failures.
|
||||||
func (p Point) GoString() string {
|
func (p Point) GoString() string {
|
||||||
|
if p.TTL != 0 || p.Value != 0 {
|
||||||
|
return fmt.Sprintf("{X:%d, Y:%d, TTL:%d, Value:%d}", p.X, p.Y, p.TTL, p.Value)
|
||||||
|
}
|
||||||
return fmt.Sprintf("{X:%d, Y:%d}", p.X, p.Y)
|
return fmt.Sprintf("{X:%d, Y:%d}", p.X, p.Y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +53,8 @@ func NewBoardState(width, height int) *BoardState {
|
||||||
Food: []Point{},
|
Food: []Point{},
|
||||||
Snakes: []Snake{},
|
Snakes: []Snake{},
|
||||||
Hazards: []Point{},
|
Hazards: []Point{},
|
||||||
|
GameState: map[string]string{},
|
||||||
|
PointState: map[Point]int{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,6 +67,14 @@ func (prevState *BoardState) Clone() *BoardState {
|
||||||
Food: append([]Point{}, prevState.Food...),
|
Food: append([]Point{}, prevState.Food...),
|
||||||
Snakes: make([]Snake, len(prevState.Snakes)),
|
Snakes: make([]Snake, len(prevState.Snakes)),
|
||||||
Hazards: append([]Point{}, prevState.Hazards...),
|
Hazards: append([]Point{}, prevState.Hazards...),
|
||||||
|
GameState: make(map[string]string, len(prevState.GameState)),
|
||||||
|
PointState: make(map[Point]int, len(prevState.PointState)),
|
||||||
|
}
|
||||||
|
for key, value := range prevState.GameState {
|
||||||
|
nextState.GameState[key] = value
|
||||||
|
}
|
||||||
|
for key, value := range prevState.PointState {
|
||||||
|
nextState.PointState[key] = value
|
||||||
}
|
}
|
||||||
for i := 0; i < len(prevState.Snakes); i++ {
|
for i := 0; i < len(prevState.Snakes); i++ {
|
||||||
nextState.Snakes[i].ID = prevState.Snakes[i].ID
|
nextState.Snakes[i].ID = prevState.Snakes[i].ID
|
||||||
|
|
@ -63,6 +87,42 @@ func (prevState *BoardState) Clone() *BoardState {
|
||||||
return nextState
|
return nextState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Builder method to set Turn and return the modified BoardState.
|
||||||
|
func (state *BoardState) WithTurn(turn int) *BoardState {
|
||||||
|
state.Turn = turn
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder method to set Food and return the modified BoardState.
|
||||||
|
func (state *BoardState) WithFood(food []Point) *BoardState {
|
||||||
|
state.Food = food
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder method to set Hazards and return the modified BoardState.
|
||||||
|
func (state *BoardState) WithHazards(hazards []Point) *BoardState {
|
||||||
|
state.Hazards = hazards
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder method to set Snakes and return the modified BoardState.
|
||||||
|
func (state *BoardState) WithSnakes(snakes []Snake) *BoardState {
|
||||||
|
state.Snakes = snakes
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder method to set State and return the modified BoardState.
|
||||||
|
func (state *BoardState) WithGameState(gameState map[string]string) *BoardState {
|
||||||
|
state.GameState = gameState
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder method to set PointState and return the modified BoardState.
|
||||||
|
func (state *BoardState) WithPointState(pointState map[Point]int) *BoardState {
|
||||||
|
state.PointState = pointState
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
// CreateDefaultBoardState is a convenience function for fully initializing a
|
// CreateDefaultBoardState is a convenience function for fully initializing a
|
||||||
// "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
|
||||||
|
|
@ -120,16 +180,16 @@ func PlaceSnakesFixed(rand Rand, b *BoardState, snakeIDs []string) error {
|
||||||
// Create start 8 points
|
// Create start 8 points
|
||||||
mn, md, mx := 1, (b.Width-1)/2, b.Width-2
|
mn, md, mx := 1, (b.Width-1)/2, b.Width-2
|
||||||
cornerPoints := []Point{
|
cornerPoints := []Point{
|
||||||
{mn, mn},
|
{X: mn, Y: mn},
|
||||||
{mn, mx},
|
{X: mn, Y: mx},
|
||||||
{mx, mn},
|
{X: mx, Y: mn},
|
||||||
{mx, mx},
|
{X: mx, Y: mx},
|
||||||
}
|
}
|
||||||
cardinalPoints := []Point{
|
cardinalPoints := []Point{
|
||||||
{mn, md},
|
{X: mn, Y: md},
|
||||||
{md, mn},
|
{X: md, Y: mn},
|
||||||
{md, mx},
|
{X: md, Y: mx},
|
||||||
{mx, md},
|
{X: mx, Y: md},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check
|
// Sanity check
|
||||||
|
|
@ -325,7 +385,7 @@ func PlaceFoodAutomatically(rand Rand, b *BoardState) error {
|
||||||
|
|
||||||
// Deprecated: will be replaced by maps.PlaceFoodFixed
|
// Deprecated: will be replaced by maps.PlaceFoodFixed
|
||||||
func PlaceFoodFixed(rand Rand, b *BoardState) error {
|
func PlaceFoodFixed(rand Rand, b *BoardState) error {
|
||||||
centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2}
|
centerCoord := Point{X: (b.Width - 1) / 2, Y: (b.Height - 1) / 2}
|
||||||
|
|
||||||
isSmallBoard := b.Width*b.Height < BoardSizeMedium*BoardSizeMedium
|
isSmallBoard := b.Width*b.Height < BoardSizeMedium*BoardSizeMedium
|
||||||
// Up to 4 snakes can be placed such that food is nearby on small boards.
|
// Up to 4 snakes can be placed such that food is nearby on small boards.
|
||||||
|
|
@ -335,10 +395,10 @@ func PlaceFoodFixed(rand Rand, b *BoardState) error {
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
snakeHead := b.Snakes[i].Body[0]
|
snakeHead := b.Snakes[i].Body[0]
|
||||||
possibleFoodLocations := []Point{
|
possibleFoodLocations := []Point{
|
||||||
{snakeHead.X - 1, snakeHead.Y - 1},
|
{X: snakeHead.X - 1, Y: snakeHead.Y - 1},
|
||||||
{snakeHead.X - 1, snakeHead.Y + 1},
|
{X: snakeHead.X - 1, Y: snakeHead.Y + 1},
|
||||||
{snakeHead.X + 1, snakeHead.Y - 1},
|
{X: snakeHead.X + 1, Y: snakeHead.Y - 1},
|
||||||
{snakeHead.X + 1, snakeHead.Y + 1},
|
{X: snakeHead.X + 1, Y: snakeHead.Y + 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any invalid/unwanted positions
|
// Remove any invalid/unwanted positions
|
||||||
|
|
@ -448,7 +508,7 @@ func GetEvenUnoccupiedPoints(b *BoardState) []Point {
|
||||||
|
|
||||||
// removeCenterCoord filters out the board's center point from a list of points.
|
// removeCenterCoord filters out the board's center point from a list of points.
|
||||||
func removeCenterCoord(b *BoardState, points []Point) []Point {
|
func removeCenterCoord(b *BoardState, points []Point) []Point {
|
||||||
centerCoord := Point{(b.Width - 1) / 2, (b.Height - 1) / 2}
|
centerCoord := Point{X: (b.Width - 1) / 2, Y: (b.Height - 1) / 2}
|
||||||
var noCenterPoints []Point
|
var noCenterPoints []Point
|
||||||
for _, p := range points {
|
for _, p := range points {
|
||||||
if p != centerCoord {
|
if p != centerCoord {
|
||||||
|
|
|
||||||
228
board_test.go
228
board_test.go
|
|
@ -8,6 +8,30 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestBoardStateClone(t *testing.T) {
|
||||||
|
empty := &BoardState{}
|
||||||
|
require.Equal(t, NewBoardState(0, 0), empty.Clone())
|
||||||
|
|
||||||
|
full := NewBoardState(11, 11).
|
||||||
|
WithTurn(99).
|
||||||
|
WithFood([]Point{{X: 1, Y: 2, TTL: 10, Value: 100}}).
|
||||||
|
WithHazards([]Point{{X: 3, Y: 4, TTL: 5, Value: 50}}).
|
||||||
|
WithSnakes([]Snake{
|
||||||
|
{
|
||||||
|
ID: "1",
|
||||||
|
Body: []Point{{X: 1, Y: 2}},
|
||||||
|
Health: 99,
|
||||||
|
EliminatedCause: EliminatedByCollision,
|
||||||
|
EliminatedOnTurn: 45,
|
||||||
|
EliminatedBy: "2",
|
||||||
|
},
|
||||||
|
}).
|
||||||
|
WithGameState(map[string]string{"example": "game data"}).
|
||||||
|
WithPointState(map[Point]int{{X: 1, Y: 1}: 42})
|
||||||
|
|
||||||
|
require.Equal(t, full, full.Clone())
|
||||||
|
}
|
||||||
|
|
||||||
func TestDev1235(t *testing.T) {
|
func TestDev1235(t *testing.T) {
|
||||||
// Small boards should no longer error and only get 1 food when num snakes > 4
|
// Small boards should no longer error and only get 1 food when num snakes > 4
|
||||||
state, err := CreateDefaultBoardState(MaxRand, BoardSizeSmall, BoardSizeSmall, []string{
|
state, err := CreateDefaultBoardState(MaxRand, BoardSizeSmall, BoardSizeSmall, []string{
|
||||||
|
|
@ -346,23 +370,23 @@ func TestPlaceSnake(t *testing.T) {
|
||||||
boardState := NewBoardState(BoardSizeSmall, BoardSizeSmall)
|
boardState := NewBoardState(BoardSizeSmall, BoardSizeSmall)
|
||||||
require.Empty(t, boardState.Snakes)
|
require.Empty(t, boardState.Snakes)
|
||||||
|
|
||||||
_ = PlaceSnake(boardState, "a", []Point{{0, 0}, {1, 0}, {1, 1}})
|
_ = PlaceSnake(boardState, "a", []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}})
|
||||||
|
|
||||||
require.Len(t, boardState.Snakes, 1)
|
require.Len(t, boardState.Snakes, 1)
|
||||||
require.Equal(t, Snake{
|
require.Equal(t, Snake{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
Body: []Point{{0, 0}, {1, 0}, {1, 1}},
|
Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}},
|
||||||
Health: SnakeMaxHealth,
|
Health: SnakeMaxHealth,
|
||||||
EliminatedCause: NotEliminated,
|
EliminatedCause: NotEliminated,
|
||||||
EliminatedBy: "",
|
EliminatedBy: "",
|
||||||
}, boardState.Snakes[0])
|
}, boardState.Snakes[0])
|
||||||
|
|
||||||
_ = PlaceSnake(boardState, "b", []Point{{0, 2}, {1, 2}, {3, 2}})
|
_ = PlaceSnake(boardState, "b", []Point{{X: 0, Y: 2}, {X: 1, Y: 2}, {X: 3, Y: 2}})
|
||||||
|
|
||||||
require.Len(t, boardState.Snakes, 2)
|
require.Len(t, boardState.Snakes, 2)
|
||||||
require.Equal(t, Snake{
|
require.Equal(t, Snake{
|
||||||
ID: "b",
|
ID: "b",
|
||||||
Body: []Point{{0, 2}, {1, 2}, {3, 2}},
|
Body: []Point{{X: 0, Y: 2}, {X: 1, Y: 2}, {X: 3, Y: 2}},
|
||||||
Health: SnakeMaxHealth,
|
Health: SnakeMaxHealth,
|
||||||
EliminatedCause: NotEliminated,
|
EliminatedCause: NotEliminated,
|
||||||
EliminatedBy: "",
|
EliminatedBy: "",
|
||||||
|
|
@ -411,9 +435,9 @@ func TestPlaceFood(t *testing.T) {
|
||||||
Width: BoardSizeSmall,
|
Width: BoardSizeSmall,
|
||||||
Height: BoardSizeSmall,
|
Height: BoardSizeSmall,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{5, 1}}},
|
{Body: []Point{{X: 5, Y: 1}}},
|
||||||
{Body: []Point{{5, 3}}},
|
{Body: []Point{{X: 5, Y: 3}}},
|
||||||
{Body: []Point{{5, 5}}},
|
{Body: []Point{{X: 5, Y: 5}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
4, // +1 because of fixed spawn locations
|
4, // +1 because of fixed spawn locations
|
||||||
|
|
@ -423,14 +447,14 @@ func TestPlaceFood(t *testing.T) {
|
||||||
Width: BoardSizeMedium,
|
Width: BoardSizeMedium,
|
||||||
Height: BoardSizeMedium,
|
Height: BoardSizeMedium,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{1, 1}}},
|
{Body: []Point{{X: 1, Y: 1}}},
|
||||||
{Body: []Point{{1, 5}}},
|
{Body: []Point{{X: 1, Y: 5}}},
|
||||||
{Body: []Point{{1, 9}}},
|
{Body: []Point{{X: 1, Y: 9}}},
|
||||||
{Body: []Point{{5, 1}}},
|
{Body: []Point{{X: 5, Y: 1}}},
|
||||||
{Body: []Point{{5, 9}}},
|
{Body: []Point{{X: 5, Y: 9}}},
|
||||||
{Body: []Point{{9, 1}}},
|
{Body: []Point{{X: 9, Y: 1}}},
|
||||||
{Body: []Point{{9, 5}}},
|
{Body: []Point{{X: 9, Y: 5}}},
|
||||||
{Body: []Point{{9, 9}}},
|
{Body: []Point{{X: 9, Y: 9}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
9, // +1 because of fixed spawn locations
|
9, // +1 because of fixed spawn locations
|
||||||
|
|
@ -440,12 +464,12 @@ func TestPlaceFood(t *testing.T) {
|
||||||
Width: BoardSizeLarge,
|
Width: BoardSizeLarge,
|
||||||
Height: BoardSizeLarge,
|
Height: BoardSizeLarge,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{1, 1}}},
|
{Body: []Point{{X: 1, Y: 1}}},
|
||||||
{Body: []Point{{1, 9}}},
|
{Body: []Point{{X: 1, Y: 9}}},
|
||||||
{Body: []Point{{1, 17}}},
|
{Body: []Point{{X: 1, Y: 17}}},
|
||||||
{Body: []Point{{17, 1}}},
|
{Body: []Point{{X: 17, Y: 1}}},
|
||||||
{Body: []Point{{17, 9}}},
|
{Body: []Point{{X: 17, Y: 9}}},
|
||||||
{Body: []Point{{17, 17}}},
|
{Body: []Point{{X: 17, Y: 17}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
7, // +1 because of fixed spawn locations
|
7, // +1 because of fixed spawn locations
|
||||||
|
|
@ -478,7 +502,7 @@ func TestPlaceFoodFixed(t *testing.T) {
|
||||||
Width: BoardSizeSmall,
|
Width: BoardSizeSmall,
|
||||||
Height: BoardSizeSmall,
|
Height: BoardSizeSmall,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{1, 3}}},
|
{Body: []Point{{X: 1, Y: 3}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -487,10 +511,10 @@ func TestPlaceFoodFixed(t *testing.T) {
|
||||||
Width: BoardSizeMedium,
|
Width: BoardSizeMedium,
|
||||||
Height: BoardSizeMedium,
|
Height: BoardSizeMedium,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{1, 1}}},
|
{Body: []Point{{X: 1, Y: 1}}},
|
||||||
{Body: []Point{{1, 5}}},
|
{Body: []Point{{X: 1, Y: 5}}},
|
||||||
{Body: []Point{{9, 5}}},
|
{Body: []Point{{X: 9, Y: 5}}},
|
||||||
{Body: []Point{{9, 9}}},
|
{Body: []Point{{X: 9, Y: 9}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -499,14 +523,14 @@ func TestPlaceFoodFixed(t *testing.T) {
|
||||||
Width: BoardSizeLarge,
|
Width: BoardSizeLarge,
|
||||||
Height: BoardSizeLarge,
|
Height: BoardSizeLarge,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{1, 1}}},
|
{Body: []Point{{X: 1, Y: 1}}},
|
||||||
{Body: []Point{{1, 9}}},
|
{Body: []Point{{X: 1, Y: 9}}},
|
||||||
{Body: []Point{{1, 17}}},
|
{Body: []Point{{X: 1, Y: 17}}},
|
||||||
{Body: []Point{{9, 1}}},
|
{Body: []Point{{X: 9, Y: 1}}},
|
||||||
{Body: []Point{{9, 17}}},
|
{Body: []Point{{X: 9, Y: 17}}},
|
||||||
{Body: []Point{{17, 1}}},
|
{Body: []Point{{X: 17, Y: 1}}},
|
||||||
{Body: []Point{{17, 9}}},
|
{Body: []Point{{X: 17, Y: 9}}},
|
||||||
{Body: []Point{{17, 17}}},
|
{Body: []Point{{X: 17, Y: 17}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -519,16 +543,16 @@ func TestPlaceFoodFixed(t *testing.T) {
|
||||||
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))
|
||||||
|
|
||||||
midPoint := Point{(test.BoardState.Width - 1) / 2, (test.BoardState.Height - 1) / 2}
|
midPoint := Point{X: (test.BoardState.Width - 1) / 2, Y: (test.BoardState.Height - 1) / 2}
|
||||||
|
|
||||||
// Make sure every snake has food within 2 moves of it
|
// Make sure every snake has food within 2 moves of it
|
||||||
for _, snake := range test.BoardState.Snakes {
|
for _, snake := range test.BoardState.Snakes {
|
||||||
head := snake.Body[0]
|
head := snake.Body[0]
|
||||||
|
|
||||||
bottomLeft := Point{head.X - 1, head.Y - 1}
|
bottomLeft := Point{X: head.X - 1, Y: head.Y - 1}
|
||||||
topLeft := Point{head.X - 1, head.Y + 1}
|
topLeft := Point{X: head.X - 1, Y: head.Y + 1}
|
||||||
bottomRight := Point{head.X + 1, head.Y - 1}
|
bottomRight := Point{X: head.X + 1, Y: head.Y - 1}
|
||||||
topRight := Point{head.X + 1, head.Y + 1}
|
topRight := Point{X: head.X + 1, Y: head.Y + 1}
|
||||||
|
|
||||||
foundFoodInTwoMoves := false
|
foundFoodInTwoMoves := false
|
||||||
for _, food := range test.BoardState.Food {
|
for _, food := range test.BoardState.Food {
|
||||||
|
|
@ -559,7 +583,7 @@ func TestPlaceFoodFixedNoRoom(t *testing.T) {
|
||||||
Width: 3,
|
Width: 3,
|
||||||
Height: 3,
|
Height: 3,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{1, 1}}},
|
{Body: []Point{{X: 1, Y: 1}}},
|
||||||
},
|
},
|
||||||
Food: []Point{},
|
Food: []Point{},
|
||||||
}
|
}
|
||||||
|
|
@ -572,10 +596,10 @@ func TestPlaceFoodFixedNoRoom_Corners(t *testing.T) {
|
||||||
Width: 7,
|
Width: 7,
|
||||||
Height: 7,
|
Height: 7,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{1, 1}}},
|
{Body: []Point{{X: 1, Y: 1}}},
|
||||||
{Body: []Point{{1, 5}}},
|
{Body: []Point{{X: 1, Y: 5}}},
|
||||||
{Body: []Point{{5, 1}}},
|
{Body: []Point{{X: 5, Y: 1}}},
|
||||||
{Body: []Point{{5, 5}}},
|
{Body: []Point{{X: 5, Y: 5}}},
|
||||||
},
|
},
|
||||||
Food: []Point{},
|
Food: []Point{},
|
||||||
}
|
}
|
||||||
|
|
@ -597,10 +621,10 @@ func TestPlaceFoodFixedNoRoom_Corners(t *testing.T) {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
expectedFood := []Point{
|
expectedFood := []Point{
|
||||||
{0, 2}, {2, 0}, // Snake @ {1, 1}
|
{X: 0, Y: 2}, {X: 2, Y: 0}, // Snake @ {X: 1, Y: 1}
|
||||||
{0, 4}, {2, 6}, // Snake @ {1, 5}
|
{X: 0, Y: 4}, {X: 2, Y: 6}, // Snake @ {X: 1, Y: 5}
|
||||||
{4, 0}, {6, 2}, // Snake @ {5, 1}
|
{X: 4, Y: 0}, {X: 6, Y: 2}, // Snake @ {X: 5, Y: 1}
|
||||||
{4, 6}, {6, 4}, // Snake @ {5, 5}
|
{X: 4, Y: 6}, {X: 6, Y: 4}, // Snake @ {X: 5, Y: 5}
|
||||||
}
|
}
|
||||||
sortPoints(expectedFood)
|
sortPoints(expectedFood)
|
||||||
sortPoints(boardState.Food)
|
sortPoints(boardState.Food)
|
||||||
|
|
@ -612,10 +636,10 @@ func TestPlaceFoodFixedNoRoom_Cardinal(t *testing.T) {
|
||||||
Width: 11,
|
Width: 11,
|
||||||
Height: 11,
|
Height: 11,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{1, 5}}},
|
{Body: []Point{{X: 1, Y: 5}}},
|
||||||
{Body: []Point{{5, 1}}},
|
{Body: []Point{{X: 5, Y: 1}}},
|
||||||
{Body: []Point{{5, 9}}},
|
{Body: []Point{{X: 5, Y: 9}}},
|
||||||
{Body: []Point{{9, 5}}},
|
{Body: []Point{{X: 9, Y: 5}}},
|
||||||
},
|
},
|
||||||
Food: []Point{},
|
Food: []Point{},
|
||||||
}
|
}
|
||||||
|
|
@ -637,10 +661,10 @@ func TestPlaceFoodFixedNoRoom_Cardinal(t *testing.T) {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
expectedFood := []Point{
|
expectedFood := []Point{
|
||||||
{0, 4}, {0, 6}, // Snake @ {1, 5}
|
{X: 0, Y: 4}, {X: 0, Y: 6}, // Snake @ {X: 1, Y: 5}
|
||||||
{4, 0}, {6, 0}, // Snake @ {5, 1}
|
{X: 4, Y: 0}, {X: 6, Y: 0}, // Snake @ {X: 5, Y: 1}
|
||||||
{4, 10}, {6, 10}, // Snake @ {5, 9}
|
{X: 4, Y: 10}, {X: 6, Y: 10}, // Snake @ {X: 5, Y: 9}
|
||||||
{10, 4}, {10, 6}, // Snake @ {9, 5}
|
{X: 10, Y: 4}, {X: 10, Y: 6}, // Snake @ {X: 9, Y: 5}
|
||||||
}
|
}
|
||||||
sortPoints(expectedFood)
|
sortPoints(expectedFood)
|
||||||
sortPoints(boardState.Food)
|
sortPoints(boardState.Food)
|
||||||
|
|
@ -653,15 +677,15 @@ func TestGetDistanceBetweenPoints(t *testing.T) {
|
||||||
B Point
|
B Point
|
||||||
Expected int
|
Expected int
|
||||||
}{
|
}{
|
||||||
{Point{0, 0}, Point{0, 0}, 0},
|
{Point{X: 0, Y: 0}, Point{X: 0, Y: 0}, 0},
|
||||||
{Point{0, 0}, Point{1, 0}, 1},
|
{Point{X: 0, Y: 0}, Point{X: 1, Y: 0}, 1},
|
||||||
{Point{0, 0}, Point{0, 1}, 1},
|
{Point{X: 0, Y: 0}, Point{X: 0, Y: 1}, 1},
|
||||||
{Point{0, 0}, Point{1, 1}, 2},
|
{Point{X: 0, Y: 0}, Point{X: 1, Y: 1}, 2},
|
||||||
{Point{0, 0}, Point{4, 4}, 8},
|
{Point{X: 0, Y: 0}, Point{X: 4, Y: 4}, 8},
|
||||||
{Point{0, 0}, Point{4, 6}, 10},
|
{Point{X: 0, Y: 0}, Point{X: 4, Y: 6}, 10},
|
||||||
{Point{8, 0}, Point{8, 0}, 0},
|
{Point{X: 8, Y: 0}, Point{X: 8, Y: 0}, 0},
|
||||||
{Point{8, 0}, Point{8, 8}, 8},
|
{Point{X: 8, Y: 0}, Point{X: 8, Y: 8}, 8},
|
||||||
{Point{8, 0}, Point{0, 8}, 16},
|
{Point{X: 8, Y: 0}, Point{X: 0, Y: 8}, 16},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|
@ -704,20 +728,20 @@ func TestGetUnoccupiedPoints(t *testing.T) {
|
||||||
Height: 1,
|
Height: 1,
|
||||||
Width: 1,
|
Width: 1,
|
||||||
},
|
},
|
||||||
[]Point{{0, 0}},
|
[]Point{{X: 0, Y: 0}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 1,
|
Height: 1,
|
||||||
Width: 2,
|
Width: 2,
|
||||||
},
|
},
|
||||||
[]Point{{0, 0}, {1, 0}},
|
[]Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 1,
|
Height: 1,
|
||||||
Width: 1,
|
Width: 1,
|
||||||
Food: []Point{{0, 0}, {101, 202}, {-4, -5}},
|
Food: []Point{{X: 0, Y: 0}, {X: 101, Y: 202}, {X: -4, Y: -5}},
|
||||||
},
|
},
|
||||||
[]Point{},
|
[]Point{},
|
||||||
},
|
},
|
||||||
|
|
@ -725,15 +749,15 @@ func TestGetUnoccupiedPoints(t *testing.T) {
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 2,
|
Width: 2,
|
||||||
Food: []Point{{0, 0}, {1, 0}},
|
Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||||
},
|
},
|
||||||
[]Point{{0, 1}, {1, 1}},
|
[]Point{{X: 0, Y: 1}, {X: 1, Y: 1}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 2,
|
Width: 2,
|
||||||
Food: []Point{{0, 0}, {0, 1}, {1, 0}, {1, 1}},
|
Food: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}},
|
||||||
},
|
},
|
||||||
[]Point{},
|
[]Point{},
|
||||||
},
|
},
|
||||||
|
|
@ -742,38 +766,38 @@ func TestGetUnoccupiedPoints(t *testing.T) {
|
||||||
Height: 4,
|
Height: 4,
|
||||||
Width: 1,
|
Width: 1,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{0, 0}}},
|
{Body: []Point{{X: 0, Y: 0}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[]Point{{0, 1}, {0, 2}, {0, 3}},
|
[]Point{{X: 0, Y: 1}, {X: 0, Y: 2}, {X: 0, Y: 3}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 3,
|
Width: 3,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
|
{Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[]Point{{0, 1}, {2, 0}, {2, 1}},
|
[]Point{{X: 0, Y: 1}, {X: 2, Y: 0}, {X: 2, Y: 1}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 3,
|
Width: 3,
|
||||||
Food: []Point{{0, 0}, {1, 0}, {1, 1}, {2, 0}},
|
Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 2, Y: 0}},
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
|
{Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}},
|
||||||
{Body: []Point{{0, 1}}},
|
{Body: []Point{{X: 0, Y: 1}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[]Point{{2, 1}},
|
[]Point{{X: 2, Y: 1}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 1,
|
Height: 1,
|
||||||
Width: 1,
|
Width: 1,
|
||||||
Hazards: []Point{{0, 0}},
|
Hazards: []Point{{X: 0, Y: 0}},
|
||||||
},
|
},
|
||||||
[]Point{},
|
[]Point{},
|
||||||
},
|
},
|
||||||
|
|
@ -781,22 +805,22 @@ func TestGetUnoccupiedPoints(t *testing.T) {
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 2,
|
Width: 2,
|
||||||
Hazards: []Point{{1, 1}},
|
Hazards: []Point{{X: 1, Y: 1}},
|
||||||
},
|
},
|
||||||
[]Point{{0, 0}, {0, 1}, {1, 0}},
|
[]Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 3,
|
Width: 3,
|
||||||
Food: []Point{{1, 1}, {2, 0}},
|
Food: []Point{{X: 1, Y: 1}, {X: 2, Y: 0}},
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
|
{Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}},
|
||||||
{Body: []Point{{0, 1}}},
|
{Body: []Point{{X: 0, Y: 1}}},
|
||||||
},
|
},
|
||||||
Hazards: []Point{{0, 0}, {1, 0}},
|
Hazards: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||||
},
|
},
|
||||||
[]Point{{2, 1}},
|
[]Point{{X: 2, Y: 1}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -819,20 +843,20 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) {
|
||||||
Height: 1,
|
Height: 1,
|
||||||
Width: 1,
|
Width: 1,
|
||||||
},
|
},
|
||||||
[]Point{{0, 0}},
|
[]Point{{X: 0, Y: 0}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 2,
|
Width: 2,
|
||||||
},
|
},
|
||||||
[]Point{{0, 0}, {1, 1}},
|
[]Point{{X: 0, Y: 0}, {X: 1, Y: 1}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 1,
|
Height: 1,
|
||||||
Width: 1,
|
Width: 1,
|
||||||
Food: []Point{{0, 0}, {101, 202}, {-4, -5}},
|
Food: []Point{{X: 0, Y: 0}, {X: 101, Y: 202}, {X: -4, Y: -5}},
|
||||||
},
|
},
|
||||||
[]Point{},
|
[]Point{},
|
||||||
},
|
},
|
||||||
|
|
@ -840,15 +864,15 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) {
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 2,
|
Width: 2,
|
||||||
Food: []Point{{0, 0}, {1, 0}},
|
Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||||
},
|
},
|
||||||
[]Point{{1, 1}},
|
[]Point{{X: 1, Y: 1}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 4,
|
Height: 4,
|
||||||
Width: 4,
|
Width: 4,
|
||||||
Food: []Point{{0, 0}, {0, 2}, {1, 1}, {1, 3}, {2, 0}, {2, 2}, {3, 1}, {3, 3}},
|
Food: []Point{{X: 0, Y: 0}, {X: 0, Y: 2}, {X: 1, Y: 1}, {X: 1, Y: 3}, {X: 2, Y: 0}, {X: 2, Y: 2}, {X: 3, Y: 1}, {X: 3, Y: 3}},
|
||||||
},
|
},
|
||||||
[]Point{},
|
[]Point{},
|
||||||
},
|
},
|
||||||
|
|
@ -857,32 +881,32 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) {
|
||||||
Height: 4,
|
Height: 4,
|
||||||
Width: 1,
|
Width: 1,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{0, 0}}},
|
{Body: []Point{{X: 0, Y: 0}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[]Point{{0, 2}},
|
[]Point{{X: 0, Y: 2}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 3,
|
Width: 3,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
|
{Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[]Point{{2, 0}},
|
[]Point{{X: 2, Y: 0}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
&BoardState{
|
&BoardState{
|
||||||
Height: 2,
|
Height: 2,
|
||||||
Width: 3,
|
Width: 3,
|
||||||
Food: []Point{{0, 0}, {1, 0}, {1, 1}, {2, 1}},
|
Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 2, Y: 1}},
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
|
{Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}},
|
||||||
{Body: []Point{{0, 1}}},
|
{Body: []Point{{X: 0, Y: 1}}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[]Point{{2, 0}},
|
[]Point{{X: 2, Y: 0}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -902,7 +926,7 @@ func TestPlaceFoodRandomly(t *testing.T) {
|
||||||
Height: 1,
|
Height: 1,
|
||||||
Width: 3,
|
Width: 3,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{Body: []Point{{1, 0}}},
|
{Body: []Point{{X: 1, Y: 0}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Food should never spawn, no room
|
// Food should never spawn, no room
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ func (gc *gameTestCase) requireValidNextState(t *testing.T, r Ruleset) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Run(gc.name, func(t *testing.T) {
|
t.Run(gc.name, func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
prev := gc.prevState.Clone() // clone to protect against mutation (so we can ru-use test cases)
|
prev := gc.prevState.Clone() // clone to protect against mutation (so we can re-use test cases)
|
||||||
nextState, err := r.CreateNextBoardState(prev, gc.moves)
|
_, nextState, err := r.Execute(prev, gc.moves)
|
||||||
require.Equal(t, gc.expectedError, err)
|
require.Equal(t, gc.expectedError, err)
|
||||||
if gc.expectedState != nil {
|
if gc.expectedState != nil {
|
||||||
require.Equal(t, gc.expectedState.Width, nextState.Width)
|
require.Equal(t, gc.expectedState.Width, nextState.Width)
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,9 @@ func NewPlayCommand() *cobra.Command {
|
||||||
if err := gameState.Initialize(); err != nil {
|
if err := gameState.Initialize(); err != nil {
|
||||||
log.ERROR.Fatalf("Error initializing game: %v", err)
|
log.ERROR.Fatalf("Error initializing game: %v", err)
|
||||||
}
|
}
|
||||||
gameState.Run()
|
if err := gameState.Run(); err != nil {
|
||||||
|
log.ERROR.Fatalf("Error running game: %v", err)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +145,6 @@ func (gameState *GameState) Initialize() error {
|
||||||
|
|
||||||
// Create settings object
|
// Create settings object
|
||||||
gameState.settings = map[string]string{
|
gameState.settings = map[string]string{
|
||||||
rules.ParamGameType: gameState.GameType,
|
|
||||||
rules.ParamFoodSpawnChance: fmt.Sprint(gameState.FoodSpawnChance),
|
rules.ParamFoodSpawnChance: fmt.Sprint(gameState.FoodSpawnChance),
|
||||||
rules.ParamMinimumFood: fmt.Sprint(gameState.MinimumFood),
|
rules.ParamMinimumFood: fmt.Sprint(gameState.MinimumFood),
|
||||||
rules.ParamHazardDamagePerTurn: fmt.Sprint(gameState.HazardDamagePerTurn),
|
rules.ParamHazardDamagePerTurn: fmt.Sprint(gameState.HazardDamagePerTurn),
|
||||||
|
|
@ -155,7 +156,7 @@ func (gameState *GameState) Initialize() error {
|
||||||
WithSeed(gameState.Seed).
|
WithSeed(gameState.Seed).
|
||||||
WithParams(gameState.settings).
|
WithParams(gameState.settings).
|
||||||
WithSolo(len(gameState.URLs) < 2).
|
WithSolo(len(gameState.URLs) < 2).
|
||||||
Ruleset()
|
NamedRuleset(gameState.GameType)
|
||||||
gameState.ruleset = ruleset
|
gameState.ruleset = ruleset
|
||||||
|
|
||||||
// Initialize snake states as empty until we can ping the snake URLs
|
// Initialize snake states as empty until we can ping the snake URLs
|
||||||
|
|
@ -173,13 +174,22 @@ func (gameState *GameState) Initialize() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup and run a full game.
|
// Setup and run a full game.
|
||||||
func (gameState *GameState) Run() {
|
func (gameState *GameState) Run() error {
|
||||||
|
var gameOver bool
|
||||||
|
var err error
|
||||||
|
|
||||||
// Setup local state for snakes
|
// Setup local state for snakes
|
||||||
gameState.snakeStates = gameState.buildSnakesFromOptions()
|
gameState.snakeStates, err = gameState.buildSnakesFromOptions()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error getting snake metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
rand.Seed(gameState.Seed)
|
rand.Seed(gameState.Seed)
|
||||||
|
|
||||||
boardState := gameState.initializeBoardFromArgs()
|
gameOver, boardState, err := gameState.initializeBoardFromArgs()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error initializing board: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
gameExporter := GameExporter{
|
gameExporter := GameExporter{
|
||||||
game: gameState.createClientGame(),
|
game: gameState.createClientGame(),
|
||||||
|
|
@ -209,7 +219,7 @@ func (gameState *GameState) Run() {
|
||||||
if gameState.ViewInBrowser {
|
if gameState.ViewInBrowser {
|
||||||
serverURL, err := boardServer.Listen()
|
serverURL, err := boardServer.Listen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ERROR.Fatalf("Error starting HTTP server: %v", err)
|
return fmt.Errorf("Error starting HTTP server: %w", err)
|
||||||
}
|
}
|
||||||
defer boardServer.Shutdown()
|
defer boardServer.Shutdown()
|
||||||
log.INFO.Printf("Board server listening on %s", serverURL)
|
log.INFO.Printf("Board server listening on %s", serverURL)
|
||||||
|
|
@ -233,13 +243,7 @@ func (gameState *GameState) Run() {
|
||||||
gameState.printState(boardState)
|
gameState.printState(boardState)
|
||||||
}
|
}
|
||||||
|
|
||||||
var endTime time.Time
|
// Export game first, if enabled, so that we capture the request for turn zero.
|
||||||
for v := false; !v; v, _ = gameState.ruleset.IsGameOver(boardState) {
|
|
||||||
if gameState.TurnDuration > 0 {
|
|
||||||
endTime = time.Now().Add(time.Duration(gameState.TurnDuration) * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export game first, if enabled, so that we save the board on turn zero
|
|
||||||
if exportGame {
|
if exportGame {
|
||||||
// The output file was designed in a way so that (nearly) every entry is equivalent to a valid API request.
|
// The output file was designed in a way so that (nearly) every entry is equivalent to a valid API request.
|
||||||
// This is meant to help unlock further development of tools such as replaying a saved game by simply copying each line and sending it as a POST request.
|
// This is meant to help unlock further development of tools such as replaying a saved game by simply copying each line and sending it as a POST request.
|
||||||
|
|
@ -255,7 +259,21 @@ func (gameState *GameState) Run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boardState = gameState.createNextBoardState(boardState)
|
var endTime time.Time
|
||||||
|
for !gameOver {
|
||||||
|
if gameState.TurnDuration > 0 {
|
||||||
|
endTime = time.Now().Add(time.Duration(gameState.TurnDuration) * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
gameOver, boardState, err = gameState.createNextBoardState(boardState)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error processing game: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gameOver {
|
||||||
|
// Stop processing here - because game over is detected at the start of the pipeline, nothing will have changed.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
if gameState.ViewMap {
|
if gameState.ViewMap {
|
||||||
gameState.printMap(boardState)
|
gameState.printMap(boardState)
|
||||||
|
|
@ -274,9 +292,7 @@ func (gameState *GameState) Run() {
|
||||||
if gameState.ViewInBrowser {
|
if gameState.ViewInBrowser {
|
||||||
boardServer.SendEvent(gameState.buildFrameEvent(boardState))
|
boardServer.SendEvent(gameState.buildFrameEvent(boardState))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Export final turn
|
|
||||||
if exportGame {
|
if exportGame {
|
||||||
for _, snakeState := range gameState.snakeStates {
|
for _, snakeState := range gameState.snakeStates {
|
||||||
snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState)
|
snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState)
|
||||||
|
|
@ -284,6 +300,7 @@ func (gameState *GameState) Run() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gameExporter.isDraw = false
|
gameExporter.isDraw = false
|
||||||
|
|
||||||
|
|
@ -320,24 +337,26 @@ func (gameState *GameState) Run() {
|
||||||
if exportGame {
|
if exportGame {
|
||||||
lines, err := gameExporter.FlushToFile(gameState.outputFile)
|
lines, err := gameExporter.FlushToFile(gameState.outputFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ERROR.Fatalf("Unable to export game. Reason: %v", err)
|
return fmt.Errorf("Unable to export game: %w", err)
|
||||||
}
|
}
|
||||||
log.INFO.Printf("Wrote %d lines to output file: %s", lines, gameState.OutputPath)
|
log.INFO.Printf("Wrote %d lines to output file: %s", lines, gameState.OutputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gameState *GameState) initializeBoardFromArgs() *rules.BoardState {
|
func (gameState *GameState) initializeBoardFromArgs() (bool, *rules.BoardState, error) {
|
||||||
snakeIds := []string{}
|
snakeIds := []string{}
|
||||||
for _, snakeState := range gameState.snakeStates {
|
for _, snakeState := range gameState.snakeStates {
|
||||||
snakeIds = append(snakeIds, snakeState.ID)
|
snakeIds = append(snakeIds, snakeState.ID)
|
||||||
}
|
}
|
||||||
boardState, err := maps.SetupBoard(gameState.gameMap.ID(), gameState.ruleset.Settings(), gameState.Width, gameState.Height, snakeIds)
|
boardState, err := maps.SetupBoard(gameState.gameMap.ID(), gameState.ruleset.Settings(), gameState.Width, gameState.Height, snakeIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ERROR.Fatalf("Error Initializing Board State: %v", err)
|
return false, nil, fmt.Errorf("Error initializing BoardState with map: %w", err)
|
||||||
}
|
}
|
||||||
boardState, err = gameState.ruleset.ModifyInitialBoardState(boardState)
|
gameOver, boardState, err := gameState.ruleset.Execute(boardState, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ERROR.Fatalf("Error Initializing Board State: %v", err)
|
return false, nil, fmt.Errorf("Error initializing BoardState with ruleset: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, snakeState := range gameState.snakeStates {
|
for _, snakeState := range gameState.snakeStates {
|
||||||
|
|
@ -351,12 +370,18 @@ func (gameState *GameState) initializeBoardFromArgs() *rules.BoardState {
|
||||||
log.WARN.Printf("Request to %v failed", u.String())
|
log.WARN.Printf("Request to %v failed", u.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return boardState
|
return gameOver, boardState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) *rules.BoardState {
|
func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) (bool, *rules.BoardState, error) {
|
||||||
stateUpdates := make(chan SnakeState, len(gameState.snakeStates))
|
// apply PreUpdateBoard before making requests to snakes
|
||||||
|
boardState, err := maps.PreUpdateBoard(gameState.gameMap, boardState, gameState.ruleset.Settings())
|
||||||
|
if err != nil {
|
||||||
|
return false, boardState, fmt.Errorf("Error pre-updating board with game map: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get moves from snakes
|
||||||
|
stateUpdates := make(chan SnakeState, len(gameState.snakeStates))
|
||||||
if gameState.Sequential {
|
if gameState.Sequential {
|
||||||
for _, snakeState := range gameState.snakeStates {
|
for _, snakeState := range gameState.snakeStates {
|
||||||
for _, snake := range boardState.Snakes {
|
for _, snake := range boardState.Snakes {
|
||||||
|
|
@ -393,19 +418,20 @@ func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) *
|
||||||
moves = append(moves, rules.SnakeMove{ID: snakeState.ID, Move: snakeState.LastMove})
|
moves = append(moves, rules.SnakeMove{ID: snakeState.ID, Move: snakeState.LastMove})
|
||||||
}
|
}
|
||||||
|
|
||||||
boardState, err := gameState.ruleset.CreateNextBoardState(boardState, moves)
|
gameOver, boardState, err := gameState.ruleset.Execute(boardState, moves)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ERROR.Fatalf("Error producing next board state: %v", err)
|
return false, boardState, fmt.Errorf("Error updating board state from ruleset: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
boardState, err = maps.UpdateBoard(gameState.gameMap.ID(), boardState, gameState.ruleset.Settings())
|
// apply PostUpdateBoard after ruleset operates on snake moves
|
||||||
|
boardState, err = maps.PostUpdateBoard(gameState.gameMap, boardState, gameState.ruleset.Settings())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ERROR.Fatalf("Error updating board with game map: %v", err)
|
return false, boardState, fmt.Errorf("Error post-updating board with game map: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
boardState.Turn += 1
|
boardState.Turn += 1
|
||||||
|
|
||||||
return boardState
|
return gameOver, boardState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gameState *GameState) getSnakeUpdate(boardState *rules.BoardState, snakeState SnakeState) SnakeState {
|
func (gameState *GameState) getSnakeUpdate(boardState *rules.BoardState, snakeState SnakeState) SnakeState {
|
||||||
|
|
@ -522,13 +548,13 @@ func (gameState *GameState) createClientGame() client.Game {
|
||||||
Ruleset: client.Ruleset{
|
Ruleset: client.Ruleset{
|
||||||
Name: gameState.ruleset.Name(),
|
Name: gameState.ruleset.Name(),
|
||||||
Version: "cli", // TODO: Use GitHub Release Version
|
Version: "cli", // TODO: Use GitHub Release Version
|
||||||
Settings: gameState.ruleset.Settings(),
|
Settings: client.ConvertRulesetSettings(gameState.ruleset.Settings()),
|
||||||
},
|
},
|
||||||
Map: gameState.gameMap.ID(),
|
Map: gameState.gameMap.ID(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState {
|
func (gameState *GameState) buildSnakesFromOptions() (map[string]SnakeState, error) {
|
||||||
bodyChars := []rune{'■', '⌀', '●', '☻', '◘', '☺', '□', '⍟'}
|
bodyChars := []rune{'■', '⌀', '●', '☻', '◘', '☺', '□', '⍟'}
|
||||||
var numSnakes int
|
var numSnakes int
|
||||||
snakes := map[string]SnakeState{}
|
snakes := map[string]SnakeState{}
|
||||||
|
|
@ -560,11 +586,11 @@ func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState {
|
||||||
if i < numURLs {
|
if i < numURLs {
|
||||||
u, err := url.ParseRequestURI(gameState.URLs[i])
|
u, err := url.ParseRequestURI(gameState.URLs[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ERROR.Fatalf("URL %v is not valid: %v", gameState.URLs[i], err)
|
return nil, fmt.Errorf("URL %v is not valid: %w", gameState.URLs[i], err)
|
||||||
}
|
}
|
||||||
snakeURL = u.String()
|
snakeURL = u.String()
|
||||||
} else {
|
} else {
|
||||||
log.ERROR.Fatalf("URL for name %v is missing", gameState.Names[i])
|
return nil, fmt.Errorf("URL for name %v is missing", gameState.Names[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
snakeState := SnakeState{
|
snakeState := SnakeState{
|
||||||
|
|
@ -573,25 +599,25 @@ func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState {
|
||||||
var snakeErr error
|
var snakeErr error
|
||||||
res, _, err := gameState.httpClient.Get(snakeURL)
|
res, _, err := gameState.httpClient.Get(snakeURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ERROR.Fatalf("Snake metadata request to %v failed: %v", snakeURL, err)
|
return nil, fmt.Errorf("Snake metadata request to %v failed: %w", snakeURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
snakeState.StatusCode = res.StatusCode
|
snakeState.StatusCode = res.StatusCode
|
||||||
|
|
||||||
if res.Body == nil {
|
if res.Body == nil {
|
||||||
log.ERROR.Fatalf("Empty response body from snake metadata URL: %v", snakeURL)
|
return nil, fmt.Errorf("Empty response body from snake metadata URL: %v", snakeURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
body, readErr := ioutil.ReadAll(res.Body)
|
body, readErr := ioutil.ReadAll(res.Body)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
log.ERROR.Fatalf("Error reading from snake metadata URL %v: %v", snakeURL, readErr)
|
return nil, fmt.Errorf("Error reading from snake metadata URL %v: %w", snakeURL, readErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
pingResponse := client.SnakeMetadataResponse{}
|
pingResponse := client.SnakeMetadataResponse{}
|
||||||
jsonErr := json.Unmarshal(body, &pingResponse)
|
jsonErr := json.Unmarshal(body, &pingResponse)
|
||||||
if jsonErr != nil {
|
if jsonErr != nil {
|
||||||
log.ERROR.Fatalf("Failed to parse response from %v: %v", snakeURL, jsonErr)
|
return nil, fmt.Errorf("Failed to parse response from %v: %w", snakeURL, jsonErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
snakeState.Head = pingResponse.Head
|
snakeState.Head = pingResponse.Head
|
||||||
|
|
@ -608,7 +634,7 @@ func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState {
|
||||||
|
|
||||||
log.INFO.Printf("Snake ID: %v URL: %v, Name: \"%v\"", snakeState.ID, snakeURL, snakeState.Name)
|
log.INFO.Printf("Snake ID: %v URL: %v, Name: \"%v\"", snakeState.ID, snakeURL, snakeState.Name)
|
||||||
}
|
}
|
||||||
return snakes
|
return snakes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gameState *GameState) printState(boardState *rules.BoardState) {
|
func (gameState *GameState) printState(boardState *rules.BoardState) {
|
||||||
|
|
@ -762,7 +788,8 @@ func (gameState *GameState) buildFrameEvent(boardState *rules.BoardState) board.
|
||||||
func serialiseSnakeRequest(snakeRequest client.SnakeRequest) []byte {
|
func serialiseSnakeRequest(snakeRequest client.SnakeRequest) []byte {
|
||||||
requestJSON, err := json.Marshal(snakeRequest)
|
requestJSON, err := json.Marshal(snakeRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ERROR.Fatalf("Error marshalling JSON from State: %v", err)
|
// This is likely to be a programming error like a unsupported type or cyclical reference
|
||||||
|
log.ERROR.Panicf("Error marshalling JSON from State: %v", err)
|
||||||
}
|
}
|
||||||
return requestJSON
|
return requestJSON
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,10 @@ func buildDefaultGameState() *GameState {
|
||||||
func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
||||||
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||||
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
||||||
state := &rules.BoardState{
|
state := rules.NewBoardState(11, 11).
|
||||||
Height: 11,
|
WithSnakes(
|
||||||
Width: 11,
|
[]rules.Snake{s1, s2},
|
||||||
Snakes: []rules.Snake{s1, s2},
|
)
|
||||||
}
|
|
||||||
s1State := SnakeState{
|
s1State := SnakeState{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Name: "ONE",
|
Name: "ONE",
|
||||||
|
|
@ -85,11 +84,8 @@ func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
||||||
func TestSettingsRequestSerialization(t *testing.T) {
|
func TestSettingsRequestSerialization(t *testing.T) {
|
||||||
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||||
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
||||||
state := &rules.BoardState{
|
state := rules.NewBoardState(11, 11).
|
||||||
Height: 11,
|
WithSnakes([]rules.Snake{s1, s2})
|
||||||
Width: 11,
|
|
||||||
Snakes: []rules.Snake{s1, s2},
|
|
||||||
}
|
|
||||||
s1State := SnakeState{
|
s1State := SnakeState{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Name: "ONE",
|
Name: "ONE",
|
||||||
|
|
@ -255,12 +251,11 @@ func TestBuildFrameEvent(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "snake fields",
|
name: "snake fields",
|
||||||
boardState: &rules.BoardState{
|
boardState: rules.NewBoardState(19, 25).
|
||||||
Turn: 99,
|
WithTurn(99).
|
||||||
Height: 19,
|
WithFood([]rules.Point{{X: 9, Y: 4}}).
|
||||||
Width: 25,
|
WithHazards([]rules.Point{{X: 8, Y: 6}}).
|
||||||
Food: []rules.Point{{X: 9, Y: 4}},
|
WithSnakes([]rules.Snake{
|
||||||
Snakes: []rules.Snake{
|
|
||||||
{
|
{
|
||||||
ID: "1",
|
ID: "1",
|
||||||
Body: []rules.Point{
|
Body: []rules.Point{
|
||||||
|
|
@ -273,9 +268,7 @@ func TestBuildFrameEvent(t *testing.T) {
|
||||||
EliminatedOnTurn: 45,
|
EliminatedOnTurn: 45,
|
||||||
EliminatedBy: "1",
|
EliminatedBy: "1",
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
Hazards: []rules.Point{{X: 8, Y: 6}},
|
|
||||||
},
|
|
||||||
snakeStates: map[string]SnakeState{
|
snakeStates: map[string]SnakeState{
|
||||||
"1": {
|
"1": {
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
|
|
@ -326,18 +319,15 @@ func TestBuildFrameEvent(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "snake errors",
|
name: "snake errors",
|
||||||
boardState: &rules.BoardState{
|
boardState: rules.NewBoardState(19, 25).
|
||||||
Height: 19,
|
WithSnakes([]rules.Snake{
|
||||||
Width: 25,
|
|
||||||
Snakes: []rules.Snake{
|
|
||||||
{
|
{
|
||||||
ID: "bad_status",
|
ID: "bad_status",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "connection_error",
|
ID: "connection_error",
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
snakeStates: map[string]SnakeState{
|
snakeStates: map[string]SnakeState{
|
||||||
"bad_status": {
|
"bad_status": {
|
||||||
StatusCode: 504,
|
StatusCode: 504,
|
||||||
|
|
@ -366,6 +356,8 @@ func TestBuildFrameEvent(t *testing.T) {
|
||||||
Error: "0:Error communicating with server",
|
Error: "0:Error communicating with server",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Food: []rules.Point{},
|
||||||
|
Hazards: []rules.Point{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -384,11 +376,7 @@ func TestBuildFrameEvent(t *testing.T) {
|
||||||
func TestGetMoveForSnake(t *testing.T) {
|
func TestGetMoveForSnake(t *testing.T) {
|
||||||
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||||
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
||||||
boardState := &rules.BoardState{
|
boardState := rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{s1, s2})
|
||||||
Height: 11,
|
|
||||||
Width: 11,
|
|
||||||
Snakes: []rules.Snake{s1, s2},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -530,11 +518,7 @@ func TestGetMoveForSnake(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateNextBoardState(t *testing.T) {
|
func TestCreateNextBoardState(t *testing.T) {
|
||||||
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||||
boardState := &rules.BoardState{
|
boardState := rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{s1})
|
||||||
Height: 11,
|
|
||||||
Width: 11,
|
|
||||||
Snakes: []rules.Snake{s1},
|
|
||||||
}
|
|
||||||
snakeState := SnakeState{
|
snakeState := SnakeState{
|
||||||
ID: s1.ID,
|
ID: s1.ID,
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
|
|
@ -549,7 +533,9 @@ func TestCreateNextBoardState(t *testing.T) {
|
||||||
gameState.snakeStates = map[string]SnakeState{s1.ID: snakeState}
|
gameState.snakeStates = map[string]SnakeState{s1.ID: snakeState}
|
||||||
gameState.httpClient = stubHTTPClient{nil, 200, func(_ string) string { return `{"move": "right"}` }, 54 * time.Millisecond}
|
gameState.httpClient = stubHTTPClient{nil, 200, func(_ string) string { return `{"move": "right"}` }, 54 * time.Millisecond}
|
||||||
|
|
||||||
nextBoardState := gameState.createNextBoardState(boardState)
|
gameOver, nextBoardState, err := gameState.createNextBoardState(boardState)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, gameOver)
|
||||||
snakeState = gameState.snakeStates[s1.ID]
|
snakeState = gameState.snakeStates[s1.ID]
|
||||||
|
|
||||||
require.NotNil(t, nextBoardState)
|
require.NotNil(t, nextBoardState)
|
||||||
|
|
@ -593,16 +579,18 @@ func TestOutputFile(t *testing.T) {
|
||||||
outputFile := new(closableBuffer)
|
outputFile := new(closableBuffer)
|
||||||
gameState.outputFile = outputFile
|
gameState.outputFile = outputFile
|
||||||
|
|
||||||
gameState.ruleset = StubRuleset{maxTurns: 1, settings: rules.Settings{
|
gameState.ruleset = StubRuleset{
|
||||||
FoodSpawnChance: 1,
|
maxTurns: 1,
|
||||||
MinimumFood: 2,
|
settings: rules.NewSettings(map[string]string{
|
||||||
HazardDamagePerTurn: 3,
|
rules.ParamFoodSpawnChance: "1",
|
||||||
RoyaleSettings: rules.RoyaleSettings{
|
rules.ParamMinimumFood: "2",
|
||||||
ShrinkEveryNTurns: 4,
|
rules.ParamHazardDamagePerTurn: "3",
|
||||||
},
|
rules.ParamShrinkEveryNTurns: "4",
|
||||||
}}
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
gameState.Run()
|
err = gameState.Run()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
lines := strings.Split(outputFile.String(), "\n")
|
lines := strings.Split(outputFile.String(), "\n")
|
||||||
require.Len(t, lines, 5)
|
require.Len(t, lines, 5)
|
||||||
|
|
@ -626,14 +614,8 @@ type StubRuleset struct {
|
||||||
|
|
||||||
func (ruleset StubRuleset) Name() string { return "standard" }
|
func (ruleset StubRuleset) Name() string { return "standard" }
|
||||||
func (ruleset StubRuleset) Settings() rules.Settings { return ruleset.settings }
|
func (ruleset StubRuleset) Settings() rules.Settings { return ruleset.settings }
|
||||||
func (ruleset StubRuleset) ModifyInitialBoardState(initialState *rules.BoardState) (*rules.BoardState, error) {
|
func (ruleset StubRuleset) Execute(prevState *rules.BoardState, moves []rules.SnakeMove) (bool, *rules.BoardState, error) {
|
||||||
return initialState, nil
|
return prevState.Turn >= ruleset.maxTurns, prevState, nil
|
||||||
}
|
|
||||||
func (ruleset StubRuleset) CreateNextBoardState(prevState *rules.BoardState, moves []rules.SnakeMove) (*rules.BoardState, error) {
|
|
||||||
return prevState, nil
|
|
||||||
}
|
|
||||||
func (ruleset StubRuleset) IsGameOver(state *rules.BoardState) (bool, error) {
|
|
||||||
return state.Turn >= ruleset.maxTurns, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type stubHTTPClient struct {
|
type stubHTTPClient struct {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ func exampleSnakeRequest() SnakeRequest {
|
||||||
Ruleset: Ruleset{
|
Ruleset: Ruleset{
|
||||||
Name: "test-ruleset-name",
|
Name: "test-ruleset-name",
|
||||||
Version: "cli",
|
Version: "cli",
|
||||||
Settings: exampleRulesetSettings,
|
Settings: ConvertRulesetSettings(exampleRulesetSettings),
|
||||||
},
|
},
|
||||||
Timeout: 33,
|
Timeout: 33,
|
||||||
Source: "league",
|
Source: "league",
|
||||||
|
|
@ -75,21 +75,9 @@ func exampleSnakeRequest() SnakeRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var exampleRulesetSettings = rules.Settings{
|
var exampleRulesetSettings = rules.NewSettings(map[string]string{
|
||||||
FoodSpawnChance: 10,
|
rules.ParamFoodSpawnChance: "10",
|
||||||
MinimumFood: 20,
|
rules.ParamMinimumFood: "20",
|
||||||
HazardDamagePerTurn: 30,
|
rules.ParamHazardDamagePerTurn: "30",
|
||||||
HazardMap: "hz_spiral",
|
rules.ParamShrinkEveryNTurns: "40",
|
||||||
HazardMapAuthor: "altersaddle",
|
})
|
||||||
|
|
||||||
RoyaleSettings: rules.RoyaleSettings{
|
|
||||||
ShrinkEveryNTurns: 40,
|
|
||||||
},
|
|
||||||
|
|
||||||
SquadSettings: rules.SquadSettings{
|
|
||||||
AllowBodyCollisions: true,
|
|
||||||
SharedElimination: true,
|
|
||||||
SharedHealth: true,
|
|
||||||
SharedLength: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -51,17 +51,45 @@ type Customizations struct {
|
||||||
type Ruleset struct {
|
type Ruleset struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Settings rules.Settings `json:"settings"`
|
Settings RulesetSettings `json:"settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RulesetSettings is deprecated: use rules.Settings instead
|
// RulesetSettings contains a static collection of a few settings that are exposed through the API.
|
||||||
type RulesetSettings rules.Settings
|
type RulesetSettings struct {
|
||||||
|
FoodSpawnChance int `json:"foodSpawnChance"`
|
||||||
|
MinimumFood int `json:"minimumFood"`
|
||||||
|
HazardDamagePerTurn int `json:"hazardDamagePerTurn"`
|
||||||
|
HazardMap string `json:"hazardMap"` // Deprecated, replaced by Game.Map
|
||||||
|
HazardMapAuthor string `json:"hazardMapAuthor"` // Deprecated, no planned replacement
|
||||||
|
RoyaleSettings RoyaleSettings `json:"royale"`
|
||||||
|
SquadSettings SquadSettings `json:"squad"` // Deprecated, provided with default fields for API compatibility
|
||||||
|
}
|
||||||
|
|
||||||
// RoyaleSettings is deprecated: use rules.RoyaleSettings instead
|
// RoyaleSettings contains settings that are specific to the "royale" game mode
|
||||||
type RoyaleSettings rules.RoyaleSettings
|
type RoyaleSettings struct {
|
||||||
|
ShrinkEveryNTurns int `json:"shrinkEveryNTurns"`
|
||||||
|
}
|
||||||
|
|
||||||
// SquadSettings is deprecated: use rules.SquadSettings instead
|
// SquadSettings contains settings that are specific to the "squad" game mode
|
||||||
type SquadSettings rules.SquadSettings
|
type SquadSettings struct {
|
||||||
|
AllowBodyCollisions bool `json:"allowBodyCollisions"`
|
||||||
|
SharedElimination bool `json:"sharedElimination"`
|
||||||
|
SharedHealth bool `json:"sharedHealth"`
|
||||||
|
SharedLength bool `json:"sharedLength"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts a rules.Settings (which can contain arbitrary settings) into the static RulesetSettings used in the client API.
|
||||||
|
func ConvertRulesetSettings(settings rules.Settings) RulesetSettings {
|
||||||
|
return RulesetSettings{
|
||||||
|
FoodSpawnChance: settings.Int(rules.ParamFoodSpawnChance, 0),
|
||||||
|
MinimumFood: settings.Int(rules.ParamMinimumFood, 0),
|
||||||
|
HazardDamagePerTurn: settings.Int(rules.ParamHazardDamagePerTurn, 0),
|
||||||
|
RoyaleSettings: RoyaleSettings{
|
||||||
|
ShrinkEveryNTurns: settings.Int(rules.ParamShrinkEveryNTurns, 0),
|
||||||
|
},
|
||||||
|
SquadSettings: SquadSettings{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Coord represents a point on the board
|
// Coord represents a point on the board
|
||||||
type Coord struct {
|
type Coord struct {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/BattlesnakeOfficial/rules"
|
|
||||||
"github.com/BattlesnakeOfficial/rules/test"
|
"github.com/BattlesnakeOfficial/rules/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
@ -19,7 +18,7 @@ func TestBuildSnakeRequestJSON(t *testing.T) {
|
||||||
|
|
||||||
func TestBuildSnakeRequestJSONEmptyRulesetSettings(t *testing.T) {
|
func TestBuildSnakeRequestJSONEmptyRulesetSettings(t *testing.T) {
|
||||||
snakeRequest := exampleSnakeRequest()
|
snakeRequest := exampleSnakeRequest()
|
||||||
snakeRequest.Game.Ruleset.Settings = rules.Settings{}
|
snakeRequest.Game.Ruleset.Settings = RulesetSettings{}
|
||||||
data, err := json.MarshalIndent(snakeRequest, "", " ")
|
data, err := json.MarshalIndent(snakeRequest, "", " ")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|
|
||||||
12
client/testdata/snake_request.json
vendored
12
client/testdata/snake_request.json
vendored
|
|
@ -8,16 +8,16 @@
|
||||||
"foodSpawnChance": 10,
|
"foodSpawnChance": 10,
|
||||||
"minimumFood": 20,
|
"minimumFood": 20,
|
||||||
"hazardDamagePerTurn": 30,
|
"hazardDamagePerTurn": 30,
|
||||||
"hazardMap": "hz_spiral",
|
"hazardMap": "",
|
||||||
"hazardMapAuthor": "altersaddle",
|
"hazardMapAuthor": "",
|
||||||
"royale": {
|
"royale": {
|
||||||
"shrinkEveryNTurns": 40
|
"shrinkEveryNTurns": 40
|
||||||
},
|
},
|
||||||
"squad": {
|
"squad": {
|
||||||
"allowBodyCollisions": true,
|
"allowBodyCollisions": false,
|
||||||
"sharedElimination": true,
|
"sharedElimination": false,
|
||||||
"sharedHealth": true,
|
"sharedHealth": false,
|
||||||
"sharedLength": true
|
"sharedLength": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -22,31 +22,6 @@ var wrappedConstrictorRulesetStages = []string{
|
||||||
StageModifySnakesAlwaysGrow,
|
StageModifySnakesAlwaysGrow,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConstrictorRuleset struct {
|
|
||||||
StandardRuleset
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) Name() string { return GameTypeConstrictor }
|
|
||||||
|
|
||||||
func (r ConstrictorRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
|
||||||
return NewPipeline(constrictorRulesetStages...).Execute(bs, s, sm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) ModifyInitialBoardState(initialBoardState *BoardState) (*BoardState, error) {
|
|
||||||
_, nextState, err := r.Execute(initialBoardState, r.Settings(), nil)
|
|
||||||
return nextState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
|
||||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
|
||||||
|
|
||||||
return nextState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) IsGameOver(b *BoardState) (bool, error) {
|
|
||||||
return GameOverStandard(b, r.Settings(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveFoodConstrictor(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
func RemoveFoodConstrictor(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
// Remove all food from the board
|
// Remove all food from the board
|
||||||
b.Food = []Point{}
|
b.Food = []Point{}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConstrictorRulesetInterface(t *testing.T) {
|
|
||||||
var _ Ruleset = (*ConstrictorRuleset)(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that two equal snakes collide and both get eliminated
|
// Test that two equal snakes collide and both get eliminated
|
||||||
// also checks:
|
// also checks:
|
||||||
// - food removed
|
// - food removed
|
||||||
|
|
@ -21,16 +17,16 @@ var constrictorMoveAndCollideMAD = gameTestCase{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{
|
{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Body: []Point{{1, 1}, {2, 1}},
|
Body: []Point{{X: 1, Y: 1}, {X: 2, Y: 1}},
|
||||||
Health: 99,
|
Health: 99,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "two",
|
ID: "two",
|
||||||
Body: []Point{{1, 2}, {2, 2}},
|
Body: []Point{{X: 1, Y: 2}, {X: 2, Y: 2}},
|
||||||
Health: 99,
|
Health: 99,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Food: []Point{{10, 10}, {9, 9}, {8, 8}},
|
Food: []Point{{X: 10, Y: 10}, {X: 9, Y: 9}, {X: 8, Y: 8}},
|
||||||
Hazards: []Point{},
|
Hazards: []Point{},
|
||||||
},
|
},
|
||||||
[]SnakeMove{
|
[]SnakeMove{
|
||||||
|
|
@ -44,7 +40,7 @@ var constrictorMoveAndCollideMAD = gameTestCase{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{
|
{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Body: []Point{{1, 2}, {1, 1}, {1, 1}},
|
Body: []Point{{X: 1, Y: 2}, {X: 1, Y: 1}, {X: 1, Y: 1}},
|
||||||
Health: 100,
|
Health: 100,
|
||||||
EliminatedCause: EliminatedByCollision,
|
EliminatedCause: EliminatedByCollision,
|
||||||
EliminatedBy: "two",
|
EliminatedBy: "two",
|
||||||
|
|
@ -52,7 +48,7 @@ var constrictorMoveAndCollideMAD = gameTestCase{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "two",
|
ID: "two",
|
||||||
Body: []Point{{1, 1}, {1, 2}, {1, 2}},
|
Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}, {X: 1, Y: 2}},
|
||||||
Health: 100,
|
Health: 100,
|
||||||
EliminatedCause: EliminatedByCollision,
|
EliminatedCause: EliminatedByCollision,
|
||||||
EliminatedBy: "one",
|
EliminatedBy: "one",
|
||||||
|
|
@ -70,15 +66,11 @@ func TestConstrictorCreateNextBoardState(t *testing.T) {
|
||||||
standardCaseErrZeroLengthSnake,
|
standardCaseErrZeroLengthSnake,
|
||||||
constrictorMoveAndCollideMAD,
|
constrictorMoveAndCollideMAD,
|
||||||
}
|
}
|
||||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
r := NewRulesetBuilder().NamedRuleset(GameTypeConstrictor)
|
||||||
ParamGameType: GameTypeConstrictor,
|
|
||||||
})
|
|
||||||
r := ConstrictorRuleset{}
|
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
gc.requireValidNextState(t, &r)
|
// test a RulesBuilder constructed instance
|
||||||
// also test a RulesBuilder constructed instance
|
gc.requireValidNextState(t, r)
|
||||||
gc.requireValidNextState(t, rb.Ruleset())
|
|
||||||
// also test a pipeline with the same settings
|
// also test a pipeline with the same settings
|
||||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeConstrictor, NewPipeline(constrictorRulesetStages...)))
|
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeConstrictor, NewPipeline(constrictorRulesetStages...)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ func (m ArcadeMazeMap) SetupBoard(initialBoardState *rules.BoardState, settings
|
||||||
editor.AddHazard(hazard)
|
editor.AddHazard(hazard)
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.MinimumFood > 0 {
|
if settings.Int(rules.ParamMinimumFood, 0) > 0 {
|
||||||
// Add food in center
|
// Add food in center
|
||||||
editor.AddFood(rules.Point{X: 9, Y: 11})
|
editor.AddFood(rules.Point{X: 9, Y: 11})
|
||||||
}
|
}
|
||||||
|
|
@ -71,11 +71,16 @@ func (m ArcadeMazeMap) SetupBoard(initialBoardState *rules.BoardState, settings
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ArcadeMazeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m ArcadeMazeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ArcadeMazeMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
rand := settings.GetRand(lastBoardState.Turn)
|
rand := settings.GetRand(lastBoardState.Turn)
|
||||||
|
|
||||||
// Respect FoodSpawnChance setting
|
// Respect FoodSpawnChance setting
|
||||||
if settings.FoodSpawnChance == 0 || rand.Intn(100) > settings.FoodSpawnChance {
|
foodSpawnChance := settings.Int(rules.ParamFoodSpawnChance, 0)
|
||||||
|
if foodSpawnChance == 0 || rand.Intn(100) > foodSpawnChance {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,11 @@ func (m CastleWallMediumHazardsMap) SetupBoard(initialBoardState *rules.BoardSta
|
||||||
return setupCastleWallBoard(m.Meta().MaxPlayers, startPositions, castleWallMediumHazards, initialBoardState, settings, editor)
|
return setupCastleWallBoard(m.Meta().MaxPlayers, startPositions, castleWallMediumHazards, initialBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m CastleWallMediumHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m CastleWallMediumHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m CastleWallMediumHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
maxFood := 2
|
maxFood := 2
|
||||||
return updateCastleWallBoard(maxFood, castleWallMediumFood, lastBoardState, settings, editor)
|
return updateCastleWallBoard(maxFood, castleWallMediumFood, lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
@ -228,7 +232,11 @@ func (m CastleWallLargeHazardsMap) SetupBoard(initialBoardState *rules.BoardStat
|
||||||
return setupCastleWallBoard(m.Meta().MaxPlayers, startPositions, castleWallLargeHazards, initialBoardState, settings, editor)
|
return setupCastleWallBoard(m.Meta().MaxPlayers, startPositions, castleWallLargeHazards, initialBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m CastleWallLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m CastleWallLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m CastleWallLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
maxFood := 2
|
maxFood := 2
|
||||||
return updateCastleWallBoard(maxFood, castleWallLargeFood, lastBoardState, settings, editor)
|
return updateCastleWallBoard(maxFood, castleWallLargeFood, lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
@ -420,7 +428,11 @@ func (m CastleWallExtraLargeHazardsMap) SetupBoard(initialBoardState *rules.Boar
|
||||||
return setupCastleWallBoard(m.Meta().MaxPlayers, startPositions, castleWallExtraLargeHazards, initialBoardState, settings, editor)
|
return setupCastleWallBoard(m.Meta().MaxPlayers, startPositions, castleWallExtraLargeHazards, initialBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m CastleWallExtraLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m CastleWallExtraLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m CastleWallExtraLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
maxFood := 4
|
maxFood := 4
|
||||||
return updateCastleWallBoard(maxFood, castleWallExtraLargeFood, lastBoardState, settings, editor)
|
return updateCastleWallBoard(maxFood, castleWallExtraLargeFood, lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ func (m EmptyMap) SetupBoard(initialBoardState *rules.BoardState, settings rules
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m EmptyMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m EmptyMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m EmptyMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,55 +28,28 @@ func TestEmptyMapSetupBoard(t *testing.T) {
|
||||||
"empty 7x7",
|
"empty 7x7",
|
||||||
rules.NewBoardState(7, 7),
|
rules.NewBoardState(7, 7),
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(7, 7),
|
||||||
Width: 7,
|
|
||||||
Height: 7,
|
|
||||||
Snakes: []rules.Snake{},
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"not enough room for snakes 7x7",
|
"not enough room for snakes 7x7",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(7, 7).WithSnakes(generateSnakes(17)),
|
||||||
Width: 7,
|
|
||||||
Height: 7,
|
|
||||||
Snakes: generateSnakes(17),
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
nil,
|
nil,
|
||||||
rules.ErrorTooManySnakes,
|
rules.ErrorTooManySnakes,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"not enough room for snakes 5x5",
|
"not enough room for snakes 5x5",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(5, 5).WithSnakes(generateSnakes(14)),
|
||||||
Width: 5,
|
|
||||||
Height: 5,
|
|
||||||
Snakes: generateSnakes(14),
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
nil,
|
nil,
|
||||||
rules.ErrorTooManySnakes,
|
rules.ErrorTooManySnakes,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"full 11x11 min",
|
"full 11x11 min",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)),
|
||||||
Width: 11,
|
|
||||||
Height: 11,
|
|
||||||
Snakes: generateSnakes(8),
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{
|
||||||
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: "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: "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: "3", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
||||||
|
|
@ -85,26 +58,14 @@ func TestEmptyMapSetupBoard(t *testing.T) {
|
||||||
{ID: "6", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, 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: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, 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},
|
{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,
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"full 11x11 max",
|
"full 11x11 max",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)),
|
||||||
Width: 11,
|
|
||||||
Height: 11,
|
|
||||||
Snakes: generateSnakes(8),
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
rules.MaxRand,
|
rules.MaxRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{
|
||||||
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: "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: "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: "3", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||||
|
|
@ -113,10 +74,7 @@ func TestEmptyMapSetupBoard(t *testing.T) {
|
||||||
{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: 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{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -139,27 +97,13 @@ func TestEmptyMapSetupBoard(t *testing.T) {
|
||||||
|
|
||||||
func TestEmptyMapUpdateBoard(t *testing.T) {
|
func TestEmptyMapUpdateBoard(t *testing.T) {
|
||||||
m := maps.EmptyMap{}
|
m := maps.EmptyMap{}
|
||||||
initialBoardState := &rules.BoardState{
|
initialBoardState := rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}})
|
||||||
Width: 2,
|
settings := rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "2").WithRand(rules.MaxRand)
|
||||||
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()
|
nextBoardState := initialBoardState.Clone()
|
||||||
|
|
||||||
err := m.UpdateBoard(initialBoardState.Clone(), settings, maps.NewBoardStateEditor(nextBoardState))
|
err := m.PostUpdateBoard(initialBoardState.Clone(), settings, maps.NewBoardStateEditor(nextBoardState))
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, &rules.BoardState{
|
expectedBoardState := rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}})
|
||||||
Width: 2,
|
require.Equal(t, expectedBoardState, nextBoardState)
|
||||||
Height: 2,
|
|
||||||
Snakes: []rules.Snake{},
|
|
||||||
Food: []rules.Point{{X: 0, Y: 0}},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
}, nextBoardState)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,21 @@ type GameMap interface {
|
||||||
// Called to generate a new board. The map is responsible for placing all snakes, food, and hazards.
|
// 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
|
SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error
|
||||||
|
|
||||||
// Called every turn to optionally update the board.
|
// Called every turn to optionally update the board before the board is sent to snakes to get their moves.
|
||||||
UpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error
|
// Changes made here will be seen by snakes before before making their moves, but users in the
|
||||||
|
// browser will see the changes at the same time as the snakes' moves.
|
||||||
|
//
|
||||||
|
// State that is stored in the map by this method will be visible to the PostUpdateBoard method
|
||||||
|
// later in the same turn, but will not nessecarily be available when processing later turns.
|
||||||
|
//
|
||||||
|
// Disclaimer: Unless you have a specific usecase like moving hazards or storing intermediate state,
|
||||||
|
// PostUpdateBoard is probably the better function to use.
|
||||||
|
PreUpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error
|
||||||
|
|
||||||
|
// Called every turn to optionally update the board after all other rules have been applied.
|
||||||
|
// Changes made here will be seen by both snakes and users in the browser, before before snakes
|
||||||
|
// make their next moves.
|
||||||
|
PostUpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
|
|
@ -166,6 +179,12 @@ type Editor interface {
|
||||||
// Note: the body values in the return value are a copy and modifying them won't affect the board.
|
// Note: the body values in the return value are a copy and modifying them won't affect the board.
|
||||||
SnakeBodies() map[string][]rules.Point
|
SnakeBodies() map[string][]rules.Point
|
||||||
|
|
||||||
|
// Get an editable reference to the BoardState's GameState field
|
||||||
|
GameState() map[string]string
|
||||||
|
|
||||||
|
// Get an editable reference to the BoardState's PointState field
|
||||||
|
PointState() map[rules.Point]int
|
||||||
|
|
||||||
// Given a list of Snakes and a list of head coordinates, randomly place
|
// Given a list of Snakes and a list of head coordinates, randomly place
|
||||||
// the snakes on those coordinates, or return an error if placement of all
|
// the snakes on those coordinates, or return an error if placement of all
|
||||||
// Snakes is impossible.
|
// Snakes is impossible.
|
||||||
|
|
@ -270,6 +289,16 @@ func (editor *BoardStateEditor) SnakeBodies() map[string][]rules.Point {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get an editable reference to the BoardState's GameState field
|
||||||
|
func (editor *BoardStateEditor) GameState() map[string]string {
|
||||||
|
return editor.boardState.GameState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an editable reference to the BoardState's PointState field
|
||||||
|
func (editor *BoardStateEditor) PointState() map[rules.Point]int {
|
||||||
|
return editor.boardState.PointState
|
||||||
|
}
|
||||||
|
|
||||||
// Given a list of Snakes and a list of head coordinates, randomly place
|
// Given a list of Snakes and a list of head coordinates, randomly place
|
||||||
// the snakes on those coordinates, or return an error if placement of all
|
// the snakes on those coordinates, or return an error if placement of all
|
||||||
// Snakes is impossible.
|
// Snakes is impossible.
|
||||||
|
|
|
||||||
|
|
@ -135,18 +135,16 @@ func TestBoardStateEditor(t *testing.T) {
|
||||||
editor.PlaceSnake("existing_snake", []rules.Point{{X: 5, Y: 2}, {X: 5, Y: 1}, {X: 5, Y: 0}}, 99)
|
editor.PlaceSnake("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)
|
editor.PlaceSnake("new_snake", []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}, 98)
|
||||||
|
|
||||||
require.Equal(t, &rules.BoardState{
|
expected := rules.NewBoardState(11, 11).
|
||||||
Width: 11,
|
WithFood([]rules.Point{
|
||||||
Height: 11,
|
|
||||||
Food: []rules.Point{
|
|
||||||
{X: 1, Y: 3},
|
{X: 1, Y: 3},
|
||||||
{X: 3, Y: 7},
|
{X: 3, Y: 7},
|
||||||
},
|
}).
|
||||||
Hazards: []rules.Point{
|
WithHazards([]rules.Point{
|
||||||
{X: 1, Y: 3},
|
{X: 1, Y: 3},
|
||||||
{X: 3, Y: 7},
|
{X: 3, Y: 7},
|
||||||
},
|
}).
|
||||||
Snakes: []rules.Snake{
|
WithSnakes([]rules.Snake{
|
||||||
{
|
{
|
||||||
ID: "existing_snake",
|
ID: "existing_snake",
|
||||||
Health: 99,
|
Health: 99,
|
||||||
|
|
@ -157,8 +155,8 @@ func TestBoardStateEditor(t *testing.T) {
|
||||||
Health: 98,
|
Health: 98,
|
||||||
Body: []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}},
|
Body: []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
}, boardState)
|
require.Equal(t, expected, boardState)
|
||||||
|
|
||||||
require.Equal(t, []rules.Point{
|
require.Equal(t, []rules.Point{
|
||||||
{X: 1, Y: 3},
|
{X: 1, Y: 3},
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,12 @@ func (m HazardPitsMap) SetupBoard(initialBoardState *rules.BoardState, settings
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m HazardPitsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m HazardPitsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m HazardPitsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -109,9 +113,10 @@ func (m HazardPitsMap) UpdateBoard(lastBoardState *rules.BoardState, settings ru
|
||||||
// Cycle 3 - 3 layers
|
// Cycle 3 - 3 layers
|
||||||
// Cycle 4-6 - 4 layers of hazards
|
// Cycle 4-6 - 4 layers of hazards
|
||||||
|
|
||||||
if lastBoardState.Turn%settings.RoyaleSettings.ShrinkEveryNTurns == 0 {
|
shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0)
|
||||||
|
if lastBoardState.Turn%shrinkEveryNTurns == 0 {
|
||||||
// Is it time to update the hazards
|
// Is it time to update the hazards
|
||||||
layers := (lastBoardState.Turn / settings.RoyaleSettings.ShrinkEveryNTurns) % 7
|
layers := (lastBoardState.Turn / shrinkEveryNTurns) % 7
|
||||||
if layers > 4 {
|
if layers > 4 {
|
||||||
layers = 4
|
layers = 4
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ func TestHazardPitsMap(t *testing.T) {
|
||||||
|
|
||||||
state = rules.NewBoardState(int(11), int(11))
|
state = rules.NewBoardState(int(11), int(11))
|
||||||
m = maps.HazardPitsMap{}
|
m = maps.HazardPitsMap{}
|
||||||
settings.RoyaleSettings.ShrinkEveryNTurns = 1
|
settings = rules.NewSettingsWithParams(rules.ParamShrinkEveryNTurns, "1")
|
||||||
editor = maps.NewBoardStateEditor(state)
|
editor = maps.NewBoardStateEditor(state)
|
||||||
require.Empty(t, state.Hazards)
|
require.Empty(t, state.Hazards)
|
||||||
err = m.SetupBoard(state, settings, editor)
|
err = m.SetupBoard(state, settings, editor)
|
||||||
|
|
@ -47,7 +47,7 @@ func TestHazardPitsMap(t *testing.T) {
|
||||||
// Verify the hazard progression through the turns
|
// Verify the hazard progression through the turns
|
||||||
for i := 0; i < 16; i++ {
|
for i := 0; i < 16; i++ {
|
||||||
state.Turn = i
|
state.Turn = i
|
||||||
err = m.UpdateBoard(state, settings, editor)
|
err = m.PostUpdateBoard(state, settings, editor)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if i == 1 {
|
if i == 1 {
|
||||||
require.Len(t, state.Hazards, 21)
|
require.Len(t, state.Hazards, 21)
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,12 @@ func (m InnerBorderHazardsMap) SetupBoard(lastBoardState *rules.BoardState, sett
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m InnerBorderHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m InnerBorderHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m InnerBorderHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConcentricRingsHazardsMap struct{}
|
type ConcentricRingsHazardsMap struct{}
|
||||||
|
|
@ -96,8 +100,12 @@ func (m ConcentricRingsHazardsMap) SetupBoard(lastBoardState *rules.BoardState,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ConcentricRingsHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m ConcentricRingsHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ConcentricRingsHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ColumnsHazardsMap struct{}
|
type ColumnsHazardsMap struct{}
|
||||||
|
|
@ -135,8 +143,12 @@ func (m ColumnsHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ColumnsHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m ColumnsHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ColumnsHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpiralHazardsMap struct{}
|
type SpiralHazardsMap struct{}
|
||||||
|
|
@ -163,8 +175,12 @@ func (m SpiralHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings
|
||||||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m SpiralHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m SpiralHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m SpiralHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -256,8 +272,12 @@ func (m ScatterFillMap) SetupBoard(lastBoardState *rules.BoardState, settings ru
|
||||||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ScatterFillMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m ScatterFillMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ScatterFillMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -308,8 +328,12 @@ func (m DirectionalExpandingBoxMap) SetupBoard(lastBoardState *rules.BoardState,
|
||||||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m DirectionalExpandingBoxMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m DirectionalExpandingBoxMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DirectionalExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -423,8 +447,12 @@ func (m ExpandingBoxMap) SetupBoard(lastBoardState *rules.BoardState, settings r
|
||||||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ExpandingBoxMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m ExpandingBoxMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -499,8 +527,12 @@ func (m ExpandingScatterMap) SetupBoard(lastBoardState *rules.BoardState, settin
|
||||||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ExpandingScatterMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m ExpandingScatterMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return (StandardMap{}).PreUpdateBoard(lastBoardState, settings, editor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ExpandingScatterMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ func TestSpiralHazardsMap(t *testing.T) {
|
||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
state.Turn = i
|
state.Turn = i
|
||||||
err = m.UpdateBoard(state, settings, editor)
|
err = m.PostUpdateBoard(state, settings, editor)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
require.NotEmpty(t, state.Hazards)
|
require.NotEmpty(t, state.Hazards)
|
||||||
|
|
@ -123,7 +123,7 @@ func TestScatterFillMap(t *testing.T) {
|
||||||
totalTurns := 11 * 11 * 2
|
totalTurns := 11 * 11 * 2
|
||||||
for i := 0; i < totalTurns; i++ {
|
for i := 0; i < totalTurns; i++ {
|
||||||
state.Turn = i
|
state.Turn = i
|
||||||
err = m.UpdateBoard(state, settings, editor)
|
err = m.PostUpdateBoard(state, settings, editor)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
require.NotEmpty(t, state.Hazards)
|
require.NotEmpty(t, state.Hazards)
|
||||||
|
|
@ -144,7 +144,7 @@ func TestDirectionalExpandingBoxMap(t *testing.T) {
|
||||||
totalTurns := 1000
|
totalTurns := 1000
|
||||||
for i := 0; i < totalTurns; i++ {
|
for i := 0; i < totalTurns; i++ {
|
||||||
state.Turn = i
|
state.Turn = i
|
||||||
err = m.UpdateBoard(state, settings, editor)
|
err = m.PostUpdateBoard(state, settings, editor)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
require.NotEmpty(t, state.Hazards)
|
require.NotEmpty(t, state.Hazards)
|
||||||
|
|
@ -165,7 +165,7 @@ func TestExpandingBoxMap(t *testing.T) {
|
||||||
totalTurns := 1000
|
totalTurns := 1000
|
||||||
for i := 0; i < totalTurns; i++ {
|
for i := 0; i < totalTurns; i++ {
|
||||||
state.Turn = i
|
state.Turn = i
|
||||||
err = m.UpdateBoard(state, settings, editor)
|
err = m.PostUpdateBoard(state, settings, editor)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
require.NotEmpty(t, state.Hazards)
|
require.NotEmpty(t, state.Hazards)
|
||||||
|
|
@ -186,7 +186,7 @@ func TestExpandingScatterMap(t *testing.T) {
|
||||||
totalTurns := 1000
|
totalTurns := 1000
|
||||||
for i := 0; i < totalTurns; i++ {
|
for i := 0; i < totalTurns; i++ {
|
||||||
state.Turn = i
|
state.Turn = i
|
||||||
err = m.UpdateBoard(state, settings, editor)
|
err = m.PostUpdateBoard(state, settings, editor)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
require.NotEmpty(t, state.Hazards)
|
require.NotEmpty(t, state.Hazards)
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,17 @@ func (m HealingPoolsMap) SetupBoard(initialBoardState *rules.BoardState, setting
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m HealingPoolsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m HealingPoolsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
if err := (StandardMap{}).UpdateBoard(lastBoardState, settings, editor); err != nil {
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m HealingPoolsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
if err := (StandardMap{}).PostUpdateBoard(lastBoardState, settings, editor); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if lastBoardState.Turn > 0 && settings.RoyaleSettings.ShrinkEveryNTurns > 0 && len(lastBoardState.Hazards) > 0 && lastBoardState.Turn%settings.RoyaleSettings.ShrinkEveryNTurns == 0 {
|
shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0)
|
||||||
|
if lastBoardState.Turn > 0 && shrinkEveryNTurns > 0 && len(lastBoardState.Hazards) > 0 && lastBoardState.Turn%shrinkEveryNTurns == 0 {
|
||||||
// Attempt to remove a healing pool every ShrinkEveryNTurns until there are none remaining
|
// Attempt to remove a healing pool every ShrinkEveryNTurns until there are none remaining
|
||||||
i := rand.Intn(len(lastBoardState.Hazards))
|
i := rand.Intn(len(lastBoardState.Hazards))
|
||||||
editor.RemoveHazard(lastBoardState.Hazards[i])
|
editor.RemoveHazard(lastBoardState.Hazards[i])
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ func TestHealingPoolsMap(t *testing.T) {
|
||||||
t.Run(fmt.Sprintf("%dx%d", tc.boardSize, tc.boardSize), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%dx%d", tc.boardSize, tc.boardSize), func(t *testing.T) {
|
||||||
m := maps.HealingPoolsMap{}
|
m := maps.HealingPoolsMap{}
|
||||||
state := rules.NewBoardState(tc.boardSize, tc.boardSize)
|
state := rules.NewBoardState(tc.boardSize, tc.boardSize)
|
||||||
settings := rules.Settings{}
|
shrinkEveryNTurns := 10
|
||||||
settings.RoyaleSettings.ShrinkEveryNTurns = 10
|
settings := rules.NewSettingsWithParams(rules.ParamShrinkEveryNTurns, fmt.Sprint(shrinkEveryNTurns))
|
||||||
|
|
||||||
// ensure the hazards are added to the board at setup
|
// ensure the hazards are added to the board at setup
|
||||||
editor := maps.NewBoardStateEditor(state)
|
editor := maps.NewBoardStateEditor(state)
|
||||||
|
|
@ -56,10 +56,10 @@ func TestHealingPoolsMap(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure the hazards are removed
|
// ensure the hazards are removed
|
||||||
totalTurns := settings.RoyaleSettings.ShrinkEveryNTurns*tc.expectedHazards + 1
|
totalTurns := shrinkEveryNTurns*tc.expectedHazards + 1
|
||||||
for i := 0; i < totalTurns; i++ {
|
for i := 0; i < totalTurns; i++ {
|
||||||
state.Turn = i
|
state.Turn = i
|
||||||
err = m.UpdateBoard(state, settings, editor)
|
err = m.PostUpdateBoard(state, settings, editor)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,17 +25,24 @@ func SetupBoard(mapID string, settings rules.Settings, width, height int, snakeI
|
||||||
return boardState, nil
|
return boardState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateBoard is a shortcut for looking up a map by ID and updating an existing board state with it.
|
// PreUpdateBoard updates a board state with a map.
|
||||||
func UpdateBoard(mapID string, previousBoardState *rules.BoardState, settings rules.Settings) (*rules.BoardState, error) {
|
func PreUpdateBoard(gameMap GameMap, previousBoardState *rules.BoardState, settings rules.Settings) (*rules.BoardState, error) {
|
||||||
gameMap, err := GetMap(mapID)
|
nextBoardState := previousBoardState.Clone()
|
||||||
|
editor := NewBoardStateEditor(nextBoardState)
|
||||||
|
|
||||||
|
err := gameMap.PreUpdateBoard(previousBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nextBoardState, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostUpdateBoard(gameMap GameMap, previousBoardState *rules.BoardState, settings rules.Settings) (*rules.BoardState, error) {
|
||||||
nextBoardState := previousBoardState.Clone()
|
nextBoardState := previousBoardState.Clone()
|
||||||
editor := NewBoardStateEditor(nextBoardState)
|
editor := NewBoardStateEditor(nextBoardState)
|
||||||
|
|
||||||
err = gameMap.UpdateBoard(previousBoardState, settings, editor)
|
err := gameMap.PostUpdateBoard(previousBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +84,11 @@ func (m StubMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m StubMap) UpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m StubMap) PreUpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m StubMap) PostUpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
if m.Error != nil {
|
if m.Error != nil {
|
||||||
return m.Error
|
return m.Error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,10 @@ func TestUpdateBoard(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
previousBoardState := &rules.BoardState{
|
previousBoardState := rules.NewBoardState(5, 5).
|
||||||
Turn: 0,
|
WithFood([]rules.Point{{X: 0, Y: 1}}).
|
||||||
Food: []rules.Point{{X: 0, Y: 1}},
|
WithHazards([]rules.Point{{X: 3, Y: 4}}).
|
||||||
Hazards: []rules.Point{{X: 3, Y: 4}},
|
WithSnakes([]rules.Snake{
|
||||||
Snakes: []rules.Snake{
|
|
||||||
{
|
{
|
||||||
ID: "1",
|
ID: "1",
|
||||||
Health: 100,
|
Health: 100,
|
||||||
|
|
@ -96,11 +95,9 @@ func TestUpdateBoard(t *testing.T) {
|
||||||
{X: 6, Y: 2},
|
{X: 6, Y: 2},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
}
|
|
||||||
|
|
||||||
maps.TestMap(testMap.ID(), testMap, func() {
|
maps.TestMap(testMap.ID(), testMap, func() {
|
||||||
boardState, err := maps.UpdateBoard(testMap.ID(), previousBoardState, rules.Settings{})
|
boardState, err := maps.PostUpdateBoard(testMap, previousBoardState, rules.Settings{})
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,12 @@ import (
|
||||||
|
|
||||||
const maxBoardWidth, maxBoardHeight = 25, 25
|
const maxBoardWidth, maxBoardHeight = 25, 25
|
||||||
|
|
||||||
var testSettings rules.Settings = rules.Settings{
|
var testSettings rules.Settings = rules.NewSettings(map[string]string{
|
||||||
FoodSpawnChance: 25,
|
rules.ParamFoodSpawnChance: "25",
|
||||||
MinimumFood: 1,
|
rules.ParamMinimumFood: "1",
|
||||||
HazardDamagePerTurn: 14,
|
rules.ParamHazardDamagePerTurn: "14",
|
||||||
RoyaleSettings: rules.RoyaleSettings{
|
rules.ParamShrinkEveryNTurns: "1",
|
||||||
ShrinkEveryNTurns: 1,
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegisteredMaps(t *testing.T) {
|
func TestRegisteredMaps(t *testing.T) {
|
||||||
for mapName, gameMap := range globalRegistry {
|
for mapName, gameMap := range globalRegistry {
|
||||||
|
|
@ -96,7 +94,7 @@ func TestRegisteredMaps(t *testing.T) {
|
||||||
|
|
||||||
passedBoardState := previousBoardState.Clone()
|
passedBoardState := previousBoardState.Clone()
|
||||||
tempBoardState := previousBoardState.Clone()
|
tempBoardState := previousBoardState.Clone()
|
||||||
err := gameMap.UpdateBoard(passedBoardState, testSettings, NewBoardStateEditor(tempBoardState))
|
err := gameMap.PostUpdateBoard(passedBoardState, testSettings, NewBoardStateEditor(tempBoardState))
|
||||||
require.NoError(t, err, "GameMap.UpdateBoard returned an error")
|
require.NoError(t, err, "GameMap.UpdateBoard returned an error")
|
||||||
require.Equal(t, previousBoardState, passedBoardState, "BoardState should not be modified directly by GameMap.UpdateBoard")
|
require.Equal(t, previousBoardState, passedBoardState, "BoardState should not be modified directly by GameMap.UpdateBoard")
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,11 @@ func (m RiverAndBridgesMediumHazardsMap) SetupBoard(initialBoardState *rules.Boa
|
||||||
return setupRiverAndBridgesBoard(riversAndBridgesMediumStartPositions, riversAndBridgesMediumHazards, initialBoardState, settings, editor)
|
return setupRiverAndBridgesBoard(riversAndBridgesMediumStartPositions, riversAndBridgesMediumHazards, initialBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m RiverAndBridgesMediumHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m RiverAndBridgesMediumHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m RiverAndBridgesMediumHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +146,11 @@ func (m RiverAndBridgesLargeHazardsMap) SetupBoard(initialBoardState *rules.Boar
|
||||||
return setupRiverAndBridgesBoard(riversAndBridgesLargeStartPositions, riversAndBridgesLargeHazards, initialBoardState, settings, editor)
|
return setupRiverAndBridgesBoard(riversAndBridgesLargeStartPositions, riversAndBridgesLargeHazards, initialBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m RiverAndBridgesLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m RiverAndBridgesLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m RiverAndBridgesLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,7 +249,11 @@ func (m RiverAndBridgesExtraLargeHazardsMap) SetupBoard(initialBoardState *rules
|
||||||
return setupRiverAndBridgesBoard(riversAndBridgesExtraLargeStartPositions, riversAndBridgesExtraLargeHazards, initialBoardState, settings, editor)
|
return setupRiverAndBridgesBoard(riversAndBridgesExtraLargeStartPositions, riversAndBridgesExtraLargeHazards, initialBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m RiverAndBridgesExtraLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m RiverAndBridgesExtraLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m RiverAndBridgesExtraLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,7 +367,11 @@ func (m IslandsAndBridgesMediumHazardsMap) SetupBoard(initialBoardState *rules.B
|
||||||
return setupRiverAndBridgesBoard(islandsAndBridgesMediumStartPositions, islandsAndBridgesMediumHazards, initialBoardState, settings, editor)
|
return setupRiverAndBridgesBoard(islandsAndBridgesMediumStartPositions, islandsAndBridgesMediumHazards, initialBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m IslandsAndBridgesMediumHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m IslandsAndBridgesMediumHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m IslandsAndBridgesMediumHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -441,7 +457,11 @@ func (m IslandsAndBridgesLargeHazardsMap) SetupBoard(initialBoardState *rules.Bo
|
||||||
return setupRiverAndBridgesBoard(islandsAndBridgesLargeStartPositions, islandsAndBridgesLargeHazards, initialBoardState, settings, editor)
|
return setupRiverAndBridgesBoard(islandsAndBridgesLargeStartPositions, islandsAndBridgesLargeHazards, initialBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m IslandsAndBridgesLargeHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m IslandsAndBridgesLargeHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m IslandsAndBridgesLargeHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
return placeRiverAndBridgesFood(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,20 +33,25 @@ func (m RoyaleHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings
|
||||||
return StandardMap{}.SetupBoard(lastBoardState, settings, editor)
|
return StandardMap{}.SetupBoard(lastBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m RoyaleHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m RoyaleHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m RoyaleHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
// Use StandardMap to populate food
|
// Use StandardMap to populate food
|
||||||
if err := (StandardMap{}).UpdateBoard(lastBoardState, settings, editor); err != nil {
|
if err := (StandardMap{}).PostUpdateBoard(lastBoardState, settings, editor); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
||||||
turn := lastBoardState.Turn + 1
|
turn := lastBoardState.Turn + 1
|
||||||
|
|
||||||
if settings.RoyaleSettings.ShrinkEveryNTurns < 1 {
|
shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0)
|
||||||
|
if shrinkEveryNTurns < 1 {
|
||||||
return errors.New("royale game can't shrink more frequently than every turn")
|
return errors.New("royale game can't shrink more frequently than every turn")
|
||||||
}
|
}
|
||||||
|
|
||||||
if turn < settings.RoyaleSettings.ShrinkEveryNTurns {
|
if turn < shrinkEveryNTurns {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +61,7 @@ func (m RoyaleHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings
|
||||||
// Get random generator for turn zero, because we're regenerating all hazards every time.
|
// Get random generator for turn zero, because we're regenerating all hazards every time.
|
||||||
randGenerator := settings.GetRand(0)
|
randGenerator := settings.GetRand(0)
|
||||||
|
|
||||||
numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns
|
numShrinks := turn / shrinkEveryNTurns
|
||||||
minX, maxX := 0, lastBoardState.Width-1
|
minX, maxX := 0, lastBoardState.Width-1
|
||||||
minY, maxY := 0, lastBoardState.Height-1
|
minY, maxY := 0, lastBoardState.Height-1
|
||||||
for i := 0; i < numShrinks; i++ {
|
for i := 0; i < numShrinks; i++ {
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,12 @@ func (m SinkholesMap) SetupBoard(initialBoardState *rules.BoardState, settings r
|
||||||
return (StandardMap{}).SetupBoard(initialBoardState, settings, editor)
|
return (StandardMap{}).SetupBoard(initialBoardState, settings, editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m SinkholesMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m SinkholesMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m SinkholesMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -42,8 +46,9 @@ func (m SinkholesMap) UpdateBoard(lastBoardState *rules.BoardState, settings rul
|
||||||
currentTurn := lastBoardState.Turn
|
currentTurn := lastBoardState.Turn
|
||||||
startTurn := 1
|
startTurn := 1
|
||||||
spawnEveryNTurns := 10
|
spawnEveryNTurns := 10
|
||||||
if settings.RoyaleSettings.ShrinkEveryNTurns > 0 {
|
shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0)
|
||||||
spawnEveryNTurns = settings.RoyaleSettings.ShrinkEveryNTurns
|
if shrinkEveryNTurns > 0 {
|
||||||
|
spawnEveryNTurns = shrinkEveryNTurns
|
||||||
}
|
}
|
||||||
maxRings := 5
|
maxRings := 5
|
||||||
if lastBoardState.Width == 7 {
|
if lastBoardState.Width == 7 {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ func TestSinkholesMap(t *testing.T) {
|
||||||
totalTurns := 100
|
totalTurns := 100
|
||||||
for i := 0; i < totalTurns; i++ {
|
for i := 0; i < totalTurns; i++ {
|
||||||
state.Turn = i
|
state.Turn = i
|
||||||
err = m.UpdateBoard(state, settings, editor)
|
err = m.PostUpdateBoard(state, settings, editor)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
require.NotEmpty(t, state.Hazards)
|
require.NotEmpty(t, state.Hazards)
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,22 @@ import (
|
||||||
"github.com/BattlesnakeOfficial/rules"
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SnailModeMap struct{}
|
type SnailModeMap struct {
|
||||||
|
lastTailPositions map[rules.Point]int // local state is preserved during the turn
|
||||||
|
}
|
||||||
|
|
||||||
// init registers this map in the global registry.
|
// init registers this map in the global registry.
|
||||||
func init() {
|
func init() {
|
||||||
globalRegistry.RegisterMap("snail_mode", SnailModeMap{})
|
globalRegistry.RegisterMap("snail_mode", &SnailModeMap{lastTailPositions: nil})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns a unique identifier for this map.
|
// ID returns a unique identifier for this map.
|
||||||
func (m SnailModeMap) ID() string {
|
func (m *SnailModeMap) ID() string {
|
||||||
return "snail_mode"
|
return "snail_mode"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Meta returns the non-functional metadata about this map.
|
// Meta returns the non-functional metadata about this map.
|
||||||
func (m SnailModeMap) Meta() Metadata {
|
func (m *SnailModeMap) Meta() Metadata {
|
||||||
return Metadata{
|
return Metadata{
|
||||||
Name: "Snail Mode",
|
Name: "Snail Mode",
|
||||||
Description: "Snakes leave behind a trail of hazards",
|
Description: "Snakes leave behind a trail of hazards",
|
||||||
|
|
@ -31,7 +33,7 @@ func (m SnailModeMap) Meta() Metadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupBoard here is pretty 'standard' and doesn't do any special setup for this game mode
|
// SetupBoard here is pretty 'standard' and doesn't do any special setup for this game mode
|
||||||
func (m SnailModeMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m *SnailModeMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
rand := settings.GetRand(0)
|
rand := settings.GetRand(0)
|
||||||
|
|
||||||
if len(initialBoardState.Snakes) > int(m.Meta().MaxPlayers) {
|
if len(initialBoardState.Snakes) > int(m.Meta().MaxPlayers) {
|
||||||
|
|
@ -57,23 +59,6 @@ func (m SnailModeMap) SetupBoard(initialBoardState *rules.BoardState, settings r
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// storeTailLocation returns an offboard point that corresponds to the given point.
|
|
||||||
// This is useful for storing state that can be accessed next turn.
|
|
||||||
func storeTailLocation(point rules.Point, height int) rules.Point {
|
|
||||||
return rules.Point{X: point.X, Y: point.Y + height}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPrevTailLocation returns the onboard point that corresponds to an offboard point.
|
|
||||||
// This is useful for restoring state that was stored last turn.
|
|
||||||
func getPrevTailLocation(point rules.Point, height int) rules.Point {
|
|
||||||
return rules.Point{X: point.X, Y: point.Y - height}
|
|
||||||
}
|
|
||||||
|
|
||||||
// outOfBounds determines if the given point is out of bounds for the current board size
|
|
||||||
func outOfBounds(p rules.Point, w, h int) bool {
|
|
||||||
return p.X < 0 || p.Y < 0 || p.X >= w || p.Y >= h
|
|
||||||
}
|
|
||||||
|
|
||||||
// doubleTail determine if the snake has a double stacked tail currently
|
// doubleTail determine if the snake has a double stacked tail currently
|
||||||
func doubleTail(snake *rules.Snake) bool {
|
func doubleTail(snake *rules.Snake) bool {
|
||||||
almostTail := snake.Body[len(snake.Body)-2]
|
almostTail := snake.Body[len(snake.Body)-2]
|
||||||
|
|
@ -86,12 +71,28 @@ func isEliminated(s *rules.Snake) bool {
|
||||||
return s.EliminatedCause != rules.NotEliminated
|
return s.EliminatedCause != rules.NotEliminated
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateBoard does the work of placing the hazards along the 'snail tail' of snakes
|
// PreUpdateBoard stores the tail position of each snake in memory, to be
|
||||||
// This is responsible for saving the current tail location off the board
|
// able to place hazards there after the snakes move.
|
||||||
// and restoring the previous tail position. This also handles removing one hazards from
|
func (m *SnailModeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
// the current stacks so the hazards tails fade as the snake moves away.
|
m.lastTailPositions = make(map[rules.Point]int)
|
||||||
func (m SnailModeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
for _, snake := range lastBoardState.Snakes {
|
||||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
if isEliminated(&snake) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Double tail means that the tail will stay on the same square for more
|
||||||
|
// than one turn, so we don't want to spawn hazards
|
||||||
|
if doubleTail(&snake) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.lastTailPositions[snake.Body[len(snake.Body)-1]] = len(snake.Body)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostUpdateBoard does the work of placing the hazards along the 'snail tail' of snakes
|
||||||
|
// This also handles removing one hazards from the current stacks so the hazards tails fade as the snake moves away.
|
||||||
|
func (m *SnailModeMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -100,79 +101,38 @@ func (m SnailModeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rul
|
||||||
// need to be cleared first.
|
// need to be cleared first.
|
||||||
editor.ClearHazards()
|
editor.ClearHazards()
|
||||||
|
|
||||||
// This is a list of all the hazards we want to add for the previous tails
|
|
||||||
// These were stored off board in the previous turn as a way to save state
|
|
||||||
// When we add the locations to this list we have already converted the off-board
|
|
||||||
// points to on-board points
|
|
||||||
tailLocations := make([]rules.Point, 0, len(lastBoardState.Snakes))
|
|
||||||
|
|
||||||
// Count the number of hazards for a given position
|
// Count the number of hazards for a given position
|
||||||
// Add non-double tail locations to a slice
|
|
||||||
hazardCounts := map[rules.Point]int{}
|
hazardCounts := map[rules.Point]int{}
|
||||||
for _, hazard := range lastBoardState.Hazards {
|
for _, hazard := range lastBoardState.Hazards {
|
||||||
|
|
||||||
// discard out of bound
|
|
||||||
if outOfBounds(hazard, lastBoardState.Width, lastBoardState.Height) {
|
|
||||||
onBoardTail := getPrevTailLocation(hazard, lastBoardState.Height)
|
|
||||||
tailLocations = append(tailLocations, onBoardTail)
|
|
||||||
} else {
|
|
||||||
hazardCounts[hazard]++
|
hazardCounts[hazard]++
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add back existing hazards, but with a stack of 1 less than before.
|
// Add back existing hazards, but with a stack of 1 less than before.
|
||||||
// This has the effect of making the snail-trail disappear over time.
|
// This has the effect of making the snail-trail disappear over time.
|
||||||
for hazard, count := range hazardCounts {
|
for hazard, count := range hazardCounts {
|
||||||
|
|
||||||
for i := 0; i < count-1; i++ {
|
for i := 0; i < count-1; i++ {
|
||||||
editor.AddHazard(hazard)
|
editor.AddHazard(hazard)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store a stack of hazards for the tail of each snake. This is stored out
|
// Place a new stack of hazards where each snake's tail used to be
|
||||||
// of bounds and then applied on the next turn. The stack count is equal
|
NewHazardLoop:
|
||||||
// the lenght of the snake.
|
for location, count := range m.lastTailPositions {
|
||||||
for _, snake := range lastBoardState.Snakes {
|
|
||||||
if isEliminated(&snake) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Double tail means that the tail will stay on the same square for more
|
|
||||||
// than one turn, so we don't want to spawn hazards
|
|
||||||
if doubleTail(&snake) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tail := snake.Body[len(snake.Body)-1]
|
|
||||||
offBoardTail := storeTailLocation(tail, lastBoardState.Height)
|
|
||||||
for i := 0; i < len(snake.Body); i++ {
|
|
||||||
editor.AddHazard(offBoardTail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read offboard tails and move them to the board. The offboard tails are
|
|
||||||
// stacked based on the length of the snake
|
|
||||||
for _, p := range tailLocations {
|
|
||||||
|
|
||||||
// Skip position if a snakes head occupies it.
|
|
||||||
// Otherwise hazard shows up in the viewer on top of a snake head, but
|
|
||||||
// does not damage the snake, which is visually confusing.
|
|
||||||
isHead := false
|
|
||||||
for _, snake := range lastBoardState.Snakes {
|
for _, snake := range lastBoardState.Snakes {
|
||||||
if isEliminated(&snake) {
|
if isEliminated(&snake) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
head := snake.Body[0]
|
head := snake.Body[0]
|
||||||
if p.X == head.X && p.Y == head.Y {
|
if location.X == head.X && location.Y == head.Y {
|
||||||
isHead = true
|
// Skip position if a snakes head occupies it.
|
||||||
break
|
// Otherwise hazard shows up in the viewer on top of a snake head, but
|
||||||
|
// does not damage the snake, which is visually confusing.
|
||||||
|
continue NewHazardLoop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isHead {
|
for i := 0; i < count; i++ {
|
||||||
continue
|
editor.AddHazard(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.AddHazard(p)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,11 @@ func (m SoloMazeMap) PlaceFood(boardState *rules.BoardState, settings rules.Sett
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m SoloMazeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m SoloMazeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m SoloMazeMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
currentLevel, e := m.ReadBitState(lastBoardState)
|
currentLevel, e := m.ReadBitState(lastBoardState)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,11 @@ func (m StandardMap) SetupBoard(initialBoardState *rules.BoardState, settings ru
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m StandardMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
func (m StandardMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m StandardMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||||
rand := settings.GetRand(lastBoardState.Turn)
|
rand := settings.GetRand(lastBoardState.Turn)
|
||||||
|
|
||||||
foodNeeded := checkFoodNeedingPlacement(rand, settings, lastBoardState)
|
foodNeeded := checkFoodNeedingPlacement(rand, settings, lastBoardState)
|
||||||
|
|
@ -69,8 +73,8 @@ func (m StandardMap) UpdateBoard(lastBoardState *rules.BoardState, settings rule
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkFoodNeedingPlacement(rand rules.Rand, settings rules.Settings, state *rules.BoardState) int {
|
func checkFoodNeedingPlacement(rand rules.Rand, settings rules.Settings, state *rules.BoardState) int {
|
||||||
minFood := int(settings.MinimumFood)
|
minFood := settings.Int(rules.ParamMinimumFood, 0)
|
||||||
foodSpawnChance := int(settings.FoodSpawnChance)
|
foodSpawnChance := settings.Int(rules.ParamFoodSpawnChance, 0)
|
||||||
numCurrentFood := len(state.Food)
|
numCurrentFood := len(state.Food)
|
||||||
|
|
||||||
if numCurrentFood < minFood {
|
if numCurrentFood < minFood {
|
||||||
|
|
|
||||||
|
|
@ -29,65 +29,29 @@ func TestStandardMapSetupBoard(t *testing.T) {
|
||||||
"empty 7x7",
|
"empty 7x7",
|
||||||
rules.NewBoardState(7, 7),
|
rules.NewBoardState(7, 7),
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(7, 7).WithFood([]rules.Point{{X: 3, Y: 3}}),
|
||||||
Width: 7,
|
|
||||||
Height: 7,
|
|
||||||
Snakes: []rules.Snake{},
|
|
||||||
Food: []rules.Point{{X: 3, Y: 3}},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"not enough room for snakes 7x7",
|
"not enough room for snakes 7x7",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(7, 7).WithSnakes(generateSnakes(17)),
|
||||||
Width: 7,
|
|
||||||
Height: 7,
|
|
||||||
Snakes: generateSnakes(17),
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
nil,
|
nil,
|
||||||
rules.ErrorTooManySnakes,
|
rules.ErrorTooManySnakes,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"not enough room for snakes 5x5",
|
"not enough room for snakes 5x5",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(5, 5).WithSnakes(generateSnakes(14)),
|
||||||
Width: 5,
|
|
||||||
Height: 5,
|
|
||||||
Snakes: generateSnakes(14),
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
nil,
|
nil,
|
||||||
rules.ErrorTooManySnakes,
|
rules.ErrorTooManySnakes,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"full 11x11 min",
|
"full 11x11 min",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)),
|
||||||
Width: 11,
|
|
||||||
Height: 11,
|
|
||||||
Snakes: generateSnakes(8),
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(11, 11).
|
||||||
Width: 11,
|
WithFood([]rules.Point{
|
||||||
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{
|
|
||||||
{X: 0, Y: 2},
|
{X: 0, Y: 2},
|
||||||
{X: 0, Y: 8},
|
{X: 0, Y: 8},
|
||||||
{X: 8, Y: 0},
|
{X: 8, Y: 0},
|
||||||
|
|
@ -97,35 +61,25 @@ func TestStandardMapSetupBoard(t *testing.T) {
|
||||||
{X: 4, Y: 10},
|
{X: 4, Y: 10},
|
||||||
{X: 10, Y: 4},
|
{X: 10, Y: 4},
|
||||||
{X: 5, Y: 5},
|
{X: 5, Y: 5},
|
||||||
},
|
}).
|
||||||
Hazards: []rules.Point{},
|
WithSnakes([]rules.Snake{
|
||||||
},
|
{ID: "1", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100},
|
||||||
|
{ID: "2", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
||||||
|
{ID: "3", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
||||||
|
{ID: "4", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
||||||
|
{ID: "5", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100},
|
||||||
|
{ID: "6", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
||||||
|
{ID: "7", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
||||||
|
{ID: "8", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||||
|
}),
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"full 11x11 max",
|
"full 11x11 max",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)),
|
||||||
Width: 11,
|
|
||||||
Height: 11,
|
|
||||||
Snakes: generateSnakes(8),
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
rules.MaxRand,
|
rules.MaxRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(11, 11).
|
||||||
Width: 11,
|
WithFood([]rules.Point{
|
||||||
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{
|
|
||||||
{X: 6, Y: 0},
|
{X: 6, Y: 0},
|
||||||
{X: 6, Y: 10},
|
{X: 6, Y: 10},
|
||||||
{X: 10, Y: 6},
|
{X: 10, Y: 6},
|
||||||
|
|
@ -135,9 +89,17 @@ func TestStandardMapSetupBoard(t *testing.T) {
|
||||||
{X: 10, Y: 8},
|
{X: 10, Y: 8},
|
||||||
{X: 2, Y: 0},
|
{X: 2, Y: 0},
|
||||||
{X: 5, Y: 5},
|
{X: 5, Y: 5},
|
||||||
},
|
}).
|
||||||
Hazards: []rules.Point{},
|
WithSnakes([]rules.Snake{
|
||||||
},
|
{ID: "1", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
||||||
|
{ID: "2", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
||||||
|
{ID: "3", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||||
|
{ID: "4", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100},
|
||||||
|
{ID: "5", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
||||||
|
{ID: "6", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
||||||
|
{ID: "7", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
||||||
|
{ID: "8", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100},
|
||||||
|
}),
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -172,132 +134,51 @@ func TestStandardMapUpdateBoard(t *testing.T) {
|
||||||
{
|
{
|
||||||
"empty no food",
|
"empty no food",
|
||||||
rules.NewBoardState(2, 2),
|
rules.NewBoardState(2, 2),
|
||||||
rules.Settings{
|
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "0", rules.ParamMinimumFood, "0"),
|
||||||
FoodSpawnChance: 0,
|
|
||||||
MinimumFood: 0,
|
|
||||||
},
|
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2),
|
||||||
Width: 2,
|
|
||||||
Height: 2,
|
|
||||||
Snakes: []rules.Snake{},
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"empty MinimumFood",
|
"empty MinimumFood",
|
||||||
rules.NewBoardState(2, 2),
|
rules.NewBoardState(2, 2),
|
||||||
rules.Settings{
|
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "0", rules.ParamMinimumFood, "2"),
|
||||||
FoodSpawnChance: 0,
|
|
||||||
MinimumFood: 2,
|
|
||||||
},
|
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}}),
|
||||||
Width: 2,
|
|
||||||
Height: 2,
|
|
||||||
Snakes: []rules.Snake{},
|
|
||||||
Food: []rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"not empty MinimumFood",
|
"not empty MinimumFood",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 1}}),
|
||||||
Width: 2,
|
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "0", rules.ParamMinimumFood, "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.MinRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 1}, {X: 0, Y: 0}}),
|
||||||
Width: 2,
|
|
||||||
Height: 2,
|
|
||||||
Snakes: []rules.Snake{},
|
|
||||||
Food: []rules.Point{{X: 0, Y: 1}, {X: 0, Y: 0}},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"empty FoodSpawnChance inactive",
|
"empty FoodSpawnChance inactive",
|
||||||
rules.NewBoardState(2, 2),
|
rules.NewBoardState(2, 2),
|
||||||
rules.Settings{
|
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"),
|
||||||
FoodSpawnChance: 50,
|
|
||||||
MinimumFood: 0,
|
|
||||||
},
|
|
||||||
rules.MinRand,
|
rules.MinRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2),
|
||||||
Width: 2,
|
|
||||||
Height: 2,
|
|
||||||
Snakes: []rules.Snake{},
|
|
||||||
Food: []rules.Point{},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"empty FoodSpawnChance active",
|
"empty FoodSpawnChance active",
|
||||||
rules.NewBoardState(2, 2),
|
rules.NewBoardState(2, 2),
|
||||||
rules.Settings{
|
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"),
|
||||||
FoodSpawnChance: 50,
|
|
||||||
MinimumFood: 0,
|
|
||||||
},
|
|
||||||
rules.MaxRand,
|
rules.MaxRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 1}}),
|
||||||
Width: 2,
|
|
||||||
Height: 2,
|
|
||||||
Snakes: []rules.Snake{},
|
|
||||||
Food: []rules.Point{{X: 0, Y: 1}},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"not empty FoodSpawnChance active",
|
"not empty FoodSpawnChance active",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}}),
|
||||||
Width: 2,
|
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"),
|
||||||
Height: 2,
|
|
||||||
Snakes: []rules.Snake{},
|
|
||||||
Food: []rules.Point{{X: 0, Y: 0}},
|
|
||||||
Hazards: []rules.Point{},
|
|
||||||
},
|
|
||||||
rules.Settings{
|
|
||||||
FoodSpawnChance: 50,
|
|
||||||
MinimumFood: 0,
|
|
||||||
},
|
|
||||||
rules.MaxRand,
|
rules.MaxRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}}),
|
||||||
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",
|
"not empty FoodSpawnChance no room",
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}}),
|
||||||
Width: 2,
|
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"),
|
||||||
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.MaxRand,
|
||||||
&rules.BoardState{
|
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}}),
|
||||||
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 {
|
for _, test := range tests {
|
||||||
|
|
@ -306,7 +187,7 @@ func TestStandardMapUpdateBoard(t *testing.T) {
|
||||||
settings := test.settings.WithRand(test.rand)
|
settings := test.settings.WithRand(test.rand)
|
||||||
editor := maps.NewBoardStateEditor(nextBoardState)
|
editor := maps.NewBoardStateEditor(nextBoardState)
|
||||||
|
|
||||||
err := m.UpdateBoard(test.initialBoardState.Clone(), settings, editor)
|
err := m.PostUpdateBoard(test.initialBoardState.Clone(), settings, editor)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.expected, nextBoardState)
|
require.Equal(t, test.expected, nextBoardState)
|
||||||
|
|
|
||||||
61
pipeline.go
61
pipeline.go
|
|
@ -38,6 +38,33 @@ var globalRegistry = StageRegistry{
|
||||||
StageMovementWrapBoundaries: MoveSnakesWrapped,
|
StageMovementWrapBoundaries: MoveSnakesWrapped,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pipeline is an ordered sequences of game stages which are executed to produce the
|
||||||
|
// next game state.
|
||||||
|
//
|
||||||
|
// If a stage produces an error or an ended game state, the pipeline is halted at that stage.
|
||||||
|
type Pipeline interface {
|
||||||
|
// Execute runs the pipeline stages and produces a next game state.
|
||||||
|
//
|
||||||
|
// If any stage produces an error or an ended game state, the pipeline
|
||||||
|
// immediately stops at that stage.
|
||||||
|
//
|
||||||
|
// Errors should be checked and the other results ignored if error is non-nil.
|
||||||
|
//
|
||||||
|
// If the pipeline is already in an error state (this can be checked by calling Err()),
|
||||||
|
// this error will be immediately returned and the pipeline will not run.
|
||||||
|
//
|
||||||
|
// After the pipeline runs, the results will be the result of the last stage that was executed.
|
||||||
|
Execute(*BoardState, Settings, []SnakeMove) (bool, *BoardState, error)
|
||||||
|
|
||||||
|
// Err provides a way to check for errors before/without calling Execute.
|
||||||
|
// Err returns an error if the Pipeline is in an error state.
|
||||||
|
// If this error is not nil, this error will also be returned from Execute, so it is
|
||||||
|
// optional to call Err.
|
||||||
|
// The idea is to reduce error-checking verbosity for the majority of cases where a
|
||||||
|
// Pipeline is immediately executed after construction (i.e. NewPipeline(...).Execute(...)).
|
||||||
|
Err() error
|
||||||
|
}
|
||||||
|
|
||||||
// StageFunc represents a single stage of an ordered pipeline and applies custom logic to the board state each turn.
|
// 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.
|
// 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)
|
// The return values are a boolean (to indicate whether the game has ended as a result of the stage)
|
||||||
|
|
@ -46,6 +73,14 @@ var globalRegistry = StageRegistry{
|
||||||
// Errors should be treated as meaning the stage failed and the board state is now invalid.
|
// Errors should be treated as meaning the stage failed and the board state is now invalid.
|
||||||
type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error)
|
type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error)
|
||||||
|
|
||||||
|
// IsInitialization checks whether the current state means the game is initialising (turn zero).
|
||||||
|
// Useful for StageFuncs that need to apply different behaviour on initialisation.
|
||||||
|
func IsInitialization(b *BoardState, settings Settings, moves []SnakeMove) bool {
|
||||||
|
// We can safely assume that the game state is in the initialisation phase when
|
||||||
|
// the turn hasn't advanced and the moves are empty
|
||||||
|
return b.Turn <= 0 && len(moves) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// StageRegistry is a mapping of stage names to stage functions
|
// StageRegistry is a mapping of stage names to stage functions
|
||||||
type StageRegistry map[string]StageFunc
|
type StageRegistry map[string]StageFunc
|
||||||
|
|
||||||
|
|
@ -76,32 +111,6 @@ func RegisterPipelineStage(s string, fn StageFunc) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pipeline is an ordered sequences of game stages which are executed to produce the
|
|
||||||
// next game state.
|
|
||||||
//
|
|
||||||
// If a stage produces an error or an ended game state, the pipeline is halted at that stage.
|
|
||||||
type Pipeline interface {
|
|
||||||
// Execute runs the pipeline stages and produces a next game state.
|
|
||||||
//
|
|
||||||
// If any stage produces an error or an ended game state, the pipeline
|
|
||||||
// immediately stops at that stage.
|
|
||||||
//
|
|
||||||
// Errors should be checked and the other results ignored if error is non-nil.
|
|
||||||
//
|
|
||||||
// If the pipeline is already in an error state (this can be checked by calling Err()),
|
|
||||||
// this error will be immediately returned and the pipeline will not run.
|
|
||||||
//
|
|
||||||
// After the pipeline runs, the results will be the result of the last stage that was executed.
|
|
||||||
Execute(*BoardState, Settings, []SnakeMove) (bool, *BoardState, error)
|
|
||||||
// Err provides a way to check for errors before/without calling Execute.
|
|
||||||
// Err returns an error if the Pipeline is in an error state.
|
|
||||||
// If this error is not nil, this error will also be returned from Execute, so it is
|
|
||||||
// optional to call Err.
|
|
||||||
// The idea is to reduce error-checking verbosity for the majority of cases where a
|
|
||||||
// Pipeline is immediately executed after construction (i.e. NewPipeline(...).Execute(...)).
|
|
||||||
Err() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// pipeline is an implementation of Pipeline
|
// pipeline is an implementation of Pipeline
|
||||||
type pipeline struct {
|
type pipeline struct {
|
||||||
// stages is a list of stages that should be executed from slice start to end
|
// stages is a list of stages that should be executed from slice start to end
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ func TestPipelineRuleset(t *testing.T) {
|
||||||
name: "test",
|
name: "test",
|
||||||
pipeline: p,
|
pipeline: p,
|
||||||
}
|
}
|
||||||
ended, err := pr.IsGameOver(&BoardState{})
|
ended, _, err := pr.Execute(&BoardState{}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, ended)
|
require.True(t, ended)
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ func TestPipelineRuleset(t *testing.T) {
|
||||||
name: "test",
|
name: "test",
|
||||||
pipeline: p,
|
pipeline: p,
|
||||||
}
|
}
|
||||||
ended, err = pr.IsGameOver(&BoardState{})
|
ended, _, err = pr.Execute(&BoardState{}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.False(t, ended)
|
require.False(t, ended)
|
||||||
|
|
||||||
|
|
@ -56,10 +56,10 @@ func TestPipelineRuleset(t *testing.T) {
|
||||||
pipeline: p,
|
pipeline: p,
|
||||||
}
|
}
|
||||||
require.Empty(t, b.Food)
|
require.Empty(t, b.Food)
|
||||||
b, err = pr.ModifyInitialBoardState(b)
|
_, b, err = pr.Execute(b, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, b.Food, "food should not be added on initialisation phase")
|
require.Empty(t, b.Food, "food should not be added on initialisation phase")
|
||||||
b, err = pr.CreateNextBoardState(b, mockSnakeMoves())
|
_, b, err = pr.Execute(b, mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, b.Food, "fodo should be added now")
|
require.NotEmpty(t, b.Food, "fodo should be added now")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,17 @@ func TestPipeline(t *testing.T) {
|
||||||
r.RegisterPipelineStage("astage", mockStageFn(false, nil))
|
r.RegisterPipelineStage("astage", mockStageFn(false, nil))
|
||||||
p = rules.NewPipelineFromRegistry(r)
|
p = rules.NewPipelineFromRegistry(r)
|
||||||
require.Equal(t, rules.ErrorNoStages, p.Err())
|
require.Equal(t, rules.ErrorNoStages, p.Err())
|
||||||
_, _, err = p.Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
_, _, err = p.Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil)
|
||||||
require.Equal(t, rules.ErrorNoStages, err)
|
require.Equal(t, rules.ErrorNoStages, err)
|
||||||
|
|
||||||
// test that an unregistered stage name errors
|
// test that an unregistered stage name errors
|
||||||
p = rules.NewPipelineFromRegistry(r, "doesntexist")
|
p = rules.NewPipelineFromRegistry(r, "doesntexist")
|
||||||
_, _, err = p.Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
_, _, err = p.Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil)
|
||||||
require.Equal(t, rules.ErrorStageNotFound, p.Err())
|
require.Equal(t, rules.ErrorStageNotFound, p.Err())
|
||||||
require.Equal(t, rules.ErrorStageNotFound, err)
|
require.Equal(t, rules.ErrorStageNotFound, err)
|
||||||
|
|
||||||
// simplest case - one stage
|
// simplest case - one stage
|
||||||
ended, next, err := rules.NewPipelineFromRegistry(r, "astage").Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
ended, next, err := rules.NewPipelineFromRegistry(r, "astage").Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, next)
|
require.NotNil(t, next)
|
||||||
|
|
@ -39,20 +39,20 @@ func TestPipeline(t *testing.T) {
|
||||||
|
|
||||||
// test that the pipeline short-circuits for a stage that errors
|
// test that the pipeline short-circuits for a stage that errors
|
||||||
r.RegisterPipelineStage("errors", mockStageFn(false, errors.New("")))
|
r.RegisterPipelineStage("errors", mockStageFn(false, errors.New("")))
|
||||||
ended, next, err = rules.NewPipelineFromRegistry(r, "errors", "astage").Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
ended, next, err = rules.NewPipelineFromRegistry(r, "errors", "astage").Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.NotNil(t, next)
|
require.NotNil(t, next)
|
||||||
require.False(t, ended)
|
require.False(t, ended)
|
||||||
|
|
||||||
// test that the pipeline short-circuits for a stage that ends
|
// test that the pipeline short-circuits for a stage that ends
|
||||||
r.RegisterPipelineStage("ends", mockStageFn(true, nil))
|
r.RegisterPipelineStage("ends", mockStageFn(true, nil))
|
||||||
ended, next, err = rules.NewPipelineFromRegistry(r, "ends", "astage").Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
ended, next, err = rules.NewPipelineFromRegistry(r, "ends", "astage").Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, next)
|
require.NotNil(t, next)
|
||||||
require.True(t, ended)
|
require.True(t, ended)
|
||||||
|
|
||||||
// test that the pipeline runs normally for multiple stages
|
// test that the pipeline runs normally for multiple stages
|
||||||
ended, next, err = rules.NewPipelineFromRegistry(r, "astage", "ends").Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
ended, next, err = rules.NewPipelineFromRegistry(r, "astage", "ends").Execute(rules.NewBoardState(0, 0), rules.Settings{}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, next)
|
require.NotNil(t, next)
|
||||||
require.True(t, ended)
|
require.True(t, ended)
|
||||||
|
|
|
||||||
41
royale.go
41
royale.go
|
|
@ -14,26 +14,6 @@ var royaleRulesetStages = []string{
|
||||||
StageSpawnHazardsShrinkMap,
|
StageSpawnHazardsShrinkMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoyaleRuleset struct {
|
|
||||||
StandardRuleset
|
|
||||||
|
|
||||||
ShrinkEveryNTurns int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RoyaleRuleset) Name() string { return GameTypeRoyale }
|
|
||||||
|
|
||||||
func (r RoyaleRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
|
||||||
return NewPipeline(royaleRulesetStages...).Execute(bs, s, sm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
|
||||||
if r.StandardRuleset.HazardDamagePerTurn < 1 {
|
|
||||||
return nil, errors.New("royale damage per turn must be greater than zero")
|
|
||||||
}
|
|
||||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
|
||||||
return nextState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
if IsInitialization(b, settings, moves) {
|
if IsInitialization(b, settings, moves) {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
@ -43,17 +23,18 @@ func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove)
|
||||||
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
||||||
turn := b.Turn + 1
|
turn := b.Turn + 1
|
||||||
|
|
||||||
if settings.RoyaleSettings.ShrinkEveryNTurns < 1 {
|
shrinkEveryNTurns := settings.Int(ParamShrinkEveryNTurns, 0)
|
||||||
|
if shrinkEveryNTurns < 1 {
|
||||||
return false, errors.New("royale game can't shrink more frequently than every turn")
|
return false, errors.New("royale game can't shrink more frequently than every turn")
|
||||||
}
|
}
|
||||||
|
|
||||||
if turn < settings.RoyaleSettings.ShrinkEveryNTurns {
|
if turn < shrinkEveryNTurns {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
randGenerator := settings.GetRand(0)
|
randGenerator := settings.GetRand(0)
|
||||||
|
|
||||||
numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns
|
numShrinks := turn / shrinkEveryNTurns
|
||||||
minX, maxX := 0, b.Width-1
|
minX, maxX := 0, b.Width-1
|
||||||
minY, maxY := 0, b.Height-1
|
minY, maxY := 0, b.Height-1
|
||||||
for i := 0; i < numShrinks; i++ {
|
for i := 0; i < numShrinks; i++ {
|
||||||
|
|
@ -72,22 +53,10 @@ func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove)
|
||||||
for x := 0; x < b.Width; x++ {
|
for x := 0; x < b.Width; x++ {
|
||||||
for y := 0; y < b.Height; y++ {
|
for y := 0; y < b.Height; y++ {
|
||||||
if x < minX || x > maxX || y < minY || y > maxY {
|
if x < minX || x > maxX || y < minY || y > maxY {
|
||||||
b.Hazards = append(b.Hazards, Point{x, y})
|
b.Hazards = append(b.Hazards, Point{X: x, Y: y})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RoyaleRuleset) IsGameOver(b *BoardState) (bool, error) {
|
|
||||||
return GameOverStandard(b, r.Settings(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r RoyaleRuleset) Settings() Settings {
|
|
||||||
s := r.StandardRuleset.Settings()
|
|
||||||
s.RoyaleSettings = RoyaleSettings{
|
|
||||||
ShrinkEveryNTurns: r.ShrinkEveryNTurns,
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,19 @@ package rules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRoyaleRulesetInterface(t *testing.T) {
|
func getRoyaleRuleset(hazardDamagePerTurn, shrinkEveryNTurns int) Ruleset {
|
||||||
var _ Ruleset = (*RoyaleRuleset)(nil)
|
settings := NewSettingsWithParams(
|
||||||
|
ParamHazardDamagePerTurn, fmt.Sprint(hazardDamagePerTurn),
|
||||||
|
ParamShrinkEveryNTurns, fmt.Sprint(shrinkEveryNTurns),
|
||||||
|
)
|
||||||
|
return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeRoyale)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoyaleDefaultSanity(t *testing.T) {
|
func TestRoyaleDefaultSanity(t *testing.T) {
|
||||||
|
|
@ -19,24 +24,19 @@ func TestRoyaleDefaultSanity(t *testing.T) {
|
||||||
{ID: "2", Body: []Point{{X: 0, Y: 1}}},
|
{ID: "2", Body: []Point{{X: 0, Y: 1}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 1}, ShrinkEveryNTurns: 0}
|
r := getRoyaleRuleset(1, 0)
|
||||||
_, err := r.CreateNextBoardState(boardState, []SnakeMove{{"1", "right"}, {"2", "right"}})
|
_, _, err := r.Execute(boardState, []SnakeMove{{"1", "right"}, {"2", "right"}})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Equal(t, errors.New("royale game can't shrink more frequently than every turn"), err)
|
require.Equal(t, errors.New("royale game can't shrink more frequently than every turn"), err)
|
||||||
|
|
||||||
r = RoyaleRuleset{ShrinkEveryNTurns: 1}
|
r = getRoyaleRuleset(1, 1)
|
||||||
_, err = r.CreateNextBoardState(boardState, []SnakeMove{})
|
_, boardState, err = r.Execute(boardState, []SnakeMove{})
|
||||||
require.Error(t, err)
|
|
||||||
require.Equal(t, errors.New("royale damage per turn must be greater than zero"), err)
|
|
||||||
|
|
||||||
r = RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 1}, ShrinkEveryNTurns: 1}
|
|
||||||
boardState, err = r.CreateNextBoardState(boardState, []SnakeMove{})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, boardState.Hazards, 0)
|
require.Len(t, boardState.Hazards, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoyaleName(t *testing.T) {
|
func TestRoyaleName(t *testing.T) {
|
||||||
r := RoyaleRuleset{}
|
r := getRoyaleRuleset(0, 0)
|
||||||
require.Equal(t, "royale", r.Name())
|
require.Equal(t, "royale", r.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,39 +57,39 @@ func TestRoyaleHazards(t *testing.T) {
|
||||||
{Width: 3, Height: 3, Turn: 9, ShrinkEveryNTurns: 10, ExpectedHazards: []Point{}},
|
{Width: 3, Height: 3, Turn: 9, ShrinkEveryNTurns: 10, ExpectedHazards: []Point{}},
|
||||||
{
|
{
|
||||||
Width: 3, Height: 3, Turn: 10, ShrinkEveryNTurns: 10,
|
Width: 3, Height: 3, Turn: 10, ShrinkEveryNTurns: 10,
|
||||||
ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}},
|
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Width: 3, Height: 3, Turn: 11, ShrinkEveryNTurns: 10,
|
Width: 3, Height: 3, Turn: 11, ShrinkEveryNTurns: 10,
|
||||||
ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}},
|
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Width: 3, Height: 3, Turn: 19, ShrinkEveryNTurns: 10,
|
Width: 3, Height: 3, Turn: 19, ShrinkEveryNTurns: 10,
|
||||||
ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}},
|
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Width: 3, Height: 3, Turn: 20, ShrinkEveryNTurns: 10,
|
Width: 3, Height: 3, Turn: 20, ShrinkEveryNTurns: 10,
|
||||||
ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 2}, {2, 2}},
|
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 2}, {X: 2, Y: 2}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Width: 3, Height: 3, Turn: 31, ShrinkEveryNTurns: 10,
|
Width: 3, Height: 3, Turn: 31, ShrinkEveryNTurns: 10,
|
||||||
ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 1}, {1, 2}, {2, 1}, {2, 2}},
|
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 1}, {X: 2, Y: 2}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Width: 3, Height: 3, Turn: 42, ShrinkEveryNTurns: 10,
|
Width: 3, Height: 3, Turn: 42, ShrinkEveryNTurns: 10,
|
||||||
ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}},
|
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Width: 3, Height: 3, Turn: 53, ShrinkEveryNTurns: 10,
|
Width: 3, Height: 3, Turn: 53, ShrinkEveryNTurns: 10,
|
||||||
ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}},
|
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Width: 3, Height: 3, Turn: 64, ShrinkEveryNTurns: 10,
|
Width: 3, Height: 3, Turn: 64, ShrinkEveryNTurns: 10,
|
||||||
ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}},
|
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Width: 3, Height: 3, Turn: 6987, ShrinkEveryNTurns: 10,
|
Width: 3, Height: 3, Turn: 6987, ShrinkEveryNTurns: 10,
|
||||||
ExpectedHazards: []Point{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {2, 0}, {2, 1}, {2, 2}},
|
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,12 +99,10 @@ func TestRoyaleHazards(t *testing.T) {
|
||||||
Width: test.Width,
|
Width: test.Width,
|
||||||
Height: test.Height,
|
Height: test.Height,
|
||||||
}
|
}
|
||||||
settings := Settings{
|
settings := NewSettingsWithParams(
|
||||||
HazardDamagePerTurn: 1,
|
ParamHazardDamagePerTurn, "1",
|
||||||
RoyaleSettings: RoyaleSettings{
|
ParamShrinkEveryNTurns, fmt.Sprint(test.ShrinkEveryNTurns),
|
||||||
ShrinkEveryNTurns: test.ShrinkEveryNTurns,
|
).WithSeed(seed)
|
||||||
},
|
|
||||||
}.WithSeed(seed)
|
|
||||||
|
|
||||||
_, err := PopulateHazardsRoyale(b, settings, mockSnakeMoves())
|
_, err := PopulateHazardsRoyale(b, settings, mockSnakeMoves())
|
||||||
require.Equal(t, test.Error, err)
|
require.Equal(t, test.Error, err)
|
||||||
|
|
@ -139,12 +137,12 @@ var royaleCaseHazardsPlaced = gameTestCase{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{
|
{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Body: []Point{{1, 1}, {1, 2}},
|
Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}},
|
||||||
Health: 100,
|
Health: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "two",
|
ID: "two",
|
||||||
Body: []Point{{3, 4}, {3, 3}},
|
Body: []Point{{X: 3, Y: 4}, {X: 3, Y: 3}},
|
||||||
Health: 100,
|
Health: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -154,7 +152,7 @@ var royaleCaseHazardsPlaced = gameTestCase{
|
||||||
EliminatedCause: EliminatedByOutOfBounds,
|
EliminatedCause: EliminatedByOutOfBounds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Food: []Point{{0, 0}, {1, 0}},
|
Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||||
Hazards: []Point{},
|
Hazards: []Point{},
|
||||||
},
|
},
|
||||||
[]SnakeMove{
|
[]SnakeMove{
|
||||||
|
|
@ -169,12 +167,12 @@ var royaleCaseHazardsPlaced = gameTestCase{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{
|
{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Body: []Point{{1, 0}, {1, 1}, {1, 1}},
|
Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 1}},
|
||||||
Health: 100,
|
Health: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "two",
|
ID: "two",
|
||||||
Body: []Point{{3, 5}, {3, 4}},
|
Body: []Point{{X: 3, Y: 5}, {X: 3, Y: 4}},
|
||||||
Health: 99,
|
Health: 99,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -184,7 +182,7 @@ var royaleCaseHazardsPlaced = gameTestCase{
|
||||||
EliminatedCause: EliminatedByOutOfBounds,
|
EliminatedCause: EliminatedByOutOfBounds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Food: []Point{{0, 0}},
|
Food: []Point{{X: 0, Y: 0}},
|
||||||
Hazards: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}, {X: 3, Y: 0}, {X: 4, Y: 0}, {X: 5, Y: 0}, {X: 6, Y: 0}, {X: 7, Y: 0}, {X: 8, Y: 0}, {X: 9, Y: 0}},
|
Hazards: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}, {X: 3, Y: 0}, {X: 4, Y: 0}, {X: 5, Y: 0}, {X: 6, Y: 0}, {X: 7, Y: 0}, {X: 8, Y: 0}, {X: 9, Y: 0}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -204,22 +202,14 @@ func TestRoyaleCreateNextBoardState(t *testing.T) {
|
||||||
*s2,
|
*s2,
|
||||||
royaleCaseHazardsPlaced,
|
royaleCaseHazardsPlaced,
|
||||||
}
|
}
|
||||||
r := RoyaleRuleset{
|
|
||||||
StandardRuleset: StandardRuleset{
|
|
||||||
HazardDamagePerTurn: 1,
|
|
||||||
},
|
|
||||||
ShrinkEveryNTurns: 1,
|
|
||||||
}
|
|
||||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||||
ParamGameType: GameTypeRoyale,
|
|
||||||
ParamHazardDamagePerTurn: "1",
|
ParamHazardDamagePerTurn: "1",
|
||||||
ParamShrinkEveryNTurns: "1",
|
ParamShrinkEveryNTurns: "1",
|
||||||
}).WithSeed(1234)
|
}).WithSeed(1234)
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
rand.Seed(1234)
|
rand.Seed(1234)
|
||||||
gc.requireValidNextState(t, &r)
|
// test a RulesBuilder constructed instance
|
||||||
// also test a RulesBuilder constructed instance
|
gc.requireValidNextState(t, rb.NamedRuleset(GameTypeRoyale))
|
||||||
gc.requireValidNextState(t, rb.Ruleset())
|
|
||||||
// also test a pipeline with the same settings
|
// also test a pipeline with the same settings
|
||||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeRoyale, NewPipeline(royaleRulesetStages...)))
|
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeRoyale, NewPipeline(royaleRulesetStages...)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
172
ruleset.go
172
ruleset.go
|
|
@ -1,16 +1,15 @@
|
||||||
package rules
|
package rules
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Ruleset interface {
|
type Ruleset interface {
|
||||||
|
// Returns the name of the ruleset, if applicable.
|
||||||
Name() string
|
Name() string
|
||||||
ModifyInitialBoardState(initialState *BoardState) (*BoardState, error)
|
|
||||||
CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error)
|
// Returns the settings used by the ruleset.
|
||||||
IsGameOver(state *BoardState) (bool, error)
|
|
||||||
// Settings provides the game settings that are relevant to the ruleset.
|
|
||||||
Settings() Settings
|
Settings() Settings
|
||||||
|
|
||||||
|
// Processes the next turn of the ruleset, returning whether the game has ended, the next BoardState, or an error.
|
||||||
|
// For turn zero (initialization), moves will be left empty.
|
||||||
|
Execute(prevState *BoardState, moves []SnakeMove) (gameOver bool, nextState *BoardState, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnakeMove struct {
|
type SnakeMove struct {
|
||||||
|
|
@ -18,68 +17,12 @@ type SnakeMove struct {
|
||||||
Move string
|
Move string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings contains all settings relevant to a game.
|
|
||||||
// It is used by game logic to take a previous game state and produce a next game state.
|
|
||||||
type Settings struct {
|
|
||||||
FoodSpawnChance int `json:"foodSpawnChance"`
|
|
||||||
MinimumFood int `json:"minimumFood"`
|
|
||||||
HazardDamagePerTurn int `json:"hazardDamagePerTurn"`
|
|
||||||
HazardMap string `json:"hazardMap"`
|
|
||||||
HazardMapAuthor string `json:"hazardMapAuthor"`
|
|
||||||
RoyaleSettings RoyaleSettings `json:"royale"`
|
|
||||||
SquadSettings SquadSettings `json:"squad"` // Deprecated, provided with default fields for API compatibility
|
|
||||||
|
|
||||||
rand Rand
|
|
||||||
seed int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a random number generator initialized based on the seed and current turn.
|
|
||||||
func (settings Settings) GetRand(turn int) Rand {
|
|
||||||
// Allow overriding the random generator for testing
|
|
||||||
if settings.rand != nil {
|
|
||||||
return settings.rand
|
|
||||||
}
|
|
||||||
|
|
||||||
if settings.seed != 0 {
|
|
||||||
return NewSeedRand(settings.seed + int64(turn))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to global random number generator if neither seed or rand are set.
|
|
||||||
return GlobalRand
|
|
||||||
}
|
|
||||||
|
|
||||||
func (settings Settings) WithRand(rand Rand) Settings {
|
|
||||||
settings.rand = rand
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
|
|
||||||
func (settings Settings) Seed() int64 {
|
|
||||||
return settings.seed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (settings Settings) WithSeed(seed int64) Settings {
|
|
||||||
settings.seed = seed
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoyaleSettings contains settings that are specific to the "royale" game mode
|
|
||||||
type RoyaleSettings struct {
|
|
||||||
ShrinkEveryNTurns int `json:"shrinkEveryNTurns"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SquadSettings contains settings that are specific to the "squad" game mode
|
|
||||||
type SquadSettings struct {
|
|
||||||
AllowBodyCollisions bool `json:"allowBodyCollisions"`
|
|
||||||
SharedElimination bool `json:"sharedElimination"`
|
|
||||||
SharedHealth bool `json:"sharedHealth"`
|
|
||||||
SharedLength bool `json:"sharedLength"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type rulesetBuilder struct {
|
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
|
rand Rand // used for random number generation
|
||||||
solo bool // if true, only 1 alive snake is required to keep the game from ending
|
solo bool // if true, only 1 alive snake is required to keep the game from ending
|
||||||
|
settings *Settings // used to set settings directly instead of via string params
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRulesetBuilder returns an instance of a builder for the Ruleset types.
|
// NewRulesetBuilder returns an instance of a builder for the Ruleset types.
|
||||||
|
|
@ -89,7 +32,7 @@ func NewRulesetBuilder() *rulesetBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithParams accepts a map of game parameters for customizing games.
|
// WithParams accepts a map of string parameters for customizing games.
|
||||||
//
|
//
|
||||||
// Parameters are copied. If called multiple times, parameters are merged such that:
|
// Parameters are copied. If called multiple times, parameters are merged such that:
|
||||||
// - existing keys in both maps get overwritten by the new ones
|
// - existing keys in both maps get overwritten by the new ones
|
||||||
|
|
@ -125,13 +68,14 @@ func (rb *rulesetBuilder) WithSolo(value bool) *rulesetBuilder {
|
||||||
return rb
|
return rb
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
|
// WithSettings sets the settings object for the ruleset directly.
|
||||||
func (rb rulesetBuilder) Ruleset() PipelineRuleset {
|
func (rb *rulesetBuilder) WithSettings(settings Settings) *rulesetBuilder {
|
||||||
name, ok := rb.params[ParamGameType]
|
rb.settings = &settings
|
||||||
if !ok {
|
return rb
|
||||||
name = GameTypeStandard
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// NamedRuleset constructs a known ruleset by using name to look up a standard pipeline.
|
||||||
|
func (rb rulesetBuilder) NamedRuleset(name string) Ruleset {
|
||||||
var stages []string
|
var stages []string
|
||||||
if rb.solo {
|
if rb.solo {
|
||||||
stages = append(stages, StageGameOverSoloSnake)
|
stages = append(stages, StageGameOverSoloSnake)
|
||||||
|
|
@ -153,63 +97,28 @@ func (rb rulesetBuilder) Ruleset() PipelineRuleset {
|
||||||
case GameTypeWrapped:
|
case GameTypeWrapped:
|
||||||
stages = append(stages, wrappedRulesetStages[1:]...)
|
stages = append(stages, wrappedRulesetStages[1:]...)
|
||||||
default:
|
default:
|
||||||
|
name = GameTypeStandard
|
||||||
stages = append(stages, standardRulesetStages[1:]...)
|
stages = append(stages, standardRulesetStages[1:]...)
|
||||||
}
|
}
|
||||||
return rb.PipelineRuleset(name, NewPipeline(stages...))
|
return rb.PipelineRuleset(name, NewPipeline(stages...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PipelineRuleset provides an implementation of the Ruleset using a pipeline with a name.
|
// PipelineRuleset constructs a ruleset with the given name and pipeline using the parameters passed to the builder.
|
||||||
// It is intended to facilitate transitioning away from legacy Ruleset implementations to Pipeline
|
// This can be used to create custom rulesets.
|
||||||
// implementations.
|
func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) Ruleset {
|
||||||
func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRuleset {
|
var settings Settings
|
||||||
|
if rb.settings != nil {
|
||||||
|
settings = *rb.settings
|
||||||
|
} else {
|
||||||
|
settings = NewSettings(rb.params).WithRand(rb.rand).WithSeed(rb.seed)
|
||||||
|
}
|
||||||
return &pipelineRuleset{
|
return &pipelineRuleset{
|
||||||
name: name,
|
name: name,
|
||||||
pipeline: p,
|
pipeline: p,
|
||||||
settings: Settings{
|
settings: settings,
|
||||||
FoodSpawnChance: paramsInt(rb.params, ParamFoodSpawnChance, 0),
|
|
||||||
MinimumFood: paramsInt(rb.params, ParamMinimumFood, 0),
|
|
||||||
HazardDamagePerTurn: paramsInt(rb.params, ParamHazardDamagePerTurn, 0),
|
|
||||||
HazardMap: rb.params[ParamHazardMap],
|
|
||||||
HazardMapAuthor: rb.params[ParamHazardMapAuthor],
|
|
||||||
RoyaleSettings: RoyaleSettings{
|
|
||||||
ShrinkEveryNTurns: paramsInt(rb.params, ParamShrinkEveryNTurns, 0),
|
|
||||||
},
|
|
||||||
rand: rb.rand,
|
|
||||||
seed: rb.seed,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// paramsBool returns the boolean value for the specified parameter.
|
|
||||||
// If the parameter doesn't exist, the default value will be returned.
|
|
||||||
// If the parameter does exist, but is not "true", false will be returned.
|
|
||||||
func paramsBool(params map[string]string, paramName string, defaultValue bool) bool {
|
|
||||||
if val, ok := params[paramName]; ok {
|
|
||||||
return val == "true"
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// paramsInt returns the int value for the specified parameter.
|
|
||||||
// If the parameter doesn't exist, the default value will be returned.
|
|
||||||
// If the parameter does exist, but is not a valid int, the default value will be returned.
|
|
||||||
func paramsInt(params map[string]string, paramName string, defaultValue int) int {
|
|
||||||
if val, ok := params[paramName]; ok {
|
|
||||||
i, err := strconv.Atoi(val)
|
|
||||||
if err == nil {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// PipelineRuleset groups the Pipeline and Ruleset methods.
|
|
||||||
// It is intended to facilitate a transition from Ruleset legacy code to Pipeline code.
|
|
||||||
type PipelineRuleset interface {
|
|
||||||
Ruleset
|
|
||||||
Pipeline
|
|
||||||
}
|
|
||||||
|
|
||||||
type pipelineRuleset struct {
|
type pipelineRuleset struct {
|
||||||
pipeline Pipeline
|
pipeline Pipeline
|
||||||
name string
|
name string
|
||||||
|
|
@ -225,33 +134,10 @@ func (r pipelineRuleset) Settings() Settings {
|
||||||
func (r pipelineRuleset) Name() string { return r.name }
|
func (r pipelineRuleset) Name() string { return r.name }
|
||||||
|
|
||||||
// impl Ruleset
|
// impl Ruleset
|
||||||
// IMPORTANT: this implementation of IsGameOver deviates from the previous Ruleset implementations
|
func (r pipelineRuleset) Execute(bs *BoardState, sm []SnakeMove) (bool, *BoardState, error) {
|
||||||
// in that it checks if the *NEXT* state results in game over, not the previous state.
|
return r.pipeline.Execute(bs, r.Settings(), sm)
|
||||||
// This is due to the design of pipelines / stage functions not having a distinction between
|
|
||||||
// checking for game over and producing a next state.
|
|
||||||
func (r *pipelineRuleset) IsGameOver(b *BoardState) (bool, error) {
|
|
||||||
gameover, _, err := r.Execute(b, r.Settings(), nil) // checks if next state is game over
|
|
||||||
return gameover, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl Ruleset
|
|
||||||
func (r pipelineRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
|
|
||||||
_, nextState, err := r.Execute(initialState, r.Settings(), nil)
|
|
||||||
return nextState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl Pipeline
|
|
||||||
func (r pipelineRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
|
||||||
return r.pipeline.Execute(bs, s, sm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl Ruleset
|
|
||||||
func (r pipelineRuleset) CreateNextBoardState(bs *BoardState, sm []SnakeMove) (*BoardState, error) {
|
|
||||||
_, nextState, err := r.Execute(bs, r.Settings(), sm)
|
|
||||||
return nextState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl Pipeline
|
|
||||||
func (r pipelineRuleset) Err() error {
|
func (r pipelineRuleset) Err() error {
|
||||||
return r.pipeline.Err()
|
return r.pipeline.Err()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,31 +10,6 @@ import (
|
||||||
_ "github.com/BattlesnakeOfficial/rules/test"
|
_ "github.com/BattlesnakeOfficial/rules/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParamInt(t *testing.T) {
|
|
||||||
require.Equal(t, 5, paramsInt(nil, "test", 5), "nil map")
|
|
||||||
require.Equal(t, 10, paramsInt(map[string]string{}, "foo", 10), "empty map")
|
|
||||||
require.Equal(t, 10, paramsInt(map[string]string{"hullo": "there"}, "hullo", 10), "invalid value")
|
|
||||||
require.Equal(t, 20, paramsInt(map[string]string{"bonjour": "20"}, "bonjour", 20), "valid value")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParamBool(t *testing.T) {
|
|
||||||
// missing values default to specified value
|
|
||||||
require.Equal(t, true, paramsBool(nil, "test", true), "nil map true")
|
|
||||||
require.Equal(t, false, paramsBool(nil, "test", false), "nil map false")
|
|
||||||
|
|
||||||
// missing values default to specified value
|
|
||||||
require.Equal(t, true, paramsBool(map[string]string{}, "foo", true), "empty map true")
|
|
||||||
require.Equal(t, false, paramsBool(map[string]string{}, "foo", false), "empty map false")
|
|
||||||
|
|
||||||
// invalid values (exist but not booL) default to false
|
|
||||||
require.Equal(t, false, paramsBool(map[string]string{"hullo": "there"}, "hullo", true), "invalid value default true")
|
|
||||||
require.Equal(t, false, paramsBool(map[string]string{"hullo": "there"}, "hullo", false), "invalid value default false")
|
|
||||||
|
|
||||||
// valid values ignore defaults
|
|
||||||
require.Equal(t, false, paramsBool(map[string]string{"bonjour": "false"}, "bonjour", false), "valid value false")
|
|
||||||
require.Equal(t, true, paramsBool(map[string]string{"bonjour": "true"}, "bonjour", false), "valid value true")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRulesetError(t *testing.T) {
|
func TestRulesetError(t *testing.T) {
|
||||||
err := (error)(RulesetError("test error string"))
|
err := (error)(RulesetError("test error string"))
|
||||||
require.Equal(t, "test error string", err.Error())
|
require.Equal(t, "test error string", err.Error())
|
||||||
|
|
@ -42,10 +17,10 @@ func TestRulesetError(t *testing.T) {
|
||||||
|
|
||||||
func TestRulesetBuilderInternals(t *testing.T) {
|
func TestRulesetBuilderInternals(t *testing.T) {
|
||||||
// test Royale with seed
|
// test Royale with seed
|
||||||
rsb := NewRulesetBuilder().WithSeed(3).WithParams(map[string]string{ParamGameType: GameTypeRoyale})
|
rsb := NewRulesetBuilder().WithSeed(3)
|
||||||
require.Equal(t, int64(3), rsb.seed)
|
require.Equal(t, int64(3), rsb.seed)
|
||||||
require.Equal(t, GameTypeRoyale, rsb.Ruleset().Name())
|
require.Equal(t, GameTypeRoyale, rsb.NamedRuleset(GameTypeRoyale).Name())
|
||||||
require.Equal(t, int64(3), rsb.Ruleset().Settings().Seed())
|
require.Equal(t, int64(3), rsb.NamedRuleset(GameTypeRoyale).Settings().Seed())
|
||||||
|
|
||||||
// test parameter merging
|
// test parameter merging
|
||||||
rsb = NewRulesetBuilder().
|
rsb = NewRulesetBuilder().
|
||||||
|
|
|
||||||
124
ruleset_test.go
124
ruleset_test.go
|
|
@ -5,102 +5,13 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/BattlesnakeOfficial/rules"
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStandardRulesetSettings(t *testing.T) {
|
|
||||||
ruleset := rules.StandardRuleset{
|
|
||||||
MinimumFood: 5,
|
|
||||||
FoodSpawnChance: 10,
|
|
||||||
HazardDamagePerTurn: 10,
|
|
||||||
HazardMap: "hz_spiral",
|
|
||||||
HazardMapAuthor: "altersaddle",
|
|
||||||
}
|
|
||||||
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
|
||||||
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
|
||||||
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
|
||||||
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
|
||||||
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWrappedRulesetSettings(t *testing.T) {
|
|
||||||
ruleset := rules.WrappedRuleset{
|
|
||||||
StandardRuleset: rules.StandardRuleset{
|
|
||||||
MinimumFood: 5,
|
|
||||||
FoodSpawnChance: 10,
|
|
||||||
HazardDamagePerTurn: 10,
|
|
||||||
HazardMap: "hz_spiral",
|
|
||||||
HazardMapAuthor: "altersaddle",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
|
||||||
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
|
||||||
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
|
||||||
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
|
||||||
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSoloRulesetSettings(t *testing.T) {
|
|
||||||
ruleset := rules.SoloRuleset{
|
|
||||||
StandardRuleset: rules.StandardRuleset{
|
|
||||||
MinimumFood: 5,
|
|
||||||
FoodSpawnChance: 10,
|
|
||||||
HazardDamagePerTurn: 10,
|
|
||||||
HazardMap: "hz_spiral",
|
|
||||||
HazardMapAuthor: "altersaddle",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
|
||||||
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
|
||||||
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
|
||||||
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
|
||||||
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRoyaleRulesetSettings(t *testing.T) {
|
|
||||||
ruleset := rules.RoyaleRuleset{
|
|
||||||
ShrinkEveryNTurns: 12,
|
|
||||||
StandardRuleset: rules.StandardRuleset{
|
|
||||||
MinimumFood: 5,
|
|
||||||
FoodSpawnChance: 10,
|
|
||||||
HazardDamagePerTurn: 10,
|
|
||||||
HazardMap: "hz_spiral",
|
|
||||||
HazardMapAuthor: "altersaddle",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Equal(t, ruleset.ShrinkEveryNTurns, ruleset.Settings().RoyaleSettings.ShrinkEveryNTurns)
|
|
||||||
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
|
||||||
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
|
||||||
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
|
||||||
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
|
||||||
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConstrictorRulesetSettings(t *testing.T) {
|
|
||||||
ruleset := rules.ConstrictorRuleset{
|
|
||||||
StandardRuleset: rules.StandardRuleset{
|
|
||||||
MinimumFood: 5,
|
|
||||||
FoodSpawnChance: 10,
|
|
||||||
HazardDamagePerTurn: 10,
|
|
||||||
HazardMap: "hz_spiral",
|
|
||||||
HazardMapAuthor: "altersaddle",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
assert.Equal(t, ruleset.MinimumFood, ruleset.Settings().MinimumFood)
|
|
||||||
assert.Equal(t, ruleset.FoodSpawnChance, ruleset.Settings().FoodSpawnChance)
|
|
||||||
assert.Equal(t, ruleset.HazardDamagePerTurn, ruleset.Settings().HazardDamagePerTurn)
|
|
||||||
assert.Equal(t, ruleset.HazardMap, ruleset.Settings().HazardMap)
|
|
||||||
assert.Equal(t, ruleset.HazardMapAuthor, ruleset.Settings().HazardMapAuthor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRulesetBuilder(t *testing.T) {
|
func TestRulesetBuilder(t *testing.T) {
|
||||||
// Test that a fresh instance can produce a Ruleset
|
// Test that a fresh instance can produce a Ruleset
|
||||||
require.NotNil(t, rules.NewRulesetBuilder().Ruleset())
|
require.NotNil(t, rules.NewRulesetBuilder().NamedRuleset(""))
|
||||||
require.Equal(t, rules.GameTypeStandard, rules.NewRulesetBuilder().Ruleset().Name(), "should default to standard game")
|
require.Equal(t, rules.GameTypeStandard, rules.NewRulesetBuilder().NamedRuleset("").Name(), "should default to standard game")
|
||||||
|
|
||||||
// test nil safety / defaults
|
|
||||||
require.NotNil(t, rules.NewRulesetBuilder().Ruleset())
|
|
||||||
|
|
||||||
// make sure it works okay for lots of game types
|
// make sure it works okay for lots of game types
|
||||||
expectedResults := []struct {
|
expectedResults := []struct {
|
||||||
|
|
@ -120,32 +31,23 @@ func TestRulesetBuilder(t *testing.T) {
|
||||||
|
|
||||||
rsb.WithParams(map[string]string{
|
rsb.WithParams(map[string]string{
|
||||||
// apply the standard rule params
|
// apply the standard rule params
|
||||||
rules.ParamGameType: expected.GameType,
|
|
||||||
rules.ParamFoodSpawnChance: "10",
|
rules.ParamFoodSpawnChance: "10",
|
||||||
rules.ParamMinimumFood: "5",
|
rules.ParamMinimumFood: "5",
|
||||||
rules.ParamHazardDamagePerTurn: "12",
|
rules.ParamHazardDamagePerTurn: "12",
|
||||||
rules.ParamHazardMap: "test",
|
|
||||||
rules.ParamHazardMapAuthor: "tester",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
require.NotNil(t, rsb.Ruleset())
|
require.NotNil(t, rsb.NamedRuleset(expected.GameType))
|
||||||
require.Equal(t, expected.GameType, rsb.Ruleset().Name())
|
require.Equal(t, expected.GameType, rsb.NamedRuleset(expected.GameType).Name())
|
||||||
// All the standard settings should always be copied over
|
// All the standard settings should always be copied over
|
||||||
require.Equal(t, 10, rsb.Ruleset().Settings().FoodSpawnChance)
|
require.Equal(t, 10, rsb.NamedRuleset(expected.GameType).Settings().Int(rules.ParamFoodSpawnChance, 0))
|
||||||
require.Equal(t, 12, rsb.Ruleset().Settings().HazardDamagePerTurn)
|
require.Equal(t, 12, rsb.NamedRuleset(expected.GameType).Settings().Int(rules.ParamHazardDamagePerTurn, 0))
|
||||||
require.Equal(t, 5, rsb.Ruleset().Settings().MinimumFood)
|
require.Equal(t, 5, rsb.NamedRuleset(expected.GameType).Settings().Int(rules.ParamMinimumFood, 0))
|
||||||
require.Equal(t, "test", rsb.Ruleset().Settings().HazardMap)
|
|
||||||
require.Equal(t, "tester", rsb.Ruleset().Settings().HazardMapAuthor)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRulesetBuilderGameOver(t *testing.T) {
|
func TestRulesetBuilderGameOver(t *testing.T) {
|
||||||
settings := rules.Settings{
|
settings := rules.NewSettingsWithParams(rules.ParamShrinkEveryNTurns, "12")
|
||||||
RoyaleSettings: rules.RoyaleSettings{
|
|
||||||
ShrinkEveryNTurns: 12,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
moves := []rules.SnakeMove{
|
moves := []rules.SnakeMove{
|
||||||
{ID: "1", Move: "up"},
|
{ID: "1", Move: "up"},
|
||||||
}
|
}
|
||||||
|
|
@ -214,13 +116,11 @@ func TestRulesetBuilderGameOver(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%v_%v", test.gameType, test.solo), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%v_%v", test.gameType, test.solo), func(t *testing.T) {
|
||||||
rsb := rules.NewRulesetBuilder().WithParams(map[string]string{
|
rsb := rules.NewRulesetBuilder().WithSettings(settings).WithSolo(test.solo)
|
||||||
rules.ParamGameType: test.gameType,
|
|
||||||
}).WithSolo(test.solo)
|
|
||||||
|
|
||||||
ruleset := rsb.Ruleset()
|
ruleset := rsb.NamedRuleset(test.gameType)
|
||||||
|
|
||||||
gameOver, _, err := ruleset.Execute(boardState, settings, moves)
|
gameOver, _, err := ruleset.Execute(boardState, moves)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.gameOver, gameOver)
|
require.Equal(t, test.gameOver, gameOver)
|
||||||
|
|
@ -234,7 +134,7 @@ func TestStageFuncContract(t *testing.T) {
|
||||||
stage = func(bs *rules.BoardState, s rules.Settings, sm []rules.SnakeMove) (bool, error) {
|
stage = func(bs *rules.BoardState, s rules.Settings, sm []rules.SnakeMove) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
ended, err := stage(nil, rules.NewRulesetBuilder().Ruleset().Settings(), nil)
|
ended, err := stage(nil, rules.NewRulesetBuilder().NamedRuleset("").Settings(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, ended)
|
require.True(t, ended)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
90
settings.go
Normal file
90
settings.go
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
package rules
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
// Settings contains all settings relevant to a game.
|
||||||
|
// The settings are stored as raw string values, which should not be accessed
|
||||||
|
// directly. Calling code should instead use the Int/Bool methods to parse them.
|
||||||
|
type Settings struct {
|
||||||
|
rawValues map[string]string
|
||||||
|
|
||||||
|
rand Rand
|
||||||
|
seed int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettings(params map[string]string) Settings {
|
||||||
|
rawValues := make(map[string]string, len(params))
|
||||||
|
|
||||||
|
// Copy incoming params into a new map
|
||||||
|
for key, value := range params {
|
||||||
|
rawValues[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return Settings{
|
||||||
|
rawValues: rawValues,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettingsWithParams(params ...string) Settings {
|
||||||
|
rawValues := map[string]string{}
|
||||||
|
|
||||||
|
for index := 1; index < len(params); index += 2 {
|
||||||
|
rawValues[params[index-1]] = params[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Settings{
|
||||||
|
rawValues: rawValues,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a random number generator initialized based on the seed and current turn.
|
||||||
|
func (settings Settings) GetRand(turn int) Rand {
|
||||||
|
// Allow overriding the random generator for testing
|
||||||
|
if settings.rand != nil {
|
||||||
|
return settings.rand
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.seed != 0 {
|
||||||
|
return NewSeedRand(settings.seed + int64(turn))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to global random number generator if neither seed or rand are set.
|
||||||
|
return GlobalRand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings Settings) WithRand(rand Rand) Settings {
|
||||||
|
settings.rand = rand
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings Settings) Seed() int64 {
|
||||||
|
return settings.seed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (settings Settings) WithSeed(seed int64) Settings {
|
||||||
|
settings.seed = seed
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool returns the boolean value for the specified parameter.
|
||||||
|
// If the parameter doesn't exist, the default value will be returned.
|
||||||
|
// If the parameter does exist, but is not "true", false will be returned.
|
||||||
|
func (settings Settings) Bool(paramName string, defaultValue bool) bool {
|
||||||
|
if val, ok := settings.rawValues[paramName]; ok {
|
||||||
|
return val == "true"
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int returns the int value for the specified parameter.
|
||||||
|
// If the parameter doesn't exist, the default value will be returned.
|
||||||
|
// If the parameter does exist, but is not a valid int, the default value will be returned.
|
||||||
|
func (settings Settings) Int(paramName string, defaultValue int) int {
|
||||||
|
if val, ok := settings.rawValues[paramName]; ok {
|
||||||
|
i, err := strconv.Atoi(val)
|
||||||
|
if err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
31
settings_test.go
Normal file
31
settings_test.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package rules_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/BattlesnakeOfficial/rules"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSettings(t *testing.T) {
|
||||||
|
params := map[string]string{
|
||||||
|
"invalidSetting": "abcd",
|
||||||
|
"intSetting": "1234",
|
||||||
|
"boolSetting": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := rules.NewSettings(params)
|
||||||
|
|
||||||
|
assert.Equal(t, 4567, settings.Int("missingIntSetting", 4567))
|
||||||
|
assert.Equal(t, 4567, settings.Int("invalidSetting", 4567))
|
||||||
|
assert.Equal(t, 1234, settings.Int("intSetting", 4567))
|
||||||
|
|
||||||
|
assert.Equal(t, false, settings.Bool("missingBoolSetting", false))
|
||||||
|
assert.Equal(t, true, settings.Bool("missingBoolSetting", true))
|
||||||
|
assert.Equal(t, false, settings.Bool("invalidSetting", true))
|
||||||
|
assert.Equal(t, true, settings.Bool("boolSetting", true))
|
||||||
|
|
||||||
|
assert.Equal(t, 4567, rules.NewSettingsWithParams("newIntSetting").Int("newIntSetting", 4567))
|
||||||
|
assert.Equal(t, 1234, rules.NewSettingsWithParams("newIntSetting", "1234").Int("newIntSetting", 4567))
|
||||||
|
assert.Equal(t, 4567, rules.NewSettingsWithParams("x", "y", "newIntSetting").Int("newIntSetting", 4567))
|
||||||
|
}
|
||||||
19
solo.go
19
solo.go
|
|
@ -9,25 +9,6 @@ var soloRulesetStages = []string{
|
||||||
StageEliminationStandard,
|
StageEliminationStandard,
|
||||||
}
|
}
|
||||||
|
|
||||||
type SoloRuleset struct {
|
|
||||||
StandardRuleset
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SoloRuleset) Name() string { return GameTypeSolo }
|
|
||||||
|
|
||||||
func (r SoloRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
|
||||||
return NewPipeline(soloRulesetStages...).Execute(bs, s, sm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SoloRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
|
||||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
|
||||||
return nextState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SoloRuleset) IsGameOver(b *BoardState) (bool, error) {
|
|
||||||
return GameOverSolo(b, r.Settings(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GameOverSolo(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
func GameOverSolo(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||||
|
|
|
||||||
47
solo_test.go
47
solo_test.go
|
|
@ -6,20 +6,21 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSoloRulesetInterface(t *testing.T) {
|
func getSoloRuleset(settings Settings) Ruleset {
|
||||||
var _ Ruleset = (*SoloRuleset)(nil)
|
return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeSolo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSoloName(t *testing.T) {
|
func TestSoloName(t *testing.T) {
|
||||||
r := SoloRuleset{}
|
r := getSoloRuleset(Settings{})
|
||||||
require.Equal(t, "solo", r.Name())
|
require.Equal(t, "solo", r.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSoloCreateNextBoardStateSanity(t *testing.T) {
|
func TestSoloCreateNextBoardStateSanity(t *testing.T) {
|
||||||
boardState := &BoardState{}
|
boardState := &BoardState{}
|
||||||
r := SoloRuleset{}
|
r := getSoloRuleset(Settings{})
|
||||||
_, err := r.CreateNextBoardState(boardState, []SnakeMove{})
|
gameOver, _, err := r.Execute(boardState, []SnakeMove{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.True(t, gameOver)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSoloIsGameOver(t *testing.T) {
|
func TestSoloIsGameOver(t *testing.T) {
|
||||||
|
|
@ -41,7 +42,7 @@ func TestSoloIsGameOver(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := SoloRuleset{}
|
r := getSoloRuleset(Settings{})
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
b := &BoardState{
|
b := &BoardState{
|
||||||
Height: 11,
|
Height: 11,
|
||||||
|
|
@ -50,7 +51,7 @@ func TestSoloIsGameOver(t *testing.T) {
|
||||||
Food: []Point{},
|
Food: []Point{},
|
||||||
}
|
}
|
||||||
|
|
||||||
actual, err := r.IsGameOver(b)
|
actual, _, err := r.Execute(b, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.Expected, actual)
|
require.Equal(t, test.Expected, actual)
|
||||||
}
|
}
|
||||||
|
|
@ -69,11 +70,11 @@ var soloCaseNotOver = gameTestCase{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{
|
{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Body: []Point{{1, 1}, {1, 2}},
|
Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}},
|
||||||
Health: 100,
|
Health: 100,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Food: []Point{{0, 0}, {1, 0}},
|
Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||||
Hazards: []Point{},
|
Hazards: []Point{},
|
||||||
},
|
},
|
||||||
[]SnakeMove{
|
[]SnakeMove{
|
||||||
|
|
@ -86,11 +87,11 @@ var soloCaseNotOver = gameTestCase{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{
|
{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Body: []Point{{1, 0}, {1, 1}, {1, 1}},
|
Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 1}},
|
||||||
Health: 100,
|
Health: 100,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Food: []Point{{0, 0}},
|
Food: []Point{{X: 0, Y: 0}},
|
||||||
Hazards: []Point{},
|
Hazards: []Point{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -104,14 +105,10 @@ func TestSoloCreateNextBoardState(t *testing.T) {
|
||||||
standardMoveAndCollideMAD,
|
standardMoveAndCollideMAD,
|
||||||
soloCaseNotOver,
|
soloCaseNotOver,
|
||||||
}
|
}
|
||||||
r := SoloRuleset{}
|
r := getSoloRuleset(Settings{})
|
||||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
|
||||||
ParamGameType: GameTypeSolo,
|
|
||||||
})
|
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
gc.requireValidNextState(t, &r)
|
// test a RulesBuilder constructed instance
|
||||||
// also test a RulesBuilder constructed instance
|
gc.requireValidNextState(t, r)
|
||||||
gc.requireValidNextState(t, rb.Ruleset())
|
|
||||||
// also test a pipeline with the same settings
|
// also test a pipeline with the same settings
|
||||||
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeSolo, NewPipeline(soloRulesetStages...)))
|
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeSolo, NewPipeline(soloRulesetStages...)))
|
||||||
}
|
}
|
||||||
|
|
@ -119,26 +116,18 @@ func TestSoloCreateNextBoardState(t *testing.T) {
|
||||||
|
|
||||||
// Test a snake running right into the wall is properly eliminated
|
// Test a snake running right into the wall is properly eliminated
|
||||||
func TestSoloEliminationOutOfBounds(t *testing.T) {
|
func TestSoloEliminationOutOfBounds(t *testing.T) {
|
||||||
r := SoloRuleset{}
|
r := getSoloRuleset(Settings{})
|
||||||
|
|
||||||
// Using MaxRand is important because it ensures that the snakes are consistently placed in a way this test will work.
|
// Using MaxRand is important because it ensures that the snakes are consistently placed in a way this test will work.
|
||||||
// Actually random placement could result in the assumptions made by this test being incorrect.
|
// Actually random placement could result in the assumptions made by this test being incorrect.
|
||||||
initialState, err := CreateDefaultBoardState(MaxRand, 2, 2, []string{"one"})
|
initialState, err := CreateDefaultBoardState(MaxRand, 2, 2, []string{"one"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, next, err := r.Execute(
|
_, next, err := r.Execute(initialState, []SnakeMove{{ID: "one", Move: "right"}})
|
||||||
initialState,
|
|
||||||
r.Settings(),
|
|
||||||
[]SnakeMove{{ID: "one", Move: "right"}},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, initialState)
|
require.NotNil(t, initialState)
|
||||||
|
|
||||||
ended, next, err := r.Execute(
|
ended, next, err := r.Execute(next, []SnakeMove{{ID: "one", Move: "right"}})
|
||||||
next,
|
|
||||||
r.Settings(),
|
|
||||||
[]SnakeMove{{ID: "one", Move: "right"}},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, initialState)
|
require.NotNil(t, initialState)
|
||||||
|
|
||||||
|
|
|
||||||
62
standard.go
62
standard.go
|
|
@ -5,14 +5,6 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StandardRuleset struct {
|
|
||||||
FoodSpawnChance int // [0, 100]
|
|
||||||
MinimumFood int
|
|
||||||
HazardDamagePerTurn int
|
|
||||||
HazardMap string // optional
|
|
||||||
HazardMapAuthor string // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
var standardRulesetStages = []string{
|
var standardRulesetStages = []string{
|
||||||
StageGameOverStandard,
|
StageGameOverStandard,
|
||||||
StageMovementStandard,
|
StageMovementStandard,
|
||||||
|
|
@ -22,23 +14,6 @@ var standardRulesetStages = []string{
|
||||||
StageEliminationStandard,
|
StageEliminationStandard,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) Name() string { return GameTypeStandard }
|
|
||||||
|
|
||||||
func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
|
|
||||||
// No-op
|
|
||||||
return initialState, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl Pipeline
|
|
||||||
func (r StandardRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
|
||||||
return NewPipeline(standardRulesetStages...).Execute(bs, s, sm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
|
||||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
|
||||||
return nextState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func MoveSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
func MoveSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
if IsInitialization(b, settings, moves) {
|
if IsInitialization(b, settings, moves) {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
@ -156,6 +131,7 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove)
|
||||||
if IsInitialization(b, settings, moves) {
|
if IsInitialization(b, settings, moves) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
hazardDamage := settings.Int(ParamHazardDamagePerTurn, 0)
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
snake := &b.Snakes[i]
|
snake := &b.Snakes[i]
|
||||||
if snake.EliminatedCause != NotEliminated {
|
if snake.EliminatedCause != NotEliminated {
|
||||||
|
|
@ -176,7 +152,7 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snake is in a hazard, reduce health
|
// Snake is in a hazard, reduce health
|
||||||
snake.Health = snake.Health - settings.HazardDamagePerTurn
|
snake.Health = snake.Health - hazardDamage
|
||||||
if snake.Health < 0 {
|
if snake.Health < 0 {
|
||||||
snake.Health = 0
|
snake.Health = 0
|
||||||
}
|
}
|
||||||
|
|
@ -393,20 +369,18 @@ func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (boo
|
||||||
if IsInitialization(b, settings, moves) {
|
if IsInitialization(b, settings, moves) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
minimumFood := settings.Int(ParamMinimumFood, 0)
|
||||||
|
foodSpawnChance := settings.Int(ParamFoodSpawnChance, 0)
|
||||||
numCurrentFood := int(len(b.Food))
|
numCurrentFood := int(len(b.Food))
|
||||||
if numCurrentFood < settings.MinimumFood {
|
if numCurrentFood < minimumFood {
|
||||||
return false, PlaceFoodRandomly(GlobalRand, b, settings.MinimumFood-numCurrentFood)
|
return false, PlaceFoodRandomly(GlobalRand, b, minimumFood-numCurrentFood)
|
||||||
}
|
}
|
||||||
if settings.FoodSpawnChance > 0 && int(rand.Intn(100)) < settings.FoodSpawnChance {
|
if foodSpawnChance > 0 && int(rand.Intn(100)) < foodSpawnChance {
|
||||||
return false, PlaceFoodRandomly(GlobalRand, b, 1)
|
return false, PlaceFoodRandomly(GlobalRand, b, 1)
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) {
|
|
||||||
return GameOverStandard(b, r.Settings(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GameOverStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
func GameOverStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
numSnakesRemaining := 0
|
numSnakesRemaining := 0
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
|
|
@ -416,25 +390,3 @@ func GameOverStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool
|
||||||
}
|
}
|
||||||
return numSnakesRemaining <= 1, nil
|
return numSnakesRemaining <= 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r StandardRuleset) Settings() Settings {
|
|
||||||
return Settings{
|
|
||||||
FoodSpawnChance: r.FoodSpawnChance,
|
|
||||||
MinimumFood: r.MinimumFood,
|
|
||||||
HazardDamagePerTurn: r.HazardDamagePerTurn,
|
|
||||||
HazardMap: r.HazardMap,
|
|
||||||
HazardMapAuthor: r.HazardMapAuthor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// impl Pipeline
|
|
||||||
func (r StandardRuleset) Err() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsInitialization checks whether the current state means the game is initialising.
|
|
||||||
func IsInitialization(b *BoardState, settings Settings, moves []SnakeMove) bool {
|
|
||||||
// We can safely assume that the game state is in the initialisation phase when
|
|
||||||
// the turn hasn't advanced and the moves are empty
|
|
||||||
return b.Turn <= 0 && len(moves) == 0
|
|
||||||
}
|
|
||||||
|
|
|
||||||
519
standard_test.go
519
standard_test.go
File diff suppressed because it is too large
Load diff
20
wrapped.go
20
wrapped.go
|
|
@ -9,22 +9,6 @@ var wrappedRulesetStages = []string{
|
||||||
StageEliminationStandard,
|
StageEliminationStandard,
|
||||||
}
|
}
|
||||||
|
|
||||||
type WrappedRuleset struct {
|
|
||||||
StandardRuleset
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *WrappedRuleset) Name() string { return GameTypeWrapped }
|
|
||||||
|
|
||||||
func (r WrappedRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
|
||||||
return NewPipeline(wrappedRulesetStages...).Execute(bs, s, sm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
|
||||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
|
||||||
|
|
||||||
return nextState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
if IsInitialization(b, settings, moves) {
|
if IsInitialization(b, settings, moves) {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
@ -47,10 +31,6 @@ func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (boo
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *WrappedRuleset) IsGameOver(b *BoardState) (bool, error) {
|
|
||||||
return GameOverStandard(b, r.Settings(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrap(value, min, max int) int {
|
func wrap(value, min, max int) int {
|
||||||
if value < min {
|
if value < min {
|
||||||
return max
|
return max
|
||||||
|
|
|
||||||
154
wrapped_test.go
154
wrapped_test.go
|
|
@ -7,15 +7,19 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func getWrappedRuleset(settings Settings) Ruleset {
|
||||||
|
return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeWrapped)
|
||||||
|
}
|
||||||
|
|
||||||
func TestLeft(t *testing.T) {
|
func TestLeft(t *testing.T) {
|
||||||
boardState := &BoardState{
|
boardState := &BoardState{
|
||||||
Width: 11,
|
Width: 11,
|
||||||
Height: 11,
|
Height: 11,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}},
|
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}},
|
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 10}}},
|
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||||
{ID: "topRight", Health: 10, Body: []Point{{10, 10}}},
|
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,17 +30,18 @@ func TestLeft(t *testing.T) {
|
||||||
{ID: "topRight", Move: "left"},
|
{ID: "topRight", Move: "left"},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := WrappedRuleset{}
|
r := getWrappedRuleset(Settings{})
|
||||||
|
|
||||||
nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves)
|
gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.False(t, gameOver)
|
||||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||||
|
|
||||||
expectedSnakes := []Snake{
|
expectedSnakes := []Snake{
|
||||||
{ID: "bottomLeft", Health: 10, Body: []Point{{10, 0}}},
|
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||||
{ID: "bottomRight", Health: 10, Body: []Point{{9, 0}}},
|
{ID: "bottomRight", Health: 10, Body: []Point{{X: 9, Y: 0}}},
|
||||||
{ID: "topLeft", Health: 10, Body: []Point{{10, 10}}},
|
{ID: "topLeft", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||||
{ID: "topRight", Health: 10, Body: []Point{{9, 10}}},
|
{ID: "topRight", Health: 10, Body: []Point{{X: 9, Y: 10}}},
|
||||||
}
|
}
|
||||||
for i, snake := range nextBoardState.Snakes {
|
for i, snake := range nextBoardState.Snakes {
|
||||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||||
|
|
@ -51,10 +56,10 @@ func TestRight(t *testing.T) {
|
||||||
Width: 11,
|
Width: 11,
|
||||||
Height: 11,
|
Height: 11,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}},
|
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}},
|
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 10}}},
|
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||||
{ID: "topRight", Health: 10, Body: []Point{{10, 10}}},
|
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,17 +70,18 @@ func TestRight(t *testing.T) {
|
||||||
{ID: "topRight", Move: "right"},
|
{ID: "topRight", Move: "right"},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := WrappedRuleset{}
|
r := getWrappedRuleset(Settings{})
|
||||||
|
|
||||||
nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves)
|
gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.False(t, gameOver)
|
||||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||||
|
|
||||||
expectedSnakes := []Snake{
|
expectedSnakes := []Snake{
|
||||||
{ID: "bottomLeft", Health: 10, Body: []Point{{1, 0}}},
|
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 1, Y: 0}}},
|
||||||
{ID: "bottomRight", Health: 10, Body: []Point{{0, 0}}},
|
{ID: "bottomRight", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||||
{ID: "topLeft", Health: 10, Body: []Point{{1, 10}}},
|
{ID: "topLeft", Health: 10, Body: []Point{{X: 1, Y: 10}}},
|
||||||
{ID: "topRight", Health: 10, Body: []Point{{0, 10}}},
|
{ID: "topRight", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||||
}
|
}
|
||||||
for i, snake := range nextBoardState.Snakes {
|
for i, snake := range nextBoardState.Snakes {
|
||||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||||
|
|
@ -90,10 +96,10 @@ func TestUp(t *testing.T) {
|
||||||
Width: 11,
|
Width: 11,
|
||||||
Height: 11,
|
Height: 11,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}},
|
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}},
|
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 10}}},
|
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||||
{ID: "topRight", Health: 10, Body: []Point{{10, 10}}},
|
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,17 +110,18 @@ func TestUp(t *testing.T) {
|
||||||
{ID: "topRight", Move: "up"},
|
{ID: "topRight", Move: "up"},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := WrappedRuleset{}
|
r := getWrappedRuleset(Settings{})
|
||||||
|
|
||||||
nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves)
|
gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.False(t, gameOver)
|
||||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||||
|
|
||||||
expectedSnakes := []Snake{
|
expectedSnakes := []Snake{
|
||||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 1}}},
|
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 1}}},
|
||||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 1}}},
|
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 1}}},
|
||||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 0}}},
|
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||||
{ID: "topRight", Health: 10, Body: []Point{{10, 0}}},
|
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||||
}
|
}
|
||||||
for i, snake := range nextBoardState.Snakes {
|
for i, snake := range nextBoardState.Snakes {
|
||||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||||
|
|
@ -129,10 +136,10 @@ func TestDown(t *testing.T) {
|
||||||
Width: 11,
|
Width: 11,
|
||||||
Height: 11,
|
Height: 11,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}},
|
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}},
|
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 10}}},
|
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||||
{ID: "topRight", Health: 10, Body: []Point{{10, 10}}},
|
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,17 +150,18 @@ func TestDown(t *testing.T) {
|
||||||
{ID: "topRight", Move: "down"},
|
{ID: "topRight", Move: "down"},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := WrappedRuleset{}
|
r := getWrappedRuleset(Settings{})
|
||||||
|
|
||||||
nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves)
|
gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.False(t, gameOver)
|
||||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||||
|
|
||||||
expectedSnakes := []Snake{
|
expectedSnakes := []Snake{
|
||||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 10}}},
|
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 10}}},
|
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 9}}},
|
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 9}}},
|
||||||
{ID: "topRight", Health: 10, Body: []Point{{10, 9}}},
|
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 9}}},
|
||||||
}
|
}
|
||||||
for i, snake := range nextBoardState.Snakes {
|
for i, snake := range nextBoardState.Snakes {
|
||||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||||
|
|
@ -168,14 +176,14 @@ func TestEdgeCrossingCollision(t *testing.T) {
|
||||||
Width: 11,
|
Width: 11,
|
||||||
Height: 11,
|
Height: 11,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{ID: "left", Health: 10, Body: []Point{{0, 5}}},
|
{ID: "left", Health: 10, Body: []Point{{X: 0, Y: 5}}},
|
||||||
{ID: "rightEdge", Health: 10, Body: []Point{
|
{ID: "rightEdge", Health: 10, Body: []Point{
|
||||||
{10, 1},
|
{X: 10, Y: 1},
|
||||||
{10, 2},
|
{X: 10, Y: 2},
|
||||||
{10, 3},
|
{X: 10, Y: 3},
|
||||||
{10, 4},
|
{X: 10, Y: 4},
|
||||||
{10, 5},
|
{X: 10, Y: 5},
|
||||||
{10, 6},
|
{X: 10, Y: 6},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -185,21 +193,22 @@ func TestEdgeCrossingCollision(t *testing.T) {
|
||||||
{ID: "rightEdge", Move: "down"},
|
{ID: "rightEdge", Move: "down"},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := WrappedRuleset{}
|
r := getWrappedRuleset(Settings{})
|
||||||
|
|
||||||
nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves)
|
gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.False(t, gameOver)
|
||||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||||
|
|
||||||
expectedSnakes := []Snake{
|
expectedSnakes := []Snake{
|
||||||
{ID: "left", Health: 0, Body: []Point{{10, 5}}, EliminatedCause: EliminatedByCollision, EliminatedBy: "rightEdge"},
|
{ID: "left", Health: 0, Body: []Point{{X: 10, Y: 5}}, EliminatedCause: EliminatedByCollision, EliminatedBy: "rightEdge"},
|
||||||
{ID: "rightEdge", Health: 10, Body: []Point{
|
{ID: "rightEdge", Health: 10, Body: []Point{
|
||||||
{10, 0},
|
{X: 10, Y: 0},
|
||||||
{10, 1},
|
{X: 10, Y: 1},
|
||||||
{10, 2},
|
{X: 10, Y: 2},
|
||||||
{10, 3},
|
{X: 10, Y: 3},
|
||||||
{10, 4},
|
{X: 10, Y: 4},
|
||||||
{10, 5},
|
{X: 10, Y: 5},
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
for i, snake := range nextBoardState.Snakes {
|
for i, snake := range nextBoardState.Snakes {
|
||||||
|
|
@ -215,11 +224,11 @@ func TestEdgeCrossingEating(t *testing.T) {
|
||||||
Width: 11,
|
Width: 11,
|
||||||
Height: 11,
|
Height: 11,
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{ID: "left", Health: 10, Body: []Point{{0, 5}, {1, 5}}},
|
{ID: "left", Health: 10, Body: []Point{{X: 0, Y: 5}, {X: 1, Y: 5}}},
|
||||||
{ID: "other", Health: 10, Body: []Point{{5, 5}}},
|
{ID: "other", Health: 10, Body: []Point{{X: 5, Y: 5}}},
|
||||||
},
|
},
|
||||||
Food: []Point{
|
Food: []Point{
|
||||||
{10, 5},
|
{X: 10, Y: 5},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,15 +237,16 @@ func TestEdgeCrossingEating(t *testing.T) {
|
||||||
{ID: "other", Move: "left"},
|
{ID: "other", Move: "left"},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := WrappedRuleset{}
|
r := getWrappedRuleset(Settings{})
|
||||||
|
|
||||||
nextBoardState, err := r.CreateNextBoardState(boardState, snakeMoves)
|
gameOver, nextBoardState, err := r.Execute(boardState, snakeMoves)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.False(t, gameOver)
|
||||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||||
|
|
||||||
expectedSnakes := []Snake{
|
expectedSnakes := []Snake{
|
||||||
{ID: "left", Health: 100, Body: []Point{{10, 5}, {0, 5}, {0, 5}}},
|
{ID: "left", Health: 100, Body: []Point{{X: 10, Y: 5}, {X: 0, Y: 5}, {X: 0, Y: 5}}},
|
||||||
{ID: "other", Health: 9, Body: []Point{{4, 5}}},
|
{ID: "other", Health: 9, Body: []Point{{X: 4, Y: 5}}},
|
||||||
}
|
}
|
||||||
for i, snake := range nextBoardState.Snakes {
|
for i, snake := range nextBoardState.Snakes {
|
||||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||||
|
|
@ -271,12 +281,12 @@ var wrappedCaseMoveAndWrap = gameTestCase{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{
|
{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Body: []Point{{0, 0}, {1, 0}},
|
Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||||
Health: 100,
|
Health: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "two",
|
ID: "two",
|
||||||
Body: []Point{{3, 4}, {3, 3}},
|
Body: []Point{{X: 3, Y: 4}, {X: 3, Y: 3}},
|
||||||
Health: 100,
|
Health: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -301,12 +311,12 @@ var wrappedCaseMoveAndWrap = gameTestCase{
|
||||||
Snakes: []Snake{
|
Snakes: []Snake{
|
||||||
{
|
{
|
||||||
ID: "one",
|
ID: "one",
|
||||||
Body: []Point{{9, 0}, {0, 0}},
|
Body: []Point{{X: 9, Y: 0}, {X: 0, Y: 0}},
|
||||||
Health: 99,
|
Health: 99,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: "two",
|
ID: "two",
|
||||||
Body: []Point{{3, 5}, {3, 4}},
|
Body: []Point{{X: 3, Y: 5}, {X: 3, Y: 4}},
|
||||||
Health: 99,
|
Health: 99,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -330,14 +340,10 @@ func TestWrappedCreateNextBoardState(t *testing.T) {
|
||||||
standardMoveAndCollideMAD,
|
standardMoveAndCollideMAD,
|
||||||
wrappedCaseMoveAndWrap,
|
wrappedCaseMoveAndWrap,
|
||||||
}
|
}
|
||||||
r := WrappedRuleset{}
|
r := getWrappedRuleset(Settings{})
|
||||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
|
||||||
ParamGameType: GameTypeWrapped,
|
|
||||||
})
|
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
gc.requireValidNextState(t, &r)
|
// test a RulesBuilder constructed instance
|
||||||
// also test a RulesBuilder constructed instance
|
gc.requireValidNextState(t, r)
|
||||||
gc.requireValidNextState(t, rb.Ruleset())
|
|
||||||
// also test a pipeline with the same settings
|
// also test a pipeline with the same settings
|
||||||
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeWrapped, NewPipeline(wrappedRulesetStages...)))
|
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeWrapped, NewPipeline(wrappedRulesetStages...)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue