Wednesday, December 3, 2008

How to Play a Card - Card Objects

This article is a little long but it fully explains MTG Forge's card objects.

I was very happy when MTG Forge version 2 was able to accomplish the steps needed in order to play a card. While paying a mana cost doesn't seem difficult, in reality it involves many little steps that have to be programmed.

First let me give you a little bit of background on how MTG Forge 1 and 2 actually encodes a Magic card. (I'm skipping a lot of details that I have described elsewhere and in the actual code. And remember that an object is just another term for a group of Java statements.) A card object is very basic and holds the card's name, type (like sorcery or creature) and any creature subtypes (like Elf or Goblin). A card object holds one or more SpellAbility objects. SpellAbility objects are used to represent all spells and activated abilities. SpellAbility objects implement the actual function of a card, like destroying all creatures.

So when you play a card, you are playing the SpellAbility object that the card is holding. The potential problem is that a card may hold many SpellAbility objects. The Royal Assassin card holds two SpellAbility objects, one is the normal summon creature spell and the other SpellAbility represents Royal Assassin's "tap: destroy target tapped creature" ability.

SpellAbility also has a method called "canPlay()" which returns true if it can currently be played and false if it cannot. A sorcery like Wrath of God has a SpellAbility that allows it to be played from your hand. Activated abilities like Royal Assassin's have SpellAbility objects that can only be used when the card is in play.

To get back to my original question, "The potential problem is that a card may hold many SpellAbility objects." When you click on a card to play it, each SpellAbility is checked to see whether it can be played. If you try to play Royal Assassin from your hand, the summon creature SpellAbility can be played while the SpellAbility representing the activated ability cannot.

Using the same object, SpellAbility, to represent all spells and abilities has saved me a TON of extra work. At first I was coding spells and activated abilities differently but after stepping back from the problem, I was able to handle both situations with the same code. Finding simple solutions takes a lot of hard work, lol.

(The stack in MTG Forge only holds SpellAbility objects. The computer AI also uses SpellAbility. In the beginning I had to write a card for you, the player, and then rewrite the whole card for the computer. The computer uses SpellAbility canPlayAI() method to determine if it should play the card. With a card like Naturalize, the computer will only play it if you have an artifact or enchantment.)

p.s.
Below is a listing of the relevant methods for Card and SpellAbility. Be aware that Card objects don't have a mana cost, only SpellAbility does. Currently SpellAbility doesn't easily handle activated abilities that have additional costs like "tap" or "sacrifice this card" and I'm still working on this problem. The resolve() method in SpellAbility is where the function of a card is put like "destroy all creatures" or "target creature gets +3/+3 until end of turn."

Card
addSpellAbility(SpellAbility sp)
getSpellAbility() : SpellAbility[]

SpellAbility
resolve()
canPlay() : boolean
canPlayAI() : boolean

getManaCost() : String
setManaCost(String)

p.s.s.
The Card object also holds extra information like a creature's attack and defense. While conceptual this might be a little incorrect, the code was easy to write. Usually I have trouble coding something if my design is wrong.

10 comments:

  1. Kudos for anyone that actually reads the whole thing, lol. I got long-winded.

    ReplyDelete
  2. Hey Forge,

    Before you get too deep into your implementation, here's an issue you should take into consideration (I didn't even realize this was an issue until too late, and then I had to rewrite/redesign a whole bunch of code). Every time a card moves to a new zone it becomes a new object. For example, if you target a creature, and then momentarily blink it out of the game and back into play, the targeting will fail because the object it pointed to is no longer in existance. I'm assuming that when you compare cards, you are probably using object identity, which means that this type of rule is very difficult to implement. Instead, i recommend that every time you move a card between zones, you make a copy of it and insert it into the new zone. So basically, there's nothing linking card objects in each zone (actually, some abilities need this, so I actually treat it as a chain of objects, i guess more like a history, each one belonging to a zone), and when the card.move_to(zone) method is called on that object, it creates a copy of itself, adds it to the new zone, removes itself from the current zone, and then sets an invalidated flag on itself (that way, the object still exists for last-known-information purposes, but nothing can be done to it).
    If you don't consider this in the beginning, you'll find yourself trying all sorts of hacks to make last-known-information work.

    ReplyDelete
  3. i was thinking about using timestamps for identifying the zone changes, but what you say about last known info seems very right. thanks for the tipp;)

    ReplyDelete
  4. Incantus,

    "Every time a card moves to a new zone it becomes a new object."

    Thankfully I realized this early on when a creature that was pumped up with Giant Growth went to the graveyard. I said, oh crap.

    To fix this I just create a new card when a card changes zones, no biggie, just like you suggested. So all creature stats are reset when the card goes to the graveyard. MTG Forge version 1 does a lot of things wrong, but it does this right ;)

    I haven't touched "last known information" (LKI). I know it is sometimes needed, but most situations don't require it. I hate edge cases. (Off the top of my head I can't think of a situation that uses LKI but I have seen faqs about it on starcitygames.com)

    ReplyDelete
  5. Forge,

    That's good. I didn't realize it until recently, and so my implementation was pretty broken while I tried to fix it (eventually i isolated all the hacks into the Zone code, and just recently eliminated it). Anyway, LKI isn't a corner case at all :) Here's a simple example:

    Giant growth on a 1/1 creature so that it's 4/4. You have a Proper Burial in play. If the opponent Terror's your creature, you should gain 4 life (not 1 life). However, the Proper Burial trigger resolves long after the creature was put into the graveyard, so you need to somehow track the old object (the one that was in play).

    ReplyDelete
  6. Persist also uses LKI.
    My own code for LKI is hacky as hell, I'll keep your piece of advice in mind, Incantus !

    ReplyDelete
  7. Incantus,

    You have a good point, I think MTG Forge would still work correctly in your example since it doesn't use layers or LKI. In MTG Forge, Proper Burial would see a 4/4 being removed from the zone, then a new card object representing the 1/1 creature would be put in the graveyard.

    In version 2, each zone fires addCard and removeCard events that can be observed and Proper Burial would just observe the removeCard event from the player's in play zone. I know "in play" is a shared zone, but it is useful to be able to observe only the player's or computer's play zone.

    ReplyDelete
  8. And for whats it worth you can read Proper Burial here

    ReplyDelete
  9. Also, it's not improper at all to keep power/toughness values in the card object. That's how Magic itself works.

    ReplyDelete
  10. Thanks MageKing, it is nice to know that I'm on the right track.

    ReplyDelete

Note: Only a member of this blog may post a comment.