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
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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue