Thursday, June 14, 2007

Programming Magic Cards is Hard

If you want to play Magic on the computer, you have to program cards. The central problem is how to encode Magic cards into a programming language. I have had e-mail asking, “How do you program Magic cards” and this article aims to answer that question. I wish I could say that my way is the easiest, best way to program Magic cards, but it is just a way.

The Magic Project lets you play against other human players over the Internet and enforces the rules unlike Apprentice. The Magic Project, sourceforge.net/projects/magic-project, uses XML for each card.

!-- {2}{b}{b} search your library for a card and put that card into your hand. then shuffle your library. -->
init>
registers>
register index="colorless" value="2"/>
register index="black" value="2"/>
/registers>
colors>black
idcards>sorcery
/init>
abilities>
activated-ability playable="this" name="" zone="hand">
cost>
pay-mana value="manacost"/>
/cost>
effects>
action ref="search-lib"/>
action ref="return-to-hand"/>
action ref="finish-spell"/>
/effects>
/activated-ability>
/abilities>
/card>

(Sorry the XML is butchered so badly.)

The Magic Project has 2,000+ cards programmed, which is a Herculean task in and of itself. The XML schema, mpvalidator.xsd, is 240kb, which is pretty long, but it has to allow for so many card variations and restrictions, like Terror that can only target non-black creatures.

My project MTG Forge, sourceforge.net/projects/mtgforge, just codes the cards into Java. MTG Forge currently has 365 cards. Let us look at Thought Courier, since he has a simple ability. The computer cannot play Thought Courier’s ability because it cannot evaluate cards, that is the meaning of “public boolean canPlayAI() {return false;}”

//*************** START *********** START **************************
if(cardName.equals("Thought Courier"))
{
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 **************************

Demonic Tutor is a little bit longer, but is very straight forward. In case you don’t know, it lets you search your library for a card and then put it into your hand.

//*************** START *********** START **************************
if(cardName.equals("Demonic Tutor"))
{
final SpellAbility spell = new Spell(card)
{
public void resolve()
{
String player = card.getController();
if(player.equals(Constant.Player.Human))
humanResolve();
else
computerResolve();
}
public void humanResolve()
{
Object check = AllZone.Display.getChoiceOptional("Select card", AllZone.Human_Library.getCards());
if(check != null)
{
PlayerZone hand = AllZone.getZone(Constant.Zone.Hand, card.getController());
AllZone.GameAction.moveTo(hand, (Card)check);
}
AllZone.GameAction.shuffle(Constant.Player.Human);
}
public void computerResolve()
{
CardList list = new CardList();
Card[] library = AllZone.Computer_Library.getCards();

//gets cards that Computer has mana to play
for(int i = 0; i < library.length; i++)
if(ComputerUtil.canPayCost(library[i].getSpellAbility()[0])) list.add(library[i]);
//pick best creature
Card c = CardFactoryUtil.AI_getBestCreature(list);
if(c == null) c = library[0]; //System.out.println("comptuer picked - " +c); AllZone.Computer_Library.remove(c);
AllZone.Computer_Hand.add(c);
}

public boolean canPlay()
{ PlayerZone library = AllZone.getZone(Constant.Zone.Library, card.getController());
return library.getCards().length != 0; }

public boolean canPlayAI() {
CardList creature = new CardList();
creature.addAll(AllZone.Computer_Library.getCards());
creature = creature.getType("Creature");
return creature.size() != 0; }

MTG Forge there is a Card class that represents cards both in your hand and in play. Every Card object has one or more spells or abilities that are added to it. The class SpellAbility implements all spells and abilities. Every spell or ability has to implement a resolve() method in the SpellAbility class, resolve() does the main functionality of the card. Thought Courier’s ability is implemented in the resolve() method. The same goes for Demonic Tutor, the resolve() method lets you actually look through your library and choose a card. Since the computer can play cards, you will see that Demonic Tutor’s resolve() method does something different depending on if you or the computer played it. MTG Forge also reads cards from a file called “cards.txt” Cards with simple abilities like haste, fear, flying, vigilance, and mana abilities don’t have to be programmed, they are just added to the text file. No extra programming has to be done for cards like Llanowar Elves and Lightning Angel.

Llanowar Elves
G
Creature Elf Druid
no text
1/1
tap: add G

Lightning Angel
1 R W U
Creature Angel
no text
3/4
Flying
Vigilance
Haste

8 comments:

Nanocore said...

Ah, now this is what I like to see!

Anonymous said...

Hear hear, good stuff!

Nanocore said...

I haven't yet gotten to dig deep into the code and looking at this snippet certainly whets my appetite. From looking at this, is it safe to assume that you have built the logic for each card into the code for each card? Would it not be better to program the function and resolve as independent functions? Then, for each card you would define what function to call as a string or index key. Then when you need to perform the function of the card you would interrogate the card for what function to call and follow the link to the class. Possibly using reflection.

Forge said...

The logic is stuffed into each card. Hopefully I will get to the point where the computer evaluates what effect the card will do. So the computer could play any card without having code stuck into each card.

Forge said...

I'm glad "this is the good stuff", lol. More good stuff on the way

Nanocore said...

Ahh, let me give a bit more desire around why I say to have separation of functions from the cards.
In the version that I was working on, the goal was to have the ability to have a wizard-like function that you would pull up a card and then be able to go through a selection process to define what type of function this card did. Only defined functions would be available, and each function would have a separate page in the wizard that would define the attributes specific for that function. In this way you could define the card function and well as level of offense and defense function which would be used by the AI. In this way, if you program the engine, then others could define cards and their function.

I had another desire for the AI or rather training the AI ...something like a software debugger stepping through, stop, change the value and then replay the last few steps etc...maybe you get the point. Again to provide the framework and then let folks more knowledgeable about the help define the strategies.

Forge said...

My initial idea was to have some sort of user interface wizard that would let me and other people program cards, but I couldn't work out the details, it was too complicated.

Nanocore, you sound like you have some sophisticated card ideas. I understand some of what you say but not all, hey I'm truthful. The way I program cards is more of a "get it done any way I can" sort of way, without much software engineering. But the card code is easy to use in a cut-and-paste sort of way.

Unknown said...

Hi

It's nice to see some of your code! So if I understand correctly, you've gone the route of programming every card separately? Is each card a class that extends card? I think that's what it looks like.

When I initially started my own project I stumbled on this quite a bit. I wanted desperately not to have to make a unique class for each card. What I wanted was to be able to break down all possible card text into a set of actions, and then to have each card instead be an instance of some subclass of Card. Like, an instance of Creature or an instance of Sorcery. Honestly, though, I'm still not sure exactly how I will approach this.

I agree with the philosophy of writing code to "just get it done", especially as a beginning programmer (which is what I am). I try as hard as I can to keep everything object-oriented and top-down... but sometimes I just wanting the program to run so badly that I'll do what it takes. Sometimes you find interesting gems that way!

A story: the other night I was finishing Gang of Four (a card game) and wanted some way to use mouse clicks to put play cards instead of having to select card and press buttons. I kind of spontaneously taught myself how to send do this with a thread. I suddenly realized how great it would be if state-based effects and static effects were handled by threads! So there is a story of how trying to hash together some code to catch mouse clicks will sometimes solve a logical problem you've been struggling with.

Thanks!

z.