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

Create StackConstraint to check whether a move is legal or not #736

Open jkomoros opened 4 years ago

jkomoros commented 4 years ago

See also #550

See the exploration in https://github.com/jkomoros/boardgame/wiki/Representing-spaces

The two main things are:

  1. Allow configuring a predicate that is run at the last stage of a Board's stack's Move methods that can error if the predicate isn't met, and then provide a collection of building block predicates that can be combined for most cases
  2. Helpers to make it really simply to create computed properties to get the int index or intSlice (for multi) for where components that match a given predicate are in a given board.

The second is now captured in #741

jkomoros commented 4 years ago

For the first bit, GameDelegate grows a LegalBoardMove(ref StatePropertyRef, instance ComponentInstance) error, which is consulted in any MoveComponent as the final step, if the GrowableStack is part of a board, and if it fails then the move is not legal.

... Wait, ideally this is something that would be easy to run in the Legal check in a move, not just at Apply time, which would be too late. ... Does it make sense to change ComponentInstance to have MayMoveTo and friends, which don't actually do the move, just say if it's possible? That might be useful enough to pop into its own issue, because presumably some logic right now duplicates all of that logic anyway.

A thought: I wonder if MaxSize et al on Stacks could be generalized to also handle this kind of configuration, because notionally it feels somewhat similar... except for SizedStacks, where the constraint actually defines how many empty slots to have...

Instead of having the configuration be on GameDelegate, there could be a configurator on NewBoard() that takes a number of TokenRestrictionPredicate. That makes it a bit more like MaxSize, although MaxSize can be changed. The upside of this is that it makes it even more straightforward about what the predicates are. The downside is that it fixes the predicates instead of them being able to be changed as the game shifts. But in the rare cases you need that, you could pass a CustomPredicate as long as it fits within the BoardSpacePredicate signature, and that could implement more complex logic.

One really annoying thing is that the struct inflaters will have to know about the different pre-configured predicates so it knows which ones to use based on struct tags... wait, maybe there's some way to smuggle out those predicates via Manager.Internals().StructInflater...

base.GameDelegate can load up the predicates library map. GameDelegate can then grow a BoardPredicateMap() that returrns a map of struct-names to creators. ... but those creators themselves have to I guess take a string value of the struct tag configuration to unpack themselves,...

Example tag-based predicates:

type gameState struct {
  base.SubState
  board boardgame.Board `board:"tokens,0,max(NUM_PLAYERS-2),unique(component.color)"`
}

... Wait, to do this properly will require actual complex constant-based expressions, which will have ot be its own issue (captured in #740)

type BoardSpacePredicate func(b Board, instance ComponentInstance, s State, boardRef StatePropertyRef) error

//If multiple predicates are provided they ALL Must match
deck.NewBoard(length, maxSize int, predicates ...BoardSpacePredicate)

... Maybe MaxSize for GrowableStacks should be done in the predicate style, and just treat Size for SizedStacks specially. And if BeforeSetUp fixes the final value of LateBoundConstants (e.g. ones that vary based on the variant passed) then it might be possible to have the auto-machinery just size them correctly in general (where else uses the size-affecting methods on stacks?)

MaxSize is represented in the JSON representation of even growable stacks, which implies it's different than predicates, which can't have a generic representation in the JSON.

Should predicates be able to have more added? (In the same way that Size can be changed on stacks after the fact... but that's different because it's represented in JSON)

The only way they can be represented in JSON is if all of the serialized representations can be converted via maps returned from ConfigureStackPredicateConstructors. That's a nice place for the base.GameDelegate to return all of the predicate constructors that are defined outside the main package.

jkomoros commented 4 years ago
jkomoros commented 4 years ago

The type is StackPredicate and the constructor is StackPredicateConstructor.

The predicate is the actual thing that does the logic. The Constructor is the thing that contains enough information to serialize and deserialize.

The predicates have to be deserialized both from struct tags, and also from JSON representations of the stack. That means that you can't attach an arbitrary stack constructor item to a stack; it has to be one of the preconfigured types that are configured on your game manager at boot up (although the ability to pass in a custom constructor and take lots of parameters gives you lots of leverage)

//Typically you only use the stack and the instances, but if you care to learn more about the world at the moment this is being considered, you also have access to the state as well as the StatePropertyRef denoting the stack you were provided. Stacks that are part of a board can inspect their BoardIndex and Board properties, or the same information in the stackRef. Note that instance might not yet be in a stack when in the DistributeComponentsToStarterStack() is in progress. Note also that instances is almost always of length one, but will be more than one for stack.MoveAllTo
type StackPredicate func(s Stack, instances []ComponentInstance, state ImmutableState, stackRef StatePropertyRef)

//StackPredicateConstructor is what GameDelegate.ConfigureStackPredicateConstructors returns. It provides enough information to serialize and deserialize a predicate.
type StackPredicateConstructor struct {
  //The name of this type of predicate, when serialized or deserialized. E.g. "maxSize"
  Name string
  //The types of the arguments passed in, from left to right. Only TypeInt, TypeBool, and TypeString are allowed
  Arguments []PropertyType
  //The constructor to call to get the predicate, which will be passed precisely the types of arguments in order of types and length that Arguments defines.
  Constructor func(args... interface{}) (StackPredicate, error)
}

type GameDelegate interface {
  //...
  ConfigureStackPredicateConstructors() []*StackPredicateConstructor
}

func (g *GameManager) deserializePredicate(input string) *instantiatedStackPredicate, error {
  //assumes the input is of a single predicate: "maxSize(3,"color")"
 //Factors out the name and () and then looks for that predicate configurer
 // Then tries to convert each argument into the type it's supposed to be
}

//Privately, stacks keep track of the predicate as long as the configuration string that was used to create it, so they can be serialized again:

type instantiatedStackPredicate struct {
  constructor *StackPredicateConstructor
  //always the length and types of the constructors' provided types
  arguments []interface{}
  predicate StackPredicate
}

//serialize will return something like 'maxSize(3,"color")'
func (i *instantiatedStackPredicate) serialize() string {
}

Stacks should have a ResetPredicates(), which resets them to the thing in the struct inflater, AddPredicate, RemoveNamedPredicate, and ClearAllPredicates(). AddPredicate should fail if the stack it's being added to already does not match the predicate.

Should the information that was captured when the predicate was made include the final values of the arguments, or also include things like constant names and expressions? I don't think they can be, because those are resolved in thee machinery before being passed into the Constructor

The predicatee strings in structs will have to be separated by ";" or something else, so we can split on them ("," is ambiguous and will require much more complex parsing, which we'd ahve to use for all struct unpacking like that, even outside of predicates. I guess you'd split on them and then keep track of the sub-strings in the resulting list that have an open and closed paranthesis in them)

Might consider having StackPredicateConstructor struct have all inner fields, and have to be created via a NewStackPredicateConstructor(name string, arguments []PropertyType, constructor func(values ...interface{})*StackPredicate, error) *StackPredicateConstructor, error, which allows them to be checked for being valid once and then doesn't require you to check constantly if somone else modified one of the public fields and made them invalid

Consider doing something like in kitchensim paramterized config, where all of the arugments are passed in in one struct. In our case it would be something like type StackPredicateConstructorArguments []PropertyType, which would then have arguments hanging off of it, like "func(s StackWhatever) Process(input string) []interface{}, error"

How to support nested expressions?

Perhaps have:

type SerializedStackPredicate string
type SerializedStackPredicateArgument string

func (s SerializedStackPredicate) Deserialize(d GameDelegate) *StackPredicate 

How to do string arguments in the struct tag? I don't think escaped quotes will work (they're interpreted by the struct tag parser as normal quotes, I think...

jkomoros commented 4 years ago

Predicates:

//We use NumComponents instead of size to help differentiate from Size of SizedStacks

//Ensures the count of the destination stack does not exceed max
maxNumComponents(max int)

//Ensures that the count of the source stack never goes below min
minNumComponents(min int)

//All of the following operate on a given propety name (both in component avlues or dynamic value versions), and work on Int, String, or EnumVal properties

//Unique asserts that no more than one item in the stack has the same enum value for the component's given enum prop name. See also uniqueDyanamic.
unique(componentPropName string)

//uniqueDynamic is like unique, but the prop name is on the DynamicValues, not the component values itself
uniqueDynamic(dynamicValuesPropName string)

//same only allows components that have the same enumv alue for the given named prop. See also sameDynamic.
same(componentPropName string)

//sameDynamic is like same but the given property is on the dynamic values, not the component values.
sameDynamic(dynamicValuesPropName string)

//maxDistinctValues counts how many different values total are in the stack. See also maxDistinctValuesDynamic.
maxDistinctValues(componentPropName string)

//maxDistinctValuesDynamic is the same as maxDistinctValues but where the enum val prop is on the component dynamic values instead.
maxDistinctValuesDynamic(dynamicValuesPropName string)

//Looks at all neighbors of the given item (defined as the ones that have an edge from the proposed stack position to the other enum value) and returns true as long as they don't exceed max
neighborsCountrsMax(max int, enumPropName string, connectedness enum.Graph)

Actually, for propnames it's better for it to be "name" whcih checks the compoent and then the dynamic values. If you want to be explicit, do "component.name" or "dynamic.name". That removes the need for each one to have a normal and dynamic variant. And then conceptuallly these are StatePropertyRef references.

jkomoros commented 4 years ago

Stack.SlotsRemaining() will have to go away if MaxSize is changed. Although that makes the implementation of stack.MoveAllTo considerably more challenging because the signature of StackPredicate is to take only a single component...

jkomoros commented 4 years ago

Won't some of these predicates be hard to write without actually moving the component to the new location (and then throwing out the modified state if it doesn't work)? Especially the neighbor ones.

jkomoros commented 4 years ago

Rename these to StackConstraint and StackConstraintConstructor

Boards should have their own list of StackConstraints, which the stacks that are part of them inherit (and can append to themselves)

jkomoros commented 4 years ago

The vast majority of these will be duplicates--have a cache of input string to vended instantiatedStackConstraint (although note that things like NUM_PLAYERS will imply a cache that's not global for a game type.

instantiatedStackConstraint and things like GameManager.stackConstraint(input string) should all be private

jkomoros commented 4 years ago

How to handle string constants in the system? They can't be in double quotes because that contains the entry in the struct tag. I guess it's not important for it to be valid Go, so single quotes are fine (that's fine for being with JSON and actually ideal).

jkomoros commented 4 years ago

The parameters passed to predicates can omit the extra state, stateRef if Stacks grow a State(), PropertyRefInState() methods in general (that shouldn't be hard; they already know which state they're in)

jkomoros commented 4 years ago

Consider having the instantiatedStackPredicate be a public type. It might literally be a string that is the canonical serialization of it, and which can be passed to gameManager to retrieve a predicate (literally the same one for the same canonicalizes string), and has a Canonical() method that returns a copy that is canonicalized.

If the concept is exposed then it can be used as a key to stack.RemovePredicate() (insteaad of a cludge about name of predicate constructor, which would disallow multiple of hte same fundamental constructor type on a stack).

It seems like the constants being expanded from their raw values in the canonical/serialized predicates is a problem because it loses the intention/nuance of the expanded constant expression. That doesn't technically matter if constants are always fixed over the life of the game, but there are cases where having a Variable is nice.

This is probably covered elsewhere in this issue, but Stacks within Boards should have their own predicates, which also layer on top of the predicates in the containing Board.

jkomoros commented 2 years ago

Blocks on #739