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:
Torben 2022-04-19 15:52:57 -07:00 committed by GitHub
parent 86ef6ad068
commit d378759d58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 723 additions and 235 deletions

View file

@ -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()
}