I have been trying to brush up on my java and wrote a small card game to do so. During this I had some problems while trying to uncouple unrelated parts.
First a motivating example:
Cards can have subtypes like Ammunition. Ammunition can be played directly like normal attack cards or be fired by other cards which causes a different effect.
Cards have to be stored in a homogeneous collection so there has to be some degree of reflection or reification to check for the concrete capabilities of a card. Unrelated parts also have to react to these actions, think for each ammunition fired this round do X
and ideally these events shouldn’t have to be triggered manually.
In summary: It should be possible to add new cards and new subtypes with new actions without having to modify (or ideally even recompile) old code. A card has to be able to implement multiple subtypes. It has to be possible to query at runtime which actions a card supports, to invoke them and to subscribe to action types and be notified whenever one is played.
The simplest solution would be to use interfaces directly and make each card implement the relevant ones. This feels somewhat unsatisfactory, however. It’s possible to check for interfaces via instanceof but intersection types aren’t first class so combining subtypes becomes arduous. Code sharing between similar cards can also become quite verbose and notifications have to be triggered manually which seems like it would cause hard to catch bugs in the long run.
My second idea was to use a heterogeneous map like IdentityHashMap<Class<?>, Feature>
so that implementations could be queried like
Optional<Ammunition> ammo = card.get(Ammunition.class); ammo.ifPresent(impl -> impl.shoot(card, source, target));
but the Ammunition should be able access the members of the card somehow, ideally without unsafe casts. The Ammunition interface can’t be generic over the concrete card type since the type token doesn’t reify type parameters. Adding a reference to the card in Ammunition works but implies a separate heterogeneous map per card instance instead of per card type. Alternatively the instance data could be stored outside of the card object so that cards are basically only unique identifiers which make it possible to query their data and actions.
This seems to work but I noticed that this just sounds like slowly reinventing entity component systems. Since the goal of the project was to play around with OOP that seems kind of counterproductive, though.
So here are my questions:
- Is this even a reasonable thing to want or am I missing a better approach that circumvents this issue?
- Is there a clean (or at least more OOP) approach to solving this?
Thanks for reading and sorry that the text got so long!