Tuesday, June 19, 2007

Design: Card

This will be the first of a set of articles that I will write talking about how I designed MTG Forge. (Well 2nd article, the blogger really messes up Java code.) Magic focuses on cards, so I have a Card object. (I capitalize object names, it’s a Java thing.) Card objects are used everywhere that you would have a regular, cardboard card, in hand, in play, in your graveyard. Card objects hold values like card type (Creature, Sorcery), attack, defense, owner, and controller. Card objects don’t really do anything in and of themselves; they just hold a bunch of related values.

Another important class SpellAbility, implements the functionality of all spells and abilities. It has an abstract resolve() method that is overridden, so Wrath of God’s resolve() would destroy everything, got it?

In MTG Forge, Card objects have one or more SpellAbility objects. A generic creature like Eager Cadet, God bless his frail body, has only one SpellAbility. All creatures are spells, so all Card objects hold at least one SpellAbility. The class CardFactory constructs Cards by adding SpellAbilities, and also implementing the core functionality of the card by the resolve() method. (Resolve() is a method so it gets parenthesis. It is just hard to talk about code, without showing code.)

Ok review, Card objects exist in hand, play, grave, and removed from game. Each Card object has at least one SpellAbility. If a creature has an activated ability like Elvish Piper, the Card will have two SpellAbilities, one SpellAbility for the creature spell, and one for the ability. SpellAbility has an abstract resolve() method that implements the card function, it does the work of the card like destroying things or doing damage, etc..


//*************** START *********** START **************************
//note the mana cost was already set, so I can use the same resolve() for both cards
if(cardName.equals("Wrath of God") || cardName.equals("Damnation"))
{
final SpellAbility spell = new Spell(card)
{
public void resolve()
{
CardList all = new CardList();
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);
}
}//resolve()
public boolean canPlayAI()
{
return 0 < CardFactoryUtil.AI_getHumanCreature().size();
}
};//SpellAbility
card.clearSpellAbility();
card.addSpellAbility(spell);
}//*************** END ************ END **************************


//*************** START *********** START **************************
//note, the card object already has a standard summon creature SpellAbility added to it
if(cardName.equals("Thought Courier"))
{
//Ability_Tap is a subclass of SpellAbility
final Ability_Tap ability = new Ability_Tap(card)
{
public boolean canPlayAI() {return false;}
public void resolve()
{
AllZone.GameAction.drawCard(card.getController());
AllZone.InputControl.setInput(CardFactoryUtil.input_discard());
}
};//SpellAbility
card.addSpellAbility(ability);
ability.setDescription("tap: Draw a card, then discard a card.");
ability.setStackDescription("Thought Courier - draw a card, then discard a card.");
ability.setBeforePayMana(new Input_NoCost_TapAbility(ability));
}//*************** END ************ END **************************


3 comments:

Forge said...

Man, I have a hard time posting code, hope this is readable.

Design Card: Boil

Ok, let’s get down to the nitty, gritty and program a new card, Boil. In case you forget, Boil destroys all Islands. The code is supposed to be put in the getCard() method in the CardFactory object.

First, you have to add these lines to cards.txt The mana cost has to have spaces like “3 G G”, or “U U”. The spaces in the mana cost are required because it was easier to program (parse).

Boil
3 R
Instant
Destroy all Islands.

Hopefully you have looked at the code. The object named card is a Card object that exists outside of this code snippet. The SpellAbility class implements all spells and abilities. The resolve() method is similar to Wrath of God, in fact I just cut-and-pasted and changed it a little.

This code gets all of the cards in play. CardList is like ArrayList but holds only Card objects. AllZone is a global object that holds all zones like human play, human grave, computer play, computer grave, stack, etc…


CardList all = new CardList();
all.addAll(AllZone.Human_Play.getCards());
all.addAll(AllZone.Computer_Play.getCards());


Below just loops through all the cards in play and destroys all Islands. Card.getType() contains all Strings that are in the type line, like Sorcery, Instant, Goblin, Creature, Bird, Basic, there is no differentiation between super type and subtype, before and after the dash. GameAction does high level actions like drawing a card, starting a new game, destroying a card, etc..


for(int i = 0; i < all.size(); i++)
{
Card c = all.get(i);
if(c.getType().contains("Island"))
AllZone.GameAction.destroy(c);
}


The computer AI calls canPlayAI() to see if it can play this card. For Boil, the AI code checks to see if the human player (you) have any Islands in play. The computer doesn’t care if he happens to have any Island in play or not.

public boolean canPlayAI()
{
CardList list = new CardList(AllZone.Human_Play.getCards());
list = list.getType("Island");
return 0 < list.size();
}


You could have the AI check to see if you have 3 or more Islands in play before playing Boil.

public boolean canPlayAI()
{
CardList list = new CardList(AllZone.Human_Play.getCards());
list = list.getType("Island");
return 2 < list.size();
}

I am very proud of the code listing for Boil. I think the code is very readable and it adheres closely to the concepts of Magic. For example the resolve() method is called after it is popped off the stack. The stack only holds SpellAbilities and the Card class mimics the use of a cardboard card.


if(cardName.equals("Boil"))
{
final SpellAbility spell = new Spell(card)
{

public void resolve()
{
CardList all = new CardList();
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.getType().contains("Island"))
AllZone.GameAction.destroy(c);
}//for

}//resolve()

public boolean canPlayAI()
{
CardList list = new CardList(AllZone.Human_Play.getCards());

list = list.getType("Island");

return 0 < list.size();
}
};//SpellAbility

card.clearSpellAbility();
card.addSpellAbility(spell);
}

Nanocore said...

I haven't put your comment next to the actual source code but do have a question about one of the lines you have here. It is the "if (cardName.equals("boil"))". This is done by the AI, correct? It isn't the resolution of the cards as they are popped off the stack during damage resolution, correct?

Given that it is for the AI, then you can create another class that handles the AI function (like SpellAbility class) and associate this with the card. This way you could create general interrogation methods for the AI to query the card to find the correct card to play in a certain situation. This opens the ability to make the AI as smart or dumb based upon the code in these methods. Just a thought...

Forge said...

The line

if (cardName.equals("Boil"))

is used just to create the card. The resolve() method is created once the card is created. So a card has resolve() even though it is just in your hand.

I'm not sure how to have the AI interrogate the card, like how would the AI know to target a creature with only a 2 toughness?
Thanks for your comments.