DEV-1761: New rules API (#118)
* DEV-1761: Clean up Ruleset interface (#115) * remove legacy ruleset types and simplify ruleset interface * remove unnecessary settings argument from Ruleset interface * decouple rules.Settings from client API and store settings as strings * DEV 1761: Add new BoardState and Point fields (#117) * add Point.TTL, Point.Value, GameState and PointState to BoardState * allow maps to access BoardState.GameState,PointState * add PreUpdateBoard and refactor snail_mode with it * fix bug where an extra turn was printed to the console * fix formatting * fix lint errors Co-authored-by: JonathanArns <jonathan.arns@googlemail.com>
This commit is contained in:
parent
639362ef46
commit
82e1999126
50 changed files with 1349 additions and 1610 deletions
92
board.go
92
board.go
|
|
@ -2,6 +2,9 @@ package rules
|
|||
|
||||
import "fmt"
|
||||
|
||||
// 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 {
|
||||
Turn int
|
||||
Height int
|
||||
|
|
@ -9,15 +12,26 @@ type BoardState struct {
|
|||
Food []Point
|
||||
Snakes []Snake
|
||||
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 {
|
||||
X int
|
||||
Y int
|
||||
X int `json:"X"`
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +53,8 @@ func NewBoardState(width, height int) *BoardState {
|
|||
Food: []Point{},
|
||||
Snakes: []Snake{},
|
||||
Hazards: []Point{},
|
||||
GameState: map[string]string{},
|
||||
PointState: map[Point]int{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +67,14 @@ func (prevState *BoardState) Clone() *BoardState {
|
|||
Food: append([]Point{}, prevState.Food...),
|
||||
Snakes: make([]Snake, len(prevState.Snakes)),
|
||||
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++ {
|
||||
nextState.Snakes[i].ID = prevState.Snakes[i].ID
|
||||
|
|
@ -63,6 +87,42 @@ func (prevState *BoardState) Clone() *BoardState {
|
|||
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
|
||||
// "default" board state with snakes and food.
|
||||
// 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
|
||||
mn, md, mx := 1, (b.Width-1)/2, b.Width-2
|
||||
cornerPoints := []Point{
|
||||
{mn, mn},
|
||||
{mn, mx},
|
||||
{mx, mn},
|
||||
{mx, mx},
|
||||
{X: mn, Y: mn},
|
||||
{X: mn, Y: mx},
|
||||
{X: mx, Y: mn},
|
||||
{X: mx, Y: mx},
|
||||
}
|
||||
cardinalPoints := []Point{
|
||||
{mn, md},
|
||||
{md, mn},
|
||||
{md, mx},
|
||||
{mx, md},
|
||||
{X: mn, Y: md},
|
||||
{X: md, Y: mn},
|
||||
{X: md, Y: mx},
|
||||
{X: mx, Y: md},
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
|
|
@ -325,7 +385,7 @@ func PlaceFoodAutomatically(rand Rand, b *BoardState) error {
|
|||
|
||||
// Deprecated: will be replaced by maps.PlaceFoodFixed
|
||||
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
|
||||
// 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++ {
|
||||
snakeHead := b.Snakes[i].Body[0]
|
||||
possibleFoodLocations := []Point{
|
||||
{snakeHead.X - 1, snakeHead.Y - 1},
|
||||
{snakeHead.X - 1, snakeHead.Y + 1},
|
||||
{snakeHead.X + 1, snakeHead.Y - 1},
|
||||
{snakeHead.X + 1, snakeHead.Y + 1},
|
||||
{X: snakeHead.X - 1, Y: snakeHead.Y - 1},
|
||||
{X: snakeHead.X - 1, Y: snakeHead.Y + 1},
|
||||
{X: snakeHead.X + 1, Y: snakeHead.Y - 1},
|
||||
{X: snakeHead.X + 1, Y: snakeHead.Y + 1},
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
for _, p := range points {
|
||||
if p != centerCoord {
|
||||
|
|
|
|||
228
board_test.go
228
board_test.go
|
|
@ -8,6 +8,30 @@ import (
|
|||
"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) {
|
||||
// Small boards should no longer error and only get 1 food when num snakes > 4
|
||||
state, err := CreateDefaultBoardState(MaxRand, BoardSizeSmall, BoardSizeSmall, []string{
|
||||
|
|
@ -346,23 +370,23 @@ func TestPlaceSnake(t *testing.T) {
|
|||
boardState := NewBoardState(BoardSizeSmall, BoardSizeSmall)
|
||||
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.Equal(t, Snake{
|
||||
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,
|
||||
EliminatedCause: NotEliminated,
|
||||
EliminatedBy: "",
|
||||
}, 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.Equal(t, Snake{
|
||||
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,
|
||||
EliminatedCause: NotEliminated,
|
||||
EliminatedBy: "",
|
||||
|
|
@ -411,9 +435,9 @@ func TestPlaceFood(t *testing.T) {
|
|||
Width: BoardSizeSmall,
|
||||
Height: BoardSizeSmall,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{5, 1}}},
|
||||
{Body: []Point{{5, 3}}},
|
||||
{Body: []Point{{5, 5}}},
|
||||
{Body: []Point{{X: 5, Y: 1}}},
|
||||
{Body: []Point{{X: 5, Y: 3}}},
|
||||
{Body: []Point{{X: 5, Y: 5}}},
|
||||
},
|
||||
},
|
||||
4, // +1 because of fixed spawn locations
|
||||
|
|
@ -423,14 +447,14 @@ func TestPlaceFood(t *testing.T) {
|
|||
Width: BoardSizeMedium,
|
||||
Height: BoardSizeMedium,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{1, 1}}},
|
||||
{Body: []Point{{1, 5}}},
|
||||
{Body: []Point{{1, 9}}},
|
||||
{Body: []Point{{5, 1}}},
|
||||
{Body: []Point{{5, 9}}},
|
||||
{Body: []Point{{9, 1}}},
|
||||
{Body: []Point{{9, 5}}},
|
||||
{Body: []Point{{9, 9}}},
|
||||
{Body: []Point{{X: 1, Y: 1}}},
|
||||
{Body: []Point{{X: 1, Y: 5}}},
|
||||
{Body: []Point{{X: 1, Y: 9}}},
|
||||
{Body: []Point{{X: 5, Y: 1}}},
|
||||
{Body: []Point{{X: 5, Y: 9}}},
|
||||
{Body: []Point{{X: 9, Y: 1}}},
|
||||
{Body: []Point{{X: 9, Y: 5}}},
|
||||
{Body: []Point{{X: 9, Y: 9}}},
|
||||
},
|
||||
},
|
||||
9, // +1 because of fixed spawn locations
|
||||
|
|
@ -440,12 +464,12 @@ func TestPlaceFood(t *testing.T) {
|
|||
Width: BoardSizeLarge,
|
||||
Height: BoardSizeLarge,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{1, 1}}},
|
||||
{Body: []Point{{1, 9}}},
|
||||
{Body: []Point{{1, 17}}},
|
||||
{Body: []Point{{17, 1}}},
|
||||
{Body: []Point{{17, 9}}},
|
||||
{Body: []Point{{17, 17}}},
|
||||
{Body: []Point{{X: 1, Y: 1}}},
|
||||
{Body: []Point{{X: 1, Y: 9}}},
|
||||
{Body: []Point{{X: 1, Y: 17}}},
|
||||
{Body: []Point{{X: 17, Y: 1}}},
|
||||
{Body: []Point{{X: 17, Y: 9}}},
|
||||
{Body: []Point{{X: 17, Y: 17}}},
|
||||
},
|
||||
},
|
||||
7, // +1 because of fixed spawn locations
|
||||
|
|
@ -478,7 +502,7 @@ func TestPlaceFoodFixed(t *testing.T) {
|
|||
Width: BoardSizeSmall,
|
||||
Height: BoardSizeSmall,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{1, 3}}},
|
||||
{Body: []Point{{X: 1, Y: 3}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -487,10 +511,10 @@ func TestPlaceFoodFixed(t *testing.T) {
|
|||
Width: BoardSizeMedium,
|
||||
Height: BoardSizeMedium,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{1, 1}}},
|
||||
{Body: []Point{{1, 5}}},
|
||||
{Body: []Point{{9, 5}}},
|
||||
{Body: []Point{{9, 9}}},
|
||||
{Body: []Point{{X: 1, Y: 1}}},
|
||||
{Body: []Point{{X: 1, Y: 5}}},
|
||||
{Body: []Point{{X: 9, Y: 5}}},
|
||||
{Body: []Point{{X: 9, Y: 9}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -499,14 +523,14 @@ func TestPlaceFoodFixed(t *testing.T) {
|
|||
Width: BoardSizeLarge,
|
||||
Height: BoardSizeLarge,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{1, 1}}},
|
||||
{Body: []Point{{1, 9}}},
|
||||
{Body: []Point{{1, 17}}},
|
||||
{Body: []Point{{9, 1}}},
|
||||
{Body: []Point{{9, 17}}},
|
||||
{Body: []Point{{17, 1}}},
|
||||
{Body: []Point{{17, 9}}},
|
||||
{Body: []Point{{17, 17}}},
|
||||
{Body: []Point{{X: 1, Y: 1}}},
|
||||
{Body: []Point{{X: 1, Y: 9}}},
|
||||
{Body: []Point{{X: 1, Y: 17}}},
|
||||
{Body: []Point{{X: 9, Y: 1}}},
|
||||
{Body: []Point{{X: 9, Y: 17}}},
|
||||
{Body: []Point{{X: 17, Y: 1}}},
|
||||
{Body: []Point{{X: 17, Y: 9}}},
|
||||
{Body: []Point{{X: 17, Y: 17}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -519,16 +543,16 @@ func TestPlaceFoodFixed(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
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
|
||||
for _, snake := range test.BoardState.Snakes {
|
||||
head := snake.Body[0]
|
||||
|
||||
bottomLeft := Point{head.X - 1, head.Y - 1}
|
||||
topLeft := Point{head.X - 1, head.Y + 1}
|
||||
bottomRight := Point{head.X + 1, head.Y - 1}
|
||||
topRight := Point{head.X + 1, head.Y + 1}
|
||||
bottomLeft := Point{X: head.X - 1, Y: head.Y - 1}
|
||||
topLeft := Point{X: head.X - 1, Y: head.Y + 1}
|
||||
bottomRight := Point{X: head.X + 1, Y: head.Y - 1}
|
||||
topRight := Point{X: head.X + 1, Y: head.Y + 1}
|
||||
|
||||
foundFoodInTwoMoves := false
|
||||
for _, food := range test.BoardState.Food {
|
||||
|
|
@ -559,7 +583,7 @@ func TestPlaceFoodFixedNoRoom(t *testing.T) {
|
|||
Width: 3,
|
||||
Height: 3,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{1, 1}}},
|
||||
{Body: []Point{{X: 1, Y: 1}}},
|
||||
},
|
||||
Food: []Point{},
|
||||
}
|
||||
|
|
@ -572,10 +596,10 @@ func TestPlaceFoodFixedNoRoom_Corners(t *testing.T) {
|
|||
Width: 7,
|
||||
Height: 7,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{1, 1}}},
|
||||
{Body: []Point{{1, 5}}},
|
||||
{Body: []Point{{5, 1}}},
|
||||
{Body: []Point{{5, 5}}},
|
||||
{Body: []Point{{X: 1, Y: 1}}},
|
||||
{Body: []Point{{X: 1, Y: 5}}},
|
||||
{Body: []Point{{X: 5, Y: 1}}},
|
||||
{Body: []Point{{X: 5, Y: 5}}},
|
||||
},
|
||||
Food: []Point{},
|
||||
}
|
||||
|
|
@ -597,10 +621,10 @@ func TestPlaceFoodFixedNoRoom_Corners(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
|
||||
expectedFood := []Point{
|
||||
{0, 2}, {2, 0}, // Snake @ {1, 1}
|
||||
{0, 4}, {2, 6}, // Snake @ {1, 5}
|
||||
{4, 0}, {6, 2}, // Snake @ {5, 1}
|
||||
{4, 6}, {6, 4}, // Snake @ {5, 5}
|
||||
{X: 0, Y: 2}, {X: 2, Y: 0}, // Snake @ {X: 1, Y: 1}
|
||||
{X: 0, Y: 4}, {X: 2, Y: 6}, // Snake @ {X: 1, Y: 5}
|
||||
{X: 4, Y: 0}, {X: 6, Y: 2}, // Snake @ {X: 5, Y: 1}
|
||||
{X: 4, Y: 6}, {X: 6, Y: 4}, // Snake @ {X: 5, Y: 5}
|
||||
}
|
||||
sortPoints(expectedFood)
|
||||
sortPoints(boardState.Food)
|
||||
|
|
@ -612,10 +636,10 @@ func TestPlaceFoodFixedNoRoom_Cardinal(t *testing.T) {
|
|||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{1, 5}}},
|
||||
{Body: []Point{{5, 1}}},
|
||||
{Body: []Point{{5, 9}}},
|
||||
{Body: []Point{{9, 5}}},
|
||||
{Body: []Point{{X: 1, Y: 5}}},
|
||||
{Body: []Point{{X: 5, Y: 1}}},
|
||||
{Body: []Point{{X: 5, Y: 9}}},
|
||||
{Body: []Point{{X: 9, Y: 5}}},
|
||||
},
|
||||
Food: []Point{},
|
||||
}
|
||||
|
|
@ -637,10 +661,10 @@ func TestPlaceFoodFixedNoRoom_Cardinal(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
|
||||
expectedFood := []Point{
|
||||
{0, 4}, {0, 6}, // Snake @ {1, 5}
|
||||
{4, 0}, {6, 0}, // Snake @ {5, 1}
|
||||
{4, 10}, {6, 10}, // Snake @ {5, 9}
|
||||
{10, 4}, {10, 6}, // Snake @ {9, 5}
|
||||
{X: 0, Y: 4}, {X: 0, Y: 6}, // Snake @ {X: 1, Y: 5}
|
||||
{X: 4, Y: 0}, {X: 6, Y: 0}, // Snake @ {X: 5, Y: 1}
|
||||
{X: 4, Y: 10}, {X: 6, Y: 10}, // Snake @ {X: 5, Y: 9}
|
||||
{X: 10, Y: 4}, {X: 10, Y: 6}, // Snake @ {X: 9, Y: 5}
|
||||
}
|
||||
sortPoints(expectedFood)
|
||||
sortPoints(boardState.Food)
|
||||
|
|
@ -653,15 +677,15 @@ func TestGetDistanceBetweenPoints(t *testing.T) {
|
|||
B Point
|
||||
Expected int
|
||||
}{
|
||||
{Point{0, 0}, Point{0, 0}, 0},
|
||||
{Point{0, 0}, Point{1, 0}, 1},
|
||||
{Point{0, 0}, Point{0, 1}, 1},
|
||||
{Point{0, 0}, Point{1, 1}, 2},
|
||||
{Point{0, 0}, Point{4, 4}, 8},
|
||||
{Point{0, 0}, Point{4, 6}, 10},
|
||||
{Point{8, 0}, Point{8, 0}, 0},
|
||||
{Point{8, 0}, Point{8, 8}, 8},
|
||||
{Point{8, 0}, Point{0, 8}, 16},
|
||||
{Point{X: 0, Y: 0}, Point{X: 0, Y: 0}, 0},
|
||||
{Point{X: 0, Y: 0}, Point{X: 1, Y: 0}, 1},
|
||||
{Point{X: 0, Y: 0}, Point{X: 0, Y: 1}, 1},
|
||||
{Point{X: 0, Y: 0}, Point{X: 1, Y: 1}, 2},
|
||||
{Point{X: 0, Y: 0}, Point{X: 4, Y: 4}, 8},
|
||||
{Point{X: 0, Y: 0}, Point{X: 4, Y: 6}, 10},
|
||||
{Point{X: 8, Y: 0}, Point{X: 8, Y: 0}, 0},
|
||||
{Point{X: 8, Y: 0}, Point{X: 8, Y: 8}, 8},
|
||||
{Point{X: 8, Y: 0}, Point{X: 0, Y: 8}, 16},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
@ -704,20 +728,20 @@ func TestGetUnoccupiedPoints(t *testing.T) {
|
|||
Height: 1,
|
||||
Width: 1,
|
||||
},
|
||||
[]Point{{0, 0}},
|
||||
[]Point{{X: 0, Y: 0}},
|
||||
},
|
||||
{
|
||||
&BoardState{
|
||||
Height: 1,
|
||||
Width: 2,
|
||||
},
|
||||
[]Point{{0, 0}, {1, 0}},
|
||||
[]Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||
},
|
||||
{
|
||||
&BoardState{
|
||||
Height: 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{},
|
||||
},
|
||||
|
|
@ -725,15 +749,15 @@ func TestGetUnoccupiedPoints(t *testing.T) {
|
|||
&BoardState{
|
||||
Height: 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{
|
||||
Height: 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{},
|
||||
},
|
||||
|
|
@ -742,38 +766,38 @@ func TestGetUnoccupiedPoints(t *testing.T) {
|
|||
Height: 4,
|
||||
Width: 1,
|
||||
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{
|
||||
Height: 2,
|
||||
Width: 3,
|
||||
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{
|
||||
Height: 2,
|
||||
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{
|
||||
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
|
||||
{Body: []Point{{0, 1}}},
|
||||
{Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}},
|
||||
{Body: []Point{{X: 0, Y: 1}}},
|
||||
},
|
||||
},
|
||||
[]Point{{2, 1}},
|
||||
[]Point{{X: 2, Y: 1}},
|
||||
},
|
||||
{
|
||||
&BoardState{
|
||||
Height: 1,
|
||||
Width: 1,
|
||||
Hazards: []Point{{0, 0}},
|
||||
Hazards: []Point{{X: 0, Y: 0}},
|
||||
},
|
||||
[]Point{},
|
||||
},
|
||||
|
|
@ -781,22 +805,22 @@ func TestGetUnoccupiedPoints(t *testing.T) {
|
|||
&BoardState{
|
||||
Height: 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{
|
||||
Height: 2,
|
||||
Width: 3,
|
||||
Food: []Point{{1, 1}, {2, 0}},
|
||||
Food: []Point{{X: 1, Y: 1}, {X: 2, Y: 0}},
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
|
||||
{Body: []Point{{0, 1}}},
|
||||
{Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 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,
|
||||
Width: 1,
|
||||
},
|
||||
[]Point{{0, 0}},
|
||||
[]Point{{X: 0, Y: 0}},
|
||||
},
|
||||
{
|
||||
&BoardState{
|
||||
Height: 2,
|
||||
Width: 2,
|
||||
},
|
||||
[]Point{{0, 0}, {1, 1}},
|
||||
[]Point{{X: 0, Y: 0}, {X: 1, Y: 1}},
|
||||
},
|
||||
{
|
||||
&BoardState{
|
||||
Height: 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{},
|
||||
},
|
||||
|
|
@ -840,15 +864,15 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) {
|
|||
&BoardState{
|
||||
Height: 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{
|
||||
Height: 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{},
|
||||
},
|
||||
|
|
@ -857,32 +881,32 @@ func TestGetEvenUnoccupiedPoints(t *testing.T) {
|
|||
Height: 4,
|
||||
Width: 1,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{0, 0}}},
|
||||
{Body: []Point{{X: 0, Y: 0}}},
|
||||
},
|
||||
},
|
||||
[]Point{{0, 2}},
|
||||
[]Point{{X: 0, Y: 2}},
|
||||
},
|
||||
{
|
||||
&BoardState{
|
||||
Height: 2,
|
||||
Width: 3,
|
||||
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{
|
||||
Height: 2,
|
||||
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{
|
||||
{Body: []Point{{0, 0}, {1, 0}, {1, 1}}},
|
||||
{Body: []Point{{0, 1}}},
|
||||
{Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 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,
|
||||
Width: 3,
|
||||
Snakes: []Snake{
|
||||
{Body: []Point{{1, 0}}},
|
||||
{Body: []Point{{X: 1, Y: 0}}},
|
||||
},
|
||||
}
|
||||
// Food should never spawn, no room
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ func (gc *gameTestCase) requireValidNextState(t *testing.T, r Ruleset) {
|
|||
t.Helper()
|
||||
t.Run(gc.name, func(t *testing.T) {
|
||||
t.Helper()
|
||||
prev := gc.prevState.Clone() // clone to protect against mutation (so we can ru-use test cases)
|
||||
nextState, err := r.CreateNextBoardState(prev, gc.moves)
|
||||
prev := gc.prevState.Clone() // clone to protect against mutation (so we can re-use test cases)
|
||||
_, nextState, err := r.Execute(prev, gc.moves)
|
||||
require.Equal(t, gc.expectedError, err)
|
||||
if gc.expectedState != nil {
|
||||
require.Equal(t, gc.expectedState.Width, nextState.Width)
|
||||
|
|
|
|||
|
|
@ -88,7 +88,9 @@ func NewPlayCommand() *cobra.Command {
|
|||
if err := gameState.Initialize(); err != nil {
|
||||
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
|
||||
gameState.settings = map[string]string{
|
||||
rules.ParamGameType: gameState.GameType,
|
||||
rules.ParamFoodSpawnChance: fmt.Sprint(gameState.FoodSpawnChance),
|
||||
rules.ParamMinimumFood: fmt.Sprint(gameState.MinimumFood),
|
||||
rules.ParamHazardDamagePerTurn: fmt.Sprint(gameState.HazardDamagePerTurn),
|
||||
|
|
@ -155,7 +156,7 @@ func (gameState *GameState) Initialize() error {
|
|||
WithSeed(gameState.Seed).
|
||||
WithParams(gameState.settings).
|
||||
WithSolo(len(gameState.URLs) < 2).
|
||||
Ruleset()
|
||||
NamedRuleset(gameState.GameType)
|
||||
gameState.ruleset = ruleset
|
||||
|
||||
// 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.
|
||||
func (gameState *GameState) Run() {
|
||||
func (gameState *GameState) Run() error {
|
||||
var gameOver bool
|
||||
var err error
|
||||
|
||||
// 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)
|
||||
|
||||
boardState := gameState.initializeBoardFromArgs()
|
||||
gameOver, boardState, err := gameState.initializeBoardFromArgs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error initializing board: %w", err)
|
||||
}
|
||||
|
||||
gameExporter := GameExporter{
|
||||
game: gameState.createClientGame(),
|
||||
|
|
@ -209,7 +219,7 @@ func (gameState *GameState) Run() {
|
|||
if gameState.ViewInBrowser {
|
||||
serverURL, err := boardServer.Listen()
|
||||
if err != nil {
|
||||
log.ERROR.Fatalf("Error starting HTTP server: %v", err)
|
||||
return fmt.Errorf("Error starting HTTP server: %w", err)
|
||||
}
|
||||
defer boardServer.Shutdown()
|
||||
log.INFO.Printf("Board server listening on %s", serverURL)
|
||||
|
|
@ -233,13 +243,7 @@ func (gameState *GameState) Run() {
|
|||
gameState.printState(boardState)
|
||||
}
|
||||
|
||||
var endTime time.Time
|
||||
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
|
||||
// Export game first, if enabled, so that we capture the request for turn zero.
|
||||
if exportGame {
|
||||
// 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.
|
||||
|
|
@ -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 {
|
||||
gameState.printMap(boardState)
|
||||
|
|
@ -274,9 +292,7 @@ func (gameState *GameState) Run() {
|
|||
if gameState.ViewInBrowser {
|
||||
boardServer.SendEvent(gameState.buildFrameEvent(boardState))
|
||||
}
|
||||
}
|
||||
|
||||
// Export final turn
|
||||
if exportGame {
|
||||
for _, snakeState := range gameState.snakeStates {
|
||||
snakeRequest := gameState.getRequestBodyForSnake(boardState, snakeState)
|
||||
|
|
@ -284,6 +300,7 @@ func (gameState *GameState) Run() {
|
|||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gameExporter.isDraw = false
|
||||
|
||||
|
|
@ -320,24 +337,26 @@ func (gameState *GameState) Run() {
|
|||
if exportGame {
|
||||
lines, err := gameExporter.FlushToFile(gameState.outputFile)
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gameState *GameState) initializeBoardFromArgs() *rules.BoardState {
|
||||
func (gameState *GameState) initializeBoardFromArgs() (bool, *rules.BoardState, error) {
|
||||
snakeIds := []string{}
|
||||
for _, snakeState := range gameState.snakeStates {
|
||||
snakeIds = append(snakeIds, snakeState.ID)
|
||||
}
|
||||
boardState, err := maps.SetupBoard(gameState.gameMap.ID(), gameState.ruleset.Settings(), gameState.Width, gameState.Height, snakeIds)
|
||||
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 {
|
||||
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 {
|
||||
|
|
@ -351,12 +370,18 @@ func (gameState *GameState) initializeBoardFromArgs() *rules.BoardState {
|
|||
log.WARN.Printf("Request to %v failed", u.String())
|
||||
}
|
||||
}
|
||||
return boardState
|
||||
return gameOver, boardState, nil
|
||||
}
|
||||
|
||||
func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) *rules.BoardState {
|
||||
stateUpdates := make(chan SnakeState, len(gameState.snakeStates))
|
||||
func (gameState *GameState) createNextBoardState(boardState *rules.BoardState) (bool, *rules.BoardState, error) {
|
||||
// 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 {
|
||||
for _, snakeState := range gameState.snakeStates {
|
||||
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})
|
||||
}
|
||||
|
||||
boardState, err := gameState.ruleset.CreateNextBoardState(boardState, moves)
|
||||
gameOver, boardState, err := gameState.ruleset.Execute(boardState, moves)
|
||||
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 {
|
||||
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
|
||||
|
||||
return boardState
|
||||
return gameOver, boardState, nil
|
||||
}
|
||||
|
||||
func (gameState *GameState) getSnakeUpdate(boardState *rules.BoardState, snakeState SnakeState) SnakeState {
|
||||
|
|
@ -522,13 +548,13 @@ func (gameState *GameState) createClientGame() client.Game {
|
|||
Ruleset: client.Ruleset{
|
||||
Name: gameState.ruleset.Name(),
|
||||
Version: "cli", // TODO: Use GitHub Release Version
|
||||
Settings: gameState.ruleset.Settings(),
|
||||
Settings: client.ConvertRulesetSettings(gameState.ruleset.Settings()),
|
||||
},
|
||||
Map: gameState.gameMap.ID(),
|
||||
}
|
||||
}
|
||||
|
||||
func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState {
|
||||
func (gameState *GameState) buildSnakesFromOptions() (map[string]SnakeState, error) {
|
||||
bodyChars := []rune{'■', '⌀', '●', '☻', '◘', '☺', '□', '⍟'}
|
||||
var numSnakes int
|
||||
snakes := map[string]SnakeState{}
|
||||
|
|
@ -560,11 +586,11 @@ func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState {
|
|||
if i < numURLs {
|
||||
u, err := url.ParseRequestURI(gameState.URLs[i])
|
||||
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()
|
||||
} 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{
|
||||
|
|
@ -573,25 +599,25 @@ func (gameState *GameState) buildSnakesFromOptions() map[string]SnakeState {
|
|||
var snakeErr error
|
||||
res, _, err := gameState.httpClient.Get(snakeURL)
|
||||
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
|
||||
|
||||
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()
|
||||
body, readErr := ioutil.ReadAll(res.Body)
|
||||
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{}
|
||||
jsonErr := json.Unmarshal(body, &pingResponse)
|
||||
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
|
||||
|
|
@ -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)
|
||||
}
|
||||
return snakes
|
||||
return snakes, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
requestJSON, err := json.Marshal(snakeRequest)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,11 +45,10 @@ func buildDefaultGameState() *GameState {
|
|||
func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
||||
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
||||
state := &rules.BoardState{
|
||||
Height: 11,
|
||||
Width: 11,
|
||||
Snakes: []rules.Snake{s1, s2},
|
||||
}
|
||||
state := rules.NewBoardState(11, 11).
|
||||
WithSnakes(
|
||||
[]rules.Snake{s1, s2},
|
||||
)
|
||||
s1State := SnakeState{
|
||||
ID: "one",
|
||||
Name: "ONE",
|
||||
|
|
@ -85,11 +84,8 @@ func TestGetIndividualBoardStateForSnake(t *testing.T) {
|
|||
func TestSettingsRequestSerialization(t *testing.T) {
|
||||
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
||||
state := &rules.BoardState{
|
||||
Height: 11,
|
||||
Width: 11,
|
||||
Snakes: []rules.Snake{s1, s2},
|
||||
}
|
||||
state := rules.NewBoardState(11, 11).
|
||||
WithSnakes([]rules.Snake{s1, s2})
|
||||
s1State := SnakeState{
|
||||
ID: "one",
|
||||
Name: "ONE",
|
||||
|
|
@ -255,12 +251,11 @@ func TestBuildFrameEvent(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "snake fields",
|
||||
boardState: &rules.BoardState{
|
||||
Turn: 99,
|
||||
Height: 19,
|
||||
Width: 25,
|
||||
Food: []rules.Point{{X: 9, Y: 4}},
|
||||
Snakes: []rules.Snake{
|
||||
boardState: rules.NewBoardState(19, 25).
|
||||
WithTurn(99).
|
||||
WithFood([]rules.Point{{X: 9, Y: 4}}).
|
||||
WithHazards([]rules.Point{{X: 8, Y: 6}}).
|
||||
WithSnakes([]rules.Snake{
|
||||
{
|
||||
ID: "1",
|
||||
Body: []rules.Point{
|
||||
|
|
@ -273,9 +268,7 @@ func TestBuildFrameEvent(t *testing.T) {
|
|||
EliminatedOnTurn: 45,
|
||||
EliminatedBy: "1",
|
||||
},
|
||||
},
|
||||
Hazards: []rules.Point{{X: 8, Y: 6}},
|
||||
},
|
||||
}),
|
||||
snakeStates: map[string]SnakeState{
|
||||
"1": {
|
||||
URL: "http://example.com",
|
||||
|
|
@ -326,18 +319,15 @@ func TestBuildFrameEvent(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "snake errors",
|
||||
boardState: &rules.BoardState{
|
||||
Height: 19,
|
||||
Width: 25,
|
||||
Snakes: []rules.Snake{
|
||||
boardState: rules.NewBoardState(19, 25).
|
||||
WithSnakes([]rules.Snake{
|
||||
{
|
||||
ID: "bad_status",
|
||||
},
|
||||
{
|
||||
ID: "connection_error",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
snakeStates: map[string]SnakeState{
|
||||
"bad_status": {
|
||||
StatusCode: 504,
|
||||
|
|
@ -366,6 +356,8 @@ func TestBuildFrameEvent(t *testing.T) {
|
|||
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) {
|
||||
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||
s2 := rules.Snake{ID: "two", Body: []rules.Point{{X: 4, Y: 3}}}
|
||||
boardState := &rules.BoardState{
|
||||
Height: 11,
|
||||
Width: 11,
|
||||
Snakes: []rules.Snake{s1, s2},
|
||||
}
|
||||
boardState := rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{s1, s2})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -530,11 +518,7 @@ func TestGetMoveForSnake(t *testing.T) {
|
|||
|
||||
func TestCreateNextBoardState(t *testing.T) {
|
||||
s1 := rules.Snake{ID: "one", Body: []rules.Point{{X: 3, Y: 3}}}
|
||||
boardState := &rules.BoardState{
|
||||
Height: 11,
|
||||
Width: 11,
|
||||
Snakes: []rules.Snake{s1},
|
||||
}
|
||||
boardState := rules.NewBoardState(11, 11).WithSnakes([]rules.Snake{s1})
|
||||
snakeState := SnakeState{
|
||||
ID: s1.ID,
|
||||
URL: "http://example.com",
|
||||
|
|
@ -549,7 +533,9 @@ func TestCreateNextBoardState(t *testing.T) {
|
|||
gameState.snakeStates = map[string]SnakeState{s1.ID: snakeState}
|
||||
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]
|
||||
|
||||
require.NotNil(t, nextBoardState)
|
||||
|
|
@ -593,16 +579,18 @@ func TestOutputFile(t *testing.T) {
|
|||
outputFile := new(closableBuffer)
|
||||
gameState.outputFile = outputFile
|
||||
|
||||
gameState.ruleset = StubRuleset{maxTurns: 1, settings: rules.Settings{
|
||||
FoodSpawnChance: 1,
|
||||
MinimumFood: 2,
|
||||
HazardDamagePerTurn: 3,
|
||||
RoyaleSettings: rules.RoyaleSettings{
|
||||
ShrinkEveryNTurns: 4,
|
||||
},
|
||||
}}
|
||||
gameState.ruleset = StubRuleset{
|
||||
maxTurns: 1,
|
||||
settings: rules.NewSettings(map[string]string{
|
||||
rules.ParamFoodSpawnChance: "1",
|
||||
rules.ParamMinimumFood: "2",
|
||||
rules.ParamHazardDamagePerTurn: "3",
|
||||
rules.ParamShrinkEveryNTurns: "4",
|
||||
}),
|
||||
}
|
||||
|
||||
gameState.Run()
|
||||
err = gameState.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
lines := strings.Split(outputFile.String(), "\n")
|
||||
require.Len(t, lines, 5)
|
||||
|
|
@ -626,14 +614,8 @@ type StubRuleset struct {
|
|||
|
||||
func (ruleset StubRuleset) Name() string { return "standard" }
|
||||
func (ruleset StubRuleset) Settings() rules.Settings { return ruleset.settings }
|
||||
func (ruleset StubRuleset) ModifyInitialBoardState(initialState *rules.BoardState) (*rules.BoardState, error) {
|
||||
return initialState, 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
|
||||
func (ruleset StubRuleset) Execute(prevState *rules.BoardState, moves []rules.SnakeMove) (bool, *rules.BoardState, error) {
|
||||
return prevState.Turn >= ruleset.maxTurns, prevState, nil
|
||||
}
|
||||
|
||||
type stubHTTPClient struct {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ func exampleSnakeRequest() SnakeRequest {
|
|||
Ruleset: Ruleset{
|
||||
Name: "test-ruleset-name",
|
||||
Version: "cli",
|
||||
Settings: exampleRulesetSettings,
|
||||
Settings: ConvertRulesetSettings(exampleRulesetSettings),
|
||||
},
|
||||
Timeout: 33,
|
||||
Source: "league",
|
||||
|
|
@ -75,21 +75,9 @@ func exampleSnakeRequest() SnakeRequest {
|
|||
}
|
||||
}
|
||||
|
||||
var exampleRulesetSettings = rules.Settings{
|
||||
FoodSpawnChance: 10,
|
||||
MinimumFood: 20,
|
||||
HazardDamagePerTurn: 30,
|
||||
HazardMap: "hz_spiral",
|
||||
HazardMapAuthor: "altersaddle",
|
||||
|
||||
RoyaleSettings: rules.RoyaleSettings{
|
||||
ShrinkEveryNTurns: 40,
|
||||
},
|
||||
|
||||
SquadSettings: rules.SquadSettings{
|
||||
AllowBodyCollisions: true,
|
||||
SharedElimination: true,
|
||||
SharedHealth: true,
|
||||
SharedLength: true,
|
||||
},
|
||||
}
|
||||
var exampleRulesetSettings = rules.NewSettings(map[string]string{
|
||||
rules.ParamFoodSpawnChance: "10",
|
||||
rules.ParamMinimumFood: "20",
|
||||
rules.ParamHazardDamagePerTurn: "30",
|
||||
rules.ParamShrinkEveryNTurns: "40",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -51,17 +51,45 @@ type Customizations struct {
|
|||
type Ruleset struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Settings rules.Settings `json:"settings"`
|
||||
Settings RulesetSettings `json:"settings"`
|
||||
}
|
||||
|
||||
// RulesetSettings is deprecated: use rules.Settings instead
|
||||
type RulesetSettings rules.Settings
|
||||
// RulesetSettings contains a static collection of a few settings that are exposed through the API.
|
||||
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
|
||||
type RoyaleSettings rules.RoyaleSettings
|
||||
// RoyaleSettings contains settings that are specific to the "royale" game mode
|
||||
type RoyaleSettings struct {
|
||||
ShrinkEveryNTurns int `json:"shrinkEveryNTurns"`
|
||||
}
|
||||
|
||||
// SquadSettings is deprecated: use rules.SquadSettings instead
|
||||
type SquadSettings rules.SquadSettings
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
type Coord struct {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/BattlesnakeOfficial/rules"
|
||||
"github.com/BattlesnakeOfficial/rules/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -19,7 +18,7 @@ func TestBuildSnakeRequestJSON(t *testing.T) {
|
|||
|
||||
func TestBuildSnakeRequestJSONEmptyRulesetSettings(t *testing.T) {
|
||||
snakeRequest := exampleSnakeRequest()
|
||||
snakeRequest.Game.Ruleset.Settings = rules.Settings{}
|
||||
snakeRequest.Game.Ruleset.Settings = RulesetSettings{}
|
||||
data, err := json.MarshalIndent(snakeRequest, "", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
|
|||
12
client/testdata/snake_request.json
vendored
12
client/testdata/snake_request.json
vendored
|
|
@ -8,16 +8,16 @@
|
|||
"foodSpawnChance": 10,
|
||||
"minimumFood": 20,
|
||||
"hazardDamagePerTurn": 30,
|
||||
"hazardMap": "hz_spiral",
|
||||
"hazardMapAuthor": "altersaddle",
|
||||
"hazardMap": "",
|
||||
"hazardMapAuthor": "",
|
||||
"royale": {
|
||||
"shrinkEveryNTurns": 40
|
||||
},
|
||||
"squad": {
|
||||
"allowBodyCollisions": true,
|
||||
"sharedElimination": true,
|
||||
"sharedHealth": true,
|
||||
"sharedLength": true
|
||||
"allowBodyCollisions": false,
|
||||
"sharedElimination": false,
|
||||
"sharedHealth": false,
|
||||
"sharedLength": false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,31 +22,6 @@ var wrappedConstrictorRulesetStages = []string{
|
|||
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) {
|
||||
// Remove all food from the board
|
||||
b.Food = []Point{}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,6 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestConstrictorRulesetInterface(t *testing.T) {
|
||||
var _ Ruleset = (*ConstrictorRuleset)(nil)
|
||||
}
|
||||
|
||||
// Test that two equal snakes collide and both get eliminated
|
||||
// also checks:
|
||||
// - food removed
|
||||
|
|
@ -21,16 +17,16 @@ var constrictorMoveAndCollideMAD = gameTestCase{
|
|||
Snakes: []Snake{
|
||||
{
|
||||
ID: "one",
|
||||
Body: []Point{{1, 1}, {2, 1}},
|
||||
Body: []Point{{X: 1, Y: 1}, {X: 2, Y: 1}},
|
||||
Health: 99,
|
||||
},
|
||||
{
|
||||
ID: "two",
|
||||
Body: []Point{{1, 2}, {2, 2}},
|
||||
Body: []Point{{X: 1, Y: 2}, {X: 2, Y: 2}},
|
||||
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{},
|
||||
},
|
||||
[]SnakeMove{
|
||||
|
|
@ -44,7 +40,7 @@ var constrictorMoveAndCollideMAD = gameTestCase{
|
|||
Snakes: []Snake{
|
||||
{
|
||||
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,
|
||||
EliminatedCause: EliminatedByCollision,
|
||||
EliminatedBy: "two",
|
||||
|
|
@ -52,7 +48,7 @@ var constrictorMoveAndCollideMAD = gameTestCase{
|
|||
},
|
||||
{
|
||||
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,
|
||||
EliminatedCause: EliminatedByCollision,
|
||||
EliminatedBy: "one",
|
||||
|
|
@ -70,15 +66,11 @@ func TestConstrictorCreateNextBoardState(t *testing.T) {
|
|||
standardCaseErrZeroLengthSnake,
|
||||
constrictorMoveAndCollideMAD,
|
||||
}
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeConstrictor,
|
||||
})
|
||||
r := ConstrictorRuleset{}
|
||||
r := NewRulesetBuilder().NamedRuleset(GameTypeConstrictor)
|
||||
for _, gc := range cases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, r)
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeConstrictor, NewPipeline(constrictorRulesetStages...)))
|
||||
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeConstrictor, NewPipeline(constrictorRulesetStages...)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ func (m ArcadeMazeMap) SetupBoard(initialBoardState *rules.BoardState, settings
|
|||
editor.AddHazard(hazard)
|
||||
}
|
||||
|
||||
if settings.MinimumFood > 0 {
|
||||
if settings.Int(rules.ParamMinimumFood, 0) > 0 {
|
||||
// Add food in center
|
||||
editor.AddFood(rules.Point{X: 9, Y: 11})
|
||||
}
|
||||
|
|
@ -71,11 +71,16 @@ func (m ArcadeMazeMap) SetupBoard(initialBoardState *rules.BoardState, settings
|
|||
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)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,11 @@ func (m CastleWallMediumHazardsMap) SetupBoard(initialBoardState *rules.BoardSta
|
|||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
return updateCastleWallBoard(maxFood, castleWallExtraLargeFood, lastBoardState, settings, editor)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ func (m EmptyMap) SetupBoard(initialBoardState *rules.BoardState, settings rules
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,55 +28,28 @@ func TestEmptyMapSetupBoard(t *testing.T) {
|
|||
"empty 7x7",
|
||||
rules.NewBoardState(7, 7),
|
||||
rules.MinRand,
|
||||
&rules.BoardState{
|
||||
Width: 7,
|
||||
Height: 7,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(7, 7),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"not enough room for snakes 7x7",
|
||||
&rules.BoardState{
|
||||
Width: 7,
|
||||
Height: 7,
|
||||
Snakes: generateSnakes(17),
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(7, 7).WithSnakes(generateSnakes(17)),
|
||||
rules.MinRand,
|
||||
nil,
|
||||
rules.ErrorTooManySnakes,
|
||||
},
|
||||
{
|
||||
"not enough room for snakes 5x5",
|
||||
&rules.BoardState{
|
||||
Width: 5,
|
||||
Height: 5,
|
||||
Snakes: generateSnakes(14),
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(5, 5).WithSnakes(generateSnakes(14)),
|
||||
rules.MinRand,
|
||||
nil,
|
||||
rules.ErrorTooManySnakes,
|
||||
},
|
||||
{
|
||||
"full 11x11 min",
|
||||
&rules.BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: generateSnakes(8),
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)),
|
||||
rules.MinRand,
|
||||
&rules.BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []rules.Snake{
|
||||
rules.NewBoardState(11, 11).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},
|
||||
|
|
@ -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: "7", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
||||
{ID: "8", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||
},
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"full 11x11 max",
|
||||
&rules.BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: generateSnakes(8),
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)),
|
||||
rules.MaxRand,
|
||||
&rules.BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []rules.Snake{
|
||||
rules.NewBoardState(11, 11).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},
|
||||
|
|
@ -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: "7", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
||||
{ID: "8", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100},
|
||||
},
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
}),
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
|
@ -139,27 +97,13 @@ func TestEmptyMapSetupBoard(t *testing.T) {
|
|||
|
||||
func TestEmptyMapUpdateBoard(t *testing.T) {
|
||||
m := maps.EmptyMap{}
|
||||
initialBoardState := &rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 0}},
|
||||
Hazards: []rules.Point{},
|
||||
}
|
||||
settings := rules.Settings{
|
||||
FoodSpawnChance: 50,
|
||||
MinimumFood: 2,
|
||||
}.WithRand(rules.MaxRand)
|
||||
initialBoardState := rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}})
|
||||
settings := rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "2").WithRand(rules.MaxRand)
|
||||
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.Equal(t, &rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 0}},
|
||||
Hazards: []rules.Point{},
|
||||
}, nextBoardState)
|
||||
expectedBoardState := rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}})
|
||||
require.Equal(t, expectedBoardState, nextBoardState)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,21 @@ type GameMap interface {
|
|||
// Called to generate a new board. The map is responsible for placing all snakes, food, and hazards.
|
||||
SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error
|
||||
|
||||
// Called every turn to optionally update the board.
|
||||
UpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error
|
||||
// Called every turn to optionally update the board before the board is sent to snakes to get their moves.
|
||||
// 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 {
|
||||
|
|
@ -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.
|
||||
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
|
||||
// the snakes on those coordinates, or return an error if placement of all
|
||||
// Snakes is impossible.
|
||||
|
|
@ -270,6 +289,16 @@ func (editor *BoardStateEditor) SnakeBodies() map[string][]rules.Point {
|
|||
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
|
||||
// the snakes on those coordinates, or return an error if placement of all
|
||||
// Snakes is impossible.
|
||||
|
|
|
|||
|
|
@ -135,18 +135,16 @@ func TestBoardStateEditor(t *testing.T) {
|
|||
editor.PlaceSnake("existing_snake", []rules.Point{{X: 5, Y: 2}, {X: 5, Y: 1}, {X: 5, Y: 0}}, 99)
|
||||
editor.PlaceSnake("new_snake", []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}}, 98)
|
||||
|
||||
require.Equal(t, &rules.BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Food: []rules.Point{
|
||||
expected := rules.NewBoardState(11, 11).
|
||||
WithFood([]rules.Point{
|
||||
{X: 1, Y: 3},
|
||||
{X: 3, Y: 7},
|
||||
},
|
||||
Hazards: []rules.Point{
|
||||
}).
|
||||
WithHazards([]rules.Point{
|
||||
{X: 1, Y: 3},
|
||||
{X: 3, Y: 7},
|
||||
},
|
||||
Snakes: []rules.Snake{
|
||||
}).
|
||||
WithSnakes([]rules.Snake{
|
||||
{
|
||||
ID: "existing_snake",
|
||||
Health: 99,
|
||||
|
|
@ -157,8 +155,8 @@ func TestBoardStateEditor(t *testing.T) {
|
|||
Health: 98,
|
||||
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{
|
||||
{X: 1, Y: 3},
|
||||
|
|
|
|||
|
|
@ -97,8 +97,12 @@ func (m HazardPitsMap) SetupBoard(initialBoardState *rules.BoardState, settings
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m HazardPitsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m HazardPitsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m HazardPitsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -109,9 +113,10 @@ func (m HazardPitsMap) UpdateBoard(lastBoardState *rules.BoardState, settings ru
|
|||
// Cycle 3 - 3 layers
|
||||
// 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
|
||||
layers := (lastBoardState.Turn / settings.RoyaleSettings.ShrinkEveryNTurns) % 7
|
||||
layers := (lastBoardState.Turn / shrinkEveryNTurns) % 7
|
||||
if layers > 4 {
|
||||
layers = 4
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func TestHazardPitsMap(t *testing.T) {
|
|||
|
||||
state = rules.NewBoardState(int(11), int(11))
|
||||
m = maps.HazardPitsMap{}
|
||||
settings.RoyaleSettings.ShrinkEveryNTurns = 1
|
||||
settings = rules.NewSettingsWithParams(rules.ParamShrinkEveryNTurns, "1")
|
||||
editor = maps.NewBoardStateEditor(state)
|
||||
require.Empty(t, state.Hazards)
|
||||
err = m.SetupBoard(state, settings, editor)
|
||||
|
|
@ -47,7 +47,7 @@ func TestHazardPitsMap(t *testing.T) {
|
|||
// Verify the hazard progression through the turns
|
||||
for i := 0; i < 16; i++ {
|
||||
state.Turn = i
|
||||
err = m.UpdateBoard(state, settings, editor)
|
||||
err = m.PostUpdateBoard(state, settings, editor)
|
||||
require.NoError(t, err)
|
||||
if i == 1 {
|
||||
require.Len(t, state.Hazards, 21)
|
||||
|
|
|
|||
|
|
@ -54,8 +54,12 @@ func (m InnerBorderHazardsMap) SetupBoard(lastBoardState *rules.BoardState, sett
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m InnerBorderHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m InnerBorderHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m InnerBorderHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||
}
|
||||
|
||||
type ConcentricRingsHazardsMap struct{}
|
||||
|
|
@ -96,8 +100,12 @@ func (m ConcentricRingsHazardsMap) SetupBoard(lastBoardState *rules.BoardState,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m ConcentricRingsHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m ConcentricRingsHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m ConcentricRingsHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||
}
|
||||
|
||||
type ColumnsHazardsMap struct{}
|
||||
|
|
@ -135,8 +143,12 @@ func (m ColumnsHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m ColumnsHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m ColumnsHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m ColumnsHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||
}
|
||||
|
||||
type SpiralHazardsMap struct{}
|
||||
|
|
@ -163,8 +175,12 @@ func (m SpiralHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings
|
|||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||
}
|
||||
|
||||
func (m SpiralHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m SpiralHazardsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m SpiralHazardsMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -256,8 +272,12 @@ func (m ScatterFillMap) SetupBoard(lastBoardState *rules.BoardState, settings ru
|
|||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||
}
|
||||
|
||||
func (m ScatterFillMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m ScatterFillMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m ScatterFillMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -308,8 +328,12 @@ func (m DirectionalExpandingBoxMap) SetupBoard(lastBoardState *rules.BoardState,
|
|||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||
}
|
||||
|
||||
func (m DirectionalExpandingBoxMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m DirectionalExpandingBoxMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m DirectionalExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -423,8 +447,12 @@ func (m ExpandingBoxMap) SetupBoard(lastBoardState *rules.BoardState, settings r
|
|||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||
}
|
||||
|
||||
func (m ExpandingBoxMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m ExpandingBoxMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m ExpandingBoxMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -499,8 +527,12 @@ func (m ExpandingScatterMap) SetupBoard(lastBoardState *rules.BoardState, settin
|
|||
return (StandardMap{}).SetupBoard(lastBoardState, settings, editor)
|
||||
}
|
||||
|
||||
func (m ExpandingScatterMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m ExpandingScatterMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ func TestSpiralHazardsMap(t *testing.T) {
|
|||
|
||||
for i := 0; i < 1000; i++ {
|
||||
state.Turn = i
|
||||
err = m.UpdateBoard(state, settings, editor)
|
||||
err = m.PostUpdateBoard(state, settings, editor)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NotEmpty(t, state.Hazards)
|
||||
|
|
@ -123,7 +123,7 @@ func TestScatterFillMap(t *testing.T) {
|
|||
totalTurns := 11 * 11 * 2
|
||||
for i := 0; i < totalTurns; i++ {
|
||||
state.Turn = i
|
||||
err = m.UpdateBoard(state, settings, editor)
|
||||
err = m.PostUpdateBoard(state, settings, editor)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NotEmpty(t, state.Hazards)
|
||||
|
|
@ -144,7 +144,7 @@ func TestDirectionalExpandingBoxMap(t *testing.T) {
|
|||
totalTurns := 1000
|
||||
for i := 0; i < totalTurns; i++ {
|
||||
state.Turn = i
|
||||
err = m.UpdateBoard(state, settings, editor)
|
||||
err = m.PostUpdateBoard(state, settings, editor)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NotEmpty(t, state.Hazards)
|
||||
|
|
@ -165,7 +165,7 @@ func TestExpandingBoxMap(t *testing.T) {
|
|||
totalTurns := 1000
|
||||
for i := 0; i < totalTurns; i++ {
|
||||
state.Turn = i
|
||||
err = m.UpdateBoard(state, settings, editor)
|
||||
err = m.PostUpdateBoard(state, settings, editor)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NotEmpty(t, state.Hazards)
|
||||
|
|
@ -186,7 +186,7 @@ func TestExpandingScatterMap(t *testing.T) {
|
|||
totalTurns := 1000
|
||||
for i := 0; i < totalTurns; i++ {
|
||||
state.Turn = i
|
||||
err = m.UpdateBoard(state, settings, editor)
|
||||
err = m.PostUpdateBoard(state, settings, editor)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NotEmpty(t, state.Hazards)
|
||||
|
|
|
|||
|
|
@ -50,12 +50,17 @@ func (m HealingPoolsMap) SetupBoard(initialBoardState *rules.BoardState, setting
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m HealingPoolsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
if err := (StandardMap{}).UpdateBoard(lastBoardState, settings, editor); err != nil {
|
||||
func (m HealingPoolsMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
i := rand.Intn(len(lastBoardState.Hazards))
|
||||
editor.RemoveHazard(lastBoardState.Hazards[i])
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ func TestHealingPoolsMap(t *testing.T) {
|
|||
t.Run(fmt.Sprintf("%dx%d", tc.boardSize, tc.boardSize), func(t *testing.T) {
|
||||
m := maps.HealingPoolsMap{}
|
||||
state := rules.NewBoardState(tc.boardSize, tc.boardSize)
|
||||
settings := rules.Settings{}
|
||||
settings.RoyaleSettings.ShrinkEveryNTurns = 10
|
||||
shrinkEveryNTurns := 10
|
||||
settings := rules.NewSettingsWithParams(rules.ParamShrinkEveryNTurns, fmt.Sprint(shrinkEveryNTurns))
|
||||
|
||||
// ensure the hazards are added to the board at setup
|
||||
editor := maps.NewBoardStateEditor(state)
|
||||
|
|
@ -56,10 +56,10 @@ func TestHealingPoolsMap(t *testing.T) {
|
|||
}
|
||||
|
||||
// ensure the hazards are removed
|
||||
totalTurns := settings.RoyaleSettings.ShrinkEveryNTurns*tc.expectedHazards + 1
|
||||
totalTurns := shrinkEveryNTurns*tc.expectedHazards + 1
|
||||
for i := 0; i < totalTurns; i++ {
|
||||
state.Turn = i
|
||||
err = m.UpdateBoard(state, settings, editor)
|
||||
err = m.PostUpdateBoard(state, settings, editor)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,17 +25,24 @@ func SetupBoard(mapID string, settings rules.Settings, width, height int, snakeI
|
|||
return boardState, nil
|
||||
}
|
||||
|
||||
// UpdateBoard is a shortcut for looking up a map by ID and updating an existing board state with it.
|
||||
func UpdateBoard(mapID string, previousBoardState *rules.BoardState, settings rules.Settings) (*rules.BoardState, error) {
|
||||
gameMap, err := GetMap(mapID)
|
||||
// PreUpdateBoard updates a board state with a map.
|
||||
func PreUpdateBoard(gameMap GameMap, previousBoardState *rules.BoardState, settings rules.Settings) (*rules.BoardState, error) {
|
||||
nextBoardState := previousBoardState.Clone()
|
||||
editor := NewBoardStateEditor(nextBoardState)
|
||||
|
||||
err := gameMap.PreUpdateBoard(previousBoardState, settings, editor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nextBoardState, nil
|
||||
}
|
||||
|
||||
func PostUpdateBoard(gameMap GameMap, previousBoardState *rules.BoardState, settings rules.Settings) (*rules.BoardState, error) {
|
||||
nextBoardState := previousBoardState.Clone()
|
||||
editor := NewBoardStateEditor(nextBoardState)
|
||||
|
||||
err = gameMap.UpdateBoard(previousBoardState, settings, editor)
|
||||
err := gameMap.PostUpdateBoard(previousBoardState, settings, editor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -77,7 +84,11 @@ func (m StubMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.
|
|||
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 {
|
||||
return m.Error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,11 +82,10 @@ func TestUpdateBoard(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
previousBoardState := &rules.BoardState{
|
||||
Turn: 0,
|
||||
Food: []rules.Point{{X: 0, Y: 1}},
|
||||
Hazards: []rules.Point{{X: 3, Y: 4}},
|
||||
Snakes: []rules.Snake{
|
||||
previousBoardState := rules.NewBoardState(5, 5).
|
||||
WithFood([]rules.Point{{X: 0, Y: 1}}).
|
||||
WithHazards([]rules.Point{{X: 3, Y: 4}}).
|
||||
WithSnakes([]rules.Snake{
|
||||
{
|
||||
ID: "1",
|
||||
Health: 100,
|
||||
|
|
@ -96,11 +95,9 @@ func TestUpdateBoard(t *testing.T) {
|
|||
{X: 6, Y: 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
})
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,14 +10,12 @@ import (
|
|||
|
||||
const maxBoardWidth, maxBoardHeight = 25, 25
|
||||
|
||||
var testSettings rules.Settings = rules.Settings{
|
||||
FoodSpawnChance: 25,
|
||||
MinimumFood: 1,
|
||||
HazardDamagePerTurn: 14,
|
||||
RoyaleSettings: rules.RoyaleSettings{
|
||||
ShrinkEveryNTurns: 1,
|
||||
},
|
||||
}
|
||||
var testSettings rules.Settings = rules.NewSettings(map[string]string{
|
||||
rules.ParamFoodSpawnChance: "25",
|
||||
rules.ParamMinimumFood: "1",
|
||||
rules.ParamHazardDamagePerTurn: "14",
|
||||
rules.ParamShrinkEveryNTurns: "1",
|
||||
})
|
||||
|
||||
func TestRegisteredMaps(t *testing.T) {
|
||||
for mapName, gameMap := range globalRegistry {
|
||||
|
|
@ -96,7 +94,7 @@ func TestRegisteredMaps(t *testing.T) {
|
|||
|
||||
passedBoardState := 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.Equal(t, previousBoardState, passedBoardState, "BoardState should not be modified directly by GameMap.UpdateBoard")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ func (m RiverAndBridgesMediumHazardsMap) SetupBoard(initialBoardState *rules.Boa
|
|||
return setupRiverAndBridgesBoard(riversAndBridgesMediumStartPositions, riversAndBridgesMediumHazards, initialBoardState, settings, editor)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +146,11 @@ func (m RiverAndBridgesLargeHazardsMap) SetupBoard(initialBoardState *rules.Boar
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +249,11 @@ func (m RiverAndBridgesExtraLargeHazardsMap) SetupBoard(initialBoardState *rules
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
@ -355,7 +367,11 @@ func (m IslandsAndBridgesMediumHazardsMap) SetupBoard(initialBoardState *rules.B
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
@ -441,7 +457,11 @@ func (m IslandsAndBridgesLargeHazardsMap) SetupBoard(initialBoardState *rules.Bo
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,20 +33,25 @@ func (m RoyaleHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings
|
|||
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
|
||||
if err := (StandardMap{}).UpdateBoard(lastBoardState, settings, editor); err != nil {
|
||||
if err := (StandardMap{}).PostUpdateBoard(lastBoardState, settings, editor); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
||||
turn := lastBoardState.Turn + 1
|
||||
|
||||
if settings.RoyaleSettings.ShrinkEveryNTurns < 1 {
|
||||
shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0)
|
||||
if shrinkEveryNTurns < 1 {
|
||||
return errors.New("royale game can't shrink more frequently than every turn")
|
||||
}
|
||||
|
||||
if turn < settings.RoyaleSettings.ShrinkEveryNTurns {
|
||||
if turn < shrinkEveryNTurns {
|
||||
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.
|
||||
randGenerator := settings.GetRand(0)
|
||||
|
||||
numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns
|
||||
numShrinks := turn / shrinkEveryNTurns
|
||||
minX, maxX := 0, lastBoardState.Width-1
|
||||
minY, maxY := 0, lastBoardState.Height-1
|
||||
for i := 0; i < numShrinks; i++ {
|
||||
|
|
|
|||
|
|
@ -33,8 +33,12 @@ func (m SinkholesMap) SetupBoard(initialBoardState *rules.BoardState, settings r
|
|||
return (StandardMap{}).SetupBoard(initialBoardState, settings, editor)
|
||||
}
|
||||
|
||||
func (m SinkholesMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
func (m SinkholesMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m SinkholesMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.PostUpdateBoard(lastBoardState, settings, editor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -42,8 +46,9 @@ func (m SinkholesMap) UpdateBoard(lastBoardState *rules.BoardState, settings rul
|
|||
currentTurn := lastBoardState.Turn
|
||||
startTurn := 1
|
||||
spawnEveryNTurns := 10
|
||||
if settings.RoyaleSettings.ShrinkEveryNTurns > 0 {
|
||||
spawnEveryNTurns = settings.RoyaleSettings.ShrinkEveryNTurns
|
||||
shrinkEveryNTurns := settings.Int(rules.ParamShrinkEveryNTurns, 0)
|
||||
if shrinkEveryNTurns > 0 {
|
||||
spawnEveryNTurns = shrinkEveryNTurns
|
||||
}
|
||||
maxRings := 5
|
||||
if lastBoardState.Width == 7 {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func TestSinkholesMap(t *testing.T) {
|
|||
totalTurns := 100
|
||||
for i := 0; i < totalTurns; i++ {
|
||||
state.Turn = i
|
||||
err = m.UpdateBoard(state, settings, editor)
|
||||
err = m.PostUpdateBoard(state, settings, editor)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NotEmpty(t, state.Hazards)
|
||||
|
|
|
|||
|
|
@ -4,20 +4,22 @@ import (
|
|||
"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.
|
||||
func init() {
|
||||
globalRegistry.RegisterMap("snail_mode", SnailModeMap{})
|
||||
globalRegistry.RegisterMap("snail_mode", &SnailModeMap{lastTailPositions: nil})
|
||||
}
|
||||
|
||||
// ID returns a unique identifier for this map.
|
||||
func (m SnailModeMap) ID() string {
|
||||
func (m *SnailModeMap) ID() string {
|
||||
return "snail_mode"
|
||||
}
|
||||
|
||||
// Meta returns the non-functional metadata about this map.
|
||||
func (m SnailModeMap) Meta() Metadata {
|
||||
func (m *SnailModeMap) Meta() Metadata {
|
||||
return Metadata{
|
||||
Name: "Snail Mode",
|
||||
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
|
||||
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)
|
||||
|
||||
if len(initialBoardState.Snakes) > int(m.Meta().MaxPlayers) {
|
||||
|
|
@ -57,23 +59,6 @@ func (m SnailModeMap) SetupBoard(initialBoardState *rules.BoardState, settings r
|
|||
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
|
||||
func doubleTail(snake *rules.Snake) bool {
|
||||
almostTail := snake.Body[len(snake.Body)-2]
|
||||
|
|
@ -86,12 +71,28 @@ func isEliminated(s *rules.Snake) bool {
|
|||
return s.EliminatedCause != rules.NotEliminated
|
||||
}
|
||||
|
||||
// UpdateBoard does the work of placing the hazards along the 'snail tail' of snakes
|
||||
// This is responsible for saving the current tail location off the board
|
||||
// and restoring the previous tail position. This also handles removing one hazards from
|
||||
// the current stacks so the hazards tails fade as the snake moves away.
|
||||
func (m SnailModeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
err := StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
|
||||
// PreUpdateBoard stores the tail position of each snake in memory, to be
|
||||
// able to place hazards there after the snakes move.
|
||||
func (m *SnailModeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
m.lastTailPositions = make(map[rules.Point]int)
|
||||
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
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
@ -100,79 +101,38 @@ func (m SnailModeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rul
|
|||
// need to be cleared first.
|
||||
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
|
||||
// Add non-double tail locations to a slice
|
||||
hazardCounts := map[rules.Point]int{}
|
||||
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]++
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
for hazard, count := range hazardCounts {
|
||||
|
||||
for i := 0; i < count-1; i++ {
|
||||
editor.AddHazard(hazard)
|
||||
}
|
||||
}
|
||||
|
||||
// Store a stack of hazards for the tail of each snake. This is stored out
|
||||
// of bounds and then applied on the next turn. The stack count is equal
|
||||
// the lenght of the snake.
|
||||
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
|
||||
// Place a new stack of hazards where each snake's tail used to be
|
||||
NewHazardLoop:
|
||||
for location, count := range m.lastTailPositions {
|
||||
for _, snake := range lastBoardState.Snakes {
|
||||
if isEliminated(&snake) {
|
||||
continue
|
||||
}
|
||||
head := snake.Body[0]
|
||||
if p.X == head.X && p.Y == head.Y {
|
||||
isHead = true
|
||||
break
|
||||
if location.X == head.X && location.Y == head.Y {
|
||||
// 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.
|
||||
continue NewHazardLoop
|
||||
}
|
||||
}
|
||||
if isHead {
|
||||
continue
|
||||
for i := 0; i < count; i++ {
|
||||
editor.AddHazard(location)
|
||||
}
|
||||
|
||||
editor.AddHazard(p)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -176,7 +176,11 @@ func (m SoloMazeMap) PlaceFood(boardState *rules.BoardState, settings rules.Sett
|
|||
}
|
||||
}
|
||||
|
||||
func (m SoloMazeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
func (m SoloMazeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m SoloMazeMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
|
||||
currentLevel, e := m.ReadBitState(lastBoardState)
|
||||
if e != nil {
|
||||
return e
|
||||
|
|
|
|||
|
|
@ -57,7 +57,11 @@ func (m StandardMap) SetupBoard(initialBoardState *rules.BoardState, settings ru
|
|||
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)
|
||||
|
||||
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 {
|
||||
minFood := int(settings.MinimumFood)
|
||||
foodSpawnChance := int(settings.FoodSpawnChance)
|
||||
minFood := settings.Int(rules.ParamMinimumFood, 0)
|
||||
foodSpawnChance := settings.Int(rules.ParamFoodSpawnChance, 0)
|
||||
numCurrentFood := len(state.Food)
|
||||
|
||||
if numCurrentFood < minFood {
|
||||
|
|
|
|||
|
|
@ -29,65 +29,29 @@ func TestStandardMapSetupBoard(t *testing.T) {
|
|||
"empty 7x7",
|
||||
rules.NewBoardState(7, 7),
|
||||
rules.MinRand,
|
||||
&rules.BoardState{
|
||||
Width: 7,
|
||||
Height: 7,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 3, Y: 3}},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(7, 7).WithFood([]rules.Point{{X: 3, Y: 3}}),
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"not enough room for snakes 7x7",
|
||||
&rules.BoardState{
|
||||
Width: 7,
|
||||
Height: 7,
|
||||
Snakes: generateSnakes(17),
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(7, 7).WithSnakes(generateSnakes(17)),
|
||||
rules.MinRand,
|
||||
nil,
|
||||
rules.ErrorTooManySnakes,
|
||||
},
|
||||
{
|
||||
"not enough room for snakes 5x5",
|
||||
&rules.BoardState{
|
||||
Width: 5,
|
||||
Height: 5,
|
||||
Snakes: generateSnakes(14),
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(5, 5).WithSnakes(generateSnakes(14)),
|
||||
rules.MinRand,
|
||||
nil,
|
||||
rules.ErrorTooManySnakes,
|
||||
},
|
||||
{
|
||||
"full 11x11 min",
|
||||
&rules.BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: generateSnakes(8),
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)),
|
||||
rules.MinRand,
|
||||
&rules.BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []rules.Snake{
|
||||
{ID: "1", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100},
|
||||
{ID: "2", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
||||
{ID: "3", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
||||
{ID: "4", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
||||
{ID: "5", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100},
|
||||
{ID: "6", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
||||
{ID: "7", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
||||
{ID: "8", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||
},
|
||||
Food: []rules.Point{
|
||||
rules.NewBoardState(11, 11).
|
||||
WithFood([]rules.Point{
|
||||
{X: 0, Y: 2},
|
||||
{X: 0, Y: 8},
|
||||
{X: 8, Y: 0},
|
||||
|
|
@ -97,35 +61,25 @@ func TestStandardMapSetupBoard(t *testing.T) {
|
|||
{X: 4, Y: 10},
|
||||
{X: 10, Y: 4},
|
||||
{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,
|
||||
},
|
||||
{
|
||||
"full 11x11 max",
|
||||
&rules.BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: generateSnakes(8),
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(11, 11).WithSnakes(generateSnakes(8)),
|
||||
rules.MaxRand,
|
||||
&rules.BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []rules.Snake{
|
||||
{ID: "1", Body: []rules.Point{{X: 5, Y: 1}, {X: 5, Y: 1}, {X: 5, Y: 1}}, Health: 100},
|
||||
{ID: "2", Body: []rules.Point{{X: 5, Y: 9}, {X: 5, Y: 9}, {X: 5, Y: 9}}, Health: 100},
|
||||
{ID: "3", Body: []rules.Point{{X: 9, Y: 5}, {X: 9, Y: 5}, {X: 9, Y: 5}}, Health: 100},
|
||||
{ID: "4", Body: []rules.Point{{X: 1, Y: 5}, {X: 1, Y: 5}, {X: 1, Y: 5}}, Health: 100},
|
||||
{ID: "5", Body: []rules.Point{{X: 1, Y: 9}, {X: 1, Y: 9}, {X: 1, Y: 9}}, Health: 100},
|
||||
{ID: "6", Body: []rules.Point{{X: 9, Y: 1}, {X: 9, Y: 1}, {X: 9, Y: 1}}, Health: 100},
|
||||
{ID: "7", Body: []rules.Point{{X: 9, Y: 9}, {X: 9, Y: 9}, {X: 9, Y: 9}}, Health: 100},
|
||||
{ID: "8", Body: []rules.Point{{X: 1, Y: 1}, {X: 1, Y: 1}, {X: 1, Y: 1}}, Health: 100},
|
||||
},
|
||||
Food: []rules.Point{
|
||||
rules.NewBoardState(11, 11).
|
||||
WithFood([]rules.Point{
|
||||
{X: 6, Y: 0},
|
||||
{X: 6, Y: 10},
|
||||
{X: 10, Y: 6},
|
||||
|
|
@ -135,9 +89,17 @@ func TestStandardMapSetupBoard(t *testing.T) {
|
|||
{X: 10, Y: 8},
|
||||
{X: 2, Y: 0},
|
||||
{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,
|
||||
},
|
||||
}
|
||||
|
|
@ -172,132 +134,51 @@ func TestStandardMapUpdateBoard(t *testing.T) {
|
|||
{
|
||||
"empty no food",
|
||||
rules.NewBoardState(2, 2),
|
||||
rules.Settings{
|
||||
FoodSpawnChance: 0,
|
||||
MinimumFood: 0,
|
||||
},
|
||||
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "0", rules.ParamMinimumFood, "0"),
|
||||
rules.MinRand,
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(2, 2),
|
||||
},
|
||||
{
|
||||
"empty MinimumFood",
|
||||
rules.NewBoardState(2, 2),
|
||||
rules.Settings{
|
||||
FoodSpawnChance: 0,
|
||||
MinimumFood: 2,
|
||||
},
|
||||
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "0", rules.ParamMinimumFood, "2"),
|
||||
rules.MinRand,
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}}),
|
||||
},
|
||||
{
|
||||
"not empty MinimumFood",
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 1}},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.Settings{
|
||||
FoodSpawnChance: 0,
|
||||
MinimumFood: 2,
|
||||
},
|
||||
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 1}}),
|
||||
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "0", rules.ParamMinimumFood, "2"),
|
||||
rules.MinRand,
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 1}, {X: 0, Y: 0}},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 1}, {X: 0, Y: 0}}),
|
||||
},
|
||||
{
|
||||
"empty FoodSpawnChance inactive",
|
||||
rules.NewBoardState(2, 2),
|
||||
rules.Settings{
|
||||
FoodSpawnChance: 50,
|
||||
MinimumFood: 0,
|
||||
},
|
||||
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"),
|
||||
rules.MinRand,
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(2, 2),
|
||||
},
|
||||
{
|
||||
"empty FoodSpawnChance active",
|
||||
rules.NewBoardState(2, 2),
|
||||
rules.Settings{
|
||||
FoodSpawnChance: 50,
|
||||
MinimumFood: 0,
|
||||
},
|
||||
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"),
|
||||
rules.MaxRand,
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 1}},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 1}}),
|
||||
},
|
||||
{
|
||||
"not empty FoodSpawnChance active",
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 0}},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.Settings{
|
||||
FoodSpawnChance: 50,
|
||||
MinimumFood: 0,
|
||||
},
|
||||
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}}),
|
||||
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"),
|
||||
rules.MaxRand,
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 1, Y: 0}}),
|
||||
},
|
||||
{
|
||||
"not empty FoodSpawnChance no room",
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.Settings{
|
||||
FoodSpawnChance: 50,
|
||||
MinimumFood: 0,
|
||||
},
|
||||
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}}),
|
||||
rules.NewSettingsWithParams(rules.ParamFoodSpawnChance, "50", rules.ParamMinimumFood, "0"),
|
||||
rules.MaxRand,
|
||||
&rules.BoardState{
|
||||
Width: 2,
|
||||
Height: 2,
|
||||
Snakes: []rules.Snake{},
|
||||
Food: []rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}},
|
||||
Hazards: []rules.Point{},
|
||||
},
|
||||
rules.NewBoardState(2, 2).WithFood([]rules.Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 1, Y: 0}, {X: 1, Y: 1}}),
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
|
|
@ -306,7 +187,7 @@ func TestStandardMapUpdateBoard(t *testing.T) {
|
|||
settings := test.settings.WithRand(test.rand)
|
||||
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.Equal(t, test.expected, nextBoardState)
|
||||
|
|
|
|||
61
pipeline.go
61
pipeline.go
|
|
@ -38,6 +38,33 @@ var globalRegistry = StageRegistry{
|
|||
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.
|
||||
// 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)
|
||||
|
|
@ -46,6 +73,14 @@ var globalRegistry = StageRegistry{
|
|||
// Errors should be treated as meaning the stage failed and the board state is now invalid.
|
||||
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
|
||||
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
|
||||
type pipeline struct {
|
||||
// stages is a list of stages that should be executed from slice start to end
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func TestPipelineRuleset(t *testing.T) {
|
|||
name: "test",
|
||||
pipeline: p,
|
||||
}
|
||||
ended, err := pr.IsGameOver(&BoardState{})
|
||||
ended, _, err := pr.Execute(&BoardState{}, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ended)
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ func TestPipelineRuleset(t *testing.T) {
|
|||
name: "test",
|
||||
pipeline: p,
|
||||
}
|
||||
ended, err = pr.IsGameOver(&BoardState{})
|
||||
ended, _, err = pr.Execute(&BoardState{}, nil)
|
||||
require.NoError(t, err)
|
||||
require.False(t, ended)
|
||||
|
||||
|
|
@ -56,10 +56,10 @@ func TestPipelineRuleset(t *testing.T) {
|
|||
pipeline: p,
|
||||
}
|
||||
require.Empty(t, b.Food)
|
||||
b, err = pr.ModifyInitialBoardState(b)
|
||||
_, b, err = pr.Execute(b, nil)
|
||||
require.NoError(t, err)
|
||||
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.NotEmpty(t, b.Food, "fodo should be added now")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,17 +21,17 @@ func TestPipeline(t *testing.T) {
|
|||
r.RegisterPipelineStage("astage", mockStageFn(false, nil))
|
||||
p = rules.NewPipelineFromRegistry(r)
|
||||
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)
|
||||
|
||||
// test that an unregistered stage name errors
|
||||
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, err)
|
||||
|
||||
// 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.NotNil(t, next)
|
||||
|
|
@ -39,20 +39,20 @@ func TestPipeline(t *testing.T) {
|
|||
|
||||
// test that the pipeline short-circuits for a stage that errors
|
||||
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.NotNil(t, next)
|
||||
require.False(t, ended)
|
||||
|
||||
// test that the pipeline short-circuits for a stage that ends
|
||||
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.NotNil(t, next)
|
||||
require.True(t, ended)
|
||||
|
||||
// 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.NotNil(t, next)
|
||||
require.True(t, ended)
|
||||
|
|
|
|||
41
royale.go
41
royale.go
|
|
@ -14,26 +14,6 @@ var royaleRulesetStages = []string{
|
|||
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) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
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
|
||||
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")
|
||||
}
|
||||
|
||||
if turn < settings.RoyaleSettings.ShrinkEveryNTurns {
|
||||
if turn < shrinkEveryNTurns {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
randGenerator := settings.GetRand(0)
|
||||
|
||||
numShrinks := turn / settings.RoyaleSettings.ShrinkEveryNTurns
|
||||
numShrinks := turn / shrinkEveryNTurns
|
||||
minX, maxX := 0, b.Width-1
|
||||
minY, maxY := 0, b.Height-1
|
||||
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 y := 0; y < b.Height; y++ {
|
||||
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
|
||||
}
|
||||
|
||||
func (r *RoyaleRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
return GameOverStandard(b, r.Settings(), nil)
|
||||
}
|
||||
|
||||
func (r RoyaleRuleset) Settings() Settings {
|
||||
s := r.StandardRuleset.Settings()
|
||||
s.RoyaleSettings = RoyaleSettings{
|
||||
ShrinkEveryNTurns: r.ShrinkEveryNTurns,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,19 @@ package rules
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRoyaleRulesetInterface(t *testing.T) {
|
||||
var _ Ruleset = (*RoyaleRuleset)(nil)
|
||||
func getRoyaleRuleset(hazardDamagePerTurn, shrinkEveryNTurns int) Ruleset {
|
||||
settings := NewSettingsWithParams(
|
||||
ParamHazardDamagePerTurn, fmt.Sprint(hazardDamagePerTurn),
|
||||
ParamShrinkEveryNTurns, fmt.Sprint(shrinkEveryNTurns),
|
||||
)
|
||||
return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeRoyale)
|
||||
}
|
||||
|
||||
func TestRoyaleDefaultSanity(t *testing.T) {
|
||||
|
|
@ -19,24 +24,19 @@ func TestRoyaleDefaultSanity(t *testing.T) {
|
|||
{ID: "2", Body: []Point{{X: 0, Y: 1}}},
|
||||
},
|
||||
}
|
||||
r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 1}, ShrinkEveryNTurns: 0}
|
||||
_, err := r.CreateNextBoardState(boardState, []SnakeMove{{"1", "right"}, {"2", "right"}})
|
||||
r := getRoyaleRuleset(1, 0)
|
||||
_, _, err := r.Execute(boardState, []SnakeMove{{"1", "right"}, {"2", "right"}})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, errors.New("royale game can't shrink more frequently than every turn"), err)
|
||||
|
||||
r = RoyaleRuleset{ShrinkEveryNTurns: 1}
|
||||
_, err = r.CreateNextBoardState(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{})
|
||||
r = getRoyaleRuleset(1, 1)
|
||||
_, boardState, err = r.Execute(boardState, []SnakeMove{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, boardState.Hazards, 0)
|
||||
}
|
||||
|
||||
func TestRoyaleName(t *testing.T) {
|
||||
r := RoyaleRuleset{}
|
||||
r := getRoyaleRuleset(0, 0)
|
||||
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: 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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
Height: test.Height,
|
||||
}
|
||||
settings := Settings{
|
||||
HazardDamagePerTurn: 1,
|
||||
RoyaleSettings: RoyaleSettings{
|
||||
ShrinkEveryNTurns: test.ShrinkEveryNTurns,
|
||||
},
|
||||
}.WithSeed(seed)
|
||||
settings := NewSettingsWithParams(
|
||||
ParamHazardDamagePerTurn, "1",
|
||||
ParamShrinkEveryNTurns, fmt.Sprint(test.ShrinkEveryNTurns),
|
||||
).WithSeed(seed)
|
||||
|
||||
_, err := PopulateHazardsRoyale(b, settings, mockSnakeMoves())
|
||||
require.Equal(t, test.Error, err)
|
||||
|
|
@ -139,12 +137,12 @@ var royaleCaseHazardsPlaced = gameTestCase{
|
|||
Snakes: []Snake{
|
||||
{
|
||||
ID: "one",
|
||||
Body: []Point{{1, 1}, {1, 2}},
|
||||
Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}},
|
||||
Health: 100,
|
||||
},
|
||||
{
|
||||
ID: "two",
|
||||
Body: []Point{{3, 4}, {3, 3}},
|
||||
Body: []Point{{X: 3, Y: 4}, {X: 3, Y: 3}},
|
||||
Health: 100,
|
||||
},
|
||||
{
|
||||
|
|
@ -154,7 +152,7 @@ var royaleCaseHazardsPlaced = gameTestCase{
|
|||
EliminatedCause: EliminatedByOutOfBounds,
|
||||
},
|
||||
},
|
||||
Food: []Point{{0, 0}, {1, 0}},
|
||||
Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||
Hazards: []Point{},
|
||||
},
|
||||
[]SnakeMove{
|
||||
|
|
@ -169,12 +167,12 @@ var royaleCaseHazardsPlaced = gameTestCase{
|
|||
Snakes: []Snake{
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
ID: "two",
|
||||
Body: []Point{{3, 5}, {3, 4}},
|
||||
Body: []Point{{X: 3, Y: 5}, {X: 3, Y: 4}},
|
||||
Health: 99,
|
||||
},
|
||||
{
|
||||
|
|
@ -184,7 +182,7 @@ var royaleCaseHazardsPlaced = gameTestCase{
|
|||
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}},
|
||||
},
|
||||
}
|
||||
|
|
@ -204,22 +202,14 @@ func TestRoyaleCreateNextBoardState(t *testing.T) {
|
|||
*s2,
|
||||
royaleCaseHazardsPlaced,
|
||||
}
|
||||
r := RoyaleRuleset{
|
||||
StandardRuleset: StandardRuleset{
|
||||
HazardDamagePerTurn: 1,
|
||||
},
|
||||
ShrinkEveryNTurns: 1,
|
||||
}
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeRoyale,
|
||||
ParamHazardDamagePerTurn: "1",
|
||||
ParamShrinkEveryNTurns: "1",
|
||||
}).WithSeed(1234)
|
||||
for _, gc := range cases {
|
||||
rand.Seed(1234)
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.NamedRuleset(GameTypeRoyale))
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeRoyale, NewPipeline(royaleRulesetStages...)))
|
||||
}
|
||||
|
|
|
|||
170
ruleset.go
170
ruleset.go
|
|
@ -1,16 +1,15 @@
|
|||
package rules
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Ruleset interface {
|
||||
// Returns the name of the ruleset, if applicable.
|
||||
Name() string
|
||||
ModifyInitialBoardState(initialState *BoardState) (*BoardState, error)
|
||||
CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error)
|
||||
IsGameOver(state *BoardState) (bool, error)
|
||||
// Settings provides the game settings that are relevant to the ruleset.
|
||||
|
||||
// Returns the settings used by the ruleset.
|
||||
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 {
|
||||
|
|
@ -18,68 +17,12 @@ type SnakeMove struct {
|
|||
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 {
|
||||
params map[string]string // game customisation parameters
|
||||
seed int64 // used for random events in games
|
||||
rand Rand // used for random number generation
|
||||
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.
|
||||
|
|
@ -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:
|
||||
// - existing keys in both maps get overwritten by the new ones
|
||||
|
|
@ -125,13 +68,14 @@ func (rb *rulesetBuilder) WithSolo(value bool) *rulesetBuilder {
|
|||
return rb
|
||||
}
|
||||
|
||||
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
|
||||
func (rb rulesetBuilder) Ruleset() PipelineRuleset {
|
||||
name, ok := rb.params[ParamGameType]
|
||||
if !ok {
|
||||
name = GameTypeStandard
|
||||
// WithSettings sets the settings object for the ruleset directly.
|
||||
func (rb *rulesetBuilder) WithSettings(settings Settings) *rulesetBuilder {
|
||||
rb.settings = &settings
|
||||
return rb
|
||||
}
|
||||
|
||||
// NamedRuleset constructs a known ruleset by using name to look up a standard pipeline.
|
||||
func (rb rulesetBuilder) NamedRuleset(name string) Ruleset {
|
||||
var stages []string
|
||||
if rb.solo {
|
||||
stages = append(stages, StageGameOverSoloSnake)
|
||||
|
|
@ -153,63 +97,28 @@ func (rb rulesetBuilder) Ruleset() PipelineRuleset {
|
|||
case GameTypeWrapped:
|
||||
stages = append(stages, wrappedRulesetStages[1:]...)
|
||||
default:
|
||||
name = GameTypeStandard
|
||||
stages = append(stages, standardRulesetStages[1:]...)
|
||||
}
|
||||
return rb.PipelineRuleset(name, NewPipeline(stages...))
|
||||
}
|
||||
|
||||
// PipelineRuleset provides an implementation of the Ruleset using a pipeline with a name.
|
||||
// It is intended to facilitate transitioning away from legacy Ruleset implementations to Pipeline
|
||||
// implementations.
|
||||
func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRuleset {
|
||||
// PipelineRuleset constructs a ruleset with the given name and pipeline using the parameters passed to the builder.
|
||||
// This can be used to create custom rulesets.
|
||||
func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) Ruleset {
|
||||
var settings Settings
|
||||
if rb.settings != nil {
|
||||
settings = *rb.settings
|
||||
} else {
|
||||
settings = NewSettings(rb.params).WithRand(rb.rand).WithSeed(rb.seed)
|
||||
}
|
||||
return &pipelineRuleset{
|
||||
name: name,
|
||||
pipeline: p,
|
||||
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,
|
||||
},
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
pipeline Pipeline
|
||||
name string
|
||||
|
|
@ -225,33 +134,10 @@ func (r pipelineRuleset) Settings() Settings {
|
|||
func (r pipelineRuleset) Name() string { return r.name }
|
||||
|
||||
// impl Ruleset
|
||||
// IMPORTANT: this implementation of IsGameOver deviates from the previous Ruleset implementations
|
||||
// in that it checks if the *NEXT* state results in game over, not the previous state.
|
||||
// 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
|
||||
func (r pipelineRuleset) Execute(bs *BoardState, sm []SnakeMove) (bool, *BoardState, error) {
|
||||
return r.pipeline.Execute(bs, r.Settings(), sm)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return r.pipeline.Err()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,31 +10,6 @@ import (
|
|||
_ "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) {
|
||||
err := (error)(RulesetError("test error string"))
|
||||
require.Equal(t, "test error string", err.Error())
|
||||
|
|
@ -42,10 +17,10 @@ func TestRulesetError(t *testing.T) {
|
|||
|
||||
func TestRulesetBuilderInternals(t *testing.T) {
|
||||
// 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, GameTypeRoyale, rsb.Ruleset().Name())
|
||||
require.Equal(t, int64(3), rsb.Ruleset().Settings().Seed())
|
||||
require.Equal(t, GameTypeRoyale, rsb.NamedRuleset(GameTypeRoyale).Name())
|
||||
require.Equal(t, int64(3), rsb.NamedRuleset(GameTypeRoyale).Settings().Seed())
|
||||
|
||||
// test parameter merging
|
||||
rsb = NewRulesetBuilder().
|
||||
|
|
|
|||
124
ruleset_test.go
124
ruleset_test.go
|
|
@ -5,102 +5,13 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/BattlesnakeOfficial/rules"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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) {
|
||||
// Test that a fresh instance can produce a Ruleset
|
||||
require.NotNil(t, rules.NewRulesetBuilder().Ruleset())
|
||||
require.Equal(t, rules.GameTypeStandard, rules.NewRulesetBuilder().Ruleset().Name(), "should default to standard game")
|
||||
|
||||
// test nil safety / defaults
|
||||
require.NotNil(t, rules.NewRulesetBuilder().Ruleset())
|
||||
require.NotNil(t, rules.NewRulesetBuilder().NamedRuleset(""))
|
||||
require.Equal(t, rules.GameTypeStandard, rules.NewRulesetBuilder().NamedRuleset("").Name(), "should default to standard game")
|
||||
|
||||
// make sure it works okay for lots of game types
|
||||
expectedResults := []struct {
|
||||
|
|
@ -120,32 +31,23 @@ func TestRulesetBuilder(t *testing.T) {
|
|||
|
||||
rsb.WithParams(map[string]string{
|
||||
// apply the standard rule params
|
||||
rules.ParamGameType: expected.GameType,
|
||||
rules.ParamFoodSpawnChance: "10",
|
||||
rules.ParamMinimumFood: "5",
|
||||
rules.ParamHazardDamagePerTurn: "12",
|
||||
rules.ParamHazardMap: "test",
|
||||
rules.ParamHazardMapAuthor: "tester",
|
||||
})
|
||||
|
||||
require.NotNil(t, rsb.Ruleset())
|
||||
require.Equal(t, expected.GameType, rsb.Ruleset().Name())
|
||||
require.NotNil(t, rsb.NamedRuleset(expected.GameType))
|
||||
require.Equal(t, expected.GameType, rsb.NamedRuleset(expected.GameType).Name())
|
||||
// All the standard settings should always be copied over
|
||||
require.Equal(t, 10, rsb.Ruleset().Settings().FoodSpawnChance)
|
||||
require.Equal(t, 12, rsb.Ruleset().Settings().HazardDamagePerTurn)
|
||||
require.Equal(t, 5, rsb.Ruleset().Settings().MinimumFood)
|
||||
require.Equal(t, "test", rsb.Ruleset().Settings().HazardMap)
|
||||
require.Equal(t, "tester", rsb.Ruleset().Settings().HazardMapAuthor)
|
||||
require.Equal(t, 10, rsb.NamedRuleset(expected.GameType).Settings().Int(rules.ParamFoodSpawnChance, 0))
|
||||
require.Equal(t, 12, rsb.NamedRuleset(expected.GameType).Settings().Int(rules.ParamHazardDamagePerTurn, 0))
|
||||
require.Equal(t, 5, rsb.NamedRuleset(expected.GameType).Settings().Int(rules.ParamMinimumFood, 0))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRulesetBuilderGameOver(t *testing.T) {
|
||||
settings := rules.Settings{
|
||||
RoyaleSettings: rules.RoyaleSettings{
|
||||
ShrinkEveryNTurns: 12,
|
||||
},
|
||||
}
|
||||
settings := rules.NewSettingsWithParams(rules.ParamShrinkEveryNTurns, "12")
|
||||
moves := []rules.SnakeMove{
|
||||
{ID: "1", Move: "up"},
|
||||
}
|
||||
|
|
@ -214,13 +116,11 @@ func TestRulesetBuilderGameOver(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%v_%v", test.gameType, test.solo), func(t *testing.T) {
|
||||
rsb := rules.NewRulesetBuilder().WithParams(map[string]string{
|
||||
rules.ParamGameType: test.gameType,
|
||||
}).WithSolo(test.solo)
|
||||
rsb := rules.NewRulesetBuilder().WithSettings(settings).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.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) {
|
||||
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.True(t, ended)
|
||||
}
|
||||
|
|
|
|||
90
settings.go
Normal file
90
settings.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package rules
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Settings contains all settings relevant to a game.
|
||||
// The settings are stored as raw string values, which should not be accessed
|
||||
// directly. Calling code should instead use the Int/Bool methods to parse them.
|
||||
type Settings struct {
|
||||
rawValues map[string]string
|
||||
|
||||
rand Rand
|
||||
seed int64
|
||||
}
|
||||
|
||||
func NewSettings(params map[string]string) Settings {
|
||||
rawValues := make(map[string]string, len(params))
|
||||
|
||||
// Copy incoming params into a new map
|
||||
for key, value := range params {
|
||||
rawValues[key] = value
|
||||
}
|
||||
|
||||
return Settings{
|
||||
rawValues: rawValues,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSettingsWithParams(params ...string) Settings {
|
||||
rawValues := map[string]string{}
|
||||
|
||||
for index := 1; index < len(params); index += 2 {
|
||||
rawValues[params[index-1]] = params[index]
|
||||
}
|
||||
|
||||
return Settings{
|
||||
rawValues: rawValues,
|
||||
}
|
||||
}
|
||||
|
||||
// Get a random number generator initialized based on the seed and current turn.
|
||||
func (settings Settings) GetRand(turn int) Rand {
|
||||
// Allow overriding the random generator for testing
|
||||
if settings.rand != nil {
|
||||
return settings.rand
|
||||
}
|
||||
|
||||
if settings.seed != 0 {
|
||||
return NewSeedRand(settings.seed + int64(turn))
|
||||
}
|
||||
|
||||
// Default to global random number generator if neither seed or rand are set.
|
||||
return GlobalRand
|
||||
}
|
||||
|
||||
func (settings Settings) WithRand(rand Rand) Settings {
|
||||
settings.rand = rand
|
||||
return settings
|
||||
}
|
||||
|
||||
func (settings Settings) Seed() int64 {
|
||||
return settings.seed
|
||||
}
|
||||
|
||||
func (settings Settings) WithSeed(seed int64) Settings {
|
||||
settings.seed = seed
|
||||
return settings
|
||||
}
|
||||
|
||||
// Bool returns the boolean value for the specified parameter.
|
||||
// If the parameter doesn't exist, the default value will be returned.
|
||||
// If the parameter does exist, but is not "true", false will be returned.
|
||||
func (settings Settings) Bool(paramName string, defaultValue bool) bool {
|
||||
if val, ok := settings.rawValues[paramName]; ok {
|
||||
return val == "true"
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Int returns the int value for the specified parameter.
|
||||
// If the parameter doesn't exist, the default value will be returned.
|
||||
// If the parameter does exist, but is not a valid int, the default value will be returned.
|
||||
func (settings Settings) Int(paramName string, defaultValue int) int {
|
||||
if val, ok := settings.rawValues[paramName]; ok {
|
||||
i, err := strconv.Atoi(val)
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
31
settings_test.go
Normal file
31
settings_test.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package rules_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/BattlesnakeOfficial/rules"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSettings(t *testing.T) {
|
||||
params := map[string]string{
|
||||
"invalidSetting": "abcd",
|
||||
"intSetting": "1234",
|
||||
"boolSetting": "true",
|
||||
}
|
||||
|
||||
settings := rules.NewSettings(params)
|
||||
|
||||
assert.Equal(t, 4567, settings.Int("missingIntSetting", 4567))
|
||||
assert.Equal(t, 4567, settings.Int("invalidSetting", 4567))
|
||||
assert.Equal(t, 1234, settings.Int("intSetting", 4567))
|
||||
|
||||
assert.Equal(t, false, settings.Bool("missingBoolSetting", false))
|
||||
assert.Equal(t, true, settings.Bool("missingBoolSetting", true))
|
||||
assert.Equal(t, false, settings.Bool("invalidSetting", true))
|
||||
assert.Equal(t, true, settings.Bool("boolSetting", true))
|
||||
|
||||
assert.Equal(t, 4567, rules.NewSettingsWithParams("newIntSetting").Int("newIntSetting", 4567))
|
||||
assert.Equal(t, 1234, rules.NewSettingsWithParams("newIntSetting", "1234").Int("newIntSetting", 4567))
|
||||
assert.Equal(t, 4567, rules.NewSettingsWithParams("x", "y", "newIntSetting").Int("newIntSetting", 4567))
|
||||
}
|
||||
19
solo.go
19
solo.go
|
|
@ -9,25 +9,6 @@ var soloRulesetStages = []string{
|
|||
StageEliminationStandard,
|
||||
}
|
||||
|
||||
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) {
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||
|
|
|
|||
47
solo_test.go
47
solo_test.go
|
|
@ -6,20 +6,21 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSoloRulesetInterface(t *testing.T) {
|
||||
var _ Ruleset = (*SoloRuleset)(nil)
|
||||
func getSoloRuleset(settings Settings) Ruleset {
|
||||
return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeSolo)
|
||||
}
|
||||
|
||||
func TestSoloName(t *testing.T) {
|
||||
r := SoloRuleset{}
|
||||
r := getSoloRuleset(Settings{})
|
||||
require.Equal(t, "solo", r.Name())
|
||||
}
|
||||
|
||||
func TestSoloCreateNextBoardStateSanity(t *testing.T) {
|
||||
boardState := &BoardState{}
|
||||
r := SoloRuleset{}
|
||||
_, err := r.CreateNextBoardState(boardState, []SnakeMove{})
|
||||
r := getSoloRuleset(Settings{})
|
||||
gameOver, _, err := r.Execute(boardState, []SnakeMove{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, gameOver)
|
||||
}
|
||||
|
||||
func TestSoloIsGameOver(t *testing.T) {
|
||||
|
|
@ -41,7 +42,7 @@ func TestSoloIsGameOver(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
r := SoloRuleset{}
|
||||
r := getSoloRuleset(Settings{})
|
||||
for _, test := range tests {
|
||||
b := &BoardState{
|
||||
Height: 11,
|
||||
|
|
@ -50,7 +51,7 @@ func TestSoloIsGameOver(t *testing.T) {
|
|||
Food: []Point{},
|
||||
}
|
||||
|
||||
actual, err := r.IsGameOver(b)
|
||||
actual, _, err := r.Execute(b, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.Expected, actual)
|
||||
}
|
||||
|
|
@ -69,11 +70,11 @@ var soloCaseNotOver = gameTestCase{
|
|||
Snakes: []Snake{
|
||||
{
|
||||
ID: "one",
|
||||
Body: []Point{{1, 1}, {1, 2}},
|
||||
Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}},
|
||||
Health: 100,
|
||||
},
|
||||
},
|
||||
Food: []Point{{0, 0}, {1, 0}},
|
||||
Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||
Hazards: []Point{},
|
||||
},
|
||||
[]SnakeMove{
|
||||
|
|
@ -86,11 +87,11 @@ var soloCaseNotOver = gameTestCase{
|
|||
Snakes: []Snake{
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
Food: []Point{{0, 0}},
|
||||
Food: []Point{{X: 0, Y: 0}},
|
||||
Hazards: []Point{},
|
||||
},
|
||||
}
|
||||
|
|
@ -104,14 +105,10 @@ func TestSoloCreateNextBoardState(t *testing.T) {
|
|||
standardMoveAndCollideMAD,
|
||||
soloCaseNotOver,
|
||||
}
|
||||
r := SoloRuleset{}
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeSolo,
|
||||
})
|
||||
r := getSoloRuleset(Settings{})
|
||||
for _, gc := range cases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, r)
|
||||
// also test a pipeline with the same settings
|
||||
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
|
||||
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.
|
||||
// Actually random placement could result in the assumptions made by this test being incorrect.
|
||||
initialState, err := CreateDefaultBoardState(MaxRand, 2, 2, []string{"one"})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, next, err := r.Execute(
|
||||
initialState,
|
||||
r.Settings(),
|
||||
[]SnakeMove{{ID: "one", Move: "right"}},
|
||||
)
|
||||
_, next, err := r.Execute(initialState, []SnakeMove{{ID: "one", Move: "right"}})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, initialState)
|
||||
|
||||
ended, next, err := r.Execute(
|
||||
next,
|
||||
r.Settings(),
|
||||
[]SnakeMove{{ID: "one", Move: "right"}},
|
||||
)
|
||||
ended, next, err := r.Execute(next, []SnakeMove{{ID: "one", Move: "right"}})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, initialState)
|
||||
|
||||
|
|
|
|||
62
standard.go
62
standard.go
|
|
@ -5,14 +5,6 @@ import (
|
|||
"sort"
|
||||
)
|
||||
|
||||
type StandardRuleset struct {
|
||||
FoodSpawnChance int // [0, 100]
|
||||
MinimumFood int
|
||||
HazardDamagePerTurn int
|
||||
HazardMap string // optional
|
||||
HazardMapAuthor string // optional
|
||||
}
|
||||
|
||||
var standardRulesetStages = []string{
|
||||
StageGameOverStandard,
|
||||
StageMovementStandard,
|
||||
|
|
@ -22,23 +14,6 @@ var standardRulesetStages = []string{
|
|||
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) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
|
|
@ -156,6 +131,7 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove)
|
|||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
hazardDamage := settings.Int(ParamHazardDamagePerTurn, 0)
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
snake := &b.Snakes[i]
|
||||
if snake.EliminatedCause != NotEliminated {
|
||||
|
|
@ -176,7 +152,7 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove)
|
|||
}
|
||||
|
||||
// Snake is in a hazard, reduce health
|
||||
snake.Health = snake.Health - settings.HazardDamagePerTurn
|
||||
snake.Health = snake.Health - hazardDamage
|
||||
if snake.Health < 0 {
|
||||
snake.Health = 0
|
||||
}
|
||||
|
|
@ -393,20 +369,18 @@ func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (boo
|
|||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
minimumFood := settings.Int(ParamMinimumFood, 0)
|
||||
foodSpawnChance := settings.Int(ParamFoodSpawnChance, 0)
|
||||
numCurrentFood := int(len(b.Food))
|
||||
if numCurrentFood < settings.MinimumFood {
|
||||
return false, PlaceFoodRandomly(GlobalRand, b, settings.MinimumFood-numCurrentFood)
|
||||
if numCurrentFood < minimumFood {
|
||||
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, 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) {
|
||||
numSnakesRemaining := 0
|
||||
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
|
||||
}
|
||||
|
||||
func (r StandardRuleset) Settings() Settings {
|
||||
return Settings{
|
||||
FoodSpawnChance: r.FoodSpawnChance,
|
||||
MinimumFood: r.MinimumFood,
|
||||
HazardDamagePerTurn: r.HazardDamagePerTurn,
|
||||
HazardMap: r.HazardMap,
|
||||
HazardMapAuthor: r.HazardMapAuthor,
|
||||
}
|
||||
}
|
||||
|
||||
// impl Pipeline
|
||||
func (r StandardRuleset) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsInitialization checks whether the current state means the game is initialising.
|
||||
func IsInitialization(b *BoardState, settings Settings, moves []SnakeMove) bool {
|
||||
// We can safely assume that the game state is in the initialisation phase when
|
||||
// the turn hasn't advanced and the moves are empty
|
||||
return b.Turn <= 0 && len(moves) == 0
|
||||
}
|
||||
|
|
|
|||
519
standard_test.go
519
standard_test.go
File diff suppressed because it is too large
Load diff
20
wrapped.go
20
wrapped.go
|
|
@ -9,22 +9,6 @@ var wrappedRulesetStages = []string{
|
|||
StageEliminationStandard,
|
||||
}
|
||||
|
||||
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) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
|
|
@ -47,10 +31,6 @@ func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (boo
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (r *WrappedRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
return GameOverStandard(b, r.Settings(), nil)
|
||||
}
|
||||
|
||||
func wrap(value, min, max int) int {
|
||||
if value < min {
|
||||
return max
|
||||
|
|
|
|||
154
wrapped_test.go
154
wrapped_test.go
|
|
@ -7,15 +7,19 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func getWrappedRuleset(settings Settings) Ruleset {
|
||||
return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeWrapped)
|
||||
}
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
boardState := &BoardState{
|
||||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []Snake{
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{10, 10}}},
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -26,17 +30,18 @@ func TestLeft(t *testing.T) {
|
|||
{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.False(t, gameOver)
|
||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||
|
||||
expectedSnakes := []Snake{
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{10, 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{9, 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{10, 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{9, 10}}},
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{X: 9, Y: 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{X: 9, Y: 10}}},
|
||||
}
|
||||
for i, snake := range nextBoardState.Snakes {
|
||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||
|
|
@ -51,10 +56,10 @@ func TestRight(t *testing.T) {
|
|||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []Snake{
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{10, 10}}},
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -65,17 +70,18 @@ func TestRight(t *testing.T) {
|
|||
{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.False(t, gameOver)
|
||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||
|
||||
expectedSnakes := []Snake{
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{1, 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{0, 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{1, 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{0, 10}}},
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 1, Y: 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{X: 1, Y: 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||
}
|
||||
for i, snake := range nextBoardState.Snakes {
|
||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||
|
|
@ -90,10 +96,10 @@ func TestUp(t *testing.T) {
|
|||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []Snake{
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{10, 10}}},
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -104,17 +110,18 @@ func TestUp(t *testing.T) {
|
|||
{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.False(t, gameOver)
|
||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||
|
||||
expectedSnakes := []Snake{
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 1}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 1}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 0}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{10, 0}}},
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 1}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 1}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||
}
|
||||
for i, snake := range nextBoardState.Snakes {
|
||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||
|
|
@ -129,10 +136,10 @@ func TestDown(t *testing.T) {
|
|||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []Snake{
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{10, 10}}},
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 0}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 0}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -143,17 +150,18 @@ func TestDown(t *testing.T) {
|
|||
{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.False(t, gameOver)
|
||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||
|
||||
expectedSnakes := []Snake{
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{0, 10}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{10, 10}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{0, 9}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{10, 9}}},
|
||||
{ID: "bottomLeft", Health: 10, Body: []Point{{X: 0, Y: 10}}},
|
||||
{ID: "bottomRight", Health: 10, Body: []Point{{X: 10, Y: 10}}},
|
||||
{ID: "topLeft", Health: 10, Body: []Point{{X: 0, Y: 9}}},
|
||||
{ID: "topRight", Health: 10, Body: []Point{{X: 10, Y: 9}}},
|
||||
}
|
||||
for i, snake := range nextBoardState.Snakes {
|
||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||
|
|
@ -168,14 +176,14 @@ func TestEdgeCrossingCollision(t *testing.T) {
|
|||
Width: 11,
|
||||
Height: 11,
|
||||
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{
|
||||
{10, 1},
|
||||
{10, 2},
|
||||
{10, 3},
|
||||
{10, 4},
|
||||
{10, 5},
|
||||
{10, 6},
|
||||
{X: 10, Y: 1},
|
||||
{X: 10, Y: 2},
|
||||
{X: 10, Y: 3},
|
||||
{X: 10, Y: 4},
|
||||
{X: 10, Y: 5},
|
||||
{X: 10, Y: 6},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
|
@ -185,21 +193,22 @@ func TestEdgeCrossingCollision(t *testing.T) {
|
|||
{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.False(t, gameOver)
|
||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||
|
||||
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{
|
||||
{10, 0},
|
||||
{10, 1},
|
||||
{10, 2},
|
||||
{10, 3},
|
||||
{10, 4},
|
||||
{10, 5},
|
||||
{X: 10, Y: 0},
|
||||
{X: 10, Y: 1},
|
||||
{X: 10, Y: 2},
|
||||
{X: 10, Y: 3},
|
||||
{X: 10, Y: 4},
|
||||
{X: 10, Y: 5},
|
||||
}},
|
||||
}
|
||||
for i, snake := range nextBoardState.Snakes {
|
||||
|
|
@ -215,11 +224,11 @@ func TestEdgeCrossingEating(t *testing.T) {
|
|||
Width: 11,
|
||||
Height: 11,
|
||||
Snakes: []Snake{
|
||||
{ID: "left", Health: 10, Body: []Point{{0, 5}, {1, 5}}},
|
||||
{ID: "other", Health: 10, Body: []Point{{5, 5}}},
|
||||
{ID: "left", Health: 10, Body: []Point{{X: 0, Y: 5}, {X: 1, Y: 5}}},
|
||||
{ID: "other", Health: 10, Body: []Point{{X: 5, Y: 5}}},
|
||||
},
|
||||
Food: []Point{
|
||||
{10, 5},
|
||||
{X: 10, Y: 5},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -228,15 +237,16 @@ func TestEdgeCrossingEating(t *testing.T) {
|
|||
{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.False(t, gameOver)
|
||||
require.Equal(t, len(boardState.Snakes), len(nextBoardState.Snakes))
|
||||
|
||||
expectedSnakes := []Snake{
|
||||
{ID: "left", Health: 100, Body: []Point{{10, 5}, {0, 5}, {0, 5}}},
|
||||
{ID: "other", Health: 9, Body: []Point{{4, 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{{X: 4, Y: 5}}},
|
||||
}
|
||||
for i, snake := range nextBoardState.Snakes {
|
||||
require.Equal(t, expectedSnakes[i].ID, snake.ID, snake.ID)
|
||||
|
|
@ -271,12 +281,12 @@ var wrappedCaseMoveAndWrap = gameTestCase{
|
|||
Snakes: []Snake{
|
||||
{
|
||||
ID: "one",
|
||||
Body: []Point{{0, 0}, {1, 0}},
|
||||
Body: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
|
||||
Health: 100,
|
||||
},
|
||||
{
|
||||
ID: "two",
|
||||
Body: []Point{{3, 4}, {3, 3}},
|
||||
Body: []Point{{X: 3, Y: 4}, {X: 3, Y: 3}},
|
||||
Health: 100,
|
||||
},
|
||||
{
|
||||
|
|
@ -301,12 +311,12 @@ var wrappedCaseMoveAndWrap = gameTestCase{
|
|||
Snakes: []Snake{
|
||||
{
|
||||
ID: "one",
|
||||
Body: []Point{{9, 0}, {0, 0}},
|
||||
Body: []Point{{X: 9, Y: 0}, {X: 0, Y: 0}},
|
||||
Health: 99,
|
||||
},
|
||||
{
|
||||
ID: "two",
|
||||
Body: []Point{{3, 5}, {3, 4}},
|
||||
Body: []Point{{X: 3, Y: 5}, {X: 3, Y: 4}},
|
||||
Health: 99,
|
||||
},
|
||||
{
|
||||
|
|
@ -330,14 +340,10 @@ func TestWrappedCreateNextBoardState(t *testing.T) {
|
|||
standardMoveAndCollideMAD,
|
||||
wrappedCaseMoveAndWrap,
|
||||
}
|
||||
r := WrappedRuleset{}
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeWrapped,
|
||||
})
|
||||
r := getWrappedRuleset(Settings{})
|
||||
for _, gc := range cases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, r)
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeWrapped, NewPipeline(wrappedRulesetStages...)))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue