DEV-1313: Add additional map types (#76)

* add helper to draw a ring of hazards

* refactor tests to not be internal tests

* add "hz_inner_wall" map

* add "hz_rings" map

* fix map registry

* fix: edge case bugs in drawRing

* remove println

* add "hz_columns"

* add "hz_rivers_bridges" map

* WIP: implementing spiral hazards map

* finish basic testing of 'hz_spiral'

* include first turn

* add "hz_hazards" map

* remove incorrect author

* add "hz_grow_box" map

* add "hz_expand_box" map

* add "hz_expand_scatter" map

* remove debug

* document the new "Range" method

* - use rules.RulesetError instead of generic error
- use a rules.Point for map rivers and bridgets map key

* use rules.RulesetError instead of errors.New

* provide more detail about boundar conditions

* fix documentation (max can be == min)

* add unit tests
This commit is contained in:
Torben 2022-06-01 11:39:31 -07:00 committed by GitHub
parent aa38bcd0eb
commit f0dc0bcb38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1129 additions and 24 deletions

View file

@ -1,18 +1,19 @@
package maps
package maps_test
import (
"testing"
"github.com/BattlesnakeOfficial/rules"
"github.com/BattlesnakeOfficial/rules/maps"
"github.com/stretchr/testify/require"
)
func TestEmptyMapInterface(t *testing.T) {
var _ GameMap = EmptyMap{}
var _ maps.GameMap = maps.EmptyMap{}
}
func TestEmptyMapSetupBoard(t *testing.T) {
m := EmptyMap{}
m := maps.EmptyMap{}
settings := rules.Settings{}
tests := []struct {
@ -122,7 +123,7 @@ func TestEmptyMapSetupBoard(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
nextBoardState := rules.NewBoardState(test.initialBoardState.Width, test.initialBoardState.Height)
editor := NewBoardStateEditor(nextBoardState)
editor := maps.NewBoardStateEditor(nextBoardState)
settings := settings.WithRand(test.rand)
err := m.SetupBoard(test.initialBoardState, settings, editor)
@ -137,7 +138,7 @@ func TestEmptyMapSetupBoard(t *testing.T) {
}
func TestEmptyMapUpdateBoard(t *testing.T) {
m := EmptyMap{}
m := maps.EmptyMap{}
initialBoardState := &rules.BoardState{
Width: 2,
Height: 2,
@ -151,7 +152,7 @@ func TestEmptyMapUpdateBoard(t *testing.T) {
}.WithRand(rules.MaxRand)
nextBoardState := initialBoardState.Clone()
err := m.UpdateBoard(initialBoardState.Clone(), settings, NewBoardStateEditor(nextBoardState))
err := m.UpdateBoard(initialBoardState.Clone(), settings, maps.NewBoardStateEditor(nextBoardState))
require.NoError(t, err)
require.Equal(t, &rules.BoardState{

671
maps/hazards.go Normal file
View file

@ -0,0 +1,671 @@
package maps
import (
"math"
"github.com/BattlesnakeOfficial/rules"
)
type InnerBorderHazardsMap struct{}
func init() {
globalRegistry.RegisterMap("hz_inner_wall", InnerBorderHazardsMap{})
globalRegistry.RegisterMap("hz_rings", ConcentricRingsHazardsMap{})
globalRegistry.RegisterMap("hz_columns", ColumnsHazardsMap{})
globalRegistry.RegisterMap("hz_rivers_bridges", RiverAndBridgesHazardsMap{})
globalRegistry.RegisterMap("hz_spiral", SpiralHazardsMap{})
globalRegistry.RegisterMap("hz_scatter", ScatterFillMap{})
globalRegistry.RegisterMap("hz_grow_box", DirectionalExpandingBoxMap{})
globalRegistry.RegisterMap("hz_expand_box", ExpandingBoxMap{})
globalRegistry.RegisterMap("hz_expand_scatter", ExpandingScatterMap{})
}
func (m InnerBorderHazardsMap) ID() string {
return "hz_inner_wall"
}
func (m InnerBorderHazardsMap) Meta() Metadata {
return Metadata{
Name: "hz_inner_wall",
Description: "Creates a static map on turn 0 that is a 1-square wall of hazard that is inset 2 squares from the edge of the board",
Author: "Battlesnake",
}
}
func (m InnerBorderHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
if err := (StandardMap{}).SetupBoard(lastBoardState, settings, editor); err != nil {
return err
}
// draw the initial, single ring of hazards
hazards, err := drawRing(lastBoardState.Width, lastBoardState.Height, 2, 2)
if err != nil {
return err
}
for _, p := range hazards {
editor.AddHazard(p)
}
return nil
}
func (m InnerBorderHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
}
type ConcentricRingsHazardsMap struct{}
func (m ConcentricRingsHazardsMap) ID() string {
return "hz_rings"
}
func (m ConcentricRingsHazardsMap) Meta() Metadata {
return Metadata{
Name: "hz_rings",
Description: "Creates a static map where there are rings of hazard sauce starting from the center with a 1 square space between the rings that has no sauce",
Author: "Battlesnake",
}
}
func (m ConcentricRingsHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
if err := (StandardMap{}).SetupBoard(lastBoardState, settings, editor); err != nil {
return err
}
// draw concentric rings of hazards
for offset := 2; offset < lastBoardState.Width/2; offset += 2 {
hazards, err := drawRing(lastBoardState.Width, lastBoardState.Height, offset, offset)
if err != nil {
return err
}
for _, p := range hazards {
editor.AddHazard(p)
}
}
return nil
}
func (m ConcentricRingsHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
}
type ColumnsHazardsMap struct{}
func (m ColumnsHazardsMap) ID() string {
return "hz_columns"
}
func (m ColumnsHazardsMap) Meta() Metadata {
return Metadata{
Name: "hz_columns",
Description: "Creates a static map on turn 0 that fills in odd squares, i.e. (1,1), (1,3), (3,3) ... with hazard sauce",
Author: "Battlesnake",
}
}
func (m ColumnsHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
if err := (StandardMap{}).SetupBoard(lastBoardState, settings, editor); err != nil {
return err
}
for x := 0; x < lastBoardState.Width; x++ {
for y := 0; y < lastBoardState.Height; y++ {
if x%2 == 1 && y%2 == 1 {
editor.AddHazard(rules.Point{X: x, Y: y})
}
}
}
return nil
}
func (m ColumnsHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
}
type SpiralHazardsMap struct{}
func (m SpiralHazardsMap) ID() string {
return "hz_spiral"
}
func (m SpiralHazardsMap) Meta() Metadata {
return Metadata{
Name: "hz_spiral",
Description: `Generates a dynamic hazard map that grows in a spiral pattern clockwise from a random point on
the map. Each 2 turns a new hazard square is added to the map`,
Author: "altersaddle",
}
}
func (m SpiralHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
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)
if err != nil {
return err
}
currentTurn := lastBoardState.Turn + 1
spawnEveryNTurns := 3
// no-op if we're not on a turn that spawns hazards
if currentTurn < spawnEveryNTurns || currentTurn%spawnEveryNTurns != 0 {
return nil
}
rand := settings.GetRand(0)
spawnArea := 0.3 // Center spiral in the middle 0.6 of the board
// randomly choose a location between the start point and the edge of the board
spawnOffsetX := int(math.Floor(float64(lastBoardState.Width) * spawnArea))
maxX := lastBoardState.Width - 1 - spawnOffsetX
startX := rand.Range(spawnOffsetX, maxX)
spawnOffsetY := int(math.Floor(float64(lastBoardState.Height) * spawnArea))
maxY := lastBoardState.Height - 1 - spawnOffsetY
startY := rand.Range(spawnOffsetY, maxY)
if currentTurn == spawnEveryNTurns {
editor.AddHazard(rules.Point{X: startX, Y: startY})
return nil
}
// determine number of rings in spiral
numRings := maxInt(startX, startY, lastBoardState.Width-startX, lastBoardState.Height-startY)
turnCtr := spawnEveryNTurns
for ring := 0; ring < numRings; ring++ {
offset := ring + 1
x := startX - ring
y := startY + offset
numSquaresInRing := 8 * offset
for i := 0; i < numSquaresInRing; i++ {
turnCtr += spawnEveryNTurns
if turnCtr > currentTurn {
break
}
if turnCtr == currentTurn && isOnBoard(lastBoardState.Width, lastBoardState.Height, x, y) {
editor.AddHazard(rules.Point{X: x, Y: y})
}
// move the "cursor"
if y == startY+offset && x < startX+offset {
// top line, move right
x += 1
} else if x == startX+offset && y > startY-offset {
// right side, go down
y -= 1
} else if y == startY-offset && x > startX-offset {
// bottom line, move left
x -= 1
} else if x == startX-offset && y < startY+offset {
y += 1
}
}
}
return nil
}
type ScatterFillMap struct{}
func (m ScatterFillMap) ID() string {
return "hz_scatter"
}
func (m ScatterFillMap) Meta() Metadata {
return Metadata{
Name: "hz_scatter",
Description: `Fills the entire board with hazard squares that are set to appear on regular turn schedule. Each square is picked at random.`,
}
}
func (m ScatterFillMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
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)
if err != nil {
return err
}
currentTurn := lastBoardState.Turn + 1
spawnEveryNTurns := 2
// no-op if we're not on a turn that spawns hazards
if currentTurn < spawnEveryNTurns || currentTurn%spawnEveryNTurns != 0 {
return nil
}
positions := make([]rules.Point, 0, lastBoardState.Width*lastBoardState.Height)
for x := 0; x < lastBoardState.Width; x++ {
for y := 0; y < lastBoardState.Height; y++ {
positions = append(positions, rules.Point{X: x, Y: y})
}
}
rand := settings.GetRand(0)
rand.Shuffle(len(positions), func(i, j int) {
positions[i], positions[j] = positions[j], positions[i]
})
editor.AddHazard(positions[(currentTurn-2)/2])
return nil
}
type DirectionalExpandingBoxMap struct{}
func (m DirectionalExpandingBoxMap) ID() string {
return "hz_grow_box"
}
func (m DirectionalExpandingBoxMap) Meta() Metadata {
return Metadata{
Name: "hz_grow_box",
Description: `Creates an area of hazard that expands from a point with one random side growing on a turn schedule.`,
}
}
func (m DirectionalExpandingBoxMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
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)
if err != nil {
return err
}
currentTurn := lastBoardState.Turn + 1
startTurn := 1
spawnEveryNTurns := 15
// no-op if we're not on a turn that spawns hazards
if (currentTurn-startTurn)%spawnEveryNTurns != 0 {
return nil
}
// no-op if we have spawned the entire board already
if len(lastBoardState.Hazards) == lastBoardState.Width*lastBoardState.Height {
return nil
}
rand := settings.GetRand(0)
startX := rand.Range(2, lastBoardState.Width-2)
startY := rand.Range(2, lastBoardState.Height-2)
if currentTurn == 1 {
editor.AddHazard(rules.Point{X: startX, Y: startY})
return nil
}
topLeft := rules.Point{X: startX, Y: startY}
bottomRight := rules.Point{X: startX, Y: startY}
// var growthDirection string
maxTurns := (currentTurn - startTurn) / spawnEveryNTurns
for i := 0; i < maxTurns; i++ {
directions := []string{}
if topLeft.X > 0 {
directions = append(directions, "left")
}
if topLeft.Y < lastBoardState.Height-1 {
directions = append(directions, "up")
}
if bottomRight.X < lastBoardState.Width-1 {
directions = append(directions, "right")
}
if bottomRight.Y > 0 {
directions = append(directions, "down")
}
if len(directions) == 0 {
return nil
}
choice := rand.Intn(len(directions))
growthDirection := directions[choice]
addHazards := i == maxTurns-1
if growthDirection == "left" {
x := topLeft.X - 1
if addHazards {
for y := bottomRight.Y; y < topLeft.Y+1; y++ {
editor.AddHazard(rules.Point{X: x, Y: y})
}
}
topLeft.X = x
} else if growthDirection == "right" {
x := bottomRight.X + 1
if addHazards {
for y := bottomRight.Y; y < topLeft.Y+1; y++ {
editor.AddHazard(rules.Point{X: x, Y: y})
}
}
bottomRight.X = x
} else if growthDirection == "up" {
y := topLeft.Y + 1
if addHazards {
for x := topLeft.X; x < bottomRight.X+1; x++ {
editor.AddHazard(rules.Point{X: x, Y: y})
}
}
topLeft.Y = y
} else if growthDirection == "down" {
y := bottomRight.Y - 1
if addHazards {
for x := topLeft.X; x < bottomRight.X+1; x++ {
editor.AddHazard(rules.Point{X: x, Y: y})
}
}
bottomRight.Y = y
}
}
return nil
}
type ExpandingBoxMap struct{}
func (m ExpandingBoxMap) ID() string {
return "hz_expand_box"
}
func (m ExpandingBoxMap) Meta() Metadata {
return Metadata{
Name: "hz_expand_box",
Description: `Generates an area of hazard that expands from a random point on the board outward in concentric rings on a periodic turn schedule.`,
}
}
func (m ExpandingBoxMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
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)
if err != nil {
return err
}
currentTurn := lastBoardState.Turn + 1
startTurn := 1 // first hazard appears on turn 1
spawnEveryNTurns := 20
// no-op if we're not on a turn that spawns hazards
if (currentTurn-startTurn)%spawnEveryNTurns != 0 {
return nil
}
// no-op if we have spawned the entire board already
if len(lastBoardState.Hazards) == lastBoardState.Width*lastBoardState.Height {
return nil
}
rand := settings.GetRand(0)
startX := rand.Range(2, lastBoardState.Width-2)
startY := rand.Range(2, lastBoardState.Width-2)
if currentTurn == startTurn {
editor.AddHazard(rules.Point{X: startX, Y: startY})
return nil
}
// determine number of rings in spiral
numRings := maxInt(startX, startY, lastBoardState.Width-startX, lastBoardState.Height-startY)
// no-op when iterations exceed the max rings
if currentTurn/spawnEveryNTurns > numRings {
return nil
}
ring := currentTurn/spawnEveryNTurns - 1
offset := ring + 1
for x := startX - offset; x < startX+offset+1; x++ {
for y := startY - offset; y < startY+offset+1; y++ {
if isOnBoard(lastBoardState.Width, lastBoardState.Height, x, y) {
if ((x == startX-offset || x == startX+offset) && y >= startY-offset && y <= startY+offset) || ((y == startY-offset || y == startY+offset) && x >= startX-offset && x <= startX+offset) {
editor.AddHazard(rules.Point{X: x, Y: y})
}
}
}
}
return nil
}
type ExpandingScatterMap struct{}
func (m ExpandingScatterMap) ID() string {
return "hz_expand_scatter"
}
func (m ExpandingScatterMap) Meta() Metadata {
return Metadata{
Name: "hz_expand_scatter",
Description: `Builds an expanding hazard area that grows from a central point in rings that are randomly filled in on a regular turn schedule.`,
}
}
func (m ExpandingScatterMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
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)
if err != nil {
return err
}
currentTurn := lastBoardState.Turn + 1
startTurn := 1 // first hazard appears on turn 1
spawnEveryNTurns := 2
// no-op if we're not on a turn that spawns hazards
if (currentTurn-startTurn)%spawnEveryNTurns != 0 {
return nil
}
// no-op if we have spawned the entire board already
if len(lastBoardState.Hazards) == lastBoardState.Width*lastBoardState.Height {
return nil
}
rand := settings.GetRand(0)
startX := rand.Range(1, lastBoardState.Width-1)
startY := rand.Range(1, lastBoardState.Width-1)
if currentTurn == startTurn {
editor.AddHazard(rules.Point{X: startX, Y: startY})
return nil
}
// determine number of rings in spiral
numRings := maxInt(startX, startY, lastBoardState.Width-startX, lastBoardState.Height-startY)
allPositions := []rules.Point{}
for ring := 0; ring < numRings; ring++ {
offset := ring + 1
positions := []rules.Point{}
for x := startX - offset; x < startX+offset+1; x++ {
for y := startY - offset; y < startY+offset+1; y++ {
if isOnBoard(lastBoardState.Width, lastBoardState.Height, x, y) {
if ((x == startX-offset || x == startX+offset) && y >= startY-offset && y <= startY+offset) || ((y == startY-offset || y == startY+offset) && x >= startX-offset && x <= startX+offset) {
positions = append(positions, rules.Point{X: x, Y: y})
}
}
}
}
// shuffle the positions so they are added scattered/randomly
rand.Shuffle(len(positions), func(i, j int) {
positions[i], positions[j] = positions[j], positions[i]
})
allPositions = append(allPositions, positions...)
}
chosenPos := currentTurn/spawnEveryNTurns - 1
editor.AddHazard(allPositions[chosenPos])
return nil
}
type RiverAndBridgesHazardsMap struct{}
func (m RiverAndBridgesHazardsMap) ID() string {
return "hz_rivers_bridges"
}
func (m RiverAndBridgesHazardsMap) Meta() Metadata {
return Metadata{
Name: "hz_rivers_bridges",
Description: `Creates fixed maps that have a lake of hazard in the middle with rivers going in the cardinal directions.
Each river has one or two 1-square "bridges" over them`,
Author: "Battlesnake",
}
}
func (m RiverAndBridgesHazardsMap) SetupBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
if err := (StandardMap{}).SetupBoard(lastBoardState, settings, editor); err != nil {
return err
}
hazards, ok := riversAndBridgesMaps[rules.Point{X: lastBoardState.Width, Y: lastBoardState.Height}]
if !ok {
return rules.RulesetError("Board size is not supported by this map")
}
for _, p := range hazards {
editor.AddHazard(p)
}
return nil
}
func (m RiverAndBridgesHazardsMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
return StandardMap{}.UpdateBoard(lastBoardState, settings, editor)
}
var riversAndBridgesMaps = map[rules.Point][]rules.Point{
{X: 11, Y: 11}: {
{X: 5, Y: 10},
{X: 5, Y: 9},
{X: 5, Y: 7},
{X: 5, Y: 6},
{X: 5, Y: 5},
{X: 5, Y: 4},
{X: 5, Y: 3},
{X: 5, Y: 0},
{X: 5, Y: 1},
{X: 6, Y: 5},
{X: 7, Y: 5},
{X: 9, Y: 5},
{X: 10, Y: 5},
{X: 4, Y: 5},
{X: 3, Y: 5},
{X: 1, Y: 5},
{X: 0, Y: 5},
},
{X: 19, Y: 19}: {
{X: 9, Y: 0},
{X: 9, Y: 1},
{X: 9, Y: 2},
{X: 9, Y: 5},
{X: 9, Y: 6},
{X: 9, Y: 7},
{X: 9, Y: 9},
{X: 9, Y: 8},
{X: 9, Y: 10},
{X: 9, Y: 12},
{X: 9, Y: 11},
{X: 9, Y: 13},
{X: 9, Y: 14},
{X: 9, Y: 16},
{X: 9, Y: 17},
{X: 9, Y: 18},
{X: 0, Y: 9},
{X: 2, Y: 9},
{X: 1, Y: 9},
{X: 3, Y: 9},
{X: 5, Y: 9},
{X: 6, Y: 9},
{X: 7, Y: 9},
{X: 8, Y: 9},
{X: 10, Y: 9},
{X: 13, Y: 9},
{X: 12, Y: 9},
{X: 11, Y: 9},
{X: 15, Y: 9},
{X: 16, Y: 9},
{X: 17, Y: 9},
{X: 18, Y: 9},
{X: 9, Y: 4},
{X: 8, Y: 10},
{X: 8, Y: 8},
{X: 10, Y: 8},
{X: 10, Y: 10},
},
{X: 25, Y: 25}: {
{X: 12, Y: 24},
{X: 12, Y: 21},
{X: 12, Y: 20},
{X: 12, Y: 19},
{X: 12, Y: 18},
{X: 12, Y: 15},
{X: 12, Y: 14},
{X: 12, Y: 13},
{X: 12, Y: 12},
{X: 12, Y: 11},
{X: 12, Y: 10},
{X: 12, Y: 9},
{X: 12, Y: 5},
{X: 12, Y: 4},
{X: 12, Y: 3},
{X: 12, Y: 0},
{X: 0, Y: 12},
{X: 3, Y: 12},
{X: 4, Y: 12},
{X: 5, Y: 12},
{X: 6, Y: 12},
{X: 9, Y: 12},
{X: 10, Y: 12},
{X: 11, Y: 12},
{X: 13, Y: 12},
{X: 14, Y: 12},
{X: 15, Y: 12},
{X: 18, Y: 12},
{X: 20, Y: 12},
{X: 19, Y: 12},
{X: 21, Y: 12},
{X: 24, Y: 12},
{X: 11, Y: 14},
{X: 10, Y: 13},
{X: 11, Y: 13},
{X: 10, Y: 11},
{X: 11, Y: 11},
{X: 11, Y: 10},
{X: 13, Y: 10},
{X: 14, Y: 11},
{X: 13, Y: 11},
{X: 13, Y: 13},
{X: 14, Y: 13},
{X: 13, Y: 14},
{X: 12, Y: 6},
{X: 12, Y: 2},
{X: 2, Y: 12},
{X: 22, Y: 12},
{X: 12, Y: 22},
{X: 16, Y: 12},
{X: 12, Y: 8},
{X: 8, Y: 12},
{X: 12, Y: 16},
},
}

217
maps/hazards_test.go Normal file
View file

@ -0,0 +1,217 @@
package maps_test
import (
"fmt"
"testing"
"github.com/BattlesnakeOfficial/rules"
"github.com/BattlesnakeOfficial/rules/maps"
"github.com/stretchr/testify/require"
)
func TestInnerBorderHazardsMap(t *testing.T) {
tests := []struct {
boardSize int
expectedHazards int
}{
{11, 32},
{19, 64},
{25, 88},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%dx%d", tc.boardSize, tc.boardSize), func(t *testing.T) {
m := maps.InnerBorderHazardsMap{}
state := rules.NewBoardState(tc.boardSize, tc.boardSize)
settings := rules.Settings{}
// ensure the ring of hazards is added to the board at setup
editor := maps.NewBoardStateEditor(state)
require.Empty(t, state.Hazards)
err := m.SetupBoard(state, settings, editor)
require.NoError(t, err)
require.NotEmpty(t, state.Hazards)
require.Len(t, state.Hazards, tc.expectedHazards)
})
}
}
func TestConcentricRingsHazardsMap(t *testing.T) {
tests := []struct {
boardSize int
expectedHazards int
}{
{11, 48},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%dx%d", tc.boardSize, tc.boardSize), func(t *testing.T) {
m := maps.ConcentricRingsHazardsMap{}
state := rules.NewBoardState(tc.boardSize, tc.boardSize)
settings := rules.Settings{}
// ensure the ring of hazards is added to the board at setup
editor := maps.NewBoardStateEditor(state)
require.Empty(t, state.Hazards)
err := m.SetupBoard(state, settings, editor)
require.NoError(t, err)
require.NotEmpty(t, state.Hazards)
require.Len(t, state.Hazards, tc.expectedHazards)
})
}
}
func TestColumnsHazardsMap(t *testing.T) {
m := maps.ColumnsHazardsMap{}
state := rules.NewBoardState(11, 11)
settings := rules.Settings{}
editor := maps.NewBoardStateEditor(state)
require.Empty(t, state.Hazards)
err := m.SetupBoard(state, settings, editor)
require.NoError(t, err)
require.NotEmpty(t, state.Hazards)
require.Len(t, state.Hazards, 25)
// a few spot checks
require.Contains(t, state.Hazards, rules.Point{X: 1, Y: 1})
require.Contains(t, state.Hazards, rules.Point{X: 1, Y: 5})
require.Contains(t, state.Hazards, rules.Point{X: 9, Y: 1})
require.Contains(t, state.Hazards, rules.Point{X: 9, Y: 9})
require.NotContains(t, state.Hazards, rules.Point{X: 0, Y: 1})
require.NotContains(t, state.Hazards, rules.Point{X: 8, Y: 4})
require.NotContains(t, state.Hazards, rules.Point{X: 2, Y: 2})
require.NotContains(t, state.Hazards, rules.Point{X: 4, Y: 9})
require.NotContains(t, state.Hazards, rules.Point{X: 1, Y: 0})
}
func TestRiversAndBridgetsHazardsMap(t *testing.T) {
// check error handling
m := maps.RiverAndBridgesHazardsMap{}
settings := rules.Settings{}
// check error for unsupported board sizes
state := rules.NewBoardState(9, 9)
editor := maps.NewBoardStateEditor(state)
err := m.SetupBoard(state, settings, editor)
require.Error(t, err)
// check all the supported sizes
for _, size := range []int{11, 19, 25} {
state = rules.NewBoardState(size, size)
editor = maps.NewBoardStateEditor(state)
require.Empty(t, state.Hazards)
err = m.SetupBoard(state, settings, editor)
require.NoError(t, err)
require.NotEmpty(t, state.Hazards)
}
}
func TestSpiralHazardsMap(t *testing.T) {
// check error handling
m := maps.SpiralHazardsMap{}
settings := rules.Settings{}
settings = settings.WithSeed(10)
state := rules.NewBoardState(11, 11)
editor := maps.NewBoardStateEditor(state)
err := m.SetupBoard(state, settings, editor)
require.NoError(t, err)
for i := 0; i < 1000; i++ {
state.Turn = i
err = m.UpdateBoard(state, settings, editor)
require.NoError(t, err)
}
require.NotEmpty(t, state.Hazards)
require.Equal(t, 11*11, len(state.Hazards), "hazards should eventually fille the entire map")
}
func TestScatterFillMap(t *testing.T) {
// check error handling
m := maps.ScatterFillMap{}
settings := rules.Settings{}
settings = settings.WithSeed(10)
state := rules.NewBoardState(11, 11)
editor := maps.NewBoardStateEditor(state)
err := m.SetupBoard(state, settings, editor)
require.NoError(t, err)
totalTurns := 11 * 11 * 2
for i := 0; i < totalTurns; i++ {
state.Turn = i
err = m.UpdateBoard(state, settings, editor)
require.NoError(t, err)
}
require.NotEmpty(t, state.Hazards)
require.Equal(t, 11*11, len(state.Hazards), "hazards should eventually fill the entire map")
}
func TestDirectionalExpandingBoxMap(t *testing.T) {
// check error handling
m := maps.DirectionalExpandingBoxMap{}
settings := rules.Settings{}
settings = settings.WithSeed(2)
state := rules.NewBoardState(11, 11)
editor := maps.NewBoardStateEditor(state)
err := m.SetupBoard(state, settings, editor)
require.NoError(t, err)
totalTurns := 1000
for i := 0; i < totalTurns; i++ {
state.Turn = i
err = m.UpdateBoard(state, settings, editor)
require.NoError(t, err)
}
require.NotEmpty(t, state.Hazards)
require.Equal(t, 11*11, len(state.Hazards), "hazards should eventually fill the entire map")
}
func TestExpandingBoxMap(t *testing.T) {
// check error handling
m := maps.ExpandingBoxMap{}
settings := rules.Settings{}
settings = settings.WithSeed(2)
state := rules.NewBoardState(11, 11)
editor := maps.NewBoardStateEditor(state)
err := m.SetupBoard(state, settings, editor)
require.NoError(t, err)
totalTurns := 1000
for i := 0; i < totalTurns; i++ {
state.Turn = i
err = m.UpdateBoard(state, settings, editor)
require.NoError(t, err)
}
require.NotEmpty(t, state.Hazards)
require.Equal(t, 11*11, len(state.Hazards), "hazards should eventually fill the entire map")
}
func TestExpandingScatterMap(t *testing.T) {
// check error handling
m := maps.ExpandingScatterMap{}
settings := rules.Settings{}
settings = settings.WithSeed(2)
state := rules.NewBoardState(11, 11)
editor := maps.NewBoardStateEditor(state)
err := m.SetupBoard(state, settings, editor)
require.NoError(t, err)
totalTurns := 1000
for i := 0; i < totalTurns; i++ {
state.Turn = i
err = m.UpdateBoard(state, settings, editor)
require.NoError(t, err)
}
require.NotEmpty(t, state.Hazards)
require.Equal(t, 11*11, len(state.Hazards), "hazards should eventually fill the entire map")
}

