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:
parent
c5810d8604
commit
c4247945ca
7 changed files with 701 additions and 83 deletions
303
maps/game_map.go
303
maps/game_map.go
|
|
@ -1,6 +1,9 @@
|
|||
package maps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/BattlesnakeOfficial/rules"
|
||||
)
|
||||
|
||||
|
|
@ -25,14 +28,55 @@ type GameMap interface {
|
|||
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.
|
||||
type Dimensions struct {
|
||||
// Width is the width, in number of board squares, of the board.
|
||||
// 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.
|
||||
// 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.
|
||||
|
|
@ -50,7 +94,7 @@ func (d sizes) IsAllowable(Width int, Height int) bool {
|
|||
}
|
||||
|
||||
for _, size := range d {
|
||||
if size.Width == uint(Width) && size.Height == uint(Height) {
|
||||
if size.Width == Width && size.Height == Height {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -67,7 +111,7 @@ func AnySize() sizes {
|
|||
// in the vertical and horizontal directions.
|
||||
// Examples:
|
||||
// - 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
|
||||
for i := min; i <= max; i += 2 {
|
||||
s = append(s, Dimensions{Width: i, Height: i})
|
||||
|
|
@ -87,107 +131,262 @@ func FixedSizes(a Dimensions, b ...Dimensions) sizes {
|
|||
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.
|
||||
type Editor interface {
|
||||
// Clears all food from the board.
|
||||
ClearFood()
|
||||
|
||||
// Clears all hazards from the board.
|
||||
ClearHazards()
|
||||
|
||||
// Adds a food to the board. Does not check for duplicates.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
type BoardStateEditor struct {
|
||||
*rules.BoardState
|
||||
boardState *rules.BoardState
|
||||
}
|
||||
|
||||
func NewBoardStateEditor(boardState *rules.BoardState) *BoardStateEditor {
|
||||
return &BoardStateEditor{
|
||||
BoardState: boardState,
|
||||
boardState: boardState,
|
||||
}
|
||||
}
|
||||
|
||||
func (editor *BoardStateEditor) ClearFood() {
|
||||
editor.Food = []rules.Point{}
|
||||
}
|
||||
|
||||
func (editor *BoardStateEditor) ClearHazards() {
|
||||
editor.Hazards = []rules.Point{}
|
||||
editor.boardState.Food = []rules.Point{}
|
||||
}
|
||||
|
||||
func (editor *BoardStateEditor) AddFood(p rules.Point) {
|
||||
editor.Food = append(editor.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})
|
||||
editor.boardState.Food = append(editor.boardState.Food, rules.Point{X: p.X, Y: p.Y})
|
||||
}
|
||||
|
||||
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 {
|
||||
editor.Food[index] = editor.Food[len(editor.Food)-1]
|
||||
editor.Food = editor.Food[:len(editor.Food)-1]
|
||||
editor.boardState.Food[index] = editor.boardState.Food[len(editor.boardState.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) {
|
||||
for index, food := range editor.Hazards {
|
||||
for index, food := range editor.boardState.Hazards {
|
||||
if food.X == p.X && food.Y == p.Y {
|
||||
editor.Hazards[index] = editor.Hazards[len(editor.Hazards)-1]
|
||||
editor.Hazards = editor.Hazards[:len(editor.Hazards)-1]
|
||||
editor.boardState.Hazards[index] = editor.boardState.Hazards[len(editor.boardState.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) {
|
||||
for index, snake := range editor.Snakes {
|
||||
for index, snake := range editor.boardState.Snakes {
|
||||
if snake.ID == id {
|
||||
editor.Snakes[index].Body = body
|
||||
editor.Snakes[index].Health = health
|
||||
editor.boardState.Snakes[index].Body = body
|
||||
editor.boardState.Snakes[index].Health = health
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
editor.Snakes = append(editor.Snakes, rules.Snake{
|
||||
editor.boardState.Snakes = append(editor.boardState.Snakes, rules.Snake{
|
||||
ID: id,
|
||||
Health: health,
|
||||
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]
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue