jkomoros / boardgame

An in-progress framework in golang to easily build boardgame Progressive Web Apps
Apache License 2.0
31 stars 4 forks source link

Notion of computedProperties #146

Closed jkomoros closed 7 years ago

jkomoros commented 7 years ago

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

jkomoros commented 7 years ago

dynamic groups for sanitization feel similar. #160

jkomoros commented 7 years ago

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.

jkomoros commented 7 years ago

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.

jkomoros commented 7 years ago

You could have State. have a computedProperties taht returns a PropertyReader object that has an underlying concrete type like States do.

jkomoros commented 7 years ago

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?

jkomoros commented 7 years ago
jkomoros commented 7 years ago

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

jkomoros commented 7 years ago

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.

jkomoros commented 7 years ago

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.

jkomoros commented 7 years ago

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

jkomoros commented 7 years ago

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.

jkomoros commented 7 years ago

ComputedProperties should be exported to the client-side state object somehow

jkomoros commented 7 years ago

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.

jkomoros commented 7 years ago

The hard part is that ComputedProperties need to be able to handle sanitized values, which might invalidate some of them.

jkomoros commented 7 years ago

Also, properites could rely on other properties...

jkomoros commented 7 years ago

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.

jkomoros commented 7 years ago

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)

jkomoros commented 7 years ago

Also, obviously, computed state's property reader should lazily calculate the properties

jkomoros commented 7 years ago

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
}
jkomoros commented 7 years ago
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)
}
jkomoros commented 7 years ago

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)
}
jkomoros commented 7 years ago

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)
}
jkomoros commented 7 years ago

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.

jkomoros commented 7 years ago