Monday, October 18, 2010

Forge's Architecture or "How to program your own Magic engine" - Part 1

The old Rocky and Bullwinkle cartoon would always announce the next episode with two separate but related titles like "Bombs Away" or "My Ears are Ringing". Today I wanted to talk about Forge's architecture which should point you in the right direction for programming your own Magic rules engine. (You should also download a copy of Magic's comprehensive rules and read through as much as you can. I remember thinking that trample worked even when a creature was blocking.)

For interested non-programmers, the word "class" or "object" means "lines of Java code". Card.getName() means that getName() is code that exists inside the Card object.


The most important classes in Forge are Card, SpellAbility, Input, and GameAction. The Card class represents a physical Magic card. Card objects are used everywhere that a real Magic card is used: player's library, graveyard, hand, battlefield, and exiled. Each Card object has a unique id so the player can know which card the AI is targeting. Shandalar had an option that allowed you to see each card's unique id.

(I was tempted to use only the card's name (String) in the library, hand and graveyard. I'm glad that I didn't because flashback was a late addition but it works beautifully because a card is a Card object and not a lousy String.)

The Card class has become more and more complicated and has grown from 200 lines to 17,000 (including whitespace and blank lines). Important Card methods are getName() : String, getSpellAbility() : SpellAbility[], getUniqueID() : int.


The SpellAbility class represents every spell or ability. A Card object holds one or more SpellAbility objects. A Card object representing Elvish Piper would hold two SpellAbility objects. One representing the 1/1 creature and one representing the activated ability. The SpellAbility for the 1/1 creature can only be played if the Card object is in the player's hand, so SpellAbility.canPlay() checks to see which zone the card is in. Likewise the SpellAbility representing the activated ability, so canPlay() only returns true if the card is on the battlefield.

SpellAbility.canPlayAI() returns true if the AI should play the card. This means that each card is responsible for evaluating the game state. SpellAbility.canPlayAI() for Giant Growth checks to see if any of the AI's creatures are attacking and then targets one creature. This type of AI is very basic but it allows the AI to use a wide variety of cards. Since each card is evaluated separately the AI won't kill a 4/4 flyer with 2 Shocks. SpellAbility.resolve() is self-explanatory and does the "action" of the card like destroying all creatures.

In Forge only SpellAbility objects can go on the stack. This has caused some complications because after you pay for a card, you can't simply move the Card object from your hand to the stack. In Forge 2, I have been thinking about technically allowing a Card object to go on the stack and then get the SpellAbility using a method like, Card.getStackSpellAbility().

The methods setTargetCard() and setTargetPlayer() are used when the card targets only one card or player. The AI also uses these methods which allows the resolve() to be the same for both the human player and the AI. (Having different resolve() methods for you and the computer is a dangerous, dangerous road that I almost went down. Duplicating resolve() for different players seems wrong because of the code duplication.)

SpellAbility.setDescription(String s)
SpellAbility.getDescription() : String

SpellAbility.setStackDescription(String s)
SpellAbility.getStackDescription() : String

These are typical get/set methods. getDescription() is used when the card is in your hand or in play, while getStackDescription() is used when the card (SpellAbility) is on the stack and should say something like "Shock - targeting Elvish Piper (23)". While these 4 methods are minor, they convey much needed information to the player.

Important SpellAbility methods include getManaCost() : String, canPlay() : boolean, canPlayAI() : boolean, resolve(), setTargetCard(Card), setTargetPlayer(String player).

(SpellAbility should use an abstract Cost class instead of getManaCost() because Cost could include tap and sacrifice costs. Forge has to hack tap and sacrifice costs in order to make them work.)

Since this was just an overview of Forge and some of you may want more details. Please leave comments about the technical details you are interested in. I have breezed over many of the small details like "how exactly do you pay for mana" and "what should the user interface look like". These are important decisions that you, the lead programmer, need to decide. Programming is the result of thousands of tiny decisions that need to miraculously work together.

Next week I'll talk about Input and GameAction.


Forge just uses Strings to represent mana and the reason that all mana strings have extra spaces "2 G G" was because it made the parsing code extra easy. This was a design decision that I had to make early on. It really doesn't matter if there are spaces or not but a decision has to be made.


Anonymous said...

Nice article!

Forge said...

The article is a little long. I try for 250 to 500 words and this one is 700 but hopefully I include some decent technical info.

HllKite said...

I'm a IT student so this in interesting stuff.

PS. dyou have a beef with frost titan? it's the only M11 titan that hasn't been implemented! I wonder in which way he wronged you.

Forge said...

Programmers tend to add cards that are interesting to them. No body is excluding Frost Titan for a reason but I'm glad that Forge has the other titans though.

Also the more complicated the card, the more time it takes to program.

Mizar said...

Wow, that is really a great article! I like how you describe the purpose of each method. I look forward to read the next part.

Anonymous said...

The information is out-of-date already. Which is not surprising considering the svc's rate of change (for the better). Thanks for the write-up.

Forge said...

This information was where Forge started at. Nowadays I don't know specifically how Forge currently does things but it has to work in a similar fashion.