20
\$\begingroup\$

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.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Hello Apis Utilis, and welcome to GDSE. Your question is well written, and it's great that you referenced the related questions. However, your question is covering a lot of different problems, and to fully cover it, a question would probably need to be enormous. You may still get a good answer, but yourself and the site will benefit if you break down your problem some more. Maybe start with building a simpler game and build up to Dominion? \$\endgroup\$ Commented Jan 9, 2013 at 7:05
  • 1
    \$\begingroup\$ I'd start from giving each card a script that modifies the game's state, and if nothing weird is going on, fall back on the default turn rules... \$\endgroup\$ Commented Jan 9, 2013 at 10:05

3 Answers 3

11
\$\begingroup\$

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).

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Wow. That chaos confetti thing. \$\endgroup\$ Commented Jan 9, 2013 at 16:34
  • \$\begingroup\$ Excellent answer, @Philipp , and this takes care of a great deal of things done in Dominion. However, there are actions that must occur immediately when a card is played, i.e. a card is played that forces another player to turn over the top card of his library and allowing the current player to say "Keep it" or "Discard it". Would you write event hooks to take care of such immediate actions, or would you need to come up with additional methods of scripting the cards? \$\endgroup\$
    – fnord
    Commented Jan 10, 2013 at 18:25
  • 2
    \$\begingroup\$ When something needs to happen immediately, the script should call the appropriate functions directly and not register a hook function. \$\endgroup\$
    – Philipp
    Commented Jan 10, 2013 at 18:44
  • \$\begingroup\$ @JariKomppa: The Unglued set was deliberately nonsensical and full of crazy cards that made no sense. My favourite was a card that made everyone take a point of damage when they said a particular word. I chose 'the'. \$\endgroup\$ Commented Mar 31, 2019 at 9:15
10
\$\begingroup\$

I gave this problem - flexible computerized card game engine - some thought some time ago.

First off, a complex card game like Chez Geek or Fluxx (and, I believe, Dominion) would require cards to be scriptable. Basically each card would come with its own bunch of scripts that may change the state of the game in various ways. This would let you give the system some future-proofing, as the scripts might be able to do things you can't think of right now, but might come in a future expansion.

Second, the rigid "turn" may be causing problems.

You need some kind of "turn stack" which contains the "special turns", such as "discard 2 cards". When the stack is empty, the default normal turn continues.

In Fluxx, it's entirely possible that one turn goes something like:

  • Pick N cards (as stated by current rules, changeable via cards)
  • Play N cards (as stated by current rules, changeable via cards)
    • One of the cards may be "take 3, play 2 of them"
      • One of those cards may well be "take another turn"
    • One of the cards may be "discard and draw"
  • If you change rules to pick more cards than you did when your turn started, pick more cards
  • If you change rules for fewer cards in hand, everybody else must discard cards immediately
  • When your turn ends, discard cards until you have N cards (changeable via cards, again), then take another turn (if you played "take another turn" card sometime in the above mess).

..and so on, and so forth. So designing a turn structure that can handle the above abuse can be rather tricky. Add to that the numerous games with "whenever" cards (like in "chez geek") where the "whenever" cards can disrupt the normal flow by, for instance, cancelling whatever card was last played..

So basically I would start from designing a very flexible turn structure, design it so that it can be described as a script (as each game would need its own "master script" handling the basic game structure). Then, any card should be scriptable; most cards probably don't do anything strange, but others do. Cards can also have various attributes - whether they can be kept in hand, played "whenever", whether they can be stored as assets (like fluxx 'keepers', or various things in 'chez geek' like food)...

I never actually started implementing any of this, so in practice you may find plenty of other challenges. The easiest way to start would be to start with whatever you know of the system you want to implement, and implement them in scriptable ways, setting as little in stone as possible, so when an expansion comes along, you won't need to revise the base system - much. =)

\$\endgroup\$
2
  • \$\begingroup\$ This is a great answer, and I would have accepted both if I could have. I broke the tie by accepting the answer by the person with the lower reputation :) \$\endgroup\$ Commented Jan 10, 2013 at 19:30
  • \$\begingroup\$ No prob, I'm used to it by now.. =) \$\endgroup\$ Commented Jan 11, 2013 at 15:45
0
\$\begingroup\$

