forked from BattlesnakeOfficial/rules
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pipeline.go
193 lines (170 loc) · 6.94 KB
/
pipeline.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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,
}
// 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)
// 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 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
}