Tuesday, January 20, 2009

SpellAbility Improvements - Part 1

The SpellAbility class represents all spells and abilities in MTG Forge. MTG Forge implements cards as basically just a bunch of SpellAbility objects, the Card class just holds one or more SpellAbility objects.

In order prevent you from playing most abilities while you are holding a card in your hand, SpellAbility has a canPlay() method which returns true if the ability can currently be played. canPlay() answers the question, "Can this SpellAbility be played when this card is in this zone?" SpellAbility also has a mana cost associated with it, since most spells and some abilities require some amount of mana.

There are many areas to improve SpellAbility. First, SpellAbility doesn't handle "tap" and "sacrifice" activated effects very well, they have to be "hacked" by tapping the card using custom code that pays the mana cost. The custom pay mana cost code just taps the card after the mana cost is paid, the same applies to sacrifice effects.

In order to generalize tap and sacrifice, a method like runBeforeStack() needs to be added to SpellAbility. runBeforeStack() would always be executed before the SpellAbility object would be put on the stack. Most of the time runBeforeStack() would be empty, but it would be a way of implements tap and sacrifice effects.

runBeforeStack() could be used to remove counters from a card. canPlay() would check to see if the card had the necessary number of counters in order for the ability to be played in the first place.

This is an example of how these SpellAbility methods work together. First, canPlay() is called, then you pay your mana costs and/or choose your targets, then runBeforeStack() would executed. Then resolve() is called after the spell or ability is resolved off of the stack.

p.s.
Lands are the only cards in MTG Forge that don't have a SpellAbility object. Lands are internally represented as cards with no abilities because all mana abilities are handled using strings instead of SpellAbility objects. In MTG Forge version 2, lands will have a SpellAbility that creates mana which will be either used immediately or added to the mana pool.

p.s.s.
"Programming can be fun, so can cryptography; however they should not be combined". --Kreitzberg and Shneiderman

11 comments:

Forge said...

This is the first part of 10 articles on SpellAbility, because it is such an important class.

Unknown said...

ANd yet, very timely as I just posted some thoughts on figuring out the why's and hows of choosing SpellAbility, Ability, Ability_Tap, Abiliity_Hand or Ability_Activated on the forum earlier.

rising fruition said...

Maybe you could generalize costs instead of adding runBeforeStack (or maybe in addition to it).

Tapping is sometimes a cost, as is sacrificing, often without a corresponding mana cost.

The Cost for a spell or ability would have a list of CostItems, which could include any kind of cost and any number of costs: life reduction, sacrificing a permanent, tapping, mana, whatever.

I haven't looked at your code, so I don't know how hard this might be.

This is very interesting. Thank you for sharing thoughts on your program's architecture.

Anonymous said...

I've discovered MTG forge 2 weeks ago and.. well that is AWESOME! I was wondering if there is any chance to somehow mix Apprentice and Mtg Forge. Let's say, if MTG can handle such card habilities, lets do it! but to allow play all the cards and test actual decks, it could be great to be able to play any card (even if the card is just a token and then you have the few buttons in one side to manually make what the card does --apprentice style -- because is understandable that include all the cards is.. a LOT of work, so it would be a little of a compromise and will allow players to test our real decks in a much better way than apprentice. --- AI would have decks made only of cards that MTG Fore can handle, but the human player would be able to create its own real deck with a mix of automatic and manual cards. Does it makes sense? if you feel contacting me or anything, I'll be happy to help! tlc1984@gmail.com

Anonymous said...

In Incantus, we use a generalized Cost object which has three main methods: precompute(), compute(), and pay().

Cost.precompute() asks the question, "Is it even possible for this cost to be paid?" For TapCosts, this checks to see if the card is untapped (or, if it's a TapCost with a cardtype match object (as in "Tap an untapped artifact you control"), if any untapped permanents matching the condition are in play). For ManaCosts, this gets the value of X, and determines which Hybrid choice you'll pay.

Cost.compute() asks "How do I want to pay this cost?" (Note that Hybrid needed to be asked first so that cost-reduction could apply here.) This asks the player how to spend generic mana (if they have more mana than they need to pay the cost), which permanents to tap/sacrifice/add counters to/remove counters from, whatever choices need to be made before actual payment can occur.

Cost.pay() finally carries through with all of the decisions made in compute(). This removes mana from your mana pool, taps or sacrifices permanents, removes permanents from the game, adds or removes counters, whatever.

Forge said...

I was joking about the 10 parts series, this is a modest 3 parts.

Generalizing the code is one of the most important attributes when programming Magic or anything else. You want the code to be as general as possible while being able to handle all of the situations that might occur. This entails that you understand the problem backwards, forwards, and sideways. Understanding the problem well enough before you start to code anything is one of the challenges of programming.

And no MTG Forge and Apprentice can't be merged but it would be fun if it could.

Forge said...

In my mind I see costs as being
mana
mana tap
sacrifice
mana sacrifice
etc..

and my goal is to try to handle all of those with the same code, NOT with specialized code.

Anonymous said...

Then you should separate then (into independent concerns, and have a composite cost that basically combines them - see the composite design pattern) So:

ManaCost
TapCost
UntapCost
AddCounterCost
RemoveCounterCost
RemoveFromGameCost
...

and then you can have a MultipleCosts class that manages lists of costs:

MultipleCosts(ManaCost(), TapCost()) etc.

Anonymous said...

Oops, i hit post too soon.

Then override the + symbol in the generic Cost object (all the costs should derive from it) so that if you add 2 cost objects you get a MultipleCosts object in return.

So:

cost = ManaCost("3W") + TapCost() + SacrificeCost()

will return a MultipleCosts object (of course, you'll have to handle the case of adding a multiple costs object to a basic cost object, but you get the idea)

Forge said...

I've never really though of combining costs, much like adding numbers

1+1+1 = 3

TapCost + SacrificeCost + ManaCost = MultipleCost

I think the decorator pattern does something like this, thanks.

Anonymous said...

Actually, i think the design might make more sense if you look at the GoF's Composite pattern.