Friday, March 29, 2019

Design of a turn-based game where actions have side-effects


I am writing a computer version of the game Dominion. It is a turn-based card game where action cards, treasure cards, and victory point cards are accumulated into a player's personal deck. I have the class structure pretty well developed, and I am starting to design the game logic. I'm using python, and I may add a simple GUI with pygame later.


The turn sequence of the players is governed by a very simple state machine. Turns pass clockwise, and a player can't exit the game before it is over. The play of a single turn is also a state machine; in general, players pass through an "action phase", a "buy phase", and a "clean-up phase" (in that order). Based on the answer to the question How to implement turn-based game engine?, the state machine is a standard technique for this situation.


My problem is that during a player's action phase, she can use an action card that has side effects, either on herself, or on one or more of the other players. For example, one action card allows a player to take a second turn immediately following the conclusion of the current turn. Another action card causes all other players to discard two cards from their hands. Yet another action card does nothing for the current turn, but allows a player to draw extra cards on her next turn. To make things even more complicated, there are frequently new expansions to the game that add new cards. It seems to me that hard-coding the results of every action card into the game's state machine would be both ugly and unadaptable. The answer to Turn-based Strategy Loop does not go into a level of detail that addresses designs to solve this problem.


What kind of programming model should I use to encompass the fact that the general pattern for taking turns can be modified by actions that take place within the turn? Should the game object keep track of the effects of every action card? Or, if the cards should implement their own effects (e.g. by implementing an interface), what setup is required to give them enough power? I have thought up a few solutions to this problem, but I am wondering if there is a standard way to solve it. Specifically, I'd like to know what object/class/whatever is responsible for keeping track of the actions that every player must do as a consequence of an action card being played, and also how that relates to temporary changes in the normal sequence of the turn state machine.




Answer



I agree with Jari Komppa that defining card effects with a powerful scripting language is the way to go. But I believe that the key to maximum flexibility is scriptable event-handling.


In order to allow cards to interact with later game events, you could add a scripting API to add "script hooks" to certain events, like the beginnings and endings of game phases, or certain actions the players can perform. That means that the script which is executed when a card is played is able to register a function which is called the next time a specific phase is reached. The number of functions which can be registred for each event should be unlimited. When there is more than one, they are then called in their order of registration (unless of course there is a core game rule which says something different).


It should be possible to register these hooks for all players or for certain players only. I would also suggest to add the posibility for hooks to decide for themselves if they should keep being called or not. In these examples the return value of the hook function (true or false) is used to express this.


Your double-turn card would then do something like this:


add_event_hook('cleanup_phase_end', current_player, function {
setNextPlayer(current_player); // make the player take another turn
return false; // unregister this hook afterwards
});


(I have no idea if Dominion even has something like a "cleanup phase" - in this example it's the hypothetical last phase of the players turn)


A card which allows every player to draw an additional card at the beginning of their draw phase would look like this:


add_event_hook('draw_phase_begin', NULL, function {
drawCard(current_player); // draw a card
return true; // keep doing this until the hook is removed explicitely
});

A card which makes the target player lose a hit point whenever they play a card would look like this:


add_event_hook('play_card', target_player, function {
changeHitPoints(target_player, -1); // remove a hit point

return true;
});

You won't get around hard-coding some game actions like drawing cards or losing hit points, because their complete definition - what exactly it means to "draw a card" - is part of the core game mechanics. For example, I know some TCGs where when your have to draw a card for whatever reason and your deck is empty, you lose the game. This rule isn't printed on every card which makes you draw cards, because it's in the rule book. So you shouldn't have to check for that lose condition in every card's script either. Checking things like that should be part of the hard-coded drawCard() function (which, by the way, would also be a good candidate for a hookable event).


By the way: It's unlikely that you will be able to plan ahead for every obscure mechanic future editions could come up with, so no matter what you do, you still will have to add new functionality for future editions once in a while (in this case, a confetti throwing minigame).


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...