This can get confusing... and Magic The Gathering is one that loves to do exceptions to rules. Trying to codify them can often be a challenge.
On the "MonoColored Hybrid" read this and this. And then this for Phyrexian mana.
However, this can boil down to something that is rather sensible.. and doesn't stick too much in a single enum or confuse the next person with an inheritance tree of enums. Enums really 'want' to be simple things, so let them be so. The simpler they are, the easier they are to use, and its a good thing to use enums.
A card has some cost. The good old fashioned lighting bolt is just "R". This is a List of some sort. It could even be empty as in the case of 0 cost artifacts (unless you want to do null object). But its a List.
Each element of the list is a SpellCost object.
A SpellCost would then be composed of:
An EnumMap of costs. The Enum that backs the map would be:
enum Mana {
WHITE,
BLUE,
BLACK,
RED,
GREEN,
COLORLESS
}
And then you would have the EnumMap be as a EnumMap<Mana, Integer>
. Something that was a "R/2" for "Red or two colorless" could then be specified in the EnumMap as: mana.put(Mana.RED,1); mana.put(Mana.COLORLESS,2);
This also handles cards that are described as "R/W G" (such as Naya Hushblade - because not all cards are monocolored hybrids).
The EnumMap and EnumSet are two of forgotten collections that are really nice once you realize what they are and do. They give you type safe entries in a map and are backed by either a simple array or a bitfield. The type safety on it can prevent a number of bugs associated with bitfields. And the type safety on the map is something to think about whoever you've got a Map<String, Object>
and wonder what happens when there's a typo in the String.
Phyrexian mana is then an attribute of the SpellCost. Its a int (though you could do it as a boolean later and yell YAGNI in my general direction). Currently the definition of the cost is "mana or two life" but I wouldn't count on that being the case forever and locking yourself into a boolean now... well.
So, you've got:
public class SpellCost {
EnumMap<Mana, Integer> mana;
int lifeCost;
...
}
And then write the associated methods for it. You might want to write a container for the cost itself rather than leaving it as a bare List
so that you can have appropriate methods against it.
The key design points for this is that it keeps the enum simple as enums aught to be. Any of the complexity of the business logic is kept within the SpellCost
class (and possibly the container class too).
I would approach this problem as I would approach any localization issue: ResourceBundle. I use a class called I18n
that has a static method called getMessage
that takes a key and optionally a list of arguments. The key gets looked up in a ResourceBundle
configured to use the default Locale
or whatever you prefer (specifics here aren't important, so long as the caller to I18n
doesn't have to know about what the Locale
is).
The I18n
class is as follows:
public final class I18n {
private I18n() {
}
private static ResourceBundle bundle;
public static String getMessage(String key) {
if(bundle == null) {
bundle = ResourceBundle.getBundle("path.to.i18n.messages");
}
return bundle.getString(key);
}
public static String getMessage(String key, Object ... arguments) {
return MessageFormat.format(getMessage(key), arguments);
}
}
In your project, you would have in package path.to.i18n, files containing messages.properties (default), messages_en.properties (Locale en), messages_it.properties (Locale it), etc.
When you need to translate an enum value, you shouldn't have to treat it any differently than any other key, except the key is the enum term.
In other words, you have:
public enum AccountStatusEnum {
Active,
Inactive,
Pending
}
When you call I18n.getMessage
, you pass with AccountStatusEnum.Active.toString()
and the translation will be found in a property file with key "Active". If you're like me, and you prefer to use lower-case keys, then you should perform toLowerCase()
on the string. Better still, you create a new I18n.getMessage
that takes an Enum rather than a key, and that method calls the String version of I18n.getMessage
with the enum value converted to string and in lowercase.
So you'd then add this to the I18n
class:
public static String getMessage(Enum<?> enumVal) {
return getMessage(enumVal.toString().toLowerCase());
}
I prefer to use this method because it incorporates the same translation logic in the same part of the program without impacting your design. You may even want to differentiate enum keys from your other keys, in which case you can begin each enum key with "enum.", so you would prepend "enum." when calling getMessage
. Just remember to name your keys in your properties files accordingly.
I hope that helps!
Best Answer
I would generally recommend using enums in this situation. Reasons:
One problem with enums is that you can't add new values to an enum at runtime (at least I don't know of any way to), so if your program will need to be able to create entirely new keys based on user input at runtime, enums can't help you with that. But you could key your data structure with "soft" enums (the string values of the enums) and user-entered strings to get a sort of hybrid solution.