Closed jkomoros closed 6 years ago
This is the source of the error in #520 . It's finicky to do it well, especially with sanitized values.
Would want these MergedStacks to be able to be output as computed properties (that might just work now that we can output interface{})
valid() error
. By default return nil, but mergedStacks actually do error checking thereimportFrom(other Stack)
that basically copies in all of the data from that other object into its own self. concatenate
and overlap
struct tags autoinflation for stacks Retunring an error from a merged stack constructor seems like it's required. (At the very least for overlapped stacks, where we have to make sure their size is the same).
but it seems like a big pain to do especially because in most cases the error will be dropped on the floor.
It also seems like merged stacks are a very common idiom for differentially-private information. Should the concept be baked in more deeply, and have SubStates have a merged stack (name, name, type) configuration that can be checked at NewGameManager time? And then changes in e.g. setSize for a stack that is linked to another stack in merged stack in overlap mode would also resize the other stack.
We could have them be a property on a SubState:
They'd be special in that in the datamodel they'd only encode the primary, secondary, and merge style.
Conceptually they'd be created in your Constructor methods:
func (g *gameDelegate) GameStateConstructor() {
deck := g.Manager().Chest().Deck("cards")
primary := deck.NewStack()
secondary := deck.NewStack()
//merged just maintains refs to those two literal stacks. The primary and secondary have a pointer to overlapStacks they're a part of, so if their size changes they can let the other one know.
merged := NewOverlappedStack(primary, secondary)
return &gameState{
primary: primary,
secondary: secondary,
merged: merged,
}
}
But of course struct based tags could be used to do that automatically:
type gameState struct {
boardgame.BaseSubState
HiddenHand boardgame.MutableStack `stack:"cards" sanitize:"order"`
RevealedHand boadgame.MutableStack `stack:"cards"`
Hand boardgame.Stack `concatenate:"HiddenHand,RevealedHand"`
}
Note that this means that all of the tooling would have to support Stack (non-mutable) in SubStates. Look at how we do enum.Val to see how hard that is to support.
In JSON output we'd output them just like a normal stack--just with duplicates. When Unmarshalling from JSON for merged stacks we'd just drop on the floor because our state is fully represented by the other stacks we're based on.
This set-up only allows easy-to-define merged stacks that are members of the same SubState object (becuase the mergedStack must be non-nil when returned from the constructor). If that doesn't fulfill your need, then you can still create a MergedStack as a computed property.
The pattern for enum.Val that we used in autoreader doesn't work for the GameState structs because it only works if you're only generating a reader. The PropertyReadSetter interface assumes that all interface types are mutable.
That implies that we need a new MergedStack type so it can be represented correctly in the various interfaces.
Alternatively we could support immutable interface types in SubStates, but I'd want it to be consistent across all of them, and the use cases for others just aren't there. In copying we'd try to get a Mutable version, which would fail, and so we'd need a ConfigureStack on PropertyReadSetConfigurer for setting these in that case.
Note that Copying merged stacks is challenging because they'd have a copy to the original stacks, not the stacks in the new state. :-/
Actually, we don't need the primary/secondary stacks to know about each other--it's just that when the state is about to be saved we'd call Valid(), which in addition to checking whether player indexes are valid, would also verify that merged stacks are valid. In fact, that might be the operative way we check for errors on merged stacks. We create one always--it's not until the Valid() check that we look to see if all of the invariants are met, so we can report them correctly. Would need to make sure that NewGameManager does that Valid() check on states in the initial set up.
This is simpler because stacks that are part of a merged stack don't have to know it in order to inform other related stacks when their size changes, and removes spooky action at a distance.
MergedStacks are really interesting, because in theory if the underlying stack objects in other SubStates never physically changed their identity, then MergedStacks are initalized and configured in the constructor and never need to change.
However, today physical stacks DO change after being constructed, because ConfigureMutableStack just physically sets the stack, and that's called in Copy. We could have MutableStacks have a Configure(other Stack) that just adopts the config of the other stack. (That then is weird because it goes against the MutableStack Configuration need, because you could do it manually by just doing readSetter.MutableStack('name').Configure(other), even if you didn't have a ReadSetConfigurer. Wait, we also use ConfigureMutableStack to auto-inflate, not just to copy.
You'd represent merged stacks simply as boardgame.Stack
. You'd get them via PropertyReader.StackProp(). But MutableStackProp would fail because they aren't a stack. And ReadSetConfigurer would grow a ConfigureStackProp(propName string, stack Stack)
. In auto inflation we'd create a merged stack of the right type and set it in with ConfigureStackProp. \
We could also have similar behavior for enum.Val (it wouldn't make sense to include for enum.Timer, because an immutable timer is nonsensical)
Then we'd just have to figure out a way for copying SubStates to work for the MergedStack--because only the auto-inflation knows how to create the stack to copy, and MergedStack.Copy() won't work because it needs a physical pointer to the stacks it merges, and that's not passed in there. And the Reader.Copy() in readerutil shouldn't have to know about MergedStacks ideally.
Whoa, state.copy doesn't use gameManager.gameStateConstructor, which does all of the auto-inflation magic. It just creates literal empty objects and then copies over items one by one. Because we use the autoInflation when we load up a refried state, and for the very first state in a game, it copies over correctly.
One thing we could do is change how we do copy to also use the inflation constructors. We'd get an empty object, copy over any things that could get MutableCopies, and then run autoinflation to fill in the rest.
Conceptually then constructors go like this:
We should then use this flow for all copies and creates by rationalizing the stateFromRecord, state.Copy, gameManager.SetUpValidators, manager.exampleState, game.starterState, and game.SetUp.
Wait, this doesn't actually work, because autoinflation conceptually should just be convenience sugar for what you can do yourself in GameStateConstructor(). Which implies we need another way for merged stacks to get a ref to their actual stacks. :-/
Just add an importFrom(other Stack)
private method to MutableStacks. And then only use it within copyReader.
state.copy() doesn't create a new state, but rather generates an empty state with enough players and then imports all of the information.
It's nice that currently if you accidentally use boardgame.Stack in your state struct, it will fail. If you do a Stack and it's empty (that is not initalized by overlap or concatenate struct tags) then we should have a descriptive error message reminding you that you generally want MutableStack.
Currently boardgame-card-stack is what merges them together. But that logic could go on server side as a computed property kind of thing. For example, Blackjack has EffectiveHand.