If I was going to do this I would use a somewhat functional programming approach and design it this way…
class Player {
String name;
int hp;
...
}
abstract class Card {
Player apply(Player p);
}
Then for each card that you have, it extends Card and implements apply. The apply method could be purely functional in that it takes an instance of Player and returns a brand-new instance of Player that is changed. Both could be considered immutable when viewed from outside of Card’s implementation.
For example:
class PlusTwoCard extends Card {
Player apply(Player p) {
Player changed = new Player(p); // copy constructor
changed.hp = p.hp + 2;
return changed;
}
}
Since the behaviour of each Card is very different but the application is uniform I would design my Card hierarchy in the above way. The functional aspect makes it much easier to unit-test each Card’s behaviour.
If this functional approach seems too weird then you could change PlusTwoCard.apply to mutate Player directly:
void apply(Player p) {
p.hp = p.hp + 2;
}
However with mutated state it’s harder to unit-test and it’s harder to track temporary effects (e.g. an enchant card that lasts for 3 rounds and increases your armour. How do you know what to return the player back to when it wears off?).