Thursday, November 8, 2007

CardFactory - Longest Method

I try to discuss both Magic and computer programming in my blog but sometimes I need to address just the boring, technical side of programming. I’ll try to make it as “user friendly” as possible but feel free to skip this if it gets too technical for you.

Cardfactory.java is one of the key objects in MTG Forge, it makes cards. CardFactory is a huge class weighing in at 423 kb and 14,000 lines (with many blank lines for readability). It is the largest class in MTG Forge and is 10 times bigger than the user interface Gui_Display2.

CardFactory’s intention is just to make cards. The longest method in CardFactory is getCard(String cardName, String owner) which returns a new Card object. This one method is 14,000 lines long, insane I know, but I have never had any trouble with it. Each card is subdivided with brackets, limiting the scope of all variables, much like a method of a class. Look at the code at the end of this article for an example of Wrath of God.

I needed some way of returning a variety of different card objects. I could have cloned objects or used another mechanism. In the new version of MTG Forge, each object is in its own separate class. So the file Wrath_of_God.java holds all of the code for Wrath of God.

I have tried to use many good software practices and design patterns. MTG Forge’s user interface just observes. SpellAbility’s resolve method is abstract, much like the command pattern. All of the mouse input uses the state pattern. The state pattern is crucial and without it I wouldn’t know how to handle the variety of user input that Magic requires. MTG Forge would not exist without the state pattern.

The two most common classes are Card and SpellAbility. The Card class is used like a physical card and exists in your hand, play, graveyard, or library. SpellAbility handles the effect that spells and abilities do when they resolve. SpellAbility has an abstract resolve method where the functionality of a card is programmed. For example the resolve method for Wrath of God would destroy everything. A Card object can hold one or more SpellAbility objects.

A Card object representing Elvish Piper which has an activated ability, will have two SpellAbilities. One will be the normal summon creature spell that puts it into play and the other SpellAbility will represent the activated ability. A Card object representing Wrath of God will only hold one SpellAbility object. Combining the classes for both spells and abilities into one class was a pivotal moment during the development of MTG Forge.

For more information about programming cards for MTG Forge, download the file 10-18-mtgforge-source.zip from sourceforge.net/projects/mtgforge and read compile.htm. To see how Magic cards can be encoded as XML files see sourceforge.net/projects/firemox
God or Damnation

//code snippet from CardFactory.java.getCard(String cardName, String owner)
//mana cost and card text is read from the file “cards.txt”
//a Card object name “card” is created earlier in the method

if(cardName.equals("Wrath of God") cardName.equals("Damnation"))
{
//the Spell class extends SpellAbility and ensures that sorceries
//and instants can only be played at the appropriate times

SpellAbility spell = new Spell(card)
{
public void resolve()
{
CardList all = new CardList();
//AllZone is a global, static class that holds all of the zones
//for each player: play, graveyard, hand, library
all.addAll(AllZone.Human_Play.getCards());
all.addAll(AllZone.Computer_Play.getCards());

for(int i = 0; i < all.size(); i++)
{
Card c = all.get(i);
if(c.isCreature())
AllZone.GameAction.destroyNoRegeneration(c);
//GameAction is a global, static class that does common game activities
//like drawing a card, shuffling library, destroying a creature, etc...
}
}//resolve()

//the computer will only play this card if canPlayAI() returns true
public boolean canPlayAI()
{
CardList human = new CardList(AllZone.Human_Play.getCards());
CardList computer = new CardList(AllZone.Computer_Play.getCards());

human = human.getType("Creature");
computer = computer.getType("Creature");

//the computer will at least destroy 2 more human creatures
return computer.size() < human.size()-1 AllZone.Computer_Life.getLife() < 7;
}//canPlayAI()
};//SpellAbility
card.addSpellAbility(spell);
return card;
}//if Wrath of God or Damnation

5 comments:

Forge said...

The code always is badly formatted. Here is the code again.

//code snippet from CardFactory.java.getCard(String cardName, String owner)
//mana cost and card text is read from the file “cards.txt”
//a Card object name “card” is created earlier in the method

if(cardName.equals("Wrath of God") || cardName.equals("Damnation"))
{
//the Spell class extends SpellAbility and ensures that sorceries
//and instants can only be played at the appropriate times

SpellAbility spell = new Spell(card)
{
public void resolve()
{
CardList all = new CardList();
//AllZone is a global, static class that holds all of the zones
//for each player: play, graveyard, hand, library
all.addAll(AllZone.Human_Play.getCards());
all.addAll(AllZone.Computer_Play.getCards());

for(int i = 0; i < all.size(); i++)
{
Card c = all.get(i);
if(c.isCreature())
AllZone.GameAction.destroyNoRegeneration(c);
//GameAction is a global, static class that does common game activities
//like drawing a card, shuffling library, destroying a creature, etc...
}
}//resolve()

//the computer will only play this card if canPlayAI() returns true
public boolean canPlayAI()
{
CardList human = new CardList(AllZone.Human_Play.getCards());
CardList computer = new CardList(AllZone.Computer_Play.getCards());

human = human.getType("Creature");
computer = computer.getType("Creature");

//the computer will at least destroy 2 more human creatures
return computer.size() < human.size()-1 || AllZone.Computer_Life.getLife() < 7;
}//canPlayAI()
};//SpellAbility
card.addSpellAbility(spell);
return card;
}//if Wrath of God or Damnation

Nanocore said...

A new version, awesome! And, thanks for the technical write up and good to see that the next version will handle the cards a bit different (code-wise). A question, without looking at the code for the cards and judging by the code snippet here, is the direction to write code for each card specifically? Or, to implement specific functionality and then implement cards that just use that general functionality? This way you could get more cards covered rather then writing specific code for specific cards . Yes, you can cut and paste code to get another card functioning, but if you have to tweak that functionality, then you have to tweak all the code that you copied and pasted. Just a thought...

Forge said...

Hi Nanocore,

To answer you question

Is the direction to write code for each card specifically? Or, to implement specific functionality and then implement cards that just use that general functionality?

I try to program general functionality when it makes sense. Keywords like haste, flying, and vigilance can be easily added to cards since MTG Forge understands what the keywords mean.

The hard this about programming Magic cards is choosing the correct target or the effect. Like Royal Assassin only lets you target a tapped creature. An effect (after a spell or ability resolves) can be simple like destroying a creature or complicated. I copy-and-paste code because different cards effects are similar.

Going back to my Royal Assasin example usually I just want to target a creature like Giant Growth but Royal Assassin says that I have to target a tapped creature. So I just copy-and-paste the regular "target creature" code and check to see is the target tapped. That is why I copy-and-paste so much.

Nanocore said...

Sounds good, thanks for the clarification.

Ben said...

You could always try a data driven approach.

Just create all the abilities (in a generic form like haste, echo etc).

And then define the cards at runtime from a data source :)