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:
Rob O'Dwyer 2022-10-28 16:49:49 -07:00 committed by GitHub
parent 639362ef46
commit 82e1999126
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1349 additions and 1610 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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)

View file

@ -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
} }

View file

@ -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 {

View file

@ -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,
},
}

View file

@ -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 {

View file

@ -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)

View file

@ -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
} }
} }
}, },

View file

@ -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{}

View file

@ -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...)))
} }
} }

View file

@ -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
} }

View file

@ -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)
} }

View file

@ -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
} }

View file

@ -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)
} }

View file

@ -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.

View file

@ -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},

View file

@ -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
} }

View file

@ -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)

View file

@ -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
} }

View file

@ -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)

View file

@ -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])

View file

@ -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)
} }

View file

@ -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
} }

View file

@ -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)

View file

@ -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")
}) })

View file

@ -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)
} }

View file

@ -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++ {

View file

@ -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 {

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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)

View file

@ -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

View file

@ -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")
} }

View file

@ -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)

View file

@ -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
}

View file

@ -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...)))
} }

View file

@ -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()
} }

View file

@ -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().

View file

@ -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
View 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
View 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
View file

@ -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 {

View file

@ -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)

View file

@ -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
}

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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...)))
} }