Friday, February 29, 2008

Programming ManaCost

Paying for a spell or ability is a fundamental part of Magic although not very flashy. No one plays Magic, just so they can tap land. The fun part is _after_ you tap your land. Anyways, I’m going to talk about how MTG Forge implements this action.

The ManaCost class has the following methods:

setManaCost(cost)
addMana(color):
toString() : returns string
isPaid() : returns boolean
isNeeded(color) : return Boolean

The basic example of how to use ManaCost is shown below.

m = ManaCost()
m.setManaCost(“1G”)
m.addMana(“G”)
m.addMana(“U”)
print m.isPaid() #prints true

Notice that ManaCost has to check if all of the colored mana costs are paid before paying the colorless cost. If this is done incorrectly, the green mana will be applied to the colorless cost and isPaid() will return false.

ManaCost is a “low level” class that is to be used by the user interface (UI) component. The UI requires a method like isNeeded() so if you click on a Forest and a Mountain is needed, it will not tap the Forest. (My explanations for some of these things sound really
stupid written out.) The UI also needs to display what mana is still needed, so it uses toString().

ManaCost is used to play Instants, Sorceries, and Abilities. The UI component will keep track of all the land that was tapped in case the user decides to cancel or doesn’t have enough mana. After the cost is paid, the UI will add the spell/ability to the stack. ManaCost does not handle 0 cost spells like Ornithopter, so the UI must handle that situation by directly adding the spell/ability to the stack. (Imagine if Magic used a queue instead of a Stack, it would be pretty weird. All the trading card games I know of like Yu-Gi-Oh and Marvel/DC VS use a stack.)

ManaCost does not handle X spells like Fireball. A 2nd class, ManaCost_X, handles X spells. ManaCost_X is very similar to ManaCost but toString(), isPaid(), and isNeeded() all work slightly different. ManaCost_X.isNeeded() always returns true, since you can always add more mana to an X spell. Spells and abilities with double X costs are not currently handled.

Hopefully this will put things more into perspective. Let’s say that playing a game of Magic is level 1. And the rules that allow the game to happen are level 2. So Magic Online (MODO) and MTG Forge have to enforce the rules so they are level 3. The rules, level 2, don’t have to think about invalid user actions, and just state that “no illegal player actions can take place.” MTG Forge and MODO have to add more “rules” to Magic in order to make everything work nicely. A tournament judge would also be level 3 because they enforce the rules. (Imagine the computer penalizing you if you for illegal game state if you didn’t draw a card, that wouldn’t be fun at all but you would learn the rules pretty quickly.)

The code below is in Python. I’m not sure if showing you code like this is interesting or not. Feel free to use it if you want to. I also have ManaCost written in Java if anyone really wants it. In MTG Forge’s source code the Java class is called ManaCost and it uses ManaPool. I have extensively tested this code because finding errors later on is really annoying and hard to correct.

#lets player pay mana costs for a card
#UI component will use this class
#handles mana costs like 2WW, 14GU, R, BBB
#does not handle X spells
#does not handle 0 cost cards like Ornithopter
class ManaCost:
def __init__(self):
self.reset()

def reset(self):
self.manaPool = {"C":0,#colorless
"W":0,
"U":0,
"B":0,
"R":0,
"G":0}
self.manaCost = ""

def setManaCost(self, manaCost):
self.reset()
self.manaCost = manaCost

#check for space, the character "C" or "X", 0 length cost, or cost that is "0"
if manaCost.find(" ") is not -1 or \
manaCost.find("C") is not -1 or \
manaCost.find("X") is not -1 or \
len(manaCost) is 0 or \
manaCost is "0":
raise RuntimeError("ManaCost : setManaCost() error, invalid mana cost - " +str(manaCost))

#convert mana cost like 2WW into mana pool
colorLess = "0"
for z in manaCost:
#is color?
if z in self.manaPool.keys():
self.manaPool[z] += 1
else:
colorLess += str(z)
#this handles double digit colorless costs like 11, or 12GG
self.manaPool["C"] += int(colorLess)

#color is string of length 1 like "1", "G", "U", or "B"
#colorless mana can ONLY be "1", NOT "2", "3", etc...
def isNeeded(self, color):
if color not in ("1", "W", "G", "U", "B", "R"):
raise RuntimeError("ManaCost : isNeeded() error, invalid color - " +color)

return 0 < self.manaPool.get(color) or 0 < self.manaPool["C"]

def isPaid(self):
for z in self.manaPool.keys():
if 0 < self.manaPool[z]:
return False

return True

def __str__(self):
if self.isPaid():
raise RuntimeError("ManaCost : str error, mana cost is paid, this method shouldn't be called")

out = ""
key = self.manaPool.keys()
#remove colorless key
key.remove("C")

for color in key:
#repeats color, makes W into WWW
out += color * self.manaPool[color]

#prefix colorless mana cost, so WW becomes 2WW
check = self.manaPool["C"]
if check is not 0:
out = str(check) + out

return out

#pays color toward the mana cost
#color is string of length 1 like "1", "G", "U", or "B"
#colorless mana can ONLY be "1", NOT "2", "3", etc...
def addMana(self, color):
#color might be an int, so convert to string
color = str(color)
if not self.isNeeded(color):
raise RuntimeError("ManaCost : addMana() error, mana not needed - " +color)

#are all colored mana requirements paid?
if(self.manaPool.get(color, 0) is 0):
self.manaPool["C"] -= 1
else:
self.manaPool[color] -= 1

5 comments:

Forge said...

Yeah the source code is badly formatted (mangled). Just cut and paste it and it should work.

Hannes Foulds said...

Right now there are two things that are of particular interest to me. One has to be MTG Forge and the other is Incantus.

I actually really wanted to develop my own Magic application, but now I am convinced that it will be much better to dive head first into one of these and actively help with the development thereof.

In light of this, is the new Python version going to be open source, and when can people like myself start contributing?

Forge said...

Well I'm glad you want to contribute. The bad news is I'm not sure how to let you (and others) help. Currently MTG Forge 2.0 in Python is about 10K of source code and I'm struggling to write a basic, graphical GUI in Pyglet. Incantus is at least stable while MTG Forge 2.0 is all in my head. Hope that helps to answer you question.

Incantus said...

Hey Forge,

So you've decided to use pyglet for MTGForge 2.0 instead of pygame? The one downside to pyglet is that there is no established gui library for things like text boxes and and buttons, so you have to roll your own (but on the upside, you aren't limited by a GUI either - see the fluidity of the selection list select an activated ability)

Forge said...

With PyGame I would have to move the window each time I ran it, since it put the window in an wierd place. That is very minor, but I know I would be running my program 10,000 times, I wanted to save 10,000 mouse clicks. Pyglet is sort of over my head, but maybe I can learn some as I go. I also have a problem with "bleeding" read my post "Working Python Code."