The gist of the game engine is driven an
apply(game, action) => game function, which accepts a player action (a JSON object), finds the corresponding handler function, applies it to the game state, and returns the resulting game state.
Game actions are derived from the current game state by a
getValues function in each of the existing action types that returns the possible values (if any) for that action. After an action completes, the next possible actions are derived and stored in the game state in
possibleActions, and the game state is sent to the players.
Basically, I have two "types" of actions:
These actions are derived from the game state when it is in an "idle" state (meaning no other actions are in the middle of occuring).
Example: Player A is in their turn, and they can use a card in their hand or one of their cards in play.
These are actions that are explicitly required as part of another action's execution. This skips the
possibleActions check, which prevents any other actions from being performed.
Example: Player A uses a card that affects another card in play. They have to choose which card to affect.
Any one of these actions can call to other explicit actions to get a value necessary for their completion, and this is the source of my difficulty.
Right now I have two ideas on how to implement this, but both have their trade-offs:
Generators are by definition pausable/resumable, so they seem a good fit for these kinds of actions that require other actions.
Example: Player A performs action
play is a generator that
yield*s to the
choose action (which is also a generator), which in turn returns the game state with a
choose action in the
possibleActions field. The iterator returned from
play is stored, the game state is sent to the player, and when the
choose action is performed by them, it is passed to the iterator via
next(action). If the iterator is
done, it means we reached the end of the
play action and we can now default to "idle" mode and compute the possible "normal" actions.
Trade-offs: One of my the things I want in my design is for the game state and game actions are serializable so the game can be resumed in case of interruption, or replayed later. Involving generators in the game state conflicts with this since they are not serializable. A possible workaround would be to serialize the last "idle" game state and all actions performed after, so the "current" game state can be reconstructed by replaying those. Still, generators add quite some complexity to the code.
The "parent" action could call the "sub" action with a callback parameter, which would be the "next" step in the execution of the "parent" action. The game state is returned to the player with the expected action in
possibleActions. When the expected action is performed, we apply its handler and then the specified callback's handler with the action's value. If the returned game state has no expected actions, we default to "idle" mode.
Trade-offs: Another thing I'd like is to define the game rules and actions in the most declarative way possible, so the focus can be on the game play and not jumping through hoops. Having to break down actions into multiple steps kinda ruins this. I'd also have to either store additional parameters for the "resuming" action in the game state or the action callback itself. Also with nested actions, passing callbacks around and resolving them feels weird.
I hope this made any sense and was enough to communicate what I'm trying to accomplish. I know every design and architecture decision comes with trade-offs, but what I'm hoping here is to get some opinions of more experienced people who might have run against similar issues and hearing about their solutions.