View file

@ -1,6 +1,8 @@
package maps
import "github.com/BattlesnakeOfficial/rules"
import (
"github.com/BattlesnakeOfficial/rules"
)
// SetupBoard is a shortcut for looking up a map by ID and initializing a new board state with it.
func SetupBoard(mapID string, settings rules.Settings, width, height int, snakeIDs []string) (*rules.BoardState, error) {
@ -87,3 +89,91 @@ func (m StubMap) UpdateBoard(previousBoardState *rules.BoardState, settings rule
}
return nil
}
// drawRing draws a ring of hazard points offset from the outer edge of the board
func drawRing(bw, bh, hOffset, vOffset int) ([]rules.Point, error) {
if bw < 1 {
return nil, rules.RulesetError("board width too small")
}
if bh < 1 {
return nil, rules.RulesetError("board height too small")
}
if hOffset >= bw-1 {
return nil, rules.RulesetError("horizontal offset too large")
}
if vOffset >= bh-1 {
return nil, rules.RulesetError("vertical offset too large")
}
if hOffset < 1 {
return nil, rules.RulesetError("horizontal offset too small")
}
if vOffset < 1 {
return nil, rules.RulesetError("vertical offset too small")
}
// calculate the start/end point of the horizontal borders
xStart := hOffset - 1
xEnd := bw - hOffset
// calculate start/end point of the vertical borders
yStart := vOffset - 1
yEnd := bh - vOffset
// we can pre-determine how many points will be in the ring and allocate a slice of exactly that size
numPoints := 2 * (xEnd - xStart + 1) // horizontal hazard points
// Add vertical walls, if there are any.
// Sometimes there are no vertical walls when the ring height is only 2.
// In that case, the vertical walls are handled by the horizontal walls
if yEnd >= yStart {
numPoints += 2*(yEnd-yStart+1) - 4
}
hazards := make([]rules.Point, 0, numPoints)
// draw horizontal walls
for x := xStart; x <= xEnd; x++ {
hazards = append(hazards,
rules.Point{X: x, Y: yStart},
rules.Point{X: x, Y: yEnd},
)
}
// draw vertical walls, but don't include corners that the horizontal walls already included
for y := yStart + 1; y <= yEnd-1; y++ {
hazards = append(hazards,
rules.Point{X: xStart, Y: y},
rules.Point{X: xEnd, Y: y},
)
}
return hazards, nil
}
func maxInt(n1 int, n ...int) int {
max := n1
for _, v := range n {
if v > max {
max = v
}
}
return max
}
func isOnBoard(w, h, x, y int) bool {
if x >= w || x < 0 {
return false
}
if y >= h || y < 0 {
return false
}
return true
}

View file

@ -0,0 +1,104 @@
package maps
import (
"testing"
"github.com/BattlesnakeOfficial/rules"
"github.com/stretchr/testify/require"
)
func TestMaxInt(t *testing.T) {
// simple case
n := maxInt(0, 1, 2, 3)
require.Equal(t, 3, n)
// use negative, out of order
n = maxInt(0, -1, 200, 3)
require.Equal(t, 200, n)
// use only 1 value, and negative
n = maxInt(-99)
require.Equal(t, -99, n)
// use duplicate values
n = maxInt(3, 3, 3)
require.Equal(t, 3, n)
// use duplicate and other values
n = maxInt(-1, 3, 5, 3, 3, 2)
require.Equal(t, 5, n)
}
func TestIsOnBoard(t *testing.T) {
// a few spot checks
require.True(t, isOnBoard(11, 11, 0, 0))
require.False(t, isOnBoard(11, 11, -1, 0))
require.True(t, isOnBoard(11, 11, 10, 10))
require.False(t, isOnBoard(11, 11, 11, 11))
require.True(t, isOnBoard(2, 2, 1, 1))
// exhaustive check on a small, non-square board
for x := 0; x < 4; x++ {
for y := 0; y < 9; y++ {
require.True(t, isOnBoard(4, 9, x, y))
}
}
}
func TestDrawRing(t *testing.T) {
_, err := drawRing(0, 11, 2, 2)
require.Equal(t, "board width too small", err.Error())
_, err = drawRing(11, 0, 2, 2)
require.Equal(t, "board height too small", err.Error())
_, err = drawRing(11, 11, 10, 2)
require.Equal(t, "horizontal offset too large", err.Error())
_, err = drawRing(11, 11, 2, 10)
require.Equal(t, "vertical offset too large", err.Error())
_, err = drawRing(11, 11, 0, 2)
require.Equal(t, "horizontal offset too small", err.Error())
_, err = drawRing(11, 11, 2, 0)
require.Equal(t, "vertical offset too small", err.Error())
_, err = drawRing(19, 1, 4, 4)
require.Equal(t, "vertical offset too large", err.Error())
_, err = drawRing(19, 1, 6, 6)
require.Equal(t, "vertical offset too large", err.Error())
_, err = drawRing(14, 7, 6, 6)
require.Equal(t, "vertical offset too large", err.Error())
_, err = drawRing(18, 10, 8, 8)
require.NoError(t, err)
ring, err := drawRing(11, 11, 2, 2)
require.NoError(t, err)
// ring should not be empty
require.NotEmpty(t, ring)
// should have exactly 32 points in this ring
require.Len(t, ring, 32)
// ensure no duplicates
seen := map[rules.Point]struct{}{}
for _, p := range ring {
// _, ok := seen[p]
require.NotContains(t, seen, p)
seen[p] = struct{}{}
}
// spot check a few known points
require.Contains(t, seen, rules.Point{X: 1, Y: 1}, "bottom left")
require.Contains(t, seen, rules.Point{X: 1, Y: 9}, "top left")
require.Contains(t, seen, rules.Point{X: 9, Y: 1}, "bottom right")
require.Contains(t, seen, rules.Point{X: 9, Y: 9}, "top right")
require.Contains(t, seen, rules.Point{X: 1, Y: 5})
require.Contains(t, seen, rules.Point{X: 6, Y: 1})
require.Contains(t, seen, rules.Point{X: 8, Y: 9})
}

View file

@ -1,32 +1,33 @@
package maps
package maps_test
import (
"errors"
"testing"
"github.com/BattlesnakeOfficial/rules"
"github.com/BattlesnakeOfficial/rules/maps"
"github.com/stretchr/testify/require"
)
func TestSetupBoard_NotFound(t *testing.T) {
_, err := SetupBoard("does_not_exist", rules.Settings{}, 10, 10, []string{})
_, err := maps.SetupBoard("does_not_exist", rules.Settings{}, 10, 10, []string{})
require.EqualError(t, err, rules.ErrorMapNotFound.Error())
}
func TestSetupBoard_Error(t *testing.T) {
testMap := StubMap{
testMap := maps.StubMap{
Id: t.Name(),
Error: errors.New("bad map update"),
}
TestMap(testMap.ID(), testMap, func() {
_, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{})
maps.TestMap(testMap.ID(), testMap, func() {
_, err := maps.SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{})
require.EqualError(t, err, "bad map update")
})
}
func TestSetupBoard(t *testing.T) {
testMap := StubMap{
testMap := maps.StubMap{
Id: t.Name(),
SnakePositions: map[string]rules.Point{
"1": {X: 3, Y: 4},
@ -42,8 +43,8 @@ func TestSetupBoard(t *testing.T) {
},
}
TestMap(testMap.ID(), testMap, func() {
boardState, err := SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{"1", "2"})
maps.TestMap(testMap.ID(), testMap, func() {
boardState, err := maps.SetupBoard(testMap.ID(), rules.Settings{}, 10, 10, []string{"1", "2"})
require.NoError(t, err)
@ -65,7 +66,7 @@ func TestSetupBoard(t *testing.T) {
}
func TestUpdateBoard(t *testing.T) {
testMap := StubMap{
testMap := maps.StubMap{
Id: t.Name(),
SnakePositions: map[string]rules.Point{
"1": {X: 3, Y: 4},
@ -98,8 +99,8 @@ func TestUpdateBoard(t *testing.T) {
},
}
TestMap(testMap.ID(), testMap, func() {
boardState, err := UpdateBoard(testMap.ID(), previousBoardState, rules.Settings{})
maps.TestMap(testMap.ID(), testMap, func() {
boardState, err := maps.UpdateBoard(testMap.ID(), previousBoardState, rules.Settings{})
require.NoError(t, err)

View file

@ -1,19 +1,20 @@
package maps
package maps_test
import (
"fmt"
"testing"
"github.com/BattlesnakeOfficial/rules"
"github.com/BattlesnakeOfficial/rules/maps"
"github.com/stretchr/testify/require"
)
func TestStandardMapInterface(t *testing.T) {
var _ GameMap = StandardMap{}
var _ maps.GameMap = maps.StandardMap{}
}
func TestStandardMapSetupBoard(t *testing.T) {
m := StandardMap{}
m := maps.StandardMap{}
settings := rules.Settings{}
tests := []struct {
@ -143,7 +144,7 @@ func TestStandardMapSetupBoard(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
nextBoardState := rules.NewBoardState(test.initialBoardState.Width, test.initialBoardState.Height)
editor := NewBoardStateEditor(nextBoardState)
editor := maps.NewBoardStateEditor(nextBoardState)
settings := settings.WithRand(test.rand)
err := m.SetupBoard(test.initialBoardState, settings, editor)
@ -158,7 +159,7 @@ func TestStandardMapSetupBoard(t *testing.T) {
}
func TestStandardMapUpdateBoard(t *testing.T) {
m := StandardMap{}
m := maps.StandardMap{}
tests := []struct {
name string
@ -303,7 +304,7 @@ func TestStandardMapUpdateBoard(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
nextBoardState := test.initialBoardState.Clone()
settings := test.settings.WithRand(test.rand)
editor := NewBoardStateEditor(nextBoardState)
editor := maps.NewBoardStateEditor(nextBoardState)
err := m.UpdateBoard(test.initialBoardState.Clone(), settings, editor)

20
rand.go
View file

@ -4,6 +4,10 @@ import "math/rand"
type Rand interface {
Intn(n int) int
// Range produces a random integer in the range of [min,max] (inclusive)
// For example, Range(1,3) could produce the values 1, 2 or 3.
// Panics if max < min (like how Intn(n) panics for n <=0)
Range(min, max int) int
Shuffle(n int, swap func(i, j int))
}
@ -12,6 +16,10 @@ var GlobalRand globalRand
type globalRand struct{}
func (globalRand) Range(min, max int) int {
return rand.Intn(max-min+1) + min
}
func (globalRand) Intn(n int) int {
return rand.Intn(n)
}
@ -36,6 +44,10 @@ func (s seedRand) Intn(n int) int {
return s.rand.Intn(n)
}
func (s seedRand) Range(min, max int) int {
return s.rand.Intn(max-min+1) + min
}
func (s seedRand) Shuffle(n int, swap func(i, j int)) {
s.rand.Shuffle(n, swap)
}
@ -51,6 +63,10 @@ func (minRand) Intn(n int) int {
return 0
}
func (minRand) Range(min, max int) int {
return min
}
func (minRand) Shuffle(n int, swap func(i, j int)) {
// no shuffling
}
@ -64,6 +80,10 @@ func (maxRand) Intn(n int) int {
return n - 1
}
func (maxRand) Range(min, max int) int {
return max
}
func (maxRand) Shuffle(n int, swap func(i, j int)) {
// rotate by one element so every element is moved
if n < 2 {