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

Capture move Legal logic in a way that can reject obviously illegal moves immediately #189

Open jkomoros opened 7 years ago

jkomoros commented 7 years ago

Things that clientside will need:

These are two different things. The first is way easier, and this issue only captures it.

jkomoros commented 7 years ago

We'd need the SanitizedProperties map client-side, so we can know when our legal method is running over sanitized values.

We could either just canonically transpile the moves' legal methods to JS via web assembly or GopherJS.

Or we could enforce that Legal methods return a LegalExpression, which is a tree and can be evaluated. The pros of this are that a) various moves can reuse sub-trees (like Only allow if targetPlayerIndex is current player), and b) the expression can be serialized as JSON and executed clientside with an engine that just has to written by the main server package, not in packages for particular games.

jkomoros commented 7 years ago

The question is if the second option is generic enough. I think it is, once we have ComputedProperties (#146).

jkomoros commented 7 years ago
type LegalExpression interface {
  Evaluate(state *State) error
}

type BooleanExpression interface {
  Evaluate(state *State) bool
}

type BooleanLogic int 

const (
  BooleanAnd BooleanLogic = iota
  BooleanOr
)

//implements LegalExpression
type ExpressionErrorGroup struct {
  SubExpressions []LegalExpression
  //If BooleanAnd, will bail at the first SubExpression that returns an error, and return that. Returns nil if no sub-expression errors.
  //If BooleanOr, will return nil as soon as it finds the first sub-expression that evaluates to Nil. If there are no no nils, will return the error from the first sub-expression
  GroupType BooleanLogic
}

//Its Evaluate() will call evaluate on the boolean expression. If the result is true, and we wantTrue, will return nil. If it's not what we want, will return an error with the Error Description.
//implements LegalExpression
type ExpressionBoolean struct {
  Expression BooleanExpression
  WantTrue bool
  ErrorDescription string
}

type ComparsionType int

const (
  ComparisonEquality ComparsionType = iota
  ComparisonInequality
  ComparisonGreaterThan
  ComparisonLessThan
  ComparisonGreaterThanOrEqualTo
  ComparsionLessThanOrEqualTo
)

//implements BooleanExpression
type SimpleBooleanExpressionComparison struct {
  Op ComparisonType
  Left ExpressionLeaf
  Right ExpressionLeaf
}

type StackComparisonType int

const (
  StackComparisonAll StackComparisonType = iota
  StackComparsionAny
  StackComparisonNone
)

//Implements BooleanExpression
type StackBooleanExpression struct {
  Op StackComparisonType
  Stack StatePropertyRef
  Other ExpressionLeaf
}

//Hmm, handling all of the possible types 

type ExpressionLeaf interface {
  Value(state *State) interface{}
}

//StatePropertyRef implements ExpressionLeaf

//Implements ExpressionLeaf
type Constant struct {
  i interface{}
}

//Value just returns the int
//Alternatively, instead of having a container type for constants, could just have ExpressionComparison see if it's a PropLeaf and call Value() on it. Then Constants don't need wrappers
type IntLeaf struct {
  Value int
}
jkomoros commented 7 years ago

Hmm, this is going to get really hard with all of the different types. GroupTypes like Stacks will want operations to make sure that given components are in a given stack, or NOT in a stack, and many others.

jkomoros commented 7 years ago

Kind of sort of related to #82

jkomoros commented 7 years ago

There will be so many extra types, that implies it should be in a separate library.

The core library just expects you to have a Legal(state) error method. The server expects your moves to have a LegalExpression() *LegalExpression to evaluate.

Hmm, thats' hard to do. We can do it with storage manager in NewServer because storage manager is atop level separate thing...