boardgame
boardgame copied to clipboard
Allow declarative legal methods
The legal methods are called constantly. You can also imagine a number of cases (e.g. an AI searching for moves to apply) where legal would have to be applied very often.
Currently that code is imperative. But you can imagine that most of it could be done in a declarative style, ala how kitchensim has trees of query logic.
You'd want this to be optional, but have some way that moves could opt into being fully declarative, nesting the sub-types' legal statements within their own and then executing them. This would then allow more static analysis and optimization.
The kitchensim repo is a good one to look at, it does this general pattern pretty nicely (although the variants and placeholders etc are likely unnecessary because moves don't need that much flexibilitly; this is just an optional optimization, not the fundamental logic bottleneck for the move)
Have a struct called MovePrecondition
in the main package that describes declarative rules to check. (In the future it might allow child statements.) the rules are things like Phase or CurrentPlayerIndex checks.
Moves have a Preconditions() []MovePrecondition
method. It returns a list of MovePreconditions, all of which must evaluate to true before the move's Legal() is checked. The MovePrecondition list cannot change during the running of the program, so it can be consulted once and then cached and have rule logic optimized to an arbitrary level. When moves nest another move, they should call their super's Preconditions and then append their own. Base move returns a zero-length []MovePrecondition
, so evertyhing can chain nicely.
A separate library of precondition
constructors allows a convenient fucntional style to generate the nested preconditions. Actual specific games probably wouldn't define preconditions themselves (not worth the complexity and overhead) but lots of moves in the moves
package would.
One oddity: normally when you embed a type of move, you can override its legal method if you want, but with Preconditions you might not realize what behavior you need to counteract. It's minor but possible--for example imagine a moves.FixUp
that you want to also allow real players to apply.
Preconditions have an error string field, that will be reported for the error string if the condition fails. That string should allow placeholders that will be replaced, e.g. actual player index vs expected player index. That might get a bit hard to do with nesting statements if you want to do things like "Current player is 0, but it should be between 2 and 5".
The engine could do lots of optimizations, for example it could put differnt moves directly into buckets by phases and only execute the Legal() methods of
Note that logic that is in precondition should not be duplicated in the Legal() chain. Conceptually the ActualLegal() is ExecuteMovePrecondition(move, state) error && then the move's legal method.
The benefits of this system are a) performance boost for the fixup loop (potentially large because it opens up lots of possibilities for optimizations), and b) the preconditions are a set of rules that can be executed by a rules engine; that allows them to be shipped to the client and executed without the game itself having to write special logic, which would allow declarative databinding to detect automatically which moves pass the preconditions, making it easy to for example gray out buttons for players when it's not hteir turn.
Actually verify that this system is significantly faster before switching to it (although the client-side logic might make it useful anyway)
Many preconditions need to rely on state on th emove set in DefaultsForState (which involves running code :-( ) . But it should be possible to tell which preconditions read fields in the move, and sort it so that ones that don't require reading the move are run first, before needing to run DefaultsforState.
MovePrecondition should have ValidConfiguration(exampleState ImmutableState, exampleMove Move) error
that can be run in GameManager setup, and a Matches(state ImmutableState, move Move, proposer PlayerIndex) error
. They could also have internal methods like readsMoveProperties() bool
Ideally MoveProgressionGroup
logic (is this move valid right now given th emoves that were just applied) would be able to be done without executing the legal method but entirely in the precondition loop. That logic is non-trivial, and would ideally not need to redo all of that logic constantly. See #640 for optimizing moveProgression checks.
This issue is related to (duplicative of?) #189 and related to #213 (client side rejecting of obviously illegal moves)
The logic of moveProgressionChecks needs to be included in the precondition matching machinery, which implies extra types and complexities, so maybe the only thing that's in boardgame core library is the interface definition of ValidConfiguration and Matches(), and all implementations are in a separate library (and move tape matching logic moves there)
Even simple tests like legalInPhase is complex because of treeenums