3

So I'm building a card game where each card has cost, damage, and durability, as well as name and description. Obviously one would think of creating a Card class:

class Card {
    int cost
    int damage
    int durability
    string name
    string decription
}

werewolf = Card(5, 10, 120, 'Werewolf', 'Deal double damage during night')
mage = Card(7, 40, 40, 'Mage', 'Deal damage without taking damage himself')

But now the problem is, I need multiple copies of each of these instances. Every player in the game can have 2 copies of each card, that means up to 8 copies of every card.

Some of the attributes are "static" i.e. always same to all similar instances, such as name, description, and cost. However, every instance has variety in health and damage; one mage might end up having more damage after a buffing spell is cast on her, and another has lower health after being damaged.

What would be a good way to implement and structure such feature? Subclasses or something, and how?

  • Do your cards change after they are instantiated? or are they static throughout the game? IE a werewolf always has a cost of 5? – RubberChickenLeader Feb 4 '17 at 16:18
  • @WindRaven Some attributes are static, but some can change (health and damage for example change constantly). I don't yet have any need for changing mana but in theory there could be a card like "Throw an enemy card back into his had, it costs 1 more" or so – Markus Meskanen Feb 4 '17 at 16:19
  • The answer to this question depends on what language you're using, so it would be useful to add this information. (But. briefly, what you're describing is an extremely good fit for languages that provide prototype-based inheritance, e.g. Javascript; make a single base card with the common details and use it as the prototype for the objects with the varying details). – Periata Breatta Feb 4 '17 at 22:45
  • 1
    Sounds like a Prototype Pattern whose intent is precisely to avoid subclassing. – edalorzo Feb 4 '17 at 22:54
4

Trying to use subclassing just to share some data is not advisable, especially in games. You'll get a more complex object model without any benefits.

One possible strategy would be to add a factory that manages creation of the cards. That automates the duplication.

With respect to your changing properties, there are a variety of ways how this could be managed. In most cases, it is probably best if the changing properties are not persisted as instance variables of a card, but are calculated on the fly from the game context. Especially if status effects are cancelled (e.g. after a number of rounds), resetting all card stats to their previous values would be tedious otherwise.

The observation that a “class” of game units shares common properties is very common. Introducing a type object is then probably the best solution. We separate each card instance with changing properties from a card types, that stores the default stats and static description:

class CardType {
  int cost
  int damage
  int durability
  string name
  string description
}

class Card {
  CardType type
  int damage
  int durability
  // perhaps add getters/properties for the fields from the type object here
}

Using composition instead of inheritance adds a lot of flexibility to your system. However, moving parts of the type system to runtime objects and making everything more dynamic also means more complexity and therefore more fragility – you'll have to balance these various factors in your design.

  • How would this work if the structure was two levels deep? Like HearthStone, where you have heroes, each of which has unique attributes like health and a list of their cards, but also common attributes like name. But then there are different types of cards, each has unique attributes and non-unique attributes? – Markus Meskanen Feb 4 '17 at 19:11
  • @MarkusMeskanen I haven't played HearthStone and therefore don't know precisely how it works, but it sounds like the hero-types are independent from the card-types. If the cards have varying attributes, then perhaps using a class hierarchy for them is best. Or maybe it's better to script all those details in a dynamic language instead, so that the varying data is stored in a hash table. How this can be done sensibly depends entirely on your requirements and use cases, there is no general answer. – amon Feb 4 '17 at 22:45
  • You might want to think about how Card's functions will work before finalizing the design – David Feb 5 '17 at 4:52
1

I wouldn't make subclasses if only the data differs. Store initial copies somewhere, such as a static Map<CardType,Card>, then have the constructor take a CardType and initialize the Card with values from the initial copy.

  • 1
    How would this work if the structure was two levels deep? Like HearthStone, where you have heroes, each of which has unique attributes like health and a list of their cards, but also common attributes like name. But then there are different types of cards, each has unique attributes and non-unique attributes? – Markus Meskanen Feb 4 '17 at 19:11
  • @MarkusMeskanen Then I'd use subclasses, cause not only the data differs. – JollyJoker Feb 5 '17 at 12:22
1

I don't have experience with the particular domain of the problem, but let me share some ideas that hopefully make at least a bit of sense.

Regardless of the problem domain, a good principle of software design is to encapsulate what varies.

There seems to be a set of fundamental properties of every card that are immutable and another set of properties that are indeed mutable.

Note: this is a indication that perhaps the abstraction of Card in this particular domain is not clear enough. Perhaps you still need to find a more proper abstraction. Think outside box or gain a better understanding of the problem domain and then you will probably get to the conclusion that the abstraction with the immutable properties is not the same as that with the mutable ones (e.g. Card vs Game vs Move or Draw, etc)

It seems that immutable properties are intrinsic to the card itself, whereas mutable properties are correlated to a particular instance of the game in which the card was played (i.e. a game event, game move, card draw, etc.).

For example, I know that Warewolf card has a health of 5, but if I record every Move/Event/Draw of the game, by reapplying the event stream I could recalculate the current health of a particular Warewolf card.

class Game {
    List<Move> moves;
}

Where Move can be

class Move {
  Player player;
  Card card;
}

Every time you play a card, you add a move event to your list. If I want to know the current state of the world, I can derive a projection by reapplying the list of moves. For performance purposes, you may decide to keep the latest projection in some other data structure within the Game with the given advantage that once the game ends, you can discard it and start all over again.

Map<Player, CurrentStateOfHisWorld>

It would seem that the game can be entirely thought in a functional way, using totally immutable data structures and perhaps be expressed just as a series of events in the form of draws or cards played, and as the events happen a new state of world is defined.

Am I making any sense?

0

I suggest treating all card values as fixed base values. You can then create separate classes for buffs and debuffs and calculate the effective damage and health values.

class Stats {
    final int damage; // final = cannot be changed
    final int durability;
}

class Card {
     final String name;
     final String description;
     final Stats stats;
}

interface Effect {
    public Stats apply(Stats stats);
}

class Injury implements Effect {
    public Stats apply(Stats stats) {
        return new Stats(stats.getDamage(), stats.getDurability() - 10);
    }
}

class DamageBuff implements Effect {
    public Stats apply(Stats stats) {
        return new Stats(stats.getDamage() * 2, stats.getDurability());
    }
}

public void example() {
    Card card = ...;
    List<Effect> effects = ...;

    Stats stats = card.getStats();
    for (Effect effect : effects) {
        stats = effect.apply(stats);
    }

    int effectiveDamage = stats.getDamage();
}

Now this is just a simple example. Your game may require some additional logic (for example ordering, e.g. making sure that some effects are applied before others), but I think the general principle could still be used.

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.