Byte-snake-engine/pipeline.go
Rob O'Dwyer 82e1999126
DEV-1761: New rules API (#118)
* DEV-1761: Clean up Ruleset interface (#115)

* remove legacy ruleset types and simplify ruleset interface

* remove unnecessary settings argument from Ruleset interface

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

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

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

* allow maps to access BoardState.GameState,PointState

* add PreUpdateBoard and refactor snail_mode with it

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

* fix formatting

* fix lint errors

Co-authored-by: JonathanArns <jonathan.arns@googlemail.com>
2022-10-28 16:49:49 -07:00

202 lines
7.4 KiB
Go

package rules
import "fmt"
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"
StageSpawnFoodNoFood = "spawn_food.no_food"
StageSpawnHazardsShrinkMap = "spawn_hazards.shrink_map"
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,
StageGameOverStandard: GameOverStandard,
StageHazardDamageStandard: DamageHazardsStandard,
StageSpawnHazardsShrinkMap: PopulateHazardsRoyale,
StageStarvationStandard: ReduceSnakeHealthStandard,
StageFeedSnakesStandard: FeedSnakesStandard,
StageEliminationStandard: EliminateSnakesStandard,
StageModifySnakesAlwaysGrow: GrowSnakesConstrictor,
StageMovementStandard: MoveSnakesStandard,
StageMovementWrapBoundaries: MoveSnakesWrapped,
}
// 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
}
// StageFunc represents a single stage of an ordered pipeline and applies custom logic to the board state each turn.
// It is expected to modify the boardState directly.
// The return values are a boolean (to indicate whether the game has ended as a result of the stage)
// and an error if any errors occurred during the stage.
//
// Errors should be treated as meaning the stage failed and the board state is now invalid.
type StageFunc func(*BoardState, Settings, []SnakeMove) (bool, error)
// IsInitialization checks whether the current state means the game is initialising (turn zero).
// Useful for StageFuncs that need to apply different behaviour on initialisation.
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
}
// StageRegistry is a mapping of stage names to stage functions
type StageRegistry map[string]StageFunc
// 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 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
}