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
This commit is contained in:
parent
86ef6ad068
commit
d378759d58
18 changed files with 723 additions and 235 deletions
2
board.go
2
board.go
|
|
@ -25,7 +25,7 @@ func NewBoardState(width, height int32) *BoardState {
|
|||
}
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of prevState that can be safely modified inside Ruleset.CreateNextBoardState
|
||||
// Clone returns a deep copy of prevState that can be safely modified without affecting the original
|
||||
func (prevState *BoardState) Clone() *BoardState {
|
||||
nextState := &BoardState{
|
||||
Turn: prevState.Turn,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ func (gc *gameTestCase) clone() *gameTestCase {
|
|||
|
||||
// requireValidNextState requires that the ruleset produces a valid next state
|
||||
func (gc *gameTestCase) requireValidNextState(t *testing.T, r Ruleset) {
|
||||
t.Helper()
|
||||
t.Run(gc.name, func(t *testing.T) {
|
||||
t.Helper()
|
||||
prev := gc.prevState.Clone() // clone to protect against mutation (so we can ru-use test cases)
|
||||
nextState, err := r.CreateNextBoardState(prev, gc.moves)
|
||||
require.Equal(t, gc.expectedError, err)
|
||||
|
|
@ -39,3 +41,9 @@ func (gc *gameTestCase) requireValidNextState(t *testing.T, r Ruleset) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func mockSnakeMoves() []SnakeMove {
|
||||
return []SnakeMove{
|
||||
{ID: "test-mock-move", Move: "mocked"},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,39 @@
|
|||
package rules
|
||||
|
||||
var constrictorRulesetStages = []string{
|
||||
StageMovementStandard,
|
||||
StageStarvationStandard,
|
||||
StageHazardDamageStandard,
|
||||
StageFeedSnakesStandard,
|
||||
StageEliminationStandard,
|
||||
StageSpawnFoodNoFood,
|
||||
StageModifySnakesAlwaysGrow,
|
||||
StageGameOverStandard,
|
||||
}
|
||||
|
||||
type ConstrictorRuleset struct {
|
||||
StandardRuleset
|
||||
}
|
||||
|
||||
func (r *ConstrictorRuleset) Name() string { return GameTypeConstrictor }
|
||||
|
||||
func (r ConstrictorRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
||||
return NewPipeline(constrictorRulesetStages...).Execute(bs, s, sm)
|
||||
}
|
||||
|
||||
func (r *ConstrictorRuleset) ModifyInitialBoardState(initialBoardState *BoardState) (*BoardState, error) {
|
||||
initialBoardState, err := r.StandardRuleset.ModifyInitialBoardState(initialBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.removeFood(initialBoardState)
|
||||
|
||||
err = r.applyConstrictorRules(initialBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return initialBoardState, nil
|
||||
_, nextState, err := r.Execute(initialBoardState, r.Settings(), nil)
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
nextState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
r.removeFood(nextState)
|
||||
|
||||
err = r.applyConstrictorRules(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nextState, nil
|
||||
}
|
||||
|
||||
func (r *ConstrictorRuleset) removeFood(b *BoardState) {
|
||||
_, _ = r.callStageFunc(RemoveFoodConstrictor, b, []SnakeMove{})
|
||||
func (r *ConstrictorRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
return GameOverStandard(b, r.Settings(), nil)
|
||||
}
|
||||
|
||||
func RemoveFoodConstrictor(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
|
|
@ -49,14 +43,12 @@ func RemoveFoodConstrictor(b *BoardState, settings Settings, moves []SnakeMove)
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (r *ConstrictorRuleset) applyConstrictorRules(b *BoardState) error {
|
||||
_, err := r.callStageFunc(GrowSnakesConstrictor, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
|
||||
func GrowSnakesConstrictor(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
// Set all snakes to max health and ensure they grow next turn
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
if len(b.Snakes[i].Body) <= 0 {
|
||||
return false, ErrorZeroLengthSnake
|
||||
}
|
||||
b.Snakes[i].Health = SnakeMaxHealth
|
||||
|
||||
tail := b.Snakes[i].Body[len(b.Snakes[i].Body)-1]
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ func TestConstrictorModifyInitialBoardState(t *testing.T) {
|
|||
{11, 11, []string{}},
|
||||
{11, 11, []string{"one", "two", "three", "four", "five"}},
|
||||
}
|
||||
|
||||
r := ConstrictorRuleset{}
|
||||
for testNum, test := range tests {
|
||||
state, err := CreateDefaultBoardState(test.Width, test.Height, test.IDs)
|
||||
|
|
@ -104,8 +103,15 @@ func TestConstrictorCreateNextBoardState(t *testing.T) {
|
|||
standardCaseErrZeroLengthSnake,
|
||||
constrictorMoveAndCollideMAD,
|
||||
}
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeConstrictor,
|
||||
})
|
||||
r := ConstrictorRuleset{}
|
||||
for _, gc := range cases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeConstrictor, NewPipeline(constrictorRulesetStages...)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
190
pipeline.go
Normal file
190
pipeline.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
package rules
|
||||
|
||||
import "fmt"
|
||||
|
||||
// StageRegistry is a mapping of stage names to stage functions
|
||||
type StageRegistry map[string]StageFunc
|
||||
|
||||
const (
|
||||
StageSpawnFoodStandard = "spawn_food.standard"
|
||||
StageGameOverStandard = "game_over.standard"
|
||||
StageStarvationStandard = "starvation.standard"
|
||||
StageFeedSnakesStandard = "feed_snakes.standard"
|
||||
StageMovementStandard = "movement.standard"
|
||||
StageHazardDamageStandard = "hazard_damage.standard"
|
||||
StageEliminationStandard = "elimination.standard"
|
||||
|
||||
StageGameOverSoloSnake = "game_over.solo_snake"
|
||||
StageGameOverBySquad = "game_over.by_squad"
|
||||
StageSpawnFoodNoFood = "spawn_food.no_food"
|
||||
StageSpawnHazardsShrinkMap = "spawn_hazards.shrink_map"
|
||||
StageEliminationResurrectSquadCollisions = "elimination.resurrect_squad_collisions"
|
||||
StageModifySnakesAlwaysGrow = "modify_snakes.always_grow"
|
||||
StageMovementWrapBoundaries = "movement.wrap_boundaries"
|
||||
StageModifySnakesShareAttributes = "modify_snakes.share_attributes"
|
||||
)
|
||||
|
||||
// globalRegistry is a global, default mapping of stage names to stage functions.
|
||||
// It can be extended by plugins through the use of registration functions.
|
||||
// Plugins that wish to extend the available game stages should call RegisterPipelineStageError
|
||||
// to add additional stages.
|
||||
var globalRegistry = StageRegistry{
|
||||
StageSpawnFoodNoFood: RemoveFoodConstrictor,
|
||||
StageSpawnFoodStandard: SpawnFoodStandard,
|
||||
StageGameOverSoloSnake: GameOverSolo,
|
||||
StageGameOverBySquad: GameOverSquad,
|
||||
StageGameOverStandard: GameOverStandard,
|
||||
StageHazardDamageStandard: DamageHazardsStandard,
|
||||
StageSpawnHazardsShrinkMap: PopulateHazardsRoyale,
|
||||
StageStarvationStandard: ReduceSnakeHealthStandard,
|
||||
StageEliminationResurrectSquadCollisions: ResurrectSnakesSquad,
|
||||
StageFeedSnakesStandard: FeedSnakesStandard,
|
||||
StageEliminationStandard: EliminateSnakesStandard,
|
||||
StageModifySnakesAlwaysGrow: GrowSnakesConstrictor,
|
||||
StageMovementStandard: MoveSnakesStandard,
|
||||
StageMovementWrapBoundaries: MoveSnakesWrapped,
|
||||
StageModifySnakesShareAttributes: ShareAttributesSquad,
|
||||
}
|
||||
|
||||
// RegisterPipelineStage adds a stage to the registry.
|
||||
// If a stage has already been mapped it will be overwritten by the newly
|
||||
// registered function.
|
||||
func (sr StageRegistry) RegisterPipelineStage(s string, fn StageFunc) {
|
||||
sr[s] = fn
|
||||
}
|
||||
|
||||
// RegisterPipelineStageError adds a stage to the registry.
|
||||
// If a stage has already been mapped an error will be returned.
|
||||
func (sr StageRegistry) RegisterPipelineStageError(s string, fn StageFunc) error {
|
||||
if _, ok := sr[s]; ok {
|
||||
return RulesetError(fmt.Sprintf("stage '%s' has already been registered", s))
|
||||
}
|
||||
|
||||
sr.RegisterPipelineStage(s, fn)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterPipelineStage adds a stage to the global stage registry.
|
||||
// It will panic if the a stage has already been registered with the same name.
|
||||
func RegisterPipelineStage(s string, fn StageFunc) {
|
||||
err := globalRegistry.RegisterPipelineStageError(s, fn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline is an ordered sequences of game stages which are executed to produce the
|
||||
// next game state.
|
||||
//
|
||||
// If a stage produces an error or an ended game state, the pipeline is halted at that stage.
|
||||
type Pipeline interface {
|
||||
// Execute runs the pipeline stages and produces a next game state.
|
||||
//
|
||||
// If any stage produces an error or an ended game state, the pipeline
|
||||
// immediately stops at that stage.
|
||||
//
|
||||
// Errors should be checked and the other results ignored if error is non-nil.
|
||||
//
|
||||
// If the pipeline is already in an error state (this can be checked by calling Err()),
|
||||
// this error will be immediately returned and the pipeline will not run.
|
||||
//
|
||||
// After the pipeline runs, the results will be the result of the last stage that was executed.
|
||||
Execute(*BoardState, Settings, []SnakeMove) (bool, *BoardState, error)
|
||||
// Err provides a way to check for errors before/without calling Execute.
|
||||
// Err returns an error if the Pipeline is in an error state.
|
||||
// If this error is not nil, this error will also be returned from Execute, so it is
|
||||
// optional to call Err.
|
||||
// The idea is to reduce error-checking verbosity for the majority of cases where a
|
||||
// Pipeline is immediately executed after construction (i.e. NewPipeline(...).Execute(...)).
|
||||
Err() error
|
||||
}
|
||||
|
||||
// pipeline is an implementation of Pipeline
|
||||
type pipeline struct {
|
||||
// stages is a list of stages that should be executed from slice start to end
|
||||
stages []StageFunc
|
||||
// if the pipeline has an error
|
||||
err error
|
||||
}
|
||||
|
||||
// NewPipeline constructs an instance of Pipeline using the global registry.
|
||||
// It is a convenience wrapper for NewPipelineFromRegistry when you want
|
||||
// to use the default, global registry.
|
||||
func NewPipeline(stageNames ...string) Pipeline {
|
||||
return NewPipelineFromRegistry(globalRegistry, stageNames...)
|
||||
}
|
||||
|
||||
// NewPipelineFromRegistry constructs an instance of Pipeline, using the specified registry
|
||||
// of pipeline stage functions.
|
||||
//
|
||||
// The order of execution for the pipeline stages will correspond to the order that
|
||||
// the stage names are provided.
|
||||
//
|
||||
// Example:
|
||||
// NewPipelineFromRegistry(r, s, "stage1", "stage2")
|
||||
// ... will result in stage "stage1" running first, then stage "stage2" running after.
|
||||
//
|
||||
// An error will be returned if an unregistered stage name is used (a name that is not
|
||||
// mapped in the registry).
|
||||
func NewPipelineFromRegistry(registry map[string]StageFunc, stageNames ...string) Pipeline {
|
||||
// this can't be useful and probably indicates a problem
|
||||
if len(registry) == 0 {
|
||||
return &pipeline{err: ErrorEmptyRegistry}
|
||||
}
|
||||
|
||||
// this also can't be useful and probably indicates a problem
|
||||
if len(stageNames) == 0 {
|
||||
return &pipeline{err: ErrorNoStages}
|
||||
}
|
||||
|
||||
p := pipeline{}
|
||||
for _, s := range stageNames {
|
||||
fn, ok := registry[s]
|
||||
if !ok {
|
||||
return pipeline{err: ErrorStageNotFound}
|
||||
}
|
||||
|
||||
p.stages = append(p.stages, fn)
|
||||
}
|
||||
|
||||
return &p
|
||||
}
|
||||
|
||||
// impl
|
||||
func (p pipeline) Err() error {
|
||||
return p.err
|
||||
}
|
||||
|
||||
// impl
|
||||
func (p pipeline) Execute(state *BoardState, settings Settings, moves []SnakeMove) (bool, *BoardState, error) {
|
||||
// Design Detail
|
||||
//
|
||||
// If the pipeline is in an error state, Execute must return that error
|
||||
// because the pipeline is invalid and cannot execute.
|
||||
//
|
||||
// This is done for API use convenience to satisfy the common pattern
|
||||
// of wanting to write NewPipeline().Execute(...).
|
||||
//
|
||||
// This way you can do that without having to do 2 error checks.
|
||||
// It defers errors from construction to being checked on execution.
|
||||
if p.err != nil {
|
||||
return false, nil, p.err
|
||||
}
|
||||
|
||||
// Actually execute
|
||||
var ended bool
|
||||
var err error
|
||||
state = state.Clone()
|
||||
for _, fn := range p.stages {
|
||||
// execute current stage
|
||||
ended, err = fn(state, settings, moves)
|
||||
|
||||
// stop if we hit any errors or if the game is ended
|
||||
if err != nil || ended {
|
||||
return ended, state, err
|
||||
}
|
||||
}
|
||||
|
||||
// return the result of the last stage as the final pipeline result
|
||||
return ended, state, err
|
||||
}
|
||||
98
pipeline_internal_test.go
Normal file
98
pipeline_internal_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package rules
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPipelineRuleset(t *testing.T) {
|
||||
r := StageRegistry{
|
||||
"doesnt_end": mockStageFn(false, nil),
|
||||
"ends": mockStageFn(true, nil),
|
||||
}
|
||||
|
||||
// Name/Error methods
|
||||
p := NewPipelineFromRegistry(r, "404doesntexist")
|
||||
pr := pipelineRuleset{
|
||||
name: "test",
|
||||
pipeline: p,
|
||||
}
|
||||
require.Equal(t, "test", pr.Name())
|
||||
require.Equal(t, ErrorStageNotFound, pr.Err())
|
||||
|
||||
// test game over when it does end
|
||||
p = NewPipelineFromRegistry(r, "doesnt_end", "ends")
|
||||
pr = pipelineRuleset{
|
||||
name: "test",
|
||||
pipeline: p,
|
||||
}
|
||||
ended, err := pr.IsGameOver(&BoardState{})
|
||||
require.NoError(t, err)
|
||||
require.True(t, ended)
|
||||
|
||||
// Test game over when it doesn't end
|
||||
p = NewPipelineFromRegistry(r, "doesnt_end")
|
||||
pr = pipelineRuleset{
|
||||
name: "test",
|
||||
pipeline: p,
|
||||
}
|
||||
ended, err = pr.IsGameOver(&BoardState{})
|
||||
require.NoError(t, err)
|
||||
require.False(t, ended)
|
||||
|
||||
// test a stage that adds food, except on initialization
|
||||
r.RegisterPipelineStage("add_food", func(bs *BoardState, s Settings, sm []SnakeMove) (bool, error) {
|
||||
if IsInitialization(bs, s, sm) {
|
||||
return false, nil
|
||||
}
|
||||
bs.Food = append(bs.Food, Point{X: 0, Y: 0})
|
||||
return false, nil
|
||||
})
|
||||
b := &BoardState{}
|
||||
p = NewPipelineFromRegistry(r, "add_food")
|
||||
pr = pipelineRuleset{
|
||||
name: "test",
|
||||
pipeline: p,
|
||||
}
|
||||
require.Empty(t, b.Food)
|
||||
b, err = pr.ModifyInitialBoardState(b)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, b.Food, "food should not be added on initialisation phase")
|
||||
b, err = pr.CreateNextBoardState(b, mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, b.Food, "fodo should be added now")
|
||||
}
|
||||
|
||||
func TestPipelineGlobals(t *testing.T) {
|
||||
oldReg := globalRegistry
|
||||
globalRegistry = StageRegistry{}
|
||||
|
||||
// ensure that we can register a function without errors
|
||||
RegisterPipelineStage("test", mockStageFn(false, nil))
|
||||
require.Contains(t, globalRegistry, "test")
|
||||
|
||||
// ensure that the global registry panics if you register an existing stage name
|
||||
require.Panics(t, func() {
|
||||
RegisterPipelineStage("test", mockStageFn(false, nil))
|
||||
})
|
||||
RegisterPipelineStage("other", mockStageFn(true, nil)) // otherwise should not panic
|
||||
|
||||
// ensure that we can build a pipeline using the global registry
|
||||
p := NewPipeline("test", "other")
|
||||
require.NotNil(t, p)
|
||||
|
||||
// ensure that it runs okay too
|
||||
ended, next, err := p.Execute(&BoardState{}, Settings{}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, next)
|
||||
require.True(t, ended)
|
||||
|
||||
globalRegistry = oldReg
|
||||
}
|
||||
|
||||
func mockStageFn(ended bool, err error) StageFunc {
|
||||
return func(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
return ended, err
|
||||
}
|
||||
}
|
||||
91
pipeline_test.go
Normal file
91
pipeline_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package rules_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/BattlesnakeOfficial/rules"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPipeline(t *testing.T) {
|
||||
r := rules.StageRegistry{}
|
||||
|
||||
// test empty registry error
|
||||
p := rules.NewPipelineFromRegistry(r)
|
||||
require.Equal(t, rules.ErrorEmptyRegistry, p.Err())
|
||||
_, _, err := p.Execute(nil, rules.Settings{}, nil)
|
||||
require.Equal(t, rules.ErrorEmptyRegistry, err)
|
||||
|
||||
// test empty stages names error
|
||||
r.RegisterPipelineStage("astage", mockStageFn(false, nil))
|
||||
p = rules.NewPipelineFromRegistry(r)
|
||||
require.Equal(t, rules.ErrorNoStages, p.Err())
|
||||
_, _, err = p.Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
||||
require.Equal(t, rules.ErrorNoStages, err)
|
||||
|
||||
// test that an unregistered stage name errors
|
||||
p = rules.NewPipelineFromRegistry(r, "doesntexist")
|
||||
_, _, err = p.Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
||||
require.Equal(t, rules.ErrorStageNotFound, p.Err())
|
||||
require.Equal(t, rules.ErrorStageNotFound, err)
|
||||
|
||||
// simplest case - one stage
|
||||
ended, next, err := rules.NewPipelineFromRegistry(r, "astage").Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, next)
|
||||
require.False(t, ended)
|
||||
|
||||
// test that the pipeline short-circuits for a stage that errors
|
||||
r.RegisterPipelineStage("errors", mockStageFn(false, errors.New("")))
|
||||
ended, next, err = rules.NewPipelineFromRegistry(r, "errors", "astage").Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
||||
require.Error(t, err)
|
||||
require.NotNil(t, next)
|
||||
require.False(t, ended)
|
||||
|
||||
// test that the pipeline short-circuits for a stage that ends
|
||||
r.RegisterPipelineStage("ends", mockStageFn(true, nil))
|
||||
ended, next, err = rules.NewPipelineFromRegistry(r, "ends", "astage").Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, next)
|
||||
require.True(t, ended)
|
||||
|
||||
// test that the pipeline runs normally for multiple stages
|
||||
ended, next, err = rules.NewPipelineFromRegistry(r, "astage", "ends").Execute(&rules.BoardState{}, rules.Settings{}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, next)
|
||||
require.True(t, ended)
|
||||
}
|
||||
|
||||
func TestStageRegistry(t *testing.T) {
|
||||
sr := rules.StageRegistry{}
|
||||
|
||||
// register a stage without error
|
||||
require.NoError(t, sr.RegisterPipelineStageError("test", mockStageFn(false, nil)))
|
||||
require.Contains(t, sr, "test")
|
||||
|
||||
// error on duplicate
|
||||
var e rules.RulesetError
|
||||
err := sr.RegisterPipelineStageError("test", mockStageFn(false, nil))
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.As(err, &e), "error should be a RulesetError")
|
||||
require.Equal(t, "stage 'test' has already been registered", err.Error())
|
||||
|
||||
// register another stage with no error
|
||||
require.NoError(t, sr.RegisterPipelineStageError("other", mockStageFn(false, nil)))
|
||||
require.Contains(t, sr, "other")
|
||||
|
||||
// register stage
|
||||
sr.RegisterPipelineStage("last", mockStageFn(false, nil))
|
||||
require.Contains(t, sr, "last")
|
||||
|
||||
// register existing stage (should just be okay and not panic or anything)
|
||||
sr.RegisterPipelineStage("test", mockStageFn(false, nil))
|
||||
}
|
||||
|
||||
func mockStageFn(ended bool, err error) rules.StageFunc {
|
||||
return func(b *rules.BoardState, settings rules.Settings, moves []rules.SnakeMove) (bool, error) {
|
||||
return ended, err
|
||||
}
|
||||
}
|
||||
47
royale.go
47
royale.go
|
|
@ -5,6 +5,17 @@ import (
|
|||
"math/rand"
|
||||
)
|
||||
|
||||
var royaleRulesetStages = []string{
|
||||
StageMovementStandard,
|
||||
StageStarvationStandard,
|
||||
StageHazardDamageStandard,
|
||||
StageFeedSnakesStandard,
|
||||
StageSpawnFoodStandard,
|
||||
StageEliminationStandard,
|
||||
StageSpawnHazardsShrinkMap,
|
||||
StageGameOverStandard,
|
||||
}
|
||||
|
||||
type RoyaleRuleset struct {
|
||||
StandardRuleset
|
||||
|
||||
|
|
@ -15,31 +26,22 @@ type RoyaleRuleset struct {
|
|||
|
||||
func (r *RoyaleRuleset) Name() string { return GameTypeRoyale }
|
||||
|
||||
func (r RoyaleRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
||||
return NewPipeline(royaleRulesetStages...).Execute(bs, s, sm)
|
||||
}
|
||||
|
||||
func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
if r.StandardRuleset.HazardDamagePerTurn < 1 {
|
||||
return nil, errors.New("royale damage per turn must be greater than zero")
|
||||
}
|
||||
|
||||
nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Royale's only job is now to populate the hazards for next turn - StandardRuleset takes care of applying hazard damage.
|
||||
err = r.populateHazards(nextBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nextBoardState, nil
|
||||
}
|
||||
|
||||
func (r *RoyaleRuleset) populateHazards(b *BoardState) error {
|
||||
_, err := r.callStageFunc(PopulateHazardsRoyale, b, []SnakeMove{})
|
||||
return err
|
||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
b.Hazards = []Point{}
|
||||
|
||||
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
||||
|
|
@ -82,6 +84,10 @@ func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove)
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (r *RoyaleRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
return GameOverStandard(b, r.Settings(), nil)
|
||||
}
|
||||
|
||||
func (r RoyaleRuleset) Settings() Settings {
|
||||
s := r.StandardRuleset.Settings()
|
||||
s.RoyaleSettings = RoyaleSettings{
|
||||
|
|
@ -90,8 +96,3 @@ func (r RoyaleRuleset) Settings() Settings {
|
|||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Adaptor for integrating stages into RoyaleRuleset
|
||||
func (r *RoyaleRuleset) callStageFunc(stage StageFunc, boardState *BoardState, moves []SnakeMove) (bool, error) {
|
||||
return stage(boardState, r.Settings(), moves)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ func TestRoyaleRulesetInterface(t *testing.T) {
|
|||
func TestRoyaleDefaultSanity(t *testing.T) {
|
||||
boardState := &BoardState{}
|
||||
r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 1}, ShrinkEveryNTurns: 0}
|
||||
_, err := r.CreateNextBoardState(boardState, []SnakeMove{})
|
||||
_, err := r.CreateNextBoardState(boardState, []SnakeMove{{"", ""}})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, errors.New("royale game can't shrink more frequently than every turn"), err)
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ func TestRoyaleHazards(t *testing.T) {
|
|||
ShrinkEveryNTurns: test.ShrinkEveryNTurns,
|
||||
}
|
||||
|
||||
err := r.populateHazards(b)
|
||||
_, err := PopulateHazardsRoyale(b, r.Settings(), mockSnakeMoves())
|
||||
require.Equal(t, test.Error, err)
|
||||
if err == nil {
|
||||
// Obstacles should match
|
||||
|
|
@ -131,7 +131,7 @@ func TestRoyalDamageNextTurn(t *testing.T) {
|
|||
stateAfterTurn := func(prevState *BoardState, turn int32) *BoardState {
|
||||
nextState := prevState.Clone()
|
||||
nextState.Turn = turn - 1
|
||||
err := r.populateHazards(nextState)
|
||||
_, err := PopulateHazardsRoyale(nextState, r.Settings(), nil)
|
||||
require.NoError(t, err)
|
||||
nextState.Turn = turn
|
||||
return nextState
|
||||
|
|
@ -265,7 +265,16 @@ func TestRoyaleCreateNextBoardState(t *testing.T) {
|
|||
ShrinkEveryNTurns: 1,
|
||||
}
|
||||
rand.Seed(0)
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeRoyale,
|
||||
ParamHazardDamagePerTurn: "1",
|
||||
ParamShrinkEveryNTurns: "1",
|
||||
})
|
||||
for _, gc := range cases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeRoyale, NewPipeline(royaleRulesetStages...)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
100
ruleset.go
100
ruleset.go
|
|
@ -36,6 +36,9 @@ const (
|
|||
ErrorNoRoomForFood = RulesetError("not enough space to place food")
|
||||
ErrorNoMoveFound = RulesetError("move not provided for snake")
|
||||
ErrorZeroLengthSnake = RulesetError("snake is length zero")
|
||||
ErrorEmptyRegistry = RulesetError("empty registry")
|
||||
ErrorNoStages = RulesetError("no stages")
|
||||
ErrorStageNotFound = RulesetError("stage not found")
|
||||
|
||||
// Ruleset / game type names
|
||||
GameTypeConstrictor = "constrictor"
|
||||
|
|
@ -104,7 +107,7 @@ func (rb *rulesetBuilder) AddSnakeToSquad(snakeID, squadName string) *rulesetBui
|
|||
}
|
||||
|
||||
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
|
||||
func (rb rulesetBuilder) Ruleset() Ruleset {
|
||||
func (rb rulesetBuilder) Ruleset() PipelineRuleset {
|
||||
standardRuleset := &StandardRuleset{
|
||||
FoodSpawnChance: paramsInt32(rb.params, ParamFoodSpawnChance, 0),
|
||||
MinimumFood: paramsInt32(rb.params, ParamMinimumFood, 0),
|
||||
|
|
@ -138,13 +141,9 @@ func (rb rulesetBuilder) Ruleset() Ruleset {
|
|||
StandardRuleset: *standardRuleset,
|
||||
}
|
||||
case GameTypeSquad:
|
||||
squadMap := map[string]string{}
|
||||
for id, squad := range rb.squads {
|
||||
squadMap[id] = squad
|
||||
}
|
||||
return &SquadRuleset{
|
||||
StandardRuleset: *standardRuleset,
|
||||
SquadMap: squadMap,
|
||||
SquadMap: rb.squadMap(),
|
||||
AllowBodyCollisions: paramsBool(rb.params, ParamAllowBodyCollisions, false),
|
||||
SharedElimination: paramsBool(rb.params, ParamSharedElimination, false),
|
||||
SharedHealth: paramsBool(rb.params, ParamSharedHealth, false),
|
||||
|
|
@ -154,6 +153,42 @@ func (rb rulesetBuilder) Ruleset() Ruleset {
|
|||
return standardRuleset
|
||||
}
|
||||
|
||||
func (rb rulesetBuilder) squadMap() map[string]string {
|
||||
squadMap := map[string]string{}
|
||||
for id, squad := range rb.squads {
|
||||
squadMap[id] = squad
|
||||
}
|
||||
return squadMap
|
||||
}
|
||||
|
||||
// PipelineRuleset provides an implementation of the Ruleset using a pipeline with a name.
|
||||
// It is intended to facilitate transitioning away from legacy Ruleset implementations to Pipeline
|
||||
// implementations.
|
||||
func (rb rulesetBuilder) PipelineRuleset(name string, p Pipeline) PipelineRuleset {
|
||||
return &pipelineRuleset{
|
||||
name: name,
|
||||
pipeline: p,
|
||||
settings: Settings{
|
||||
FoodSpawnChance: paramsInt32(rb.params, ParamFoodSpawnChance, 0),
|
||||
MinimumFood: paramsInt32(rb.params, ParamMinimumFood, 0),
|
||||
HazardDamagePerTurn: paramsInt32(rb.params, ParamHazardDamagePerTurn, 0),
|
||||
HazardMap: rb.params[ParamHazardMap],
|
||||
HazardMapAuthor: rb.params[ParamHazardMapAuthor],
|
||||
RoyaleSettings: RoyaleSettings{
|
||||
seed: rb.seed,
|
||||
ShrinkEveryNTurns: paramsInt32(rb.params, ParamShrinkEveryNTurns, 0),
|
||||
},
|
||||
SquadSettings: SquadSettings{
|
||||
squadMap: rb.squadMap(),
|
||||
AllowBodyCollisions: paramsBool(rb.params, ParamAllowBodyCollisions, false),
|
||||
SharedElimination: paramsBool(rb.params, ParamSharedElimination, false),
|
||||
SharedHealth: paramsBool(rb.params, ParamSharedHealth, false),
|
||||
SharedLength: paramsBool(rb.params, ParamSharedLength, false),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// paramsBool returns the boolean value for the specified parameter.
|
||||
// If the parameter doesn't exist, the default value will be returned.
|
||||
// If the parameter does exist, but is not "true", false will be returned.
|
||||
|
|
@ -239,3 +274,56 @@ type SquadSettings struct {
|
|||
//
|
||||
// Errors should be treated as meaning the stage failed and the board state is now invalid.
|
||||
type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error)
|
||||
|
||||
// PipelineRuleset groups the Pipeline and Ruleset methods.
|
||||
// It is intended to facilitate a transition from Ruleset legacy code to Pipeline code.
|
||||
type PipelineRuleset interface {
|
||||
Ruleset
|
||||
Pipeline
|
||||
}
|
||||
|
||||
type pipelineRuleset struct {
|
||||
pipeline Pipeline
|
||||
name string
|
||||
settings Settings
|
||||
}
|
||||
|
||||
// impl Ruleset
|
||||
func (r pipelineRuleset) Settings() Settings {
|
||||
return r.settings
|
||||
}
|
||||
|
||||
// impl Ruleset
|
||||
func (r pipelineRuleset) Name() string { return r.name }
|
||||
|
||||
// impl Ruleset
|
||||
// IMPORTANT: this implementation of IsGameOver deviates from the previous Ruleset implementations
|
||||
// in that it checks if the *NEXT* state results in game over, not the previous state.
|
||||
// This is due to the design of pipelines / stage functions not having a distinction between
|
||||
// checking for game over and producing a next state.
|
||||
func (r *pipelineRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
gameover, _, err := r.Execute(b, r.Settings(), nil) // checks if next state is game over
|
||||
return gameover, err
|
||||
}
|
||||
|
||||
// impl Ruleset
|
||||
func (r pipelineRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
|
||||
_, nextState, err := r.Execute(initialState, r.Settings(), nil)
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
// impl Pipeline
|
||||
func (r pipelineRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
||||
return r.pipeline.Execute(bs, s, sm)
|
||||
}
|
||||
|
||||
// impl Ruleset
|
||||
func (r pipelineRuleset) CreateNextBoardState(bs *BoardState, sm []SnakeMove) (*BoardState, error) {
|
||||
_, nextState, err := r.Execute(bs, r.Settings(), sm)
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
// impl Pipeline
|
||||
func (r pipelineRuleset) Err() error {
|
||||
return r.pipeline.Err()
|
||||
}
|
||||
|
|
|
|||
16
solo.go
16
solo.go
|
|
@ -1,13 +1,27 @@
|
|||
package rules
|
||||
|
||||
var soloRulesetStages = []string{
|
||||
StageMovementStandard,
|
||||
StageStarvationStandard,
|
||||
StageHazardDamageStandard,
|
||||
StageFeedSnakesStandard,
|
||||
StageSpawnFoodStandard,
|
||||
StageEliminationStandard,
|
||||
StageGameOverSoloSnake,
|
||||
}
|
||||
|
||||
type SoloRuleset struct {
|
||||
StandardRuleset
|
||||
}
|
||||
|
||||
func (r *SoloRuleset) Name() string { return GameTypeSolo }
|
||||
|
||||
func (r SoloRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
||||
return NewPipeline(soloRulesetStages...).Execute(bs, s, sm)
|
||||
}
|
||||
|
||||
func (r *SoloRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
return r.callStageFunc(GameOverSolo, b, []SnakeMove{})
|
||||
return GameOverSolo(b, r.Settings(), nil)
|
||||
}
|
||||
|
||||
func GameOverSolo(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,14 @@ func TestSoloCreateNextBoardState(t *testing.T) {
|
|||
soloCaseNotOver,
|
||||
}
|
||||
r := SoloRuleset{}
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeSolo,
|
||||
})
|
||||
for _, gc := range cases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeSolo, NewPipeline(soloRulesetStages...)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
squad.go
57
squad.go
|
|
@ -4,6 +4,18 @@ import (
|
|||
"errors"
|
||||
)
|
||||
|
||||
var squadRulesetStages = []string{
|
||||
StageMovementStandard,
|
||||
StageStarvationStandard,
|
||||
StageHazardDamageStandard,
|
||||
StageFeedSnakesStandard,
|
||||
StageSpawnFoodStandard,
|
||||
StageEliminationStandard,
|
||||
StageEliminationResurrectSquadCollisions,
|
||||
StageModifySnakesShareAttributes,
|
||||
StageGameOverBySquad,
|
||||
}
|
||||
|
||||
type SquadRuleset struct {
|
||||
StandardRuleset
|
||||
|
||||
|
|
@ -18,23 +30,13 @@ type SquadRuleset struct {
|
|||
|
||||
func (r *SquadRuleset) Name() string { return GameTypeSquad }
|
||||
|
||||
func (r SquadRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
||||
return NewPipeline(squadRulesetStages...).Execute(bs, s, sm)
|
||||
}
|
||||
|
||||
func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.resurrectSquadBodyCollisions(nextBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.shareSquadAttributes(nextBoardState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nextBoardState, nil
|
||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
func areSnakesOnSameSquad(squadMap map[string]string, snake *Snake, other *Snake) bool {
|
||||
|
|
@ -45,12 +47,10 @@ func areSnakeIDsOnSameSquad(squadMap map[string]string, snakeID string, otherID
|
|||
return squadMap[snakeID] == squadMap[otherID]
|
||||
}
|
||||
|
||||
func (r *SquadRuleset) resurrectSquadBodyCollisions(b *BoardState) error {
|
||||
_, err := r.callStageFunc(ResurrectSnakesSquad, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
|
||||
func ResurrectSnakesSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
if !settings.SquadSettings.AllowBodyCollisions {
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -71,12 +71,10 @@ func ResurrectSnakesSquad(b *BoardState, settings Settings, moves []SnakeMove) (
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (r *SquadRuleset) shareSquadAttributes(b *BoardState) error {
|
||||
_, err := r.callStageFunc(ShareAttributesSquad, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
|
||||
func ShareAttributesSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
squadSettings := settings.SquadSettings
|
||||
|
||||
if !(squadSettings.SharedElimination || squadSettings.SharedLength || squadSettings.SharedHealth) {
|
||||
|
|
@ -120,7 +118,7 @@ func ShareAttributesSquad(b *BoardState, settings Settings, moves []SnakeMove) (
|
|||
}
|
||||
|
||||
func (r *SquadRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
return r.callStageFunc(GameOverSquad, b, []SnakeMove{})
|
||||
return GameOverSquad(b, r.Settings(), nil)
|
||||
}
|
||||
|
||||
func GameOverSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
|
|
@ -152,8 +150,3 @@ func (r SquadRuleset) Settings() Settings {
|
|||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Adaptor for integrating stages into SquadRuleset
|
||||
func (r *SquadRuleset) callStageFunc(stage StageFunc, boardState *BoardState, moves []SnakeMove) (bool, error) {
|
||||
return stage(boardState, r.Settings(), moves)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ func TestSquadCreateNextBoardStateSanity(t *testing.T) {
|
|||
func TestSquadResurrectSquadBodyCollisionsSanity(t *testing.T) {
|
||||
boardState := &BoardState{}
|
||||
r := SquadRuleset{}
|
||||
err := r.resurrectSquadBodyCollisions(boardState)
|
||||
_, err := ResurrectSnakesSquad(boardState, r.Settings(), nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSquadSharedAttributesSanity(t *testing.T) {
|
||||
boardState := &BoardState{}
|
||||
r := SquadRuleset{}
|
||||
err := r.shareSquadAttributes(boardState)
|
||||
_, err := ShareAttributesSquad(boardState, r.Settings(), nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ func TestSquadAllowBodyCollisions(t *testing.T) {
|
|||
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
||||
|
||||
r := SquadRuleset{SquadMap: squadMap, AllowBodyCollisions: true}
|
||||
err := r.resurrectSquadBodyCollisions(boardState)
|
||||
_, err := ResurrectSnakesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
||||
|
|
@ -113,7 +113,7 @@ func TestSquadAllowBodyCollisionsEliminatedByNotSet(t *testing.T) {
|
|||
"2": "red",
|
||||
},
|
||||
}
|
||||
err := r.resurrectSquadBodyCollisions(boardState)
|
||||
_, err := ResurrectSnakesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ func TestSquadShareSquadHealth(t *testing.T) {
|
|||
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
||||
|
||||
r := SquadRuleset{SharedHealth: true, SquadMap: squadMap}
|
||||
err := r.shareSquadAttributes(boardState)
|
||||
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
||||
|
|
@ -202,7 +202,7 @@ func TestSquadSharedLength(t *testing.T) {
|
|||
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
||||
|
||||
r := SquadRuleset{SharedLength: true, SquadMap: squadMap}
|
||||
err := r.shareSquadAttributes(boardState)
|
||||
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
||||
|
|
@ -255,7 +255,7 @@ func TestSquadSharedElimination(t *testing.T) {
|
|||
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
||||
|
||||
r := SquadRuleset{SharedElimination: true, SquadMap: squadMap}
|
||||
err := r.shareSquadAttributes(boardState)
|
||||
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
||||
|
|
@ -291,7 +291,7 @@ func TestSquadSharedAttributesErrorLengthZero(t *testing.T) {
|
|||
"2": "red",
|
||||
},
|
||||
}
|
||||
err := r.shareSquadAttributes(boardState)
|
||||
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
|
|
@ -547,8 +547,19 @@ func TestSquadCreateNextBoardState(t *testing.T) {
|
|||
},
|
||||
}
|
||||
rand.Seed(0)
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeSquad,
|
||||
})
|
||||
rb.WithSeed(0)
|
||||
for s, ss := range r.SquadMap {
|
||||
rb = rb.AddSnakeToSquad(s, ss)
|
||||
}
|
||||
for _, gc := range standardCases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeSquad, NewPipeline(squadRulesetStages...)))
|
||||
}
|
||||
|
||||
extendedCases := []gameTestCase{
|
||||
|
|
@ -557,7 +568,15 @@ func TestSquadCreateNextBoardState(t *testing.T) {
|
|||
}
|
||||
r.SharedHealth = true
|
||||
r.AllowBodyCollisions = true
|
||||
rb = rb.WithParams(map[string]string{
|
||||
ParamSharedHealth: "true",
|
||||
ParamAllowBodyCollisions: "true",
|
||||
})
|
||||
for _, gc := range extendedCases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, rb.PipelineRuleset(GameTypeSquad, NewPipeline(squadRulesetStages...)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
119
standard.go
119
standard.go
|
|
@ -13,6 +13,16 @@ type StandardRuleset struct {
|
|||
HazardMapAuthor string // optional
|
||||
}
|
||||
|
||||
var standardRulesetStages = []string{
|
||||
StageMovementStandard,
|
||||
StageStarvationStandard,
|
||||
StageHazardDamageStandard,
|
||||
StageFeedSnakesStandard,
|
||||
StageSpawnFoodStandard,
|
||||
StageEliminationStandard,
|
||||
StageGameOverStandard,
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) Name() string { return GameTypeStandard }
|
||||
|
||||
func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
|
||||
|
|
@ -20,55 +30,22 @@ func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*Bo
|
|||
return initialState, nil
|
||||
}
|
||||
|
||||
// impl Pipeline
|
||||
func (r StandardRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
||||
return NewPipeline(standardRulesetStages...).Execute(bs, s, sm)
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
// We specifically want to copy prevState, so as not to alter it directly.
|
||||
nextState := prevState.Clone()
|
||||
|
||||
err := r.moveSnakes(nextState, moves)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.reduceSnakeHealth(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.maybeDamageHazards(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// bvanvugt: We specifically want this to happen before elimination for two reasons:
|
||||
// 1) We want snakes to be able to eat on their very last turn and still survive.
|
||||
// 2) So that head-to-head collisions on food still remove the food.
|
||||
// This does create an artifact though, where head-to-head collisions
|
||||
// of equal length actually show length + 1 and full health, as if both snakes ate.
|
||||
err = r.maybeFeedSnakes(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.maybeSpawnFood(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.maybeEliminateSnakes(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nextState, nil
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
||||
_, err := r.callStageFunc(MoveSnakesStandard, b, moves)
|
||||
return err
|
||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
func MoveSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
// If no moves are passed, pass on modifying the initial board state
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// no-op when moves are empty
|
||||
if len(moves) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -164,12 +141,10 @@ func getDefaultMove(snakeBody []Point) string {
|
|||
return MoveUp
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) reduceSnakeHealth(b *BoardState) error {
|
||||
_, err := r.callStageFunc(ReduceSnakeHealthStandard, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
|
||||
func ReduceSnakeHealthStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||
b.Snakes[i].Health = b.Snakes[i].Health - 1
|
||||
|
|
@ -178,12 +153,10 @@ func ReduceSnakeHealthStandard(b *BoardState, settings Settings, moves []SnakeMo
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) maybeDamageHazards(b *BoardState) error {
|
||||
_, err := r.callStageFunc(DamageHazardsStandard, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
|
||||
func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
snake := &b.Snakes[i]
|
||||
if snake.EliminatedCause != NotEliminated {
|
||||
|
|
@ -218,12 +191,10 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove)
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) maybeEliminateSnakes(b *BoardState) error {
|
||||
_, err := r.callStageFunc(EliminateSnakesStandard, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
|
||||
func EliminateSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
// First order snake indices by length.
|
||||
// In multi-collision scenarios we want to always attribute elimination to the longest snake.
|
||||
snakeIndicesByLength := make([]int, len(b.Snakes))
|
||||
|
|
@ -378,11 +349,6 @@ func snakeHasLostHeadToHead(s *Snake, other *Snake) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) maybeFeedSnakes(b *BoardState) error {
|
||||
_, err := r.callStageFunc(FeedSnakesStandard, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
|
||||
func FeedSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
newFood := []Point{}
|
||||
for _, food := range b.Food {
|
||||
|
|
@ -421,12 +387,10 @@ func growSnake(snake *Snake) {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *StandardRuleset) maybeSpawnFood(b *BoardState) error {
|
||||
_, err := r.callStageFunc(SpawnFoodStandard, b, []SnakeMove{})
|
||||
return err
|
||||
}
|
||||
|
||||
func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
numCurrentFood := int32(len(b.Food))
|
||||
if numCurrentFood < settings.MinimumFood {
|
||||
return false, PlaceFoodRandomly(b, settings.MinimumFood-numCurrentFood)
|
||||
|
|
@ -438,7 +402,7 @@ func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (boo
|
|||
}
|
||||
|
||||
func (r *StandardRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
return r.callStageFunc(GameOverStandard, b, []SnakeMove{})
|
||||
return GameOverStandard(b, r.Settings(), nil)
|
||||
}
|
||||
|
||||
func GameOverStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
|
|
@ -461,7 +425,14 @@ func (r StandardRuleset) Settings() Settings {
|
|||
}
|
||||
}
|
||||
|
||||
// Adaptor for integrating stages into StandardRuleset
|
||||
func (r *StandardRuleset) callStageFunc(stage StageFunc, boardState *BoardState, moves []SnakeMove) (bool, error) {
|
||||
return stage(boardState, r.Settings(), moves)
|
||||
// impl Pipeline
|
||||
func (r StandardRuleset) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsInitialization checks whether the current state means the game is initialising.
|
||||
func IsInitialization(b *BoardState, settings Settings, moves []SnakeMove) bool {
|
||||
// We can safely assume that the game state is in the initialisation phase when
|
||||
// the turn hasn't advanced and the moves are empty
|
||||
return b.Turn <= 0 && len(moves) == 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,8 +218,15 @@ func TestStandardCreateNextBoardState(t *testing.T) {
|
|||
standardMoveAndCollideMAD,
|
||||
}
|
||||
r := StandardRuleset{}
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeStandard,
|
||||
})
|
||||
for _, gc := range cases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeStandard, NewPipeline(standardRulesetStages...)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -552,7 +559,7 @@ func TestMoveSnakes(t *testing.T) {
|
|||
{ID: "two", Move: test.MoveTwo},
|
||||
{ID: "three", Move: test.MoveThree},
|
||||
}
|
||||
err := r.moveSnakes(b, moves)
|
||||
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.Snakes, 3)
|
||||
|
|
@ -597,7 +604,7 @@ func TestMoveSnakesWrongID(t *testing.T) {
|
|||
}
|
||||
|
||||
r := StandardRuleset{}
|
||||
err := r.moveSnakes(b, moves)
|
||||
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||
require.Equal(t, ErrorNoMoveFound, err)
|
||||
}
|
||||
|
||||
|
|
@ -622,7 +629,7 @@ func TestMoveSnakesNotEnoughMoves(t *testing.T) {
|
|||
}
|
||||
|
||||
r := StandardRuleset{}
|
||||
err := r.moveSnakes(b, moves)
|
||||
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||
require.Equal(t, ErrorNoMoveFound, err)
|
||||
}
|
||||
|
||||
|
|
@ -647,7 +654,7 @@ func TestMoveSnakesExtraMovesIgnored(t *testing.T) {
|
|||
}
|
||||
|
||||
r := StandardRuleset{}
|
||||
err := r.moveSnakes(b, moves)
|
||||
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []Point{{1, 0}}, b.Snakes[0].Body)
|
||||
}
|
||||
|
|
@ -699,7 +706,7 @@ func TestMoveSnakesDefault(t *testing.T) {
|
|||
}
|
||||
moves := []SnakeMove{{ID: "one", Move: test.Move}}
|
||||
|
||||
err := r.moveSnakes(b, moves)
|
||||
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.Snakes, 1)
|
||||
require.Equal(t, len(test.Body), len(b.Snakes[0].Body))
|
||||
|
|
@ -795,25 +802,25 @@ func TestReduceSnakeHealth(t *testing.T) {
|
|||
}
|
||||
|
||||
r := StandardRuleset{}
|
||||
err := r.reduceSnakeHealth(b)
|
||||
_, err := ReduceSnakeHealthStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b.Snakes[0].Health, int32(98))
|
||||
require.Equal(t, b.Snakes[1].Health, int32(1))
|
||||
require.Equal(t, b.Snakes[2].Health, int32(50))
|
||||
|
||||
err = r.reduceSnakeHealth(b)
|
||||
_, err = ReduceSnakeHealthStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b.Snakes[0].Health, int32(97))
|
||||
require.Equal(t, b.Snakes[1].Health, int32(0))
|
||||
require.Equal(t, b.Snakes[2].Health, int32(50))
|
||||
|
||||
err = r.reduceSnakeHealth(b)
|
||||
_, err = ReduceSnakeHealthStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b.Snakes[0].Health, int32(96))
|
||||
require.Equal(t, b.Snakes[1].Health, int32(-1))
|
||||
require.Equal(t, b.Snakes[2].Health, int32(50))
|
||||
|
||||
err = r.reduceSnakeHealth(b)
|
||||
_, err = ReduceSnakeHealthStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, b.Snakes[0].Health, int32(95))
|
||||
require.Equal(t, b.Snakes[1].Health, int32(-2))
|
||||
|
|
@ -1214,7 +1221,7 @@ func TestMaybeEliminateSnakes(t *testing.T) {
|
|||
Height: 10,
|
||||
Snakes: test.Snakes,
|
||||
}
|
||||
err := r.maybeEliminateSnakes(b)
|
||||
_, err := EliminateSnakesStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.Equal(t, test.Err, err)
|
||||
for i, snake := range b.Snakes {
|
||||
require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause)
|
||||
|
|
@ -1254,7 +1261,7 @@ func TestMaybeEliminateSnakesPriority(t *testing.T) {
|
|||
r := StandardRuleset{}
|
||||
for _, test := range tests {
|
||||
b := &BoardState{Width: 10, Height: 10, Snakes: test.Snakes}
|
||||
err := r.maybeEliminateSnakes(b)
|
||||
_, err := EliminateSnakesStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
for i, snake := range b.Snakes {
|
||||
require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause, snake.ID)
|
||||
|
|
@ -1320,7 +1327,7 @@ func TestMaybeDamageHazards(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
b := &BoardState{Snakes: test.Snakes, Hazards: test.Hazards, Food: test.Food}
|
||||
r := StandardRuleset{HazardDamagePerTurn: 100}
|
||||
err := r.maybeDamageHazards(b)
|
||||
_, err := DamageHazardsStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, snake := range b.Snakes {
|
||||
|
|
@ -1361,7 +1368,7 @@ func TestHazardDamagePerTurn(t *testing.T) {
|
|||
}
|
||||
r := StandardRuleset{HazardDamagePerTurn: test.HazardDamagePerTurn}
|
||||
|
||||
err := r.maybeDamageHazards(b)
|
||||
_, err := DamageHazardsStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.Equal(t, test.Error, err)
|
||||
require.Equal(t, test.ExpectedHealth, b.Snakes[0].Health)
|
||||
require.Equal(t, test.ExpectedEliminationCause, b.Snakes[0].EliminatedCause)
|
||||
|
|
@ -1441,7 +1448,7 @@ func TestMaybeFeedSnakes(t *testing.T) {
|
|||
Snakes: test.Snakes,
|
||||
Food: test.Food,
|
||||
}
|
||||
err := r.maybeFeedSnakes(b)
|
||||
_, err := FeedSnakesStandard(b, r.Settings(), nil)
|
||||
require.NoError(t, err, test.Name)
|
||||
require.Equal(t, len(test.ExpectedSnakes), len(b.Snakes), test.Name)
|
||||
for i := 0; i < len(b.Snakes); i++ {
|
||||
|
|
@ -1477,7 +1484,7 @@ func TestMaybeSpawnFoodMinimum(t *testing.T) {
|
|||
Food: test.Food,
|
||||
}
|
||||
|
||||
err := r.maybeSpawnFood(b)
|
||||
_, err := SpawnFoodStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.ExpectedFood, len(b.Food))
|
||||
}
|
||||
|
|
@ -1495,7 +1502,7 @@ func TestMaybeSpawnFoodZeroChance(t *testing.T) {
|
|||
Food: []Point{},
|
||||
}
|
||||
for i := 0; i < 1000; i++ {
|
||||
err := r.maybeSpawnFood(b)
|
||||
_, err := SpawnFoodStandard(b, r.Settings(), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(b.Food), 0)
|
||||
}
|
||||
|
|
@ -1513,7 +1520,7 @@ func TestMaybeSpawnFoodHundredChance(t *testing.T) {
|
|||
Food: []Point{},
|
||||
}
|
||||
for i := 1; i <= 22; i++ {
|
||||
err := r.maybeSpawnFood(b)
|
||||
_, err := SpawnFoodStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, i, len(b.Food))
|
||||
}
|
||||
|
|
@ -1547,7 +1554,7 @@ func TestMaybeSpawnFoodHalfChance(t *testing.T) {
|
|||
}
|
||||
|
||||
rand.Seed(test.Seed)
|
||||
err := r.maybeSpawnFood(b)
|
||||
_, err := SpawnFoodStandard(b, r.Settings(), mockSnakeMoves())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.ExpectedFood, int32(len(b.Food)), "Seed %d", test.Seed)
|
||||
}
|
||||
|
|
|
|||
61
wrapped.go
61
wrapped.go
|
|
@ -1,53 +1,36 @@
|
|||
package rules
|
||||
|
||||
var wrappedRulesetStages = []string{
|
||||
StageMovementWrapBoundaries,
|
||||
StageStarvationStandard,
|
||||
StageHazardDamageStandard,
|
||||
StageFeedSnakesStandard,
|
||||
StageSpawnFoodStandard,
|
||||
StageEliminationStandard,
|
||||
StageGameOverStandard,
|
||||
}
|
||||
|
||||
type WrappedRuleset struct {
|
||||
StandardRuleset
|
||||
}
|
||||
|
||||
func (r *WrappedRuleset) Name() string { return GameTypeWrapped }
|
||||
|
||||
func (r WrappedRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
||||
return NewPipeline(wrappedRulesetStages...).Execute(bs, s, sm)
|
||||
}
|
||||
|
||||
func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||
nextState := prevState.Clone()
|
||||
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||
|
||||
err := r.moveSnakes(nextState, moves)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.reduceSnakeHealth(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.maybeDamageHazards(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.maybeFeedSnakes(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.maybeSpawnFood(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = r.maybeEliminateSnakes(nextState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nextState, nil
|
||||
}
|
||||
|
||||
func (r *WrappedRuleset) moveSnakes(b *BoardState, moves []SnakeMove) error {
|
||||
_, err := r.callStageFunc(MoveSnakesWrapped, b, moves)
|
||||
return err
|
||||
return nextState, err
|
||||
}
|
||||
|
||||
func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||
if IsInitialization(b, settings, moves) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
_, err := MoveSnakesStandard(b, settings, moves)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -65,6 +48,10 @@ func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (boo
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (r *WrappedRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||
return GameOverStandard(b, r.Settings(), nil)
|
||||
}
|
||||
|
||||
func wrap(value, min, max int32) int32 {
|
||||
if value < min {
|
||||
return max
|
||||
|
|
|
|||
|
|
@ -331,7 +331,14 @@ func TestWrappedCreateNextBoardState(t *testing.T) {
|
|||
wrappedCaseMoveAndWrap,
|
||||
}
|
||||
r := WrappedRuleset{}
|
||||
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||
ParamGameType: GameTypeWrapped,
|
||||
})
|
||||
for _, gc := range cases {
|
||||
gc.requireValidNextState(t, &r)
|
||||
// also test a RulesBuilder constructed instance
|
||||
gc.requireValidNextState(t, rb.Ruleset())
|
||||
// also test a pipeline with the same settings
|
||||
gc.requireValidNextState(t, NewRulesetBuilder().PipelineRuleset(GameTypeWrapped, NewPipeline(wrappedRulesetStages...)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue