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 {
|
func (prevState *BoardState) Clone() *BoardState {
|
||||||
nextState := &BoardState{
|
nextState := &BoardState{
|
||||||
Turn: prevState.Turn,
|
Turn: prevState.Turn,
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ func (gc *gameTestCase) clone() *gameTestCase {
|
||||||
|
|
||||||
// requireValidNextState requires that the ruleset produces a valid next state
|
// requireValidNextState requires that the ruleset produces a valid next state
|
||||||
func (gc *gameTestCase) requireValidNextState(t *testing.T, r Ruleset) {
|
func (gc *gameTestCase) requireValidNextState(t *testing.T, r Ruleset) {
|
||||||
|
t.Helper()
|
||||||
t.Run(gc.name, func(t *testing.T) {
|
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)
|
prev := gc.prevState.Clone() // clone to protect against mutation (so we can ru-use test cases)
|
||||||
nextState, err := r.CreateNextBoardState(prev, gc.moves)
|
nextState, err := r.CreateNextBoardState(prev, gc.moves)
|
||||||
require.Equal(t, gc.expectedError, err)
|
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
|
package rules
|
||||||
|
|
||||||
|
var constrictorRulesetStages = []string{
|
||||||
|
StageMovementStandard,
|
||||||
|
StageStarvationStandard,
|
||||||
|
StageHazardDamageStandard,
|
||||||
|
StageFeedSnakesStandard,
|
||||||
|
StageEliminationStandard,
|
||||||
|
StageSpawnFoodNoFood,
|
||||||
|
StageModifySnakesAlwaysGrow,
|
||||||
|
StageGameOverStandard,
|
||||||
|
}
|
||||||
|
|
||||||
type ConstrictorRuleset struct {
|
type ConstrictorRuleset struct {
|
||||||
StandardRuleset
|
StandardRuleset
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) Name() string { return GameTypeConstrictor }
|
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) {
|
func (r *ConstrictorRuleset) ModifyInitialBoardState(initialBoardState *BoardState) (*BoardState, error) {
|
||||||
initialBoardState, err := r.StandardRuleset.ModifyInitialBoardState(initialBoardState)
|
_, nextState, err := r.Execute(initialBoardState, r.Settings(), nil)
|
||||||
if err != nil {
|
return nextState, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.removeFood(initialBoardState)
|
|
||||||
|
|
||||||
err = r.applyConstrictorRules(initialBoardState)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return initialBoardState, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
func (r *ConstrictorRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||||
nextState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r.removeFood(nextState)
|
return nextState, err
|
||||||
|
|
||||||
err = r.applyConstrictorRules(nextState)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextState, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ConstrictorRuleset) removeFood(b *BoardState) {
|
func (r *ConstrictorRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
_, _ = r.callStageFunc(RemoveFoodConstrictor, b, []SnakeMove{})
|
return GameOverStandard(b, r.Settings(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveFoodConstrictor(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
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
|
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) {
|
func GrowSnakesConstrictor(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
// Set all snakes to max health and ensure they grow next turn
|
// Set all snakes to max health and ensure they grow next turn
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
|
if len(b.Snakes[i].Body) <= 0 {
|
||||||
|
return false, ErrorZeroLengthSnake
|
||||||
|
}
|
||||||
b.Snakes[i].Health = SnakeMaxHealth
|
b.Snakes[i].Health = SnakeMaxHealth
|
||||||
|
|
||||||
tail := b.Snakes[i].Body[len(b.Snakes[i].Body)-1]
|
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{}},
|
||||||
{11, 11, []string{"one", "two", "three", "four", "five"}},
|
{11, 11, []string{"one", "two", "three", "four", "five"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
r := ConstrictorRuleset{}
|
r := ConstrictorRuleset{}
|
||||||
for testNum, test := range tests {
|
for testNum, test := range tests {
|
||||||
state, err := CreateDefaultBoardState(test.Width, test.Height, test.IDs)
|
state, err := CreateDefaultBoardState(test.Width, test.Height, test.IDs)
|
||||||
|
|
@ -104,8 +103,15 @@ func TestConstrictorCreateNextBoardState(t *testing.T) {
|
||||||
standardCaseErrZeroLengthSnake,
|
standardCaseErrZeroLengthSnake,
|
||||||
constrictorMoveAndCollideMAD,
|
constrictorMoveAndCollideMAD,
|
||||||
}
|
}
|
||||||
|
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||||
|
ParamGameType: GameTypeConstrictor,
|
||||||
|
})
|
||||||
r := ConstrictorRuleset{}
|
r := ConstrictorRuleset{}
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
gc.requireValidNextState(t, &r)
|
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"
|
"math/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var royaleRulesetStages = []string{
|
||||||
|
StageMovementStandard,
|
||||||
|
StageStarvationStandard,
|
||||||
|
StageHazardDamageStandard,
|
||||||
|
StageFeedSnakesStandard,
|
||||||
|
StageSpawnFoodStandard,
|
||||||
|
StageEliminationStandard,
|
||||||
|
StageSpawnHazardsShrinkMap,
|
||||||
|
StageGameOverStandard,
|
||||||
|
}
|
||||||
|
|
||||||
type RoyaleRuleset struct {
|
type RoyaleRuleset struct {
|
||||||
StandardRuleset
|
StandardRuleset
|
||||||
|
|
||||||
|
|
@ -15,31 +26,22 @@ type RoyaleRuleset struct {
|
||||||
|
|
||||||
func (r *RoyaleRuleset) Name() string { return GameTypeRoyale }
|
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) {
|
func (r *RoyaleRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||||
if r.StandardRuleset.HazardDamagePerTurn < 1 {
|
if r.StandardRuleset.HazardDamagePerTurn < 1 {
|
||||||
return nil, errors.New("royale damage per turn must be greater than zero")
|
return nil, errors.New("royale damage per turn must be greater than zero")
|
||||||
}
|
}
|
||||||
|
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||||
nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
return nextState, err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
func PopulateHazardsRoyale(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
if IsInitialization(b, settings, moves) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
b.Hazards = []Point{}
|
b.Hazards = []Point{}
|
||||||
|
|
||||||
// Royale uses the current turn to generate hazards, not the previous turn that's in the board state
|
// 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
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RoyaleRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
|
return GameOverStandard(b, r.Settings(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (r RoyaleRuleset) Settings() Settings {
|
func (r RoyaleRuleset) Settings() Settings {
|
||||||
s := r.StandardRuleset.Settings()
|
s := r.StandardRuleset.Settings()
|
||||||
s.RoyaleSettings = RoyaleSettings{
|
s.RoyaleSettings = RoyaleSettings{
|
||||||
|
|
@ -90,8 +96,3 @@ func (r RoyaleRuleset) Settings() Settings {
|
||||||
}
|
}
|
||||||
return s
|
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) {
|
func TestRoyaleDefaultSanity(t *testing.T) {
|
||||||
boardState := &BoardState{}
|
boardState := &BoardState{}
|
||||||
r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 1}, ShrinkEveryNTurns: 0}
|
r := RoyaleRuleset{StandardRuleset: StandardRuleset{HazardDamagePerTurn: 1}, ShrinkEveryNTurns: 0}
|
||||||
_, err := r.CreateNextBoardState(boardState, []SnakeMove{})
|
_, err := r.CreateNextBoardState(boardState, []SnakeMove{{"", ""}})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Equal(t, errors.New("royale game can't shrink more frequently than every turn"), 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,
|
ShrinkEveryNTurns: test.ShrinkEveryNTurns,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.populateHazards(b)
|
_, err := PopulateHazardsRoyale(b, r.Settings(), mockSnakeMoves())
|
||||||
require.Equal(t, test.Error, err)
|
require.Equal(t, test.Error, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Obstacles should match
|
// Obstacles should match
|
||||||
|
|
@ -131,7 +131,7 @@ func TestRoyalDamageNextTurn(t *testing.T) {
|
||||||
stateAfterTurn := func(prevState *BoardState, turn int32) *BoardState {
|
stateAfterTurn := func(prevState *BoardState, turn int32) *BoardState {
|
||||||
nextState := prevState.Clone()
|
nextState := prevState.Clone()
|
||||||
nextState.Turn = turn - 1
|
nextState.Turn = turn - 1
|
||||||
err := r.populateHazards(nextState)
|
_, err := PopulateHazardsRoyale(nextState, r.Settings(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
nextState.Turn = turn
|
nextState.Turn = turn
|
||||||
return nextState
|
return nextState
|
||||||
|
|
@ -265,7 +265,16 @@ func TestRoyaleCreateNextBoardState(t *testing.T) {
|
||||||
ShrinkEveryNTurns: 1,
|
ShrinkEveryNTurns: 1,
|
||||||
}
|
}
|
||||||
rand.Seed(0)
|
rand.Seed(0)
|
||||||
|
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||||
|
ParamGameType: GameTypeRoyale,
|
||||||
|
ParamHazardDamagePerTurn: "1",
|
||||||
|
ParamShrinkEveryNTurns: "1",
|
||||||
|
})
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
gc.requireValidNextState(t, &r)
|
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")
|
ErrorNoRoomForFood = RulesetError("not enough space to place food")
|
||||||
ErrorNoMoveFound = RulesetError("move not provided for snake")
|
ErrorNoMoveFound = RulesetError("move not provided for snake")
|
||||||
ErrorZeroLengthSnake = RulesetError("snake is length zero")
|
ErrorZeroLengthSnake = RulesetError("snake is length zero")
|
||||||
|
ErrorEmptyRegistry = RulesetError("empty registry")
|
||||||
|
ErrorNoStages = RulesetError("no stages")
|
||||||
|
ErrorStageNotFound = RulesetError("stage not found")
|
||||||
|
|
||||||
// Ruleset / game type names
|
// Ruleset / game type names
|
||||||
GameTypeConstrictor = "constrictor"
|
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.
|
// Ruleset constructs a customised ruleset using the parameters passed to the builder.
|
||||||
func (rb rulesetBuilder) Ruleset() Ruleset {
|
func (rb rulesetBuilder) Ruleset() PipelineRuleset {
|
||||||
standardRuleset := &StandardRuleset{
|
standardRuleset := &StandardRuleset{
|
||||||
FoodSpawnChance: paramsInt32(rb.params, ParamFoodSpawnChance, 0),
|
FoodSpawnChance: paramsInt32(rb.params, ParamFoodSpawnChance, 0),
|
||||||
MinimumFood: paramsInt32(rb.params, ParamMinimumFood, 0),
|
MinimumFood: paramsInt32(rb.params, ParamMinimumFood, 0),
|
||||||
|
|
@ -138,13 +141,9 @@ func (rb rulesetBuilder) Ruleset() Ruleset {
|
||||||
StandardRuleset: *standardRuleset,
|
StandardRuleset: *standardRuleset,
|
||||||
}
|
}
|
||||||
case GameTypeSquad:
|
case GameTypeSquad:
|
||||||
squadMap := map[string]string{}
|
|
||||||
for id, squad := range rb.squads {
|
|
||||||
squadMap[id] = squad
|
|
||||||
}
|
|
||||||
return &SquadRuleset{
|
return &SquadRuleset{
|
||||||
StandardRuleset: *standardRuleset,
|
StandardRuleset: *standardRuleset,
|
||||||
SquadMap: squadMap,
|
SquadMap: rb.squadMap(),
|
||||||
AllowBodyCollisions: paramsBool(rb.params, ParamAllowBodyCollisions, false),
|
AllowBodyCollisions: paramsBool(rb.params, ParamAllowBodyCollisions, false),
|
||||||
SharedElimination: paramsBool(rb.params, ParamSharedElimination, false),
|
SharedElimination: paramsBool(rb.params, ParamSharedElimination, false),
|
||||||
SharedHealth: paramsBool(rb.params, ParamSharedHealth, false),
|
SharedHealth: paramsBool(rb.params, ParamSharedHealth, false),
|
||||||
|
|
@ -154,6 +153,42 @@ func (rb rulesetBuilder) Ruleset() Ruleset {
|
||||||
return standardRuleset
|
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.
|
// paramsBool returns the boolean value for the specified parameter.
|
||||||
// If the parameter doesn't exist, the default value will be returned.
|
// 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.
|
// 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.
|
// Errors should be treated as meaning the stage failed and the board state is now invalid.
|
||||||
type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error)
|
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
|
package rules
|
||||||
|
|
||||||
|
var soloRulesetStages = []string{
|
||||||
|
StageMovementStandard,
|
||||||
|
StageStarvationStandard,
|
||||||
|
StageHazardDamageStandard,
|
||||||
|
StageFeedSnakesStandard,
|
||||||
|
StageSpawnFoodStandard,
|
||||||
|
StageEliminationStandard,
|
||||||
|
StageGameOverSoloSnake,
|
||||||
|
}
|
||||||
|
|
||||||
type SoloRuleset struct {
|
type SoloRuleset struct {
|
||||||
StandardRuleset
|
StandardRuleset
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SoloRuleset) Name() string { return GameTypeSolo }
|
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) {
|
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) {
|
func GameOverSolo(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,14 @@ func TestSoloCreateNextBoardState(t *testing.T) {
|
||||||
soloCaseNotOver,
|
soloCaseNotOver,
|
||||||
}
|
}
|
||||||
r := SoloRuleset{}
|
r := SoloRuleset{}
|
||||||
|
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||||
|
ParamGameType: GameTypeSolo,
|
||||||
|
})
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
gc.requireValidNextState(t, &r)
|
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"
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var squadRulesetStages = []string{
|
||||||
|
StageMovementStandard,
|
||||||
|
StageStarvationStandard,
|
||||||
|
StageHazardDamageStandard,
|
||||||
|
StageFeedSnakesStandard,
|
||||||
|
StageSpawnFoodStandard,
|
||||||
|
StageEliminationStandard,
|
||||||
|
StageEliminationResurrectSquadCollisions,
|
||||||
|
StageModifySnakesShareAttributes,
|
||||||
|
StageGameOverBySquad,
|
||||||
|
}
|
||||||
|
|
||||||
type SquadRuleset struct {
|
type SquadRuleset struct {
|
||||||
StandardRuleset
|
StandardRuleset
|
||||||
|
|
||||||
|
|
@ -18,23 +30,13 @@ type SquadRuleset struct {
|
||||||
|
|
||||||
func (r *SquadRuleset) Name() string { return GameTypeSquad }
|
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) {
|
func (r *SquadRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||||
nextBoardState, err := r.StandardRuleset.CreateNextBoardState(prevState, moves)
|
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||||
if err != nil {
|
return nextState, err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func areSnakesOnSameSquad(squadMap map[string]string, snake *Snake, other *Snake) bool {
|
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]
|
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) {
|
func ResurrectSnakesSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
if IsInitialization(b, settings, moves) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
if !settings.SquadSettings.AllowBodyCollisions {
|
if !settings.SquadSettings.AllowBodyCollisions {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
@ -71,12 +71,10 @@ func ResurrectSnakesSquad(b *BoardState, settings Settings, moves []SnakeMove) (
|
||||||
return false, nil
|
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) {
|
func ShareAttributesSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
if IsInitialization(b, settings, moves) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
squadSettings := settings.SquadSettings
|
squadSettings := settings.SquadSettings
|
||||||
|
|
||||||
if !(squadSettings.SharedElimination || squadSettings.SharedLength || squadSettings.SharedHealth) {
|
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) {
|
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) {
|
func GameOverSquad(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
|
@ -152,8 +150,3 @@ func (r SquadRuleset) Settings() Settings {
|
||||||
}
|
}
|
||||||
return s
|
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) {
|
func TestSquadResurrectSquadBodyCollisionsSanity(t *testing.T) {
|
||||||
boardState := &BoardState{}
|
boardState := &BoardState{}
|
||||||
r := SquadRuleset{}
|
r := SquadRuleset{}
|
||||||
err := r.resurrectSquadBodyCollisions(boardState)
|
_, err := ResurrectSnakesSquad(boardState, r.Settings(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSquadSharedAttributesSanity(t *testing.T) {
|
func TestSquadSharedAttributesSanity(t *testing.T) {
|
||||||
boardState := &BoardState{}
|
boardState := &BoardState{}
|
||||||
r := SquadRuleset{}
|
r := SquadRuleset{}
|
||||||
err := r.shareSquadAttributes(boardState)
|
_, err := ShareAttributesSquad(boardState, r.Settings(), nil)
|
||||||
require.NoError(t, err)
|
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")
|
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
||||||
|
|
||||||
r := SquadRuleset{SquadMap: squadMap, AllowBodyCollisions: true}
|
r := SquadRuleset{SquadMap: squadMap, AllowBodyCollisions: true}
|
||||||
err := r.resurrectSquadBodyCollisions(boardState)
|
_, err := ResurrectSnakesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
||||||
|
|
@ -113,7 +113,7 @@ func TestSquadAllowBodyCollisionsEliminatedByNotSet(t *testing.T) {
|
||||||
"2": "red",
|
"2": "red",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := r.resurrectSquadBodyCollisions(boardState)
|
_, err := ResurrectSnakesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||||
require.Error(t, err)
|
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")
|
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
||||||
|
|
||||||
r := SquadRuleset{SharedHealth: true, SquadMap: squadMap}
|
r := SquadRuleset{SharedHealth: true, SquadMap: squadMap}
|
||||||
err := r.shareSquadAttributes(boardState)
|
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
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")
|
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
||||||
|
|
||||||
r := SquadRuleset{SharedLength: true, SquadMap: squadMap}
|
r := SquadRuleset{SharedLength: true, SquadMap: squadMap}
|
||||||
err := r.shareSquadAttributes(boardState)
|
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
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")
|
require.Equal(t, len(squadMap), len(boardState.Snakes), "squad map is wrong size, error in test setup")
|
||||||
|
|
||||||
r := SquadRuleset{SharedElimination: true, SquadMap: squadMap}
|
r := SquadRuleset{SharedElimination: true, SquadMap: squadMap}
|
||||||
err := r.shareSquadAttributes(boardState)
|
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
require.Equal(t, len(boardState.Snakes), len(testSnakes))
|
||||||
|
|
@ -291,7 +291,7 @@ func TestSquadSharedAttributesErrorLengthZero(t *testing.T) {
|
||||||
"2": "red",
|
"2": "red",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := r.shareSquadAttributes(boardState)
|
_, err := ShareAttributesSquad(boardState, r.Settings(), mockSnakeMoves())
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -547,8 +547,19 @@ func TestSquadCreateNextBoardState(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rand.Seed(0)
|
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 {
|
for _, gc := range standardCases {
|
||||||
gc.requireValidNextState(t, &r)
|
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{
|
extendedCases := []gameTestCase{
|
||||||
|
|
@ -557,7 +568,15 @@ func TestSquadCreateNextBoardState(t *testing.T) {
|
||||||
}
|
}
|
||||||
r.SharedHealth = true
|
r.SharedHealth = true
|
||||||
r.AllowBodyCollisions = true
|
r.AllowBodyCollisions = true
|
||||||
|
rb = rb.WithParams(map[string]string{
|
||||||
|
ParamSharedHealth: "true",
|
||||||
|
ParamAllowBodyCollisions: "true",
|
||||||
|
})
|
||||||
for _, gc := range extendedCases {
|
for _, gc := range extendedCases {
|
||||||
gc.requireValidNextState(t, &r)
|
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...)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
117
standard.go
117
standard.go
|
|
@ -13,6 +13,16 @@ type StandardRuleset struct {
|
||||||
HazardMapAuthor string // optional
|
HazardMapAuthor string // optional
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var standardRulesetStages = []string{
|
||||||
|
StageMovementStandard,
|
||||||
|
StageStarvationStandard,
|
||||||
|
StageHazardDamageStandard,
|
||||||
|
StageFeedSnakesStandard,
|
||||||
|
StageSpawnFoodStandard,
|
||||||
|
StageEliminationStandard,
|
||||||
|
StageGameOverStandard,
|
||||||
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) Name() string { return GameTypeStandard }
|
func (r *StandardRuleset) Name() string { return GameTypeStandard }
|
||||||
|
|
||||||
func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
|
func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*BoardState, error) {
|
||||||
|
|
@ -20,55 +30,22 @@ func (r *StandardRuleset) ModifyInitialBoardState(initialState *BoardState) (*Bo
|
||||||
return initialState, nil
|
return initialState, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
// impl Pipeline
|
||||||
// We specifically want to copy prevState, so as not to alter it directly.
|
func (r StandardRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
||||||
nextState := prevState.Clone()
|
return NewPipeline(standardRulesetStages...).Execute(bs, s, sm)
|
||||||
|
|
||||||
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 {
|
func (r *StandardRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||||
_, err := r.callStageFunc(MoveSnakesStandard, b, moves)
|
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||||
return err
|
return nextState, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func MoveSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
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 {
|
if len(moves) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
@ -164,12 +141,10 @@ func getDefaultMove(snakeBody []Point) string {
|
||||||
return MoveUp
|
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) {
|
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++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
if b.Snakes[i].EliminatedCause == NotEliminated {
|
if b.Snakes[i].EliminatedCause == NotEliminated {
|
||||||
b.Snakes[i].Health = b.Snakes[i].Health - 1
|
b.Snakes[i].Health = b.Snakes[i].Health - 1
|
||||||
|
|
@ -178,12 +153,10 @@ func ReduceSnakeHealthStandard(b *BoardState, settings Settings, moves []SnakeMo
|
||||||
return false, nil
|
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) {
|
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++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
snake := &b.Snakes[i]
|
snake := &b.Snakes[i]
|
||||||
if snake.EliminatedCause != NotEliminated {
|
if snake.EliminatedCause != NotEliminated {
|
||||||
|
|
@ -218,12 +191,10 @@ func DamageHazardsStandard(b *BoardState, settings Settings, moves []SnakeMove)
|
||||||
return false, nil
|
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) {
|
func EliminateSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
if IsInitialization(b, settings, moves) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
// First order snake indices by length.
|
// First order snake indices by length.
|
||||||
// In multi-collision scenarios we want to always attribute elimination to the longest snake.
|
// In multi-collision scenarios we want to always attribute elimination to the longest snake.
|
||||||
snakeIndicesByLength := make([]int, len(b.Snakes))
|
snakeIndicesByLength := make([]int, len(b.Snakes))
|
||||||
|
|
@ -378,11 +349,6 @@ func snakeHasLostHeadToHead(s *Snake, other *Snake) bool {
|
||||||
return false
|
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) {
|
func FeedSnakesStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
newFood := []Point{}
|
newFood := []Point{}
|
||||||
for _, food := range b.Food {
|
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) {
|
func SpawnFoodStandard(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
if IsInitialization(b, settings, moves) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
numCurrentFood := int32(len(b.Food))
|
numCurrentFood := int32(len(b.Food))
|
||||||
if numCurrentFood < settings.MinimumFood {
|
if numCurrentFood < settings.MinimumFood {
|
||||||
return false, PlaceFoodRandomly(b, settings.MinimumFood-numCurrentFood)
|
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) {
|
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) {
|
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
|
// impl Pipeline
|
||||||
func (r *StandardRuleset) callStageFunc(stage StageFunc, boardState *BoardState, moves []SnakeMove) (bool, error) {
|
func (r StandardRuleset) Err() error {
|
||||||
return stage(boardState, r.Settings(), moves)
|
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,
|
standardMoveAndCollideMAD,
|
||||||
}
|
}
|
||||||
r := StandardRuleset{}
|
r := StandardRuleset{}
|
||||||
|
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||||
|
ParamGameType: GameTypeStandard,
|
||||||
|
})
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
gc.requireValidNextState(t, &r)
|
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: "two", Move: test.MoveTwo},
|
||||||
{ID: "three", Move: test.MoveThree},
|
{ID: "three", Move: test.MoveThree},
|
||||||
}
|
}
|
||||||
err := r.moveSnakes(b, moves)
|
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, b.Snakes, 3)
|
require.Len(t, b.Snakes, 3)
|
||||||
|
|
@ -597,7 +604,7 @@ func TestMoveSnakesWrongID(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
r := StandardRuleset{}
|
||||||
err := r.moveSnakes(b, moves)
|
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||||
require.Equal(t, ErrorNoMoveFound, err)
|
require.Equal(t, ErrorNoMoveFound, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -622,7 +629,7 @@ func TestMoveSnakesNotEnoughMoves(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
r := StandardRuleset{}
|
||||||
err := r.moveSnakes(b, moves)
|
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||||
require.Equal(t, ErrorNoMoveFound, err)
|
require.Equal(t, ErrorNoMoveFound, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -647,7 +654,7 @@ func TestMoveSnakesExtraMovesIgnored(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
r := StandardRuleset{}
|
||||||
err := r.moveSnakes(b, moves)
|
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []Point{{1, 0}}, b.Snakes[0].Body)
|
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}}
|
moves := []SnakeMove{{ID: "one", Move: test.Move}}
|
||||||
|
|
||||||
err := r.moveSnakes(b, moves)
|
_, err := MoveSnakesStandard(b, r.Settings(), moves)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, b.Snakes, 1)
|
require.Len(t, b.Snakes, 1)
|
||||||
require.Equal(t, len(test.Body), len(b.Snakes[0].Body))
|
require.Equal(t, len(test.Body), len(b.Snakes[0].Body))
|
||||||
|
|
@ -795,25 +802,25 @@ func TestReduceSnakeHealth(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
r := StandardRuleset{}
|
r := StandardRuleset{}
|
||||||
err := r.reduceSnakeHealth(b)
|
_, err := ReduceSnakeHealthStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, b.Snakes[0].Health, int32(98))
|
require.Equal(t, b.Snakes[0].Health, int32(98))
|
||||||
require.Equal(t, b.Snakes[1].Health, int32(1))
|
require.Equal(t, b.Snakes[1].Health, int32(1))
|
||||||
require.Equal(t, b.Snakes[2].Health, int32(50))
|
require.Equal(t, b.Snakes[2].Health, int32(50))
|
||||||
|
|
||||||
err = r.reduceSnakeHealth(b)
|
_, err = ReduceSnakeHealthStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, b.Snakes[0].Health, int32(97))
|
require.Equal(t, b.Snakes[0].Health, int32(97))
|
||||||
require.Equal(t, b.Snakes[1].Health, int32(0))
|
require.Equal(t, b.Snakes[1].Health, int32(0))
|
||||||
require.Equal(t, b.Snakes[2].Health, int32(50))
|
require.Equal(t, b.Snakes[2].Health, int32(50))
|
||||||
|
|
||||||
err = r.reduceSnakeHealth(b)
|
_, err = ReduceSnakeHealthStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, b.Snakes[0].Health, int32(96))
|
require.Equal(t, b.Snakes[0].Health, int32(96))
|
||||||
require.Equal(t, b.Snakes[1].Health, int32(-1))
|
require.Equal(t, b.Snakes[1].Health, int32(-1))
|
||||||
require.Equal(t, b.Snakes[2].Health, int32(50))
|
require.Equal(t, b.Snakes[2].Health, int32(50))
|
||||||
|
|
||||||
err = r.reduceSnakeHealth(b)
|
_, err = ReduceSnakeHealthStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, b.Snakes[0].Health, int32(95))
|
require.Equal(t, b.Snakes[0].Health, int32(95))
|
||||||
require.Equal(t, b.Snakes[1].Health, int32(-2))
|
require.Equal(t, b.Snakes[1].Health, int32(-2))
|
||||||
|
|
@ -1214,7 +1221,7 @@ func TestMaybeEliminateSnakes(t *testing.T) {
|
||||||
Height: 10,
|
Height: 10,
|
||||||
Snakes: test.Snakes,
|
Snakes: test.Snakes,
|
||||||
}
|
}
|
||||||
err := r.maybeEliminateSnakes(b)
|
_, err := EliminateSnakesStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.Equal(t, test.Err, err)
|
require.Equal(t, test.Err, err)
|
||||||
for i, snake := range b.Snakes {
|
for i, snake := range b.Snakes {
|
||||||
require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause)
|
require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause)
|
||||||
|
|
@ -1254,7 +1261,7 @@ func TestMaybeEliminateSnakesPriority(t *testing.T) {
|
||||||
r := StandardRuleset{}
|
r := StandardRuleset{}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
b := &BoardState{Width: 10, Height: 10, Snakes: test.Snakes}
|
b := &BoardState{Width: 10, Height: 10, Snakes: test.Snakes}
|
||||||
err := r.maybeEliminateSnakes(b)
|
_, err := EliminateSnakesStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
for i, snake := range b.Snakes {
|
for i, snake := range b.Snakes {
|
||||||
require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause, snake.ID)
|
require.Equal(t, test.ExpectedEliminatedCauses[i], snake.EliminatedCause, snake.ID)
|
||||||
|
|
@ -1320,7 +1327,7 @@ func TestMaybeDamageHazards(t *testing.T) {
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
b := &BoardState{Snakes: test.Snakes, Hazards: test.Hazards, Food: test.Food}
|
b := &BoardState{Snakes: test.Snakes, Hazards: test.Hazards, Food: test.Food}
|
||||||
r := StandardRuleset{HazardDamagePerTurn: 100}
|
r := StandardRuleset{HazardDamagePerTurn: 100}
|
||||||
err := r.maybeDamageHazards(b)
|
_, err := DamageHazardsStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for i, snake := range b.Snakes {
|
for i, snake := range b.Snakes {
|
||||||
|
|
@ -1361,7 +1368,7 @@ func TestHazardDamagePerTurn(t *testing.T) {
|
||||||
}
|
}
|
||||||
r := StandardRuleset{HazardDamagePerTurn: test.HazardDamagePerTurn}
|
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.Error, err)
|
||||||
require.Equal(t, test.ExpectedHealth, b.Snakes[0].Health)
|
require.Equal(t, test.ExpectedHealth, b.Snakes[0].Health)
|
||||||
require.Equal(t, test.ExpectedEliminationCause, b.Snakes[0].EliminatedCause)
|
require.Equal(t, test.ExpectedEliminationCause, b.Snakes[0].EliminatedCause)
|
||||||
|
|
@ -1441,7 +1448,7 @@ func TestMaybeFeedSnakes(t *testing.T) {
|
||||||
Snakes: test.Snakes,
|
Snakes: test.Snakes,
|
||||||
Food: test.Food,
|
Food: test.Food,
|
||||||
}
|
}
|
||||||
err := r.maybeFeedSnakes(b)
|
_, err := FeedSnakesStandard(b, r.Settings(), nil)
|
||||||
require.NoError(t, err, test.Name)
|
require.NoError(t, err, test.Name)
|
||||||
require.Equal(t, len(test.ExpectedSnakes), len(b.Snakes), test.Name)
|
require.Equal(t, len(test.ExpectedSnakes), len(b.Snakes), test.Name)
|
||||||
for i := 0; i < len(b.Snakes); i++ {
|
for i := 0; i < len(b.Snakes); i++ {
|
||||||
|
|
@ -1477,7 +1484,7 @@ func TestMaybeSpawnFoodMinimum(t *testing.T) {
|
||||||
Food: test.Food,
|
Food: test.Food,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.maybeSpawnFood(b)
|
_, err := SpawnFoodStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.ExpectedFood, len(b.Food))
|
require.Equal(t, test.ExpectedFood, len(b.Food))
|
||||||
}
|
}
|
||||||
|
|
@ -1495,7 +1502,7 @@ func TestMaybeSpawnFoodZeroChance(t *testing.T) {
|
||||||
Food: []Point{},
|
Food: []Point{},
|
||||||
}
|
}
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
err := r.maybeSpawnFood(b)
|
_, err := SpawnFoodStandard(b, r.Settings(), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, len(b.Food), 0)
|
require.Equal(t, len(b.Food), 0)
|
||||||
}
|
}
|
||||||
|
|
@ -1513,7 +1520,7 @@ func TestMaybeSpawnFoodHundredChance(t *testing.T) {
|
||||||
Food: []Point{},
|
Food: []Point{},
|
||||||
}
|
}
|
||||||
for i := 1; i <= 22; i++ {
|
for i := 1; i <= 22; i++ {
|
||||||
err := r.maybeSpawnFood(b)
|
_, err := SpawnFoodStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, i, len(b.Food))
|
require.Equal(t, i, len(b.Food))
|
||||||
}
|
}
|
||||||
|
|
@ -1547,7 +1554,7 @@ func TestMaybeSpawnFoodHalfChance(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
rand.Seed(test.Seed)
|
rand.Seed(test.Seed)
|
||||||
err := r.maybeSpawnFood(b)
|
_, err := SpawnFoodStandard(b, r.Settings(), mockSnakeMoves())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, test.ExpectedFood, int32(len(b.Food)), "Seed %d", test.Seed)
|
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
|
package rules
|
||||||
|
|
||||||
|
var wrappedRulesetStages = []string{
|
||||||
|
StageMovementWrapBoundaries,
|
||||||
|
StageStarvationStandard,
|
||||||
|
StageHazardDamageStandard,
|
||||||
|
StageFeedSnakesStandard,
|
||||||
|
StageSpawnFoodStandard,
|
||||||
|
StageEliminationStandard,
|
||||||
|
StageGameOverStandard,
|
||||||
|
}
|
||||||
|
|
||||||
type WrappedRuleset struct {
|
type WrappedRuleset struct {
|
||||||
StandardRuleset
|
StandardRuleset
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *WrappedRuleset) Name() string { return GameTypeWrapped }
|
func (r *WrappedRuleset) Name() string { return GameTypeWrapped }
|
||||||
|
|
||||||
func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
func (r WrappedRuleset) Execute(bs *BoardState, s Settings, sm []SnakeMove) (bool, *BoardState, error) {
|
||||||
nextState := prevState.Clone()
|
return NewPipeline(wrappedRulesetStages...).Execute(bs, s, sm)
|
||||||
|
|
||||||
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 {
|
func (r *WrappedRuleset) CreateNextBoardState(prevState *BoardState, moves []SnakeMove) (*BoardState, error) {
|
||||||
_, err := r.callStageFunc(MoveSnakesWrapped, b, moves)
|
_, nextState, err := r.Execute(prevState, r.Settings(), moves)
|
||||||
return err
|
|
||||||
|
return nextState, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (bool, error) {
|
||||||
|
if IsInitialization(b, settings, moves) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
_, err := MoveSnakesStandard(b, settings, moves)
|
_, err := MoveSnakesStandard(b, settings, moves)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|
@ -65,6 +48,10 @@ func MoveSnakesWrapped(b *BoardState, settings Settings, moves []SnakeMove) (boo
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *WrappedRuleset) IsGameOver(b *BoardState) (bool, error) {
|
||||||
|
return GameOverStandard(b, r.Settings(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
func wrap(value, min, max int32) int32 {
|
func wrap(value, min, max int32) int32 {
|
||||||
if value < min {
|
if value < min {
|
||||||
return max
|
return max
|
||||||
|
|
|
||||||
|
|
@ -331,7 +331,14 @@ func TestWrappedCreateNextBoardState(t *testing.T) {
|
||||||
wrappedCaseMoveAndWrap,
|
wrappedCaseMoveAndWrap,
|
||||||
}
|
}
|
||||||
r := WrappedRuleset{}
|
r := WrappedRuleset{}
|
||||||
|
rb := NewRulesetBuilder().WithParams(map[string]string{
|
||||||
|
ParamGameType: GameTypeWrapped,
|
||||||
|
})
|
||||||
for _, gc := range cases {
|
for _, gc := range cases {
|
||||||
gc.requireValidNextState(t, &r)
|
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