0
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ The only way I can think of approaching this is to have an 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 the Action Stack would have to be a generic or interface Type. \$\endgroup\$ – Draco18s no longer trusts SE Jul 13 '18 at 19:16
  • \$\begingroup\$ But imagining we have a nested sequence A->B->C, where each action depends on the value of the next. User performs action C, apply calls 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\$ – Thiatt Jul 13 '18 at 20:53
  • \$\begingroup\$ I've never tried to implement a MTG style effect stack, so I don't have any concrete solutions. Sorry. \$\endgroup\$ – Draco18s no longer trusts SE Jul 13 '18 at 22:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.