Byte-snake-engine/royale_test.go

217 lines
6.6 KiB
Go
Raw Normal View History

2020-07-25 17:37:41 -07:00
package rules
import (
"errors"
"fmt"
"math/rand"
2020-07-25 17:37:41 -07:00
"testing"
"github.com/stretchr/testify/require"
)
func getRoyaleRuleset(hazardDamagePerTurn, shrinkEveryNTurns int) Ruleset {
settings := NewSettingsWithParams(
ParamHazardDamagePerTurn, fmt.Sprint(hazardDamagePerTurn),
ParamShrinkEveryNTurns, fmt.Sprint(shrinkEveryNTurns),
)
return NewRulesetBuilder().WithSettings(settings).NamedRuleset(GameTypeRoyale)
2020-07-25 17:37:41 -07:00
}
func TestRoyaleDefaultSanity(t *testing.T) {
boardState := &BoardState{
Snakes: []Snake{
{ID: "1", Body: []Point{{X: 0, Y: 0}}},
{ID: "2", Body: []Point{{X: 0, Y: 1}}},
},
}
r := getRoyaleRuleset(1, 0)
_, _, err := r.Execute(boardState, []SnakeMove{{"1", "right"}, {"2", "right"}})
2020-07-25 17:37:41 -07:00
require.Error(t, err)
require.Equal(t, errors.New("royale game can't shrink more frequently than every turn"), err)
2020-07-25 17:37:41 -07:00
r = getRoyaleRuleset(1, 1)
_, boardState, err = r.Execute(boardState, []SnakeMove{})
2020-07-25 17:37:41 -07:00
require.NoError(t, err)
require.Len(t, boardState.Hazards, 0)
2020-07-25 17:37:41 -07:00
}
func TestRoyaleName(t *testing.T) {
r := getRoyaleRuleset(0, 0)
require.Equal(t, "royale", r.Name())
}
func TestRoyaleHazards(t *testing.T) {
2020-07-29 13:14:42 -07:00
seed := int64(25543234525)
2020-07-25 17:37:41 -07:00
tests := []struct {
Width int
Height int
Turn int
ShrinkEveryNTurns int
Error error
ExpectedHazards []Point
2020-07-25 17:37:41 -07:00
}{
{Error: errors.New("royale game can't shrink more frequently than every turn")},
{ShrinkEveryNTurns: 1, ExpectedHazards: []Point{}},
{Turn: 1, ShrinkEveryNTurns: 1, ExpectedHazards: []Point{}},
{Width: 3, Height: 3, Turn: 1, ShrinkEveryNTurns: 10, ExpectedHazards: []Point{}},
{Width: 3, Height: 3, Turn: 9, ShrinkEveryNTurns: 10, ExpectedHazards: []Point{}},
2020-07-25 17:37:41 -07:00
{
Width: 3, Height: 3, Turn: 10, ShrinkEveryNTurns: 10,
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}},
2020-07-25 17:37:41 -07:00
},
{
Width: 3, Height: 3, Turn: 11, ShrinkEveryNTurns: 10,
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}},
2020-07-25 17:37:41 -07:00
},
{
Width: 3, Height: 3, Turn: 19, ShrinkEveryNTurns: 10,
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}},
2020-07-25 17:37:41 -07:00
},
{
Width: 3, Height: 3, Turn: 20, ShrinkEveryNTurns: 10,
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 2}, {X: 2, Y: 2}},
},
{
Width: 3, Height: 3, Turn: 31, ShrinkEveryNTurns: 10,
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 1}, {X: 2, Y: 2}},
},
{
Width: 3, Height: 3, Turn: 42, ShrinkEveryNTurns: 10,
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}},
},
{
Width: 3, Height: 3, Turn: 53, ShrinkEveryNTurns: 10,
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}},
},
{
Width: 3, Height: 3, Turn: 64, ShrinkEveryNTurns: 10,
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}},
},
{
Width: 3, Height: 3, Turn: 6987, ShrinkEveryNTurns: 10,
ExpectedHazards: []Point{{X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 2}, {X: 2, Y: 0}, {X: 2, Y: 1}, {X: 2, Y: 2}},
2020-07-25 17:37:41 -07:00
},
}
for _, test := range tests {
b := &BoardState{
Turn: test.Turn - 1,
Width: test.Width,
Height: test.Height,
}
settings := NewSettingsWithParams(
ParamHazardDamagePerTurn, "1",
ParamShrinkEveryNTurns, fmt.Sprint(test.ShrinkEveryNTurns),
).WithSeed(seed)
2020-07-25 17:37:41 -07:00
_, err := PopulateHazardsRoyale(b, settings, mockSnakeMoves())
2020-07-25 17:37:41 -07:00
require.Equal(t, test.Error, err)
if err == nil {
// Obstacles should match
require.Equal(t, test.ExpectedHazards, b.Hazards)
for _, expectedP := range test.ExpectedHazards {
2020-07-25 17:37:41 -07:00
wasFound := false
for _, actualP := range b.Hazards {
2020-07-25 17:37:41 -07:00
if expectedP == actualP {
wasFound = true
break
}
}
require.True(t, wasFound)
}
}
}
}
// Checks that hazards get placed
// also that:
// - snakes move properly
// - snake gets health from eating
// - food gets consumed
// - health is decreased
var royaleCaseHazardsPlaced = gameTestCase{
"Royale Case Hazards Placed",
&BoardState{
Width: 10,
Height: 10,
Snakes: []Snake{
{
ID: "one",
Body: []Point{{X: 1, Y: 1}, {X: 1, Y: 2}},
Health: 100,
},
{
ID: "two",
Body: []Point{{X: 3, Y: 4}, {X: 3, Y: 3}},
Health: 100,
},
{
ID: "three",
Body: []Point{},
Health: 100,
EliminatedCause: EliminatedByOutOfBounds,
},
},
Food: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}},
Hazards: []Point{},
},
[]SnakeMove{
{ID: "one", Move: MoveDown},
{ID: "two", Move: MoveUp},
{ID: "three", Move: MoveLeft}, // Should be ignored
},
nil,
&BoardState{
Width: 10,
Height: 10,
Snakes: []Snake{
{
ID: "one",
Body: []Point{{X: 1, Y: 0}, {X: 1, Y: 1}, {X: 1, Y: 1}},
Health: 100,
},
{
ID: "two",
Body: []Point{{X: 3, Y: 5}, {X: 3, Y: 4}},
Health: 99,
},
{
ID: "three",
Body: []Point{},
Health: 100,
EliminatedCause: EliminatedByOutOfBounds,
},
},
Food: []Point{{X: 0, Y: 0}},
Hazards: []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}, {X: 3, Y: 0}, {X: 4, Y: 0}, {X: 5, Y: 0}, {X: 6, Y: 0}, {X: 7, Y: 0}, {X: 8, Y: 0}, {X: 9, Y: 0}},
},
}
func TestRoyaleCreateNextBoardState(t *testing.T) {
// add expected hazards to the standard cases that need them
s1 := standardCaseMoveEatAndGrow.clone()
s1.expectedState.Hazards = []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}, {X: 3, Y: 0}, {X: 4, Y: 0}, {X: 5, Y: 0}, {X: 6, Y: 0}, {X: 7, Y: 0}, {X: 8, Y: 0}, {X: 9, Y: 0}}
s2 := standardMoveAndCollideMAD.clone()
s2.expectedState.Hazards = []Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 2, Y: 0}, {X: 3, Y: 0}, {X: 4, Y: 0}, {X: 5, Y: 0}, {X: 6, Y: 0}, {X: 7, Y: 0}, {X: 8, Y: 0}, {X: 9, Y: 0}}
cases := []gameTestCase{
// inherits these test cases from standard
standardCaseErrNoMoveFound,
standardCaseErrZeroLengthSnake,
*s1,
*s2,
royaleCaseHazardsPlaced,
}
DEV-1096 - add a new "pipeline" concept (#67) * add a new "pipeline" concept - added new Pipeline type which is a series of stages - added a global registry to facilitate plugin architecture - 100% test coverage * Refactor rulesets to provide and use Pipeline * fix copypasta comments * fix lint for unused method * include game over stages in ruleset pipelines * clean up unused private standard methods * remove unused private methods in squad ruleset * remove unused private methods in royale ruleset * refactor: pipeline clone + return next board state * YAGNI: remove unused Append * refactor: improve stage names * add no-op behavior to stages for initial state * refactor: no-op decision within stage functions * remove misleading comment that isn't true * dont bother checking for init in gameover stages * remove redundant test * refactor: provide a combined ruleset/pipeline type * fix: movement no-op for GameOver check IsGameOver needs to run pipeline, move snakes needs to no-op for that * add test coverage * refactor: improve stage names and use constants * add Error method Support error checking before calling Execute() * update naming to be American style * panic when overwriting stages in global registry * rename "Error" method and improve docs * use testify lib for panic assertion * remove redundant food stage * use ruleset-specific logic for game over checks * re-work Pipeline errors * rework errors again * add defensive check for zero length snake * use old logic which checks current state, not next * add warning about how PipelineRuleset checks for game over
2022-04-19 15:52:57 -07:00
rb := NewRulesetBuilder().WithParams(map[string]string{
ParamHazardDamagePerTurn: "1",
ParamShrinkEveryNTurns: "1",
}).WithSeed(1234)
for _, gc := range cases {
rand.Seed(1234)
// test a RulesBuilder constructed instance
gc.requireValidNextState(t, rb.NamedRuleset(GameTypeRoyale))
DEV-1096 - add a new "pipeline" concept (#67) * add a new "pipeline" concept - added new Pipeline type which is a series of stages - added a global registry to facilitate plugin architecture - 100% test coverage * Refactor rulesets to provide and use Pipeline * fix copypasta comments * fix lint for unused method * include game over stages in ruleset pipelines * clean up unused private standard methods * remove unused private methods in squad ruleset * remove unused private methods in royale ruleset * refactor: pipeline clone + return next board state * YAGNI: remove unused Append * refactor: improve stage names * add no-op behavior to stages for initial state * refactor: no-op decision within stage functions * remove misleading comment that isn't true * dont bother checking for init in gameover stages * remove redundant test * refactor: provide a combined ruleset/pipeline type * fix: movement no-op for GameOver check IsGameOver needs to run pipeline, move snakes needs to no-op for that * add test coverage * refactor: improve stage names and use constants * add Error method Support error checking before calling Execute() * update naming to be American style * panic when overwriting stages in global registry * rename "Error" method and improve docs * use testify lib for panic assertion * remove redundant food stage * use ruleset-specific logic for game over checks * re-work Pipeline errors * rework errors again * add defensive check for zero length snake * use old logic which checks current state, not next * add warning about how PipelineRuleset checks for game over
2022-04-19 15:52:57 -07:00
// also test a pipeline with the same settings
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeRoyale, NewPipeline(royaleRulesetStages...)))
}
}