Closed jkomoros closed 7 years ago
boardgame/enum.go
type Enum struct {
names map[EnumConstant]string
low, high int
}
func (e *Enum) Valid(v EnumConstant) bool {
if v < low || v > high {
return false
}
return true
}
func (e *Enum) String(v EnumConstant) string {
return e.names[v]
}
//EnumProperty should be used like a Stack: as a property on Game or Player state. You create them in EmptyGameState initialized with the enum name
type EnumProperty struct {
enumName string
value EnumConstant
}
func (e *EnumProperty) Valid() bool {
return e.state.game.getEnum(e.enumName).Valid(e.value)
}
func (e *EnumProperty) String() string {
//... similar to Valid()
}
func (e *EnumProperty) Value() EnumConstant {
//... similar to Valid()
}
func (e *EnumProperty) SetValue(v EnumConstant) bool {
enum := e.state.game.getEnum(e.enumName)
if e.enumName != v.enum {
return false
}
if !enum.Valid(v) {
return false
}
e.value = v
return true
}
type EnumConstant struct {
enum string
value int
}
//enumConstant can't have a String() because we don't have a canonical reference to the enum, we just know its name
type (e *EnumConstant) Int() int {
return e.int
}
func (g *GameManager) AddEnum(name string, values ...EnumConstant) {
//Create a new Enum with the given name. The values will automatically get stringified by taking the name of them, getting rid of the prefix of name, and using those as the string value.
}
mygame/components.go
const (
CardBlue = iota
CardGreen
CardRed
)
mygame/main.go
//in NewManager...
manager.AddEnum("Card",CardBlue, CardGreen, CardRed)
//...
Another approach is to have all enums be unique ints.
The pattern would look like:
const (
CardUnknown = iota
CardSpade
CardHeart
)
const (
ColorBlue = CardHeart + 1 + iota
ColorGreen
ColorRed
)
Also, if you register enums with pointers does that allow us to get the constant name?
OK, that above pattern does work, and the fact that what I was calling EnumConstant
are actually ints is useful. So going again:
boardgame/enum.go
type enumRecord struct {
enumName string
str string
}
type GameManager struct {
//...
enums map[int]enumRecord
//...
}
func (g *GameManager) AddEnum(enumName, values int...) error {
for _, v := range values {
if _, ok := g.enums[v]; ok {
//Already registered
return errors.New("Already registered")
}
//TODO: set default str using reflection or something
g.enums[v] = enumRecord{enumName, ""}
}
return nil
}
func (g *GameManager) EnumMembership(value int) string {
return g.enums[value].enumName
}
type EnumProperty struct {
statePtr *state
enumName string
value int
}
func (e *EnumProperty) SetValue(v int) error {
enumName := e.statePtr.game.manager.EnumMembership(v)
if enumName != e.enumName {
return errors.New("Set of a value not part of enum")
}
e.value = v
}
func (e *EnumProperty) Value() int {
return e.value
}
func NewEnumProperty(enumName string) *EnumProperty {
return &EnumProperty{nil, enumName, -1}
}
Enums are included in the Chest payload
The fact that you need a constructor for Enum is a huge pain (and also breaks this), and makes it hard for them to be in places that totally make sense, like Moves.
What if you created a set of constructors at the top-level at the very beginning that were connected to an enummanager? (Does this even make sense?)
type Enum struct {
Name string
Manager *EnumManager
}
//Does something like this work? Is it even useful?
var enum = &boardgame.EnumManager{}
var ColorEnum = enum.NewEnum("Color")
const (
ColorUnknown = iota
ColorRed
ColorGreen
ColorBlue
)
func init() {
enum.Add("Color", map[int]string{
ColorUnknown: "Unknown",
ColorRed: "Red",
ColorGreen: "Green",
ColorBlue: "Blue",
})
}
func (g *gameDelegate) EmptyGameState() {
return &gameState{
MyEnum: boardgame.NewEnumValue(ColorEnum)
}
}
If we were to go this route we'd have to bring our own, pre-finished EnumManager to Chest, and all games of a certain type would share the same one. (That's OK, as long as they're frozen)
What's the set of things that can be done outside a method? Do they have to all be statically computable? That's OK...
type EnumManager struct {
enums map[string]*Enum
//If you call Finish() and this is not nil, return an error... allowing us to have NewEnum be usable at top-level
failedEnums []string
}
type Enum struct {
Name string
Manager *EnumManager
}
func NewEnumManager() *EnumManager {
return &EnumManager{}
}
func (e *EnumManager) NewEnum(name string, values map[int]string) *Enum {
//Process values
return &Enum{name, e}
}
//Doesn't need an inflate; always has its enum! ... wait, does it in UnmarshalJSON? ... I think it does, but it didn't when I was doing this earlier just because I hadn't called inflate before it was called...
type EnumValue struct {
enum *Enum
val int
}
func (e *Enum) NewValue() *EnumValue {
return &EnumValue{e, e.DefaultValue()}
}
func (e *EnumValue) SetValue(value int) bool {
}
func (e *EnumValue) String() string {
}
func (e *EnumValue) Lock() {
}
func (e *EnumValue) SetValueFromString(value string) bool {
v := e.enum.ValueFromString(string)
return e.SetValue(v)
}
const (
ColorUnknown = iota
ColorRed
ColorGreen
)
var enumManager = NewEnumManager()
//How to handle when this is an error? We can't do any processing at top-level... NewEnum could keep track of that you tried to create Color but it failed, and then have Finish() fail later with that message
var ColorEnum = enumManager.NewEnum("Color", map[int]string{
ColorUnknown: "Unknown",
ColorRed: "Red",
ColorGreen: "Green",
})
func (g *gameDelegate) EmptyGameState() {
return &gameState{
MyColor: ColorEnum.NewValue(),
}
}
... Wait, why do we insist that enum values not overlap within a set?
the reason is so that we can expand an int from an unknown enum from the serialization into an EnumValue that references the proper Enum... as long as we know the EnumSet in use. (that is, the field doesn't need any decoration; as long as you know it's an enum value and have the enumset in use, it can just be represented as a bare int and we can expand it).
If we just serailize to string for storage and for transmission then we don't need to transmit the enumset in component chest, and we also don't need to be able to do that party trick... which means enums can overlap, which is much less of a pain to use.
How we do or don't support Moves having enum will be the decider here... it also depends on what is idiomatic for ComponentValues to have. If it's just the bare int then it might invalidate all of this because we'll need a client-side map, which means we'll need all of these party tricks. If componentValues do just use the bare int, how are we supposed to know client-side how to expand them?
We could have a enum.Constant
which is how you tell us that the thing is an int that indexes into the int set and should be expanded whenever being serialized? If that's the case then any time you have a constant int it should be one of these enum.Constants
So when you use enum.Constant
it means, this is not just some random int it's actually a constant representing a value in an enum, but which enum isn't really important. An enum.Value
is used when you you want to say "this value can only be set to the constants in this specific enum".
This would then mean that we'd need to support enum.Constants as a PropertyType.
You'd use an enum.Value for things like State to verify we never set to an illegal one; you'd use enum.Constant for things where either it doesn't change or you don't need enum type safety and all you want to do is communicate that this thing is not-just-an-int.
But like we couldn't have MarshalJSON treat enum.Constant specially because it doesn't know where the enumSet is.
Is there a way to support named constant sets? We'd have to treat them all as Ints for the purpose of PropertyReaders, and then autoreader would have to cast to the right underlying type in SetInt() for them, which would be a bit of a pain.
That would get you the settable type safety ish, but you still could set with a value that wasn't valid. And it wouldn't give you the stringablity.
You'd have to do something where set.Add() would have to take map[interface{}]string where the kind of the thing must be int. Would we then need to use reflection for anything other than Add()? If so, that's a dealbreaker.
How do we make it so we can convert enum.Constant to a string correctly even without knowing which enum it's part of?
We could switch to having enum only ever have the global enum set. ... No, that doesn't work because we have multiple game managers each with a different enum set.
Enum.Value --> Enum.Var
We should use Enum.Vars everywhere. They aren't that much overhead and then we can be explicit, and remove the whole no-overlapping int thing.
Enum.MustLockedVar(ColorRed) gives a pre-locked var for that thing or panics. Useful for set up of component values.
... Now we just need to think about moves and how they;ll be sent back over the wire.
Moves may have an EnumVar too, just the biggest annoyance is that in initalizer you have to explicitly expand the enum.
Is there a way to have each enum have a different type registered, and then whenever we see that we treat it like an EnumVar? Like, you embed the Enum.Var in your type? That doesn't work naively because even EnumVars ahve to be initialized to the right value.
... But we could so something where in the property reader we read the Name() and SetVar(*inner) on it.
enum/main.go
type Var interface {
//Typically this is the only one you implement
Name() string
//Typically the rest of these are just part of the embedded struct
Value() int
SetValue(int) error
String() string
}
func (e *Set) AddType(myType Var) {
//Hmm, how do we get the Var to know what its inner Enum is?
}
mygame/components.go
type Color struct{
*enum.InnerVar
}
type gameState struct {
TokenColor *Color
}
Hmmm, maybe the way it works is the object that is created is an Enum itself... no, that won't work because you need it to be a single value...
Okay, trying again with the thing up there:
enum/main.go
type Constant interface {
Value() int
String() string
Enum() *Enum
MarshalJSON() ([]byte, error)
UnmarshalJSON([]byte) error
}
type Var interface {
Constant
SetValue(int) error
}
//These are the default implementation of Var that enum.NewVar() returns. We also return it for NewConstant().
type varImpl struct {
enum *Enum
value int
}
type AutoVar interface {
//Typically this is the only one you implement. It is how the PropertyReader knows which enum to fetch
Name() string
//This is on the AutoVarImpl struct
SetVar(inner Var)
//We also implement Var
Var
}
//This is what your custom auto var will embedd
type AutoVarImpl struct {
Var
}
func (v *AutoVarImpl) SetVar(var Var) {
v.Var = var
}
boardgame/reader.go
func verifyReader(reader PropertyReader) {
for propName, propType := range reader.Props() {
switch propType {
case TypeEnumVar:
//Actually it might be nil at this point... :-/
autoVar, ok := reader.EnumVar(propName).(enum.AutoVar)
if ok {
autoVar.SetVar(chest.Enums().Enum(autoVar.Name()).NewVar())
}
}
}
}
mygame/components.go
const (
ColorRed = iota
ColorGreen
)
var Enums = enum.NewSet()
var ColorEnum = Enums.MustAdd("Color", map[int]string{
ColorRed: "Red",
ColorGreen: "Green",
})
type Color struct {
//Non-pointer receiver, so the space is allocated with a single new
enum.AutoVarImpl
}
//This is the only thing my custom type needs to implement; the rest is handled by AutoVarImpl
type (c *Color) Name() string {
return "Color"
}
type cardValue struct {
Value enums.Constant
}
func newDeck() *boardgame.Deck {
deck := boardgame.NewDeck()
deck.AddComponent(&cardValue{
Value: ColorEnum.MustNewConstant(ColorRed),
})
}
mygame/state.go
//autoreader will look through this, see statically that Color implements Var, and have it return for VarProp()
type gameState struct {
MyColor enum.Var
}
func (g *gameDelegate) EmptyGameState() boardgame.SubState {
return &gameState{
MyColor: ColorEnum.NewVar(),
}
}
mygame/moves.go
type myMove struct {
//You must use a Non-pointer struct field here otherwise it won't be allocated with New()
MyColor Color
}
var myMoveConfig = boardgame.MoveConfig{
MoveConstructor: func() boardgame.Move {
return new(myMove)
}
}
The enum.Var vs enum.Constant break down works quite well. What's not clear is AutoVar... in order for the MoveConstructor's New() to also allocate space for the Color, it has to be a non-pointer reference. But doesn't that mean that accessing it in the future will be a huge pain? And if we can't have a one-line MoveConstructor then we might as well not have AutoVar.
Another option for a one-line move constructor is to have a struct tag on the Move you return. When you install a move we fetch one, do reflection on it ONCE to figure out which Enum name it is and store the propName --> enumName mapping, then every time you create a new one we will use the PropertyReadSetter to instantiate a Var to the right item. ... But that doesn't really work if you need a specific const value. Although a const doesn't make any sense for a move anyway.
verify readers should be run on moves as well if we have Vars on them
Moves should be verified at install that they don't have any illegal prop types (don't we already do this?)
The crazy struct tagging could be expanded to stacks, and then we could have a generalized Expander that takes a constructor and processes them using reflection at start and later knows how to process them without reflection
All remaining todos are captured in new issues
For example anything in components.go there should be a relatively easy way to:
1) Verify that a given value is in the enum. Get its string value, tell if it's invalid, and tell which one is greater. 2) Have access to those enums clientside in an auto-generated file
Would be useful for for example Valentine, but also a lot of other aspects.
Perhaps it's a new PropertyType of Enum, where a game is configured with a named enum, which is effectively like a deck. Then the enum will have properties to say its string value and verify that it's legal.
In early designs these were called "Colors" I believe.
Goals: 1) Easy to compare to actual constants in Go for type safety, e.g. theColor == ColorRed 2) Can always verify that the property that is an enum is never set to an illegal value 3) Way to convert to a string 4) Can be shipped to client in an auto-generated way and auto-expanded 5) They aren't high overhead memory/CPU wise 6) They don't require a lot of annoyance to use (e.g. val, err := prop.GetConstantEnum("bam"); if val != nil; ...)
Nice to have: 1) You can have them based on an underlying type like
type Color int