17
\$\begingroup\$

Consider a card game, such as Hearthstone.

There are hundreds of cards that do a wide variety of things, some of which are unique even to a single card! For instance, there is a card (called Nozdormu) that reduces player turns to only 15 seconds!

When you have such a wide variety of potential effects, how do you avoid magic numbers and one-off checks all over your code? How does one avoid a "Check_Nozdormu_In_Play" method in the PlayerTurnTime class? And how can one organize the code such that when you add even more effects, you don't need to refactor core systems to support stuff they've never had to support before?

\$\endgroup\$
  • \$\begingroup\$ Is this really a performance problem? I mean, you can do crazy amount of stuff with modern CPUs in almost no time.. \$\endgroup\$ – Jari Komppa Aug 3 '15 at 16:36
  • 11
    \$\begingroup\$ Who said anything about performance problems? The main problem I would see is constantly needing to adjust all your code every time you make a new card. \$\endgroup\$ – jhocking Aug 3 '15 at 16:37
  • 2
    \$\begingroup\$ so add a scripting language and script each card. \$\endgroup\$ – Jari Komppa Aug 3 '15 at 16:38
  • 1
    \$\begingroup\$ No time to make a proper answer, but instead of having the e.g. Nozdormu check and 15 second adjustment inside the "PlayerTurnTime" class code that handles player turns, you can code the "PlayerTurnTime" class to call a [class-, if you want] function supplied from outside at specific points. Then Nozdormu card code (and all other cards that need to affect the same plance) can implement a function for that adjustment and inject that function in the PlayerTurnTime class when needed. It might be useful to read about strategy pattern and dependency injection from the classic Design Patterns book \$\endgroup\$ – Peteris Aug 3 '15 at 20:45
  • 2
    \$\begingroup\$ At some point I have to wonder if adding ad-hoc checks to the relevant bits of code is the simplest solution. \$\endgroup\$ – immibis Aug 4 '15 at 0:29
12
\$\begingroup\$

Have you looked into entity component systems and event messaging strategies?

Status effects should be components of some sort which can apply their persistent effects in an OnCreate() method, expire their effects in OnRemoved() and subscribe to game event messages to apply effects which occur as a reaction to something happening.

If the effect is persistently conditional (lasts for X turns, but applies only under certain circumstances) you may need to check for those conditions at various phases.

Then, you just make sure that your game has no default magic numbers also. Make sure everything that can be changed is a data driven variable rather than hard coded defaults with variables used for any exceptions.

In this way, you never assume what the turn length will be. It's always a constantly checked variable that can be changed by any effect and possibly undone later by the effect when it expires. You never check for exceptions before defaulting to your magic number.

\$\endgroup\$
  • 2
    \$\begingroup\$ "Make sure everything that can be changed is a data driven variable rather than hard coded defaults with variables used for any exceptions." - Ooh, I quite rather like that. That helps a lot, I think! \$\endgroup\$ – Sable Dreamer Aug 3 '15 at 17:30
  • \$\begingroup\$ Could you elaborate on "apply their persistent effects"? Wouldnt subscribing to turnStarted and then changing Length value make the code undebugable and or even worse produce inconsistent results(when interacting between similar effects)? \$\endgroup\$ – wondra Aug 3 '15 at 17:52
  • \$\begingroup\$ Only for subscribers that would assume any given time span. You have to model carefully. It may be good to have current turn time be different from player turn time. PTT would be checked to create a new turn. CTT could be checked by cards. If an effect should increase the current time, the timer UI should naturally follow suit if it is stateless. \$\endgroup\$ – RobStone Aug 3 '15 at 17:54
  • \$\begingroup\$ To better answer the question. Nothing else stores the turn time or anything based off of that. Always check for it. \$\endgroup\$ – RobStone Aug 3 '15 at 18:02
11
\$\begingroup\$

RobStone is on the right track, but I wanted to elaborate since this is exactly what I did when I wrote Dungeon Ho!, a Roguelike that had a very complex effects system for weapons and spells.

Each card should have a set of effects attached to it, defined in such a way that it can indicate what the effect is, what it targets, how, and for how long. For example, a "damage the opponent" effect might look something like this;

Effect type: deal damage (enumeration, string, what-have-you)
Effect amount: 20
Source: my weapon
Target: opponent
Effect Cost: 20
Cost Type: Mana

Then, when the effect fires, have a generic routine handle the processing of the effect. Like an idiot, I used a huge case/switch statement:

switch (effect_type)
{
     case DAMAGE:

     break;
}

