DEV 1676: Add maps helper functions (#111)

* add utility methods to Editor and BoardStateEditor

* add Meta.Validate

* allow setting Meta.MinPlayers to zero

* remove uints in map sizes

* use Meta.Validate in HazardPitsMap
This commit is contained in:
Rob O'Dwyer 2022-09-13 13:11:43 -07:00 committed by GitHub
parent c5810d8604
commit c4247945ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 701 additions and 83 deletions

View file

@ -10,7 +10,7 @@ func init() {
globalRegistry.RegisterMap("hz_castle_wall_xl", CastleWallExtraLargeHazardsMap{}) globalRegistry.RegisterMap("hz_castle_wall_xl", CastleWallExtraLargeHazardsMap{})
} }
func setupCastleWallBoard(maxPlayers uint, startingPositions []rules.Point, hazards []rules.Point, initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { func setupCastleWallBoard(maxPlayers int, startingPositions []rules.Point, hazards []rules.Point, initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
rand := settings.GetRand(initialBoardState.Turn) rand := settings.GetRand(initialBoardState.Turn)
if len(initialBoardState.Snakes) > int(maxPlayers) { if len(initialBoardState.Snakes) > int(maxPlayers) {

View file

@ -21,8 +21,8 @@ func TestCastleWallHazardsMap(t *testing.T) {
tests := []struct { tests := []struct {
Map maps.GameMap Map maps.GameMap
Width uint Width int
Height uint Height int
}{ }{
{maps.CastleWallMediumHazardsMap{}, 11, 11}, {maps.CastleWallMediumHazardsMap{}, 11, 11},
{maps.CastleWallLargeHazardsMap{}, 19, 19}, {maps.CastleWallLargeHazardsMap{}, 19, 19},
@ -31,7 +31,7 @@ func TestCastleWallHazardsMap(t *testing.T) {
// check all the supported sizes // check all the supported sizes
for _, test := range tests { for _, test := range tests {
state = rules.NewBoardState(int(test.Width), int(test.Height)) state = rules.NewBoardState(test.Width, test.Height)
state.Snakes = append(state.Snakes, rules.Snake{ID: "1", Body: []rules.Point{}}) state.Snakes = append(state.Snakes, rules.Snake{ID: "1", Body: []rules.Point{}})
editor = maps.NewBoardStateEditor(state) editor = maps.NewBoardStateEditor(state)
require.Empty(t, state.Hazards) require.Empty(t, state.Hazards)

View file

@ -1,6 +1,9 @@
package maps package maps
import ( import (
"fmt"
"strings"
"github.com/BattlesnakeOfficial/rules" "github.com/BattlesnakeOfficial/rules"
) )
@ -25,14 +28,55 @@ type GameMap interface {
UpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error UpdateBoard(previousBoardState *rules.BoardState, settings rules.Settings, editor Editor) error
} }
type Metadata struct {
Name string
Author string
Description string
// Version is the current version of the game map.
// Each time a map is changed, the version number should be incremented by 1.
Version int
// MinPlayers is the minimum number of players that the map supports.
MinPlayers int
// MaxPlayers is the maximum number of players that the map supports.
MaxPlayers int
// BoardSizes is a list of supported board sizes. Board sizes can fall into one of 3 categories:
// 1. one fixed size (i.e. [11x11])
// 2. multiple, fixed sizes (i.e. [11x11, 19x19, 25x25])
// 3. "unlimited" sizes (the board is not fixed and can scale to any reasonable size)
BoardSizes sizes
// Tags is a list of strings use to categorize the map.
Tags []string
}
func (meta Metadata) Validate(boardState *rules.BoardState) error {
if !meta.BoardSizes.IsAllowable(boardState.Width, boardState.Height) {
var sizesStrings []string
for _, size := range meta.BoardSizes {
sizesStrings = append(sizesStrings, fmt.Sprintf("%dx%d", size.Width, size.Height))
}
return rules.RulesetError("This map can only be played on these board sizes: " + strings.Join(sizesStrings, ", "))
}
if meta.MinPlayers != 0 && len(boardState.Snakes) < int(meta.MinPlayers) {
return rules.RulesetError(fmt.Sprintf("This map can only be played with %d-%d players", meta.MinPlayers, meta.MaxPlayers))
}
if meta.MaxPlayers != 0 && len(boardState.Snakes) > int(meta.MaxPlayers) {
return rules.RulesetError(fmt.Sprintf("This map can only be played with %d-%d players", meta.MinPlayers, meta.MaxPlayers))
}
return nil
}
// Dimensions describes the size of a Battlesnake board. // Dimensions describes the size of a Battlesnake board.
type Dimensions struct { type Dimensions struct {
// Width is the width, in number of board squares, of the board. // Width is the width, in number of board squares, of the board.
// The value 0 has a special meaning to mean unlimited. // The value 0 has a special meaning to mean unlimited.
Width uint Width int
// Height is the height, in number of board squares, of the board. // Height is the height, in number of board squares, of the board.
// The value 0 has a special meaning to mean unlimited. // The value 0 has a special meaning to mean unlimited.
Height uint Height int
} }
// sizes is a list of board sizes that a map supports. // sizes is a list of board sizes that a map supports.
@ -50,7 +94,7 @@ func (d sizes) IsAllowable(Width int, Height int) bool {
} }
for _, size := range d { for _, size := range d {
if size.Width == uint(Width) && size.Height == uint(Height) { if size.Width == Width && size.Height == Height {
return true return true
} }
} }
@ -67,7 +111,7 @@ func AnySize() sizes {
// in the vertical and horizontal directions. // in the vertical and horizontal directions.
// Examples: // Examples:
// - OddSizes(11,21) produces [(11,11), (13,13), (15,15), (17,17), (19,19), (21,21)] // - OddSizes(11,21) produces [(11,11), (13,13), (15,15), (17,17), (19,19), (21,21)]
func OddSizes(min, max uint) sizes { func OddSizes(min, max int) sizes {
var s sizes var s sizes
for i := min; i <= max; i += 2 { for i := min; i <= max; i += 2 {
s = append(s, Dimensions{Width: i, Height: i}) s = append(s, Dimensions{Width: i, Height: i})
@ -87,107 +131,262 @@ func FixedSizes(a Dimensions, b ...Dimensions) sizes {
return s return s
} }
type Metadata struct {
Name string
Author string
Description string
// Version is the current version of the game map.
// Each time a map is changed, the version number should be incremented by 1.
Version uint
// MinPlayers is the minimum number of players that the map supports.
MinPlayers uint
// MaxPlayers is the maximum number of players that the map supports.
MaxPlayers uint
// BoardSizes is a list of supported board sizes. Board sizes can fall into one of 3 categories:
// 1. one fixed size (i.e. [11x11])
// 2. multiple, fixed sizes (i.e. [11x11, 19x19, 25x25])
// 3. "unlimited" sizes (the board is not fixed and can scale to any reasonable size)
BoardSizes sizes
// Tags is a list of strings use to categorize the map.
Tags []string
}
// Editor is used by GameMap implementations to modify the board state. // Editor is used by GameMap implementations to modify the board state.
type Editor interface { type Editor interface {
// Clears all food from the board. // Clears all food from the board.
ClearFood() ClearFood()
// Clears all hazards from the board.
ClearHazards()
// Adds a food to the board. Does not check for duplicates. // Adds a food to the board. Does not check for duplicates.
AddFood(rules.Point) AddFood(rules.Point)
// Adds a hazard to the board. Does not check for duplicates.
AddHazard(rules.Point)
// Removes all food from a specific tile on the board. // Removes all food from a specific tile on the board.
RemoveFood(rules.Point) RemoveFood(rules.Point)
// Get the locations of food currently on the board.
// Note: the return value is a copy and modifying it won't affect the board.
Food() []rules.Point
// Clears all hazards from the board.
ClearHazards()
// Adds a hazard to the board. Does not check for duplicates.
AddHazard(rules.Point)
// Removes all hazards from a specific tile on the board. // Removes all hazards from a specific tile on the board.
RemoveHazard(rules.Point) RemoveHazard(rules.Point)
// Get the locations of hazards currently on the board.
// Note: the return value is a copy and modifying it won't affect the board.
Hazards() []rules.Point
// Updates the body and health of a snake. // Updates the body and health of a snake.
PlaceSnake(id string, body []rules.Point, health int) PlaceSnake(id string, body []rules.Point, health int)
// Get the bodies of all non-eliminated snakes currently on the board, keyed by Snake ID
// Note: the body values in the return value are a copy and modifying them won't affect the board.
SnakeBodies() map[string][]rules.Point
// 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.
PlaceSnakesRandomlyAtPositions(rand rules.Rand, snakes []rules.Snake, heads []rules.Point, bodyLength int) error
// Returns true if the provided point on the board is occupied by a snake body, food, and/or hazard.
IsOccupied(point rules.Point, snakes, hazards, food bool) bool
// Get a set of all points on the board the are occupied by snake bodies, food, and/or hazards.
// The value for each point will be set to true in the return value if that point is occupied by one of the selected objects.
OccupiedPoints(snakes, hazards, food bool) map[rules.Point]bool
// Given a list of points, return only those that are unoccupied by snake bodies, food, and/or hazards.
FilterUnoccupiedPoints(targets []rules.Point, snakes, hazards, food bool) []rules.Point
// Shuffle the provided slice of points randomly using the provided rules.Rand
ShufflePoints(rules.Rand, []rules.Point)
} }
// An Editor backed by a BoardState. // An Editor backed by a BoardState.
type BoardStateEditor struct { type BoardStateEditor struct {
*rules.BoardState boardState *rules.BoardState
} }
func NewBoardStateEditor(boardState *rules.BoardState) *BoardStateEditor { func NewBoardStateEditor(boardState *rules.BoardState) *BoardStateEditor {
return &BoardStateEditor{ return &BoardStateEditor{
BoardState: boardState, boardState: boardState,
} }
} }
func (editor *BoardStateEditor) ClearFood() { func (editor *BoardStateEditor) ClearFood() {
editor.Food = []rules.Point{} editor.boardState.Food = []rules.Point{}
}
func (editor *BoardStateEditor) ClearHazards() {
editor.Hazards = []rules.Point{}
} }
func (editor *BoardStateEditor) AddFood(p rules.Point) { func (editor *BoardStateEditor) AddFood(p rules.Point) {
editor.Food = append(editor.Food, rules.Point{X: p.X, Y: p.Y}) editor.boardState.Food = append(editor.boardState.Food, rules.Point{X: p.X, Y: p.Y})
}
func (editor *BoardStateEditor) AddHazard(p rules.Point) {
editor.Hazards = append(editor.Hazards, rules.Point{X: p.X, Y: p.Y})
} }
func (editor *BoardStateEditor) RemoveFood(p rules.Point) { func (editor *BoardStateEditor) RemoveFood(p rules.Point) {
for index, food := range editor.Food { for index, food := range editor.boardState.Food {
if food.X == p.X && food.Y == p.Y { if food.X == p.X && food.Y == p.Y {
editor.Food[index] = editor.Food[len(editor.Food)-1] editor.boardState.Food[index] = editor.boardState.Food[len(editor.boardState.Food)-1]
editor.Food = editor.Food[:len(editor.Food)-1] editor.boardState.Food = editor.boardState.Food[:len(editor.boardState.Food)-1]
} }
} }
} }
// Get the locations of food currently on the board.
// Note: the return value is read-only.
func (editor *BoardStateEditor) Food() []rules.Point {
return append([]rules.Point(nil), editor.boardState.Food...)
}
func (editor *BoardStateEditor) ClearHazards() {
editor.boardState.Hazards = []rules.Point{}
}
func (editor *BoardStateEditor) AddHazard(p rules.Point) {
editor.boardState.Hazards = append(editor.boardState.Hazards, rules.Point{X: p.X, Y: p.Y})
}
func (editor *BoardStateEditor) RemoveHazard(p rules.Point) { func (editor *BoardStateEditor) RemoveHazard(p rules.Point) {
for index, food := range editor.Hazards { for index, food := range editor.boardState.Hazards {
if food.X == p.X && food.Y == p.Y { if food.X == p.X && food.Y == p.Y {
editor.Hazards[index] = editor.Hazards[len(editor.Hazards)-1] editor.boardState.Hazards[index] = editor.boardState.Hazards[len(editor.boardState.Hazards)-1]
editor.Hazards = editor.Hazards[:len(editor.Hazards)-1] editor.boardState.Hazards = editor.boardState.Hazards[:len(editor.boardState.Hazards)-1]
} }
} }
} }
// Get the locations of hazards currently on the board.
// Note: the return value is read-only.
func (editor *BoardStateEditor) Hazards() []rules.Point {
return append([]rules.Point(nil), editor.boardState.Hazards...)
}
func (editor *BoardStateEditor) PlaceSnake(id string, body []rules.Point, health int) { func (editor *BoardStateEditor) PlaceSnake(id string, body []rules.Point, health int) {
for index, snake := range editor.Snakes { for index, snake := range editor.boardState.Snakes {
if snake.ID == id { if snake.ID == id {
editor.Snakes[index].Body = body editor.boardState.Snakes[index].Body = body
editor.Snakes[index].Health = health editor.boardState.Snakes[index].Health = health
return return
} }
} }
editor.Snakes = append(editor.Snakes, rules.Snake{ editor.boardState.Snakes = append(editor.boardState.Snakes, rules.Snake{
ID: id, ID: id,
Health: health, Health: health,
Body: body, Body: body,
}) })
} }
// Get the bodies of all non-eliminated snakes currently on the board.
// Note: the return value is read-only.
func (editor *BoardStateEditor) SnakeBodies() map[string][]rules.Point {
result := make(map[string][]rules.Point, len(editor.boardState.Snakes))
for _, snake := range editor.boardState.Snakes {
result[snake.ID] = append([]rules.Point(nil), snake.Body...)
}
return result
}
// 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.
func (editor *BoardStateEditor) PlaceSnakesRandomlyAtPositions(rand rules.Rand, snakes []rules.Snake, heads []rules.Point, bodyLength int) error {
if len(snakes) > len(heads) {
return rules.ErrorTooManySnakes
}
// Shuffle starting points
editor.ShufflePoints(rand, heads)
// Assign starting points to snakes in order
for index, snake := range snakes {
head := heads[index]
body := make([]rules.Point, bodyLength)
for i := 0; i < bodyLength; i++ {
body[i] = head
}
editor.PlaceSnake(snake.ID, body, rules.SnakeMaxHealth)
}
return nil
}
// Returns true if the provided point on the board is occupied by a snake body, food, and/or hazard.
func (editor *BoardStateEditor) IsOccupied(point rules.Point, snakes, hazards, food bool) bool {
if food {
for _, food := range editor.boardState.Food {
if food == point {
return true
}
}
}
if hazards {
for _, hazard := range editor.boardState.Hazards {
if hazard == point {
return true
}
}
}
if snakes {
for _, snake := range editor.boardState.Snakes {
for _, body := range snake.Body {
if body == point {
return true
}
}
}
}
return false
}
// Get a set of all points on the board the are occupied by snake bodies, food, and/or hazards.
// The value for each point will be set to true in the return value if that point is occupied by one of the selected objects.
func (editor *BoardStateEditor) OccupiedPoints(snakes, hazards, food bool) map[rules.Point]bool {
boardState := editor.boardState
result := make(map[rules.Point]bool, len(boardState.Food)+len(boardState.Hazards)+len(boardState.Snakes)*3)
if food {
for _, food := range editor.boardState.Food {
result[food] = true
}
}
if hazards {
for _, hazard := range editor.boardState.Hazards {
result[hazard] = true
}
}
if snakes {
for _, snake := range editor.boardState.Snakes {
for _, body := range snake.Body {
result[body] = true
}
}
}
return result
}
// Given a list of points, return only those that are unoccupied by snake bodies, food, and/or hazards.
func (editor *BoardStateEditor) FilterUnoccupiedPoints(targets []rules.Point, snakes, hazards, food bool) []rules.Point {
result := make([]rules.Point, 0, len(targets))
targetLoop:
for _, point := range targets {
if food {
for _, food := range editor.boardState.Food {
if food == point {
continue targetLoop
}
}
}
if hazards {
for _, hazard := range editor.boardState.Hazards {
if hazard == point {
continue targetLoop
}
}
}
if snakes {
for _, snake := range editor.boardState.Snakes {
for _, body := range snake.Body {
if body == point {
continue targetLoop
}
}
}
}
result = append(result, point)
}
return result
}
func (editor *BoardStateEditor) ShufflePoints(rand rules.Rand, points []rules.Point) {
rand.Shuffle(len(points), func(i int, j int) {
points[i], points[j] = points[j], points[i]
})
}

View file

@ -7,6 +7,110 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMetadataValidate(t *testing.T) {
for label, test := range map[string]struct {
metadata Metadata
boardState *rules.BoardState
expected error
}{
"unlimited": {
Metadata{
BoardSizes: AnySize(),
},
rules.NewBoardState(99, 99),
nil,
},
"in sizes": {
Metadata{
BoardSizes: OddSizes(7, 25),
},
rules.NewBoardState(7, 7),
nil,
},
"too small": {
Metadata{
BoardSizes: OddSizes(7, 25),
},
rules.NewBoardState(6, 6),
rules.RulesetError("This map can only be played on these board sizes: 7x7, 9x9, 11x11, 13x13, 15x15, 17x17, 19x19, 21x21, 23x23, 25x25"),
},
"too large": {
Metadata{
BoardSizes: OddSizes(7, 25),
},
rules.NewBoardState(26, 26),
rules.RulesetError("This map can only be played on these board sizes: 7x7, 9x9, 11x11, 13x13, 15x15, 17x17, 19x19, 21x21, 23x23, 25x25"),
},
"valid players": {
Metadata{
BoardSizes: AnySize(),
MinPlayers: 4,
MaxPlayers: 4,
},
&rules.BoardState{
Snakes: []rules.Snake{
{ID: "1"},
{ID: "2"},
{ID: "3"},
{ID: "4"},
},
},
nil,
},
"too few players": {
Metadata{
BoardSizes: AnySize(),
MinPlayers: 3,
MaxPlayers: 4,
},
&rules.BoardState{
Snakes: []rules.Snake{
{ID: "1"},
{ID: "2"},
},
},
rules.RulesetError("This map can only be played with 3-4 players"),
},
"too many players": {
Metadata{
BoardSizes: AnySize(),
MinPlayers: 3,
MaxPlayers: 4,
},
&rules.BoardState{
Snakes: []rules.Snake{
{ID: "1"},
{ID: "2"},
{ID: "3"},
{ID: "4"},
{ID: "5"},
},
},
rules.RulesetError("This map can only be played with 3-4 players"),
},
} {
t.Run(label, func(t *testing.T) {
actual := test.metadata.Validate(test.boardState)
require.Equal(t, test.expected, actual)
})
}
}
func TestMapSizes(t *testing.T) {
s := FixedSizes(Dimensions{11, 12})
require.Equal(t, s[0].Width, 11)
require.Equal(t, s[0].Height, 12)
s = FixedSizes(Dimensions{11, 11}, Dimensions{19, 25})
require.Len(t, s, 2)
require.Equal(t, s[1].Width, 19)
require.Equal(t, s[1].Height, 25)
s = AnySize()
require.Len(t, s, 1, "unlimited maps should have just one dimensions")
require.True(t, s.IsUnlimited())
}
func TestBoardStateEditorInterface(t *testing.T) { func TestBoardStateEditorInterface(t *testing.T) {
var _ Editor = (*BoardStateEditor)(nil) var _ Editor = (*BoardStateEditor)(nil)
} }
@ -18,7 +122,7 @@ func TestBoardStateEditor(t *testing.T) {
Health: 100, Health: 100,
}) })
editor := BoardStateEditor{BoardState: boardState} editor := BoardStateEditor{boardState: boardState}
editor.AddFood(rules.Point{X: 1, Y: 3}) editor.AddFood(rules.Point{X: 1, Y: 3})
editor.AddFood(rules.Point{X: 3, Y: 6}) editor.AddFood(rules.Point{X: 3, Y: 6})
@ -56,6 +160,26 @@ func TestBoardStateEditor(t *testing.T) {
}, },
}, boardState) }, boardState)
require.Equal(t, []rules.Point{
{X: 1, Y: 3},
{X: 3, Y: 7},
}, editor.Food())
require.Equal(t, []rules.Point{
{X: 1, Y: 3},
{X: 3, Y: 7},
}, editor.Hazards())
require.Equal(t, map[string][]rules.Point{
"existing_snake": {
{X: 5, Y: 2}, {X: 5, Y: 1}, {X: 5, Y: 0},
},
"new_snake": {
{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1},
},
}, editor.SnakeBodies())
editor.ClearFood() editor.ClearFood()
require.Equal(t, []rules.Point{}, boardState.Food) require.Equal(t, []rules.Point{}, boardState.Food)
@ -63,17 +187,317 @@ func TestBoardStateEditor(t *testing.T) {
require.Equal(t, []rules.Point{}, boardState.Hazards) require.Equal(t, []rules.Point{}, boardState.Hazards)
} }
func TestMapSizes(t *testing.T) { func TestBoardStateEditorPlaceSnakesRandomlyAtPositions(t *testing.T) {
s := FixedSizes(Dimensions{11, 12}) for label, test := range map[string]struct {
require.Equal(t, s[0].Width, uint(11)) rand rules.Rand
require.Equal(t, s[0].Height, uint(12)) initialSnakes []rules.Snake
heads []rules.Point
bodyLength int
expectedError error
expectedSnakes []rules.Snake
}{
"empty": {
rules.MinRand,
[]rules.Snake{},
[]rules.Point{},
0,
nil,
[]rules.Snake{},
},
"too many snakes": {
rules.MinRand,
[]rules.Snake{
{ID: "1"}, {ID: "2"}, {ID: "3"},
},
[]rules.Point{{X: 3, Y: 3}, {X: 6, Y: 2}},
3,
rules.ErrorTooManySnakes,
nil,
},
"success unshuffled": {
rules.MinRand,
[]rules.Snake{
{ID: "1"}, {ID: "2"},
},
[]rules.Point{{X: 3, Y: 3}, {X: 6, Y: 2}},
3,
nil,
[]rules.Snake{
{
ID: "1",
Body: []rules.Point{{X: 3, Y: 3}, {X: 3, Y: 3}, {X: 3, Y: 3}},
Health: rules.SnakeMaxHealth,
}, {
ID: "2",
Body: []rules.Point{{X: 6, Y: 2}, {X: 6, Y: 2}, {X: 6, Y: 2}},
Health: rules.SnakeMaxHealth,
},
},
},
"success shuffled": {
rules.MaxRand,
[]rules.Snake{
{ID: "1"}, {ID: "2"},
},
[]rules.Point{{X: 3, Y: 3}, {X: 6, Y: 2}},
3,
nil,
[]rules.Snake{
{
ID: "1",
Body: []rules.Point{{X: 6, Y: 2}, {X: 6, Y: 2}, {X: 6, Y: 2}},
Health: rules.SnakeMaxHealth,
}, {
ID: "2",
Body: []rules.Point{{X: 3, Y: 3}, {X: 3, Y: 3}, {X: 3, Y: 3}},
Health: rules.SnakeMaxHealth,
},
},
},
} {
t.Run(label, func(t *testing.T) {
boardState := rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall)
boardState.Snakes = test.initialSnakes
editor := NewBoardStateEditor(boardState)
s = FixedSizes(Dimensions{11, 11}, Dimensions{19, 25}) err := editor.PlaceSnakesRandomlyAtPositions(test.rand, test.initialSnakes, test.heads, test.bodyLength)
require.Len(t, s, 2) if test.expectedError != nil {
require.Equal(t, s[1].Width, uint(19)) require.Equal(t, test.expectedError, err)
require.Equal(t, s[1].Height, uint(25)) } else {
require.Equal(t, test.expectedSnakes, boardState.Snakes)
s = AnySize() }
require.Len(t, s, 1, "unlimited maps should have just one dimensions") })
require.True(t, s.IsUnlimited()) }
}
func TestBoardStateEditorIsOccupied(t *testing.T) {
for label, test := range map[string]struct {
boardState *rules.BoardState
point rules.Point
snakes, hazards, food bool
expected bool
}{
"empty board": {
rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall),
rules.Point{X: 3, Y: 3},
true, true, true,
false,
},
"unoccupied": {
&rules.BoardState{
Food: []rules.Point{{X: 1, Y: 1}},
Hazards: []rules.Point{{X: 2, Y: 2}},
Snakes: []rules.Snake{
{
ID: "1",
Body: []rules.Point{{X: 3, Y: 3}},
},
},
},
rules.Point{X: 2, Y: 3},
true, true, true,
false,
},
"food": {
&rules.BoardState{
Food: []rules.Point{{X: 1, Y: 1}},
},
rules.Point{X: 1, Y: 1},
false, false, true,
true,
},
"ignored food": {
&rules.BoardState{
Food: []rules.Point{{X: 1, Y: 1}},
},
rules.Point{X: 1, Y: 1},
false, false, false,
false,
},
"hazard": {
&rules.BoardState{
Hazards: []rules.Point{{X: 1, Y: 1}},
},
rules.Point{X: 1, Y: 1},
false, true, false,
true,
},
"ignored hazard": {
&rules.BoardState{
Food: []rules.Point{{X: 1, Y: 1}},
},
rules.Point{X: 1, Y: 1},
false, false, false,
false,
},
"snake": {
&rules.BoardState{
Snakes: []rules.Snake{
{
ID: "1",
Body: []rules.Point{{X: 1, Y: 1}},
},
},
},
rules.Point{X: 1, Y: 1},
true, false, false,
true,
},
"ignored snake": {
&rules.BoardState{
Snakes: []rules.Snake{
{
ID: "1",
Body: []rules.Point{{X: 1, Y: 1}},
},
},
},
rules.Point{X: 1, Y: 1},
false, false, false,
false,
},
} {
t.Run(label, func(t *testing.T) {
editor := NewBoardStateEditor(test.boardState)
actual := editor.IsOccupied(test.point, test.snakes, test.hazards, test.food)
require.Equal(t, test.expected, actual)
})
}
}
func TestBoardStateEditorOccupiedPoints(t *testing.T) {
testBoardState := &rules.BoardState{
Food: []rules.Point{{X: 1, Y: 1}},
Hazards: []rules.Point{{X: 2, Y: 2}},
Snakes: []rules.Snake{
{
ID: "1",
Body: []rules.Point{{X: 3, Y: 3}},
},
},
}
for label, test := range map[string]struct {
boardState *rules.BoardState
snakes, hazards, food bool
expected map[rules.Point]bool
}{
"empty board": {
rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall),
true, true, true,
map[rules.Point]bool{},
},
"all types": {
testBoardState,
true, true, true,
map[rules.Point]bool{
{X: 1, Y: 1}: true,
{X: 2, Y: 2}: true,
{X: 3, Y: 3}: true,
},
},
"ignore snakes": {
testBoardState,
false, true, true,
map[rules.Point]bool{
{X: 1, Y: 1}: true,
{X: 2, Y: 2}: true,
},
},
"ignore hazards": {
testBoardState,
true, false, true,
map[rules.Point]bool{
{X: 1, Y: 1}: true,
{X: 3, Y: 3}: true,
},
},
"ignore food": {
testBoardState,
true, true, false,
map[rules.Point]bool{
{X: 2, Y: 2}: true,
{X: 3, Y: 3}: true,
},
},
} {
t.Run(label, func(t *testing.T) {
editor := NewBoardStateEditor(test.boardState)
actual := editor.OccupiedPoints(test.snakes, test.hazards, test.food)
require.Equal(t, test.expected, actual)
})
}
}
func TestBoardStateEditorFilterUnoccupiedPoints(t *testing.T) {
testBoardState := &rules.BoardState{
Food: []rules.Point{{X: 1, Y: 1}},
Hazards: []rules.Point{{X: 2, Y: 2}},
Snakes: []rules.Snake{
{
ID: "1",
Body: []rules.Point{{X: 3, Y: 3}},
},
},
}
for label, test := range map[string]struct {
boardState *rules.BoardState
targets []rules.Point
snakes, hazards, food bool
expected []rules.Point
}{
"empty": {
rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall),
[]rules.Point{},
true, true, true,
[]rules.Point{},
},
"all types": {
testBoardState,
[]rules.Point{{X: 3, Y: 3}, {X: 1, Y: 1}, {X: 2, Y: 2}, {X: 2, Y: 1}},
true, true, true,
[]rules.Point{{X: 2, Y: 1}},
},
"ignore snakes": {
testBoardState,
[]rules.Point{{X: 3, Y: 3}, {X: 1, Y: 1}, {X: 2, Y: 2}, {X: 2, Y: 1}},
false, true, true,
[]rules.Point{{X: 3, Y: 3}, {X: 2, Y: 1}},
},
"ignore hazards": {
testBoardState,
[]rules.Point{{X: 3, Y: 3}, {X: 1, Y: 1}, {X: 2, Y: 2}, {X: 2, Y: 1}},
true, false, true,
[]rules.Point{{X: 2, Y: 2}, {X: 2, Y: 1}},
},
"ignore food": {
testBoardState,
[]rules.Point{{X: 3, Y: 3}, {X: 1, Y: 1}, {X: 2, Y: 2}, {X: 2, Y: 1}},
true, true, false,
[]rules.Point{{X: 1, Y: 1}, {X: 2, Y: 1}},
},
} {
t.Run(label, func(t *testing.T) {
editor := NewBoardStateEditor(test.boardState)
actual := editor.FilterUnoccupiedPoints(test.targets, test.snakes, test.hazards, test.food)
require.Equal(t, test.expected, actual)
})
}
}
func TestBoardStateEditorShufflePoints(t *testing.T) {
editor := NewBoardStateEditor(rules.NewBoardState(rules.BoardSizeSmall, rules.BoardSizeSmall))
points := []rules.Point{{X: 4, Y: 0}, {X: 3, Y: 1}, {X: 2, Y: 2}, {X: 1, Y: 3}, {X: 0, Y: 4}}
editor.ShufflePoints(rules.MaxRand, points)
expected := []rules.Point{{X: 3, Y: 1}, {X: 2, Y: 2}, {X: 1, Y: 3}, {X: 0, Y: 4}, {X: 4, Y: 0}}
require.Equal(t, expected, points)
} }

View file

@ -20,8 +20,8 @@ func (m HazardPitsMap) Meta() Metadata {
Description: "A map that that fills in grid-like pattern of squares with pits filled with hazard sauce. Every N turns the pits will fill with another layer of sauce up to a maximum of 4 layers which last a few cycles, then the pits drain and the pattern repeats", Description: "A map that that fills in grid-like pattern of squares with pits filled with hazard sauce. Every N turns the pits will fill with another layer of sauce up to a maximum of 4 layers which last a few cycles, then the pits drain and the pattern repeats",
Author: "Battlesnake", Author: "Battlesnake",
Version: 1, Version: 1,
MinPlayers: 1, MinPlayers: 0,
MaxPlayers: 4, MaxPlayers: len(hazardPitStartPositions),
BoardSizes: FixedSizes(Dimensions{11, 11}), BoardSizes: FixedSizes(Dimensions{11, 11}),
Tags: []string{TAG_FOOD_PLACEMENT, TAG_HAZARD_PLACEMENT, TAG_SNAKE_PLACEMENT}, Tags: []string{TAG_FOOD_PLACEMENT, TAG_HAZARD_PLACEMENT, TAG_SNAKE_PLACEMENT},
} }
@ -47,12 +47,8 @@ func (m HazardPitsMap) AddHazardPits(board *rules.BoardState, settings rules.Set
} }
func (m HazardPitsMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error { func (m HazardPitsMap) SetupBoard(initialBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
if !m.Meta().BoardSizes.IsAllowable(initialBoardState.Width, initialBoardState.Height) { if err := m.Meta().Validate(initialBoardState); err != nil {
return rules.RulesetError("This map can only be played on a 11x11 board") return err
}
if len(initialBoardState.Snakes) > len(hazardPitStartPositions) {
return rules.ErrorTooManySnakes
} }
rand := settings.GetRand(0) rand := settings.GetRand(0)

View file

@ -25,7 +25,6 @@ func TestRegisteredMaps(t *testing.T) {
require.Equalf(t, mapName, gameMap.ID(), "%#v game map doesn't return its own ID", mapName) require.Equalf(t, mapName, gameMap.ID(), "%#v game map doesn't return its own ID", mapName)
meta := gameMap.Meta() meta := gameMap.Meta()
require.True(t, meta.Version > 0, fmt.Sprintf("registered maps must have a valid version (>= 1) - '%d' is invalid", meta.Version)) require.True(t, meta.Version > 0, fmt.Sprintf("registered maps must have a valid version (>= 1) - '%d' is invalid", meta.Version))
require.NotZero(t, meta.MinPlayers, "registered maps must have minimum players declared")
require.NotZero(t, meta.MaxPlayers, "registered maps must have maximum players declared") require.NotZero(t, meta.MaxPlayers, "registered maps must have maximum players declared")
require.LessOrEqual(t, meta.MaxPlayers, meta.MaxPlayers, "max players should always be >= min players") require.LessOrEqual(t, meta.MaxPlayers, meta.MaxPlayers, "max players should always be >= min players")
require.NotEmpty(t, meta.BoardSizes, "registered maps must have at least one supported size declared") require.NotEmpty(t, meta.BoardSizes, "registered maps must have at least one supported size declared")
@ -37,7 +36,7 @@ func TestRegisteredMaps(t *testing.T) {
for i := meta.MinPlayers; i < meta.MaxPlayers; i++ { for i := meta.MinPlayers; i < meta.MaxPlayers; i++ {
t.Run(fmt.Sprintf("%d players", i), func(t *testing.T) { t.Run(fmt.Sprintf("%d players", i), func(t *testing.T) {
initialBoardState := rules.NewBoardState(int(mapSize.Width), int(mapSize.Height)) initialBoardState := rules.NewBoardState(int(mapSize.Width), int(mapSize.Height))
for j := uint(0); j < i; j++ { for j := 0; j < i; j++ {
initialBoardState.Snakes = append(initialBoardState.Snakes, rules.Snake{ID: fmt.Sprint(j), Body: []rules.Point{}}) initialBoardState.Snakes = append(initialBoardState.Snakes, rules.Snake{ID: fmt.Sprint(j), Body: []rules.Point{}})
} }
err := gameMap.SetupBoard(initialBoardState, testSettings, NewBoardStateEditor(initialBoardState)) err := gameMap.SetupBoard(initialBoardState, testSettings, NewBoardStateEditor(initialBoardState))
@ -50,7 +49,7 @@ func TestRegisteredMaps(t *testing.T) {
for _, mapSize := range meta.BoardSizes { for _, mapSize := range meta.BoardSizes {
t.Run(fmt.Sprintf("%dx%d map size", mapSize.Width, mapSize.Height), func(t *testing.T) { t.Run(fmt.Sprintf("%dx%d map size", mapSize.Width, mapSize.Height), func(t *testing.T) {
initialBoardState := rules.NewBoardState(int(mapSize.Width), int(mapSize.Height)) initialBoardState := rules.NewBoardState(int(mapSize.Width), int(mapSize.Height))
for i := uint(0); i < meta.MaxPlayers; i++ { for i := 0; i < meta.MaxPlayers; i++ {
initialBoardState.Snakes = append(initialBoardState.Snakes, rules.Snake{ID: fmt.Sprint(i), Body: []rules.Point{}}) initialBoardState.Snakes = append(initialBoardState.Snakes, rules.Snake{ID: fmt.Sprint(i), Body: []rules.Point{}})
} }
err := gameMap.SetupBoard(initialBoardState, testSettings, NewBoardStateEditor(initialBoardState)) err := gameMap.SetupBoard(initialBoardState, testSettings, NewBoardStateEditor(initialBoardState))

View file

@ -21,8 +21,8 @@ func TestRiversAndBridgetsHazardsMap(t *testing.T) {
tests := []struct { tests := []struct {
Map maps.GameMap Map maps.GameMap
Width uint Width int
Height uint Height int
}{ }{
{maps.RiverAndBridgesMediumHazardsMap{}, 11, 11}, {maps.RiverAndBridgesMediumHazardsMap{}, 11, 11},
{maps.RiverAndBridgesLargeHazardsMap{}, 19, 19}, {maps.RiverAndBridgesLargeHazardsMap{}, 19, 19},
@ -33,7 +33,7 @@ func TestRiversAndBridgetsHazardsMap(t *testing.T) {
// check all the supported sizes // check all the supported sizes
for _, test := range tests { for _, test := range tests {
state = rules.NewBoardState(int(test.Width), int(test.Height)) state = rules.NewBoardState(test.Width, test.Height)
state.Snakes = append(state.Snakes, rules.Snake{ID: "1", Body: []rules.Point{}}) state.Snakes = append(state.Snakes, rules.Snake{ID: "1", Body: []rules.Point{}})
editor = maps.NewBoardStateEditor(state) editor = maps.NewBoardStateEditor(state)
require.Empty(t, state.Hazards) require.Empty(t, state.Hazards)