I'm developing a card game in JavaScript in a functional programming style and I'm unsure what is the best way to implement the flow of player actions that require other player actions for their execution.
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:
State-derived 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.
Explicit actions
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
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(card X). 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.
Action callbacks
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.
Action Stack: as effects get created (a card being played goes onto the stack, when it resolves, it adds a new effect to the stack, etc.) Your main game loop would resolve one effect at a time and those effects may require their own inputs, that the main loop would have to wait on input before the effect resolves (removing itself from the stack). How to actually code this, I'm not sure, though theAction Stackwould have to be a generic or interface Type. \$\endgroup\$applycalls the C handler with the value, C handler does something to game state. How can C return the new game state and pass a value to the next action in the chain? It will have to explicitly pop the next action from the stack to do this. It ends up being the same as my nr. 2 solution, but instead of passing a callback to the action, you push it to the stack \$\endgroup\$