But a far better and more modular way to do it is via polymorphism. Create an Effect class that wraps all of this data, create a subclass for each type of effect, and then have that class override an onExecute() method specific to the class.

class Effect
{
    Object source;
    int amount;

    public void onExecute(Object target)
    {
          // Do nothing
    }
}

class DamageEffect extends Effect
{
    public void onExecute(Object target)
    {
          target.health -= amount;
    }
}

So we'd have a basic Effect class, then a DamageEffect class with an onExecute() method, so in our processing code we'd just go;

Effect effect = card.getActiveEffect();

effect.onExecute();

The way to deal with knowing what's in play is to create a Vector/Array/linked list/etc. of active effects (of type Effect, the base class) attached to any object (including the playfield/"game"), so rather than having to check if a particular effect is in play, you just loop through all of the effects attached to the object(s) and let them execute. If an effect is not attached to an object, it's not in play.

Effect effect;

for (int o = 0; o < objects.length; o++)
{
    for (int e = 0; e < objects[o].effects.length; e++)
    {
         effect = objects[o].effects[e];

         effect.onExecute();
    }
}
\$\endgroup\$
  • \$\begingroup\$ This is exactly how I've done it. The beauty here is that you have essentially a data-driven system, and you can adjust the logic rather easily on a per-effect basis. Usually you will have to do some condition checking in the effect's execution logic, but it's still a lot more coherent since these checks are just for the effect in question. \$\endgroup\$ – manabreak Aug 4 '15 at 4:48
1
\$\begingroup\$

I’ll offer a handful of suggestions. Some of them contradict each other. But maybe some are useful.

Consider lists versus flags

You can iterate over the world and check a flag on each item to decide whether to do the flag-thing. Or you can keep a list of only those items that should do the flag-thing.

Consider lists & enumerations

You can keep adding boolean fields to your item class, isAThis and isAThat. Or you can have a list of strings or enum elements, like { “isAThis”, “isAThat”} or { IS_A_THIS, IS_A_THAT}. That way you can add new ones in the enumeration (or string consts) without adding fields. Not that there's anything really wrong with adding fields...

Consider function pointers

Instead of a list of flags or enums, could have a list of actions to execute for that item in different contexts. (Entity-ish…)

Consider objects

Some people prefer data-driven, or scripted, or component entity approaches. But old fashioned objects hierarchies are worth considering too. The base class needs to accept the actions, like “play this card for turn-phase B” or whatever. Then each kind of card can override and respond as appropriate. There’s probably a player object and game object as well, so the game can do things like, if(player->isAllowedToPlay()) { do the play…}.

Consider debug-ability

Once nice thing about a pile of flag fields is that you can examine & print out every item's state the same way. If state is represented by different types, or bags of components, or function pointers, or being in different lists, it may not be enough to just look at the item's fields. It's all tradeoffs.

Eventually, refactoring: Consider unit tests

No matter how much you generalize your architecture, you’ll be able to imagine things that it doesn’t cover. Then you’ll have to refactor. Maybe a little, maybe a lot.

A way to make this safer is with a body of unit tests. That way you can be confident that even though you rearranged things underneath (maybe by a lot!) the existing functionality still works. Each unit test looks, generally, like this:

void test1()
{
   Game game;
   game.addThis();
   game.setupThat(); // use primary or backdoor API to get game to known state

   game.playCard(something something).

   int x = game.getSomeInternalState;
   assertEquals(“did it do what we wanted?”, x, 23); // fail if x isn’t 23
}

As you can see, keeping those top-level API calls on game (or player, card, &c) stable is key to the unit testing strategy.

\$\endgroup\$
0
\$\begingroup\$

Instead of thinking of each card individually, start thinking in terms of categories of effects, and cards contain one or more of these categories. For example, to calculate the amount of time in a turn, you can loop through all cards in play and check the "manipulate turn duration" category of each card that contains that category. Each card then increments or overwrites the turn duration based on the rules you've decided.

This is essentially a mini-component system, where each "card" object is simply a container for a bunch of effect components.

\$\endgroup\$
  • \$\begingroup\$ Since the cards - and future cards too - can do just about anything, I'd expect each card to carry a script. Still I'm pretty sure this is not a real performance problem.. \$\endgroup\$ – Jari Komppa Aug 3 '15 at 16:37
  • 4
    \$\begingroup\$ as per the main comments: Nobody (other than you) has said anything about performance problems. As for full scripting as an alternative, elaborate on that in an answer. \$\endgroup\$ – jhocking Aug 3 '15 at 16:44

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.