Hearthstone seems to do things relatable and honestly i think the best way to achieve flexibility is through an ECS engine with a data oriented design. Been trying to make a hearthstone clone and it's proven impossible otherwise. All the edge cases. If your facing a lot of these weird edge cases then that's probably the best way to go about it. I'm pretty biased though from recent experience trying out this technique.

Edit: ECS might not even be needed depending on the kind of flexibility and optimization your wanting. It's just one way of accomplishing this. DOD I've mistakenly thought of as procedural programming though they relate a lot. What I mean is. That you should consider doing away with OOP entirely or mostly at least and instead focus your attention on the data and how it's organized. Avoid inheritance and methods. Instead focus on public functions(systems) to manipulate your card data. Each action isn't a templated thing or logic of any kind but is instead raw data. Where your systems then use it to perform logic. Integer switch case or using an integer to access an array of function pointers help figure out the desired logic from the input data efficiently.

Basic rules to follow are that you should avoid tying logic directly together with data, you should avoid making data depend on one another as much as possible(exceptions may apply), and that when your wanting flexible logic that feels out of reach... Consider converting it into data.

There's benefits to be had doing this. Each card may have an enum(s) value or string(s) to represent their action(s). This intern allows you to design cards through text or json files and allow the program to automatically import them. If you make player actions a list of data this gives even more flexibility especially if a card depends on past logic like hearthstone does, or if you'd like to save the game or a replay of a game at any point. There's potential to create A.I. more easily. Especially when using a "utility systems" instead of a "behavior tree". Networking also becomes easier because instead of needing to figure out how to get entire possibly polymorphic objects to transfer over the wire and how serialization would be setup after the fact, your already having your game objects be nothing more than simple data which ends up being really easy to move around. And last but definitely not least this allows you to optimize easier because instead of wasting time worrying about code your able to better organize your data so the processor will have an easier time dealing with it. Python may have issues here but look up "cache line" and how it relates to game dev. Not important for prototyping stuff perhaps but down the road it'll come in handy big time.

Some useful links.

Note: ECS allows one to dynamically add/remove variables(called components) at runtime. An example c program of how ECS "might" look(there's a ton fo ways of doing it).

unsigned int textureID = ECSRegisterComponent("texture", sizeof(struct Texture));
unsigned int positionID = ECSRegisterComponent("position", sizeof(struct Point2DI));
for (unsigned int i = 0; i < 10; i++) {
    void *newEnt = ECSGetNewEntity();
    struct Point2DI pos = { 0 + i * 64, 0 };
    struct Texture tex;
    getTexture("test.png", &tex);
    ECSAddComponentToEntity(newEnt, &pos, positionID);
    ECSAddComponentToEntity(newEnt, &tex, textureID);
}
void *ent = ECSGetParentEntity(textureID, 3);
ECSDestroyEntity(ent);

Creates a bunch of entities with texture and position data and at the end destroys an entity who has a texture component which happens to be at the third index of the texture component array. Looks quirky but is one way of doing things. Here's an example of how you'd render everything which has a texture component.

unsigned int textureCount;
unsigned int positionID = ECSGetComponentTypeFromName("position");
unsigned int textureID = ECSGetComponentTypeFromName("texture");
struct Texture *textures = ECSGetAllComponentsOfType(textureID, &textureCount);
for (unsigned int i = 0; i < textureCount; i++) {
    void *parentEntity = ECSGetParentEntity(textureID, i);
    struct Point2DI *drawPos = ECSGetComponentFromEntity(positionID, parentEntity);
    if (drawPos) {
        struct Texture *t = &textures[i];
        drawTexture(t, drawPos->x, drawPos->y);
    }
}
\$\endgroup\$
3
  • 1
    \$\begingroup\$ This answer would be better if it went into some more detail about how you would recommend setting up your data-oriented ECS and applying it to solve this specific problem. \$\endgroup\$
    – DMGregory
    Commented Mar 30, 2019 at 8:08
  • \$\begingroup\$ Updated thank you for pointing that out. \$\endgroup\$
    – Blue_Pyro
    Commented Mar 30, 2019 at 22:51
  • \$\begingroup\$ In general, i think it's bad to tell someone "how" to setup this kind of approach but instead let them design their own solution. It proves to be both a good way of practicing and allows a potentially better solution to the problem. When thinking about data more than logic in this manner it ends up being that there's a lot of ways of accomplishing the same thing and it all depends on the needs of the application. As well as programmer time/knowledge. \$\endgroup\$
    – Blue_Pyro
    Commented Mar 30, 2019 at 22:58

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .