Closed jkomoros closed 7 years ago
dynamic groups for sanitization feel similar. #160
Notion of computedProperties on GameManager.
func(state State) interface{}, stored in a map. After each state update, run them and store the results in a map[string]interface{}.
Ideally there'd be some type safety, and some way to cache the results by defining the properties they depend on or something.
In a perfect world they'd e wired up so that the State object would be able to return them, too--does that require states to have a link back to manager?
Then anyone with a State object could treat computedProperties mostly like just normal properties.
You could have State. have a computedProperties taht returns a PropertyReader object that has an underlying concrete type like States do.
States grow a Computed() *ComputedProperties method.
Under the covers, their impl does this:
if (s.computed == nil) {
s.computed = boardgame.NewComputedProperties(state, config)
}
return s.computed
Where config is a collection of maps and stuff that was created once in their state.go and then handed out there.
const LegaPropertyType TypeInt TypeBool TypeGrowableStack TypeSizedStack ... )
ComputedProperties has: Props() map[string]LegalPropertyType, as well as IntProp(name) int, BoolProp(name) bool, etc.
... is that the signature that PropertyReader should have??
TODO: how do we handle properties for a specific user, like HandValue?
If that is the new signature of PropertyReader, (a Reader()) then we can have a Computed() that also returns a PropertyReader. Easy peasy!
... Except how to handle the notion of a property for a specific User.
Nah, that's fine, if ComputedProperties embeds PropertyReader as well as Player(index int) PropertyReader
There are some use cases where the input to the computedProperty has to look at all state (e.g. numGhosts). There are some use cases where it should be tied to a specific PlayerState.
There are two types of computed properties: generic ones and player-specific-ones.
type ComputedProperties interface {
PropertyReader
Player(index int) PropertyReader
}
This will be much nicer to use once #164 is fixed and PropertyReader has convenient typed getters.
In practice most states will create a DefaultComputedProperties like so:
//In boardgame
type PropertyComputer func(state State) interface{}
type DefaultComputedProperties struct {
config *ComputedPropertiesConfig
memoizedValues <whatevertype>
}
var myConfig *boardgame.ComputedPropertiesConfig
func init() {
//The config is an expensive, big object--so we should compute it once when we init and then use the same one everywhere.
//TODO config should actually be split up by types, ala propertyReader.
myConfig = &boardgame.ComputedPropertiesConfig{
Game: map[string]boardgame.PropertyComputer{
"NumGhosts" : func(state) interface{} {
s := state.(*myState)
return s.NumGhosts()
}
},
Player: map[string]boardgame.PropertyComputer {
"HandValue" : func(state, index) interface{} {
s := state.(*myState)
p := s.Players[index]
return p.HandValue()
}
}
}
}
func (s *State) Computed() boardgame.ComputedProperties {
if s.computed == nil {
s.computed = boardgame.NewDefaultComputedProperties(s, myConfig)
}
return s.computed
}
ComputedPropertiesConfig is kind of ugly to expose from boardgame, which implies that it's actually a sub-package that does it, similar to #132
As noted in the example above, the majority of funcs will be small shims that just call the equivalent method on the specific concrete state struct underneath.
The map[string]boardgame.PlayerPropertyComputer and all of that is so messy--such long variables! That again is an argument for it being in a sub-package, ala #132.
ComputedProperties should be exported to the client-side state object somehow
ComputedProperties shoudl return an error if the state object htey are given is sanitized in a way that makes it impossible to calculate the real value. Basically, they might be passed a sanitized or unsanitized state and they shoudl be resilient to that.
The hard part is that ComputedProperties need to be able to handle sanitized values, which might invalidate some of them.
Also, properites could rely on other properties...
Also, the interface of PropertyReader/Player(int)PropertyReader doesn't work, because some computed properties need to get the entire state object.
Ideally your config would include which properties you depended on (other computed properties, game properties, player state properties (if you asked for one you'd get them all)) and you'd be handed a PropertyReader with that information and just that information. That would help ensure that you actually defined your dependencies. And then we could calculate the dependency chain for computed properties that rely on other computed properties.
Computed properties likely require more proprerty types. Like blackjack.EffectiveHand is a playingcards.card. So I guess just have an interface for the unsupported types? (gross)
Also, obviously, computed state's property reader should lazily calculate the properties
type GroupType int
const (
GroupGame GroupType = iota
GroupPlayer
GroupComputedProperty
)
type StatePropertyRef struct {
Group GroupType //If GroupType is GroupPlayer, then we'll make sure that each player is referenced
PropName string
}
type PlayerStatePropertyRef string
func (s *StatePropertyRef) Value(state *State) interface{} {
// Fetch the given property from the state and return it
}
type ComputedPropertyDefinition struct {
Arguments []StatePropertyRef
//F will be called with the Value() of each StatePropertyRef in arg called, in order.
F func(...args interface{}) (interface{}, error)
}
type ComputedPropertyReader {
PropertyReader
Player(index int) PropertyReader
}
func (s *State) Computed() ComputedPropertyReader {
//Either memoized to only return the ones that are asked for, or precomputed (if we store all of them every time)
}
It seems really finicky to have everything be interfaces. I like the idea that you describe what your inputs are from state, and we return a State-like thing that has PropertyReaders that will error if you touch one you didn't tell us you needed.
type ComputedProperties interface {
PropertyReader
Player(int) PropertyReader
}
func (s *State) Computed() ComputedProperties {
//Lazily creates a new computedProperties by passing the config from delegate.ComputedPropertiesConfig
}
type ComputedPropertiesConfig struct {
Definitions map[string]ComputedPropertyDefinition
PlayerDefinitions map[string]ComputedPlayerPropertyDefinition
}
type GameDelegate interface {
//...
//Returns the config to use for computing properties
ComputedPropertiesConfig() ComputedPropertiesConfig
//...
}
type ShadowPlayerState struct {
PropertyReader
Computed PropertyReader
}
//These are passed to the Func of ComputedProperty. Reading a property that wasn't configured as a dependency will return a zero value and error.
type ShadowState struct {
Game PropertyReader
Players []ShadowPlayerState
Computed PropertyReader
}
type ComputedPropertyDefinition struct {
Dependencies []StatePropertyRef
//F will be called with a shadow state where you are only allowed to read out the properties you listed as dependencies.
F func(state *ShadowState) (interface{}, error)
}
type PlayerStatePropertyRef struct {
Group GroupType //only Player, Computed allowed
PropName string
}
type ComputedPlayerPropertyDefinition struct {
Dependencies []PlayerStatePropertyRef
F func(playerState *ShadowPlayerState) (interface{}, error)
}
Hmmm, PlayerComputedProperty feels very different from GlobalComputedProperty. How do we store the computed properties when we serialize them? I guess in the Computed JSON blob that's exported we also export an array of PlayerComputedProperties, one for each player.
Things that can be fully derived from the rest of state, but are useful to have, or expensive to calculate.
In the future it will be nice to be able to create little bundles of script that can be runclientside to create these, too.
gameView.expandGameState should be where we calculate and plug those in.
Part of this was originally captured in #79