DEV-1761: New rules API (#118)

* DEV-1761: Clean up Ruleset interface (#115)

* remove legacy ruleset types and simplify ruleset interface

* remove unnecessary settings argument from Ruleset interface

* decouple rules.Settings from client API and store settings as strings

* DEV 1761: Add new BoardState and Point fields (#117)

* add Point.TTL, Point.Value, GameState and PointState to BoardState

* allow maps to access BoardState.GameState,PointState

* add PreUpdateBoard and refactor snail_mode with it

* fix bug where an extra turn was printed to the console

* fix formatting

* fix lint errors

Co-authored-by: JonathanArns <jonathan.arns@googlemail.com>
This commit is contained in:
Rob O'Dwyer 2022-10-28 16:49:49 -07:00 committed by GitHub
parent 639362ef46
commit 82e1999126
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1349 additions and 1610 deletions

View file

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

View file

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

View file

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

View file

@ -28,95 +28,53 @@ 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{
{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{},
Hazards: []rules.Point{},
},
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},
{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{},
Hazards: []rules.Point{},
},
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},
{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,
},
}
@ -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)
}

View file

@ -24,8 +24,21 @@ type GameMap interface {
// Called to generate a new board. The map is responsible for placing all snakes, food, and hazards.
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.

View file

@ -135,18 +135,16 @@ func TestBoardStateEditor(t *testing.T) {
editor.PlaceSnake("existing_snake", []rules.Point{{X: 5, Y: 2}, {X: 5, Y: 1}, {X: 5, Y: 0}}, 99)
editor.PlaceSnake("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},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,8 +40,8 @@ func TestHealingPoolsMap(t *testing.T) {
t.Run(fmt.Sprintf("%dx%d", tc.boardSize, tc.boardSize), func(t *testing.T) {
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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -176,7 +176,11 @@ func (m SoloMazeMap) PlaceFood(boardState *rules.BoardState, settings rules.Sett
}
}
func (m SoloMazeMap) UpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
func (m SoloMazeMap) PreUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
return nil
}
func (m SoloMazeMap) PostUpdateBoard(lastBoardState *rules.BoardState, settings rules.Settings, editor Editor) error {
currentLevel, e := m.ReadBitState(lastBoardState)
if e != nil {
return e

View file

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

View file

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