-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmove.go
456 lines (386 loc) · 16.6 KB
/
move.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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
package boardgame
import (
"encoding/json"
"time"
"github.com/jkomoros/boardgame/errors"
)
//MoveType represents a type of a move in a game, and information about that
//MoveType. New Moves are constructed by calling its NewMove() method. Fields
//are hidden to prevent modifying them once a game has been SetUp. New ones
//cannot be created directly; they are created via
//GameManager.AddMoveType(moveTypeConfig).
type moveType struct {
name string
constructor func() Move
validator *StructInflater
customConfiguration PropertyCollection
manager *GameManager
}
//MoveConfig is a collection of information used to create a Move. Your
//delegate's ConfigureMoves() will emit a slice of them to define which moves
//are valid for your game. Typically you'll use moves.Combine, moves.Add,
//moves.AddWithPhase, combined with moves.AutoConfigurer.Configure() to
//generate these. This is an interface and not a concrete struct because other
//packages, like moves, add more behavior to the ones they return. If you want
//just a vanilla one without using the moves package, use NewMoveConfig.
type MoveConfig interface {
//Name is the name for this type of move. No other Move structs
//in use in this game should have the same name, but it should be human-
//friendly. For example, "Place Token" is a reasonable name, as long as no
//other types of Move-structs will return that name in this game. Required.
Name() string
//Constructor should return a zero-valued Move of the given type. Normally
//very simple: just a new(MyMoveType). Required. The moves you create may
//not have an fields of Stack, Board, or Timer type, but may have enum.Val
//type. Those fields must be non-nil; like delegate.GameStateConstructor
//and others, a StructInflater will be created for each move type, which
//allows you to provide inflation configuration via struct tags. See
//StructInflater for more. Like ConfigurableSubState, all of the
//properties to persist must be accessible via their ReadSetConfigurer, as
//that is how the core engine serializes them, re-inflates them from
//storage, and copies them.
Constructor() func() Move
//CustomConfiguration is an optional PropertyCollection. Some move types--
//especially in the `moves` package--stash configuration options here that
//will change how all moves of this type behave. Individual moves would
//reach through via Info().CustomConfiguration() to retrieve the
//values stored there. Different move types will store different types of
//information there--to avoid a collision the convention is to use a
//string name that starts with your fully qualified package name, then a
//dot, then the propertyname, like so:
//"github.com/jkomoros/boardgame/moves.MoveName". Those strings are often
//encoded as package-private constants, and a
//interfaces.CustomConfigurationOption functor factory is provided to set
//those from outside the package. Generally you don't use this directly,
//but moves.AutoConfigurer will help you set these for what specific
//moves in that package expect.
CustomConfiguration() PropertyCollection
}
type defaultMoveConfig struct {
name string
constructor func() Move
customConfiguration PropertyCollection
}
func (d defaultMoveConfig) Name() string {
return d.name
}
func (d defaultMoveConfig) Constructor() func() Move {
return d.constructor
}
func (d defaultMoveConfig) CustomConfiguration() PropertyCollection {
return d.customConfiguration
}
//NewMoveConfig returns a simple MoveConfig that will return the provided
//parameters from its getters. Typically you don't use this, but rather use
//the output of moves.AutoConfigurer.Config().
func NewMoveConfig(name string, constructor func() Move, customConfiguration PropertyCollection) MoveConfig {
return defaultMoveConfig{
name,
constructor,
customConfiguration,
}
}
const newMoveTypeErrNoManagerPassed = "No manager passed, so we can'd do validation"
//NewMoveType takes a MoveConfig and returns a MoveType associated with
//the given manager. The returned move type will not yet have been added to
//the manager in question. In general you don't call this directly, and
//instead use manager.AddMove, which accepts a MoveConfig.
func newMoveType(config MoveConfig, manager *GameManager) (*moveType, error) {
if config == nil {
return nil, errors.New("No config provided")
}
if config.Name() == "" {
return nil, errors.New("No name provided")
}
if config.Constructor() == nil {
return nil, errors.New("No MoveConstructor provided")
}
exampleMove := config.Constructor()()
if exampleMove == nil {
return nil, errors.New("Constructor returned nil")
}
readSetter := exampleMove.ReadSetter()
if readSetter == nil {
return nil, errors.New("Constructor's readsetter returned nil")
}
var validator *StructInflater
var err error
//moves.Defaultconfig will call this without a manager. So return a half-
//useful object in that case... but also an error so anyone else who
//checks the error will ignore the half-useful move type.
if manager != nil {
validator, err = NewStructInflater(exampleMove, moveTypeIllegalPropTypes, manager.Chest())
if err != nil {
return nil, errors.New("Couldn't create validator: " + err.Error())
}
} else {
//moves.DefaultConfig hackily looks for exactly this error string.
err = errors.New(newMoveTypeErrNoManagerPassed)
}
return &moveType{
name: config.Name(),
constructor: config.Constructor(),
customConfiguration: config.CustomConfiguration(),
validator: validator,
manager: manager,
}, err
}
//OrphanExampleMove returns a move from the config that will be similar to a
//real move, in terms of struct-based auto-inflation, etc. This is exposed
//primarily for moves.AutoConfigrer, and generally shouldn't be used by
//others.
func (m *ManagerInternals) OrphanExampleMove(config MoveConfig) (Move, error) {
throwAwayMoveType, err := newMoveType(config, nil)
if err != nil {
//Look for exatly the single kind of error we're OK with. Yes, this is a hack.
if err.Error() != newMoveTypeErrNoManagerPassed {
return nil, errors.New("Couldn't create intermediate move type: " + err.Error())
}
}
return throwAwayMoveType.NewMove(nil), nil
}
//MoveInfo is an object that contains meta-information about a move. It is
//fetched via move.Info().
type MoveInfo struct {
moveType *moveType
version int
initiator int
name string
timestamp time.Time
}
//Move is the struct that are how all modifications are made to States after
//initialization. Packages define structs that implement different Moves for all
//types of valid modifications. Moves are objects your own packages will
//returen. Use base.Move or moves.Default for a convenient composable base Move
//that will allow you to skip most of the boilerplate overhead. Your Move is
//similar to a SubState in that all of the persistable properties must be one of
//the enumerated types in PropertyType, excluding a few types. Your Moves are
//installed based on what your GameDelegate returns from ConfigureMoves(). See
//MoveConfig for more about things that must be true about structs you return.
//The two primary methods for your game logic are Legal() and Apply().
type Move interface {
//Legal returns nil if this proposed move is legal at the given state, or
//an error if the move is not legal. The error message may be shown
//directly to the end- user so be sure to make it user friendly. proposer
//is set to the notional player that is proposing the move. proposer might
//be a valid player index, or AdminPlayerIndex (for example, if it is a
//FixUpMove it will typically be AdminPlayerIndex). AdminPlayerIndex is
//always allowed to make any move. It will never be ObserverPlayerIndex,
//because by definition Observers may not make moves. If you want to check
//that the person proposing is able to apply the move for the given
//player, and that it is their turn, you would do something like test
//m.TargetPlayerIndex.Equivalent(proposer),
//m.TargetPlayerIndex.Equivalent(game.CurrentPlayer). Legal is one of the
//most key parts of logic for your game type. It is important for fix up
//moves in particular to have carefully-designed Legal() methods, as the
//ProposeFixUpMove on base.GameDelegate (which you almost always use)
//walks through each move and returns the first one that is legal at this
//game state--so if one of your moves is erroneously legal more often than
//it should be it could be mistakenly applied, perhaps in an infinite
//loop!
Legal(state ImmutableState, proposer PlayerIndex) error
//Apply applies the move to the state by modifying hte right properties.
//It is handed a copy of the state to modify. If error is non-nil it will
//not be applied to the game. It should not be called directly; use
//Game.ProposeMove. Legal() will have been called before and returned nil.
//Apply is the only place (outside of some of the Game initalization logic
//on GameDelegate) where you are allowed to modify the state direclty and
//are passed a State, not an ImmutableState.
Apply(state State) error
//All of the methods below this point are typically provided by base.Move
//and not necessary to be modified.
//Sets the move to have reasonable defaults for the given state.For
//example, if the Move has a TargetPlayerIndex property, a reasonable
//default is state.CurrentPlayer(). DefaultsForState is used to set
//reasonable defaults for fix up moves. Typically you can skip this.
DefaultsForState(state ImmutableState)
//HelpText is a human-readable sentence describing what the move does in
//general. HelpText should be the same for all moves of the same type, and
//should not vary with the Move's specific properties. For example, the
//HelpText for "Place Token" might be "Places the current user's token in
//the specified slot on the board." Primarily useful just to show to a
//user in an interface.
HelpText() string
//Info returns the MoveInfo object that was affiliated with this object by
//SetInfo. It includes information about when the move was applied, the
//name of the move, and other information.
Info() *MoveInfo
//SetInfo will be called after the constructor is called to set the
//information, including what type the move is.
SetInfo(m *MoveInfo)
//TopLevelStruct should return the value that was set via
//SetTopLevelStruct. It returns the Move that is at the top of the
//embedding chain (because structs that are embedded anonymously can only
//access themselves and not their embedders). This is useful because in a
//number of cases embedded moves (for example moves in the moves package)
//need to consult a method on their embedder to see if any of their
//behavior should be overridden.
TopLevelStruct() Move
//SetTopLevelStruct is called right after the move is constructed, with
//the top-level struct. This should be returned from TopLevelStruct.
SetTopLevelStruct(m Move)
//Moves alos have a ValidConfiguration, because moves, especially
//sub-classes of the moves package, require set-up that can only be verified
//at run time (for example, verifying that the embedder implements a certain
//inteface)
ConfigurationValidator
//Moves, like ConfigurableSubStates, must only have all of their
//important, persistable properties available to be inspected and modified
//via a PropertyReadSetConfigurer. The game engine will use that interface
//to create new moves, inflate old moves from storage, and copy moves.
//Typically you generate this automatically for your moves with `boargame-
//util codegen`.
ReadSetConfigurer
}
//ConfigurationValidator is an interface that certain types must implement.
//These will be called typically during NewGameManager set up, and are an
//opportunity for the structs to report configuration errors that can only be
//discovered at runtime. If an error is reported then NewGameManager will fail,
//which means that the misconfiguration can be detected early almost
//guaranteeing it will be detected by the game package author. For example, many
//moves in the moves package must be embedded in structs that contain particular
//methods in the embedding struct, and that can only be validated at runtime.
//Typically you don't need to implement this yourself; the types of structs that
//have it will have a stub implementation in the base package, and the primary
//beneficiaries of this are more complex embeddable library structs like those
//found in the moves package.
type ConfigurationValidator interface {
//ValidConfiguration will be checked when the NewGameManager is being set
//up, and if it returns an error the manager will fail to be created.
ValidConfiguration(exampleState State) error
}
//StorageRecordForMove returns a MoveStorageRecord. Can't hang off of Move
//itself since Moves are provided by users of the library.
func StorageRecordForMove(move Move, currentPhase int, proposer PlayerIndex) *MoveStorageRecord {
blob, err := json.MarshalIndent(move, "", "\t")
if err != nil {
return nil
}
return &MoveStorageRecord{
Name: move.Info().Name(),
Version: move.Info().version,
Initiator: move.Info().initiator,
Timestamp: move.Info().timestamp,
Phase: currentPhase,
Proposer: proposer,
Blob: blob,
}
}
//Name returns the name of the move type that this move is, based on the value
//passed in the affiliated MoveConfig from your GameDelegate.ConfigureMoves().
//Calling manager.ExampleMove() with that string value will return a similar
//struct.
func (m *MoveInfo) Name() string {
return m.name
}
//Version returns the version of this move--or the version that it will be
//when successfully committed.
func (m *MoveInfo) Version() int {
return m.version
}
//Timestamp returns the time that the given move was made.
func (m *MoveInfo) Timestamp() time.Time {
return m.timestamp
}
//CustomConfiguration returns the configuration object associated with this
//move when it was installed from its MoveConfig.CustomConfiguration().
func (m *MoveInfo) CustomConfiguration() PropertyCollection {
if m.moveType == nil {
return nil
}
return m.moveType.customConfiguration
}
//Initiator returns the move version that initiated this causal chain: the
//player Move that was applied that led to this chain of fix up moves as
//proposed by GameDelegate.ProposeFixUpMove. The Initiator of a PlayerMove is
//its own version, so this value will be less than or equal to its own
//version. The value of Initator is unspecified until after the move has been
//successfully committed.
func (m *MoveInfo) Initiator() int {
return m.initiator
}
var moveTypeIllegalPropTypes = map[PropertyType]bool{
TypeStack: true,
TypeBoard: true,
TypeTimer: true,
}
//Name returns the unique name for this type of move.
func (m *moveType) Name() string {
return m.name
}
//NewMove returns a new move of this type, with defaults set for the given
//state. If state is nil, then DefaultsForState will not be called.
func (m *moveType) NewMove(state ImmutableState) Move {
move := m.constructor()
if move == nil {
return nil
}
info := &MoveInfo{
moveType: m,
name: m.Name(),
}
if state != nil {
info.version = state.Version() + 1
}
move.SetInfo(info)
move.SetTopLevelStruct(move)
//validator might be nil if we have a half-functioning MoveType. (Like
//what will be returned, along with an error, when NewMoveType is called
//during moves.DefaultConfig)
if m.validator != nil {
if err := m.validator.Inflate(move, state); err != nil {
m.manager.Logger().Error("AutoInflate had an error: " + err.Error())
return nil
}
if err := m.validator.Valid(move); err != nil {
m.manager.Logger().Error("Move was not valid: " + err.Error())
return nil
}
}
if state != nil {
move.DefaultsForState(state)
}
return move
}
//We implement a private stub of base.Move in this package just for the
//convience of our own test structs.
type baseMove struct {
info MoveInfo
topLevelStruct Move
}
//baseFixUpMove is same as baseMove but returns true for IsFixUp.
type baseFixUpMove struct {
baseMove
}
func (d *baseMove) HelpText() string {
return "Unimplemented"
}
func (d *baseMove) SetInfo(m *MoveInfo) {
d.info = *m
}
func (d *baseMove) Info() *MoveInfo {
return &d.info
}
func (d *baseMove) SetTopLevelStruct(m Move) {
d.topLevelStruct = m
}
func (d *baseMove) TopLevelStruct() Move {
return d.topLevelStruct
}
func (d *baseMove) IsFixUp() bool {
return false
}
func (d *baseFixUpMove) IsFixUp() bool {
return true
}
//DefaultsForState doesn't do anything
func (d *baseMove) DefaultsForState(state ImmutableState) {
return
}
//Description defaults to returning the Type's HelpText()
func (d *baseMove) Description() string {
return d.TopLevelStruct().HelpText()
}
func (d *baseMove) ValidConfiguration(exampleState State) error {
return nil
}