MTG
Forge Source Code
MTG
Forge lets you play Magic: The Gathering against a computer opponent using all
of the rules.
All
source code is released under the GNU license.
Developers
Blog: mtgrares.blogspot.com
The
source code compiles with at least Java 1.4.
The source code will compile with Java 1.6 but you have to turn off all
of the warnings since the code doesn't use generics.
You can
get the Java compiler from www.java.com/getjava
To compile, unzip everything into the same directory and type “javac *.java” To run
it type “java Gui_NewGame” To change the message displayed in title bar
like “MTG Forge - 10/15/2008” change the variable Constant.ProgramName.
Card and SpellAbility
The Card
class represents regular, cardboard cards and the SpellAbility
class represents the effect that any card does when it is played. SpellAbility is an
abstract class and the resolve() method must be
implemented. The resolve()
method of Wrath of God actually destroys all the creatures.
CardFactory
All the
code for the Magic cards is in CardFactory.java The basic card
information like name, mana cost, text is in the file
“cards.txt” CardFactory
gets the card text from cards.txt and then adds the correct SpellAbility
object to the Card object. For example, CardFactory adds a SpellAbility
object to Wrath of God that destroys all creatures.
Input
The
Input class handles all of the mouse input.
All phases are Input objects. The
Input class also handles choosing a card’s target. The Input class also handles paying the mana cost.
Required Files
In order
to run, MTG Forge requires the following files.
If one of these files is missing, you will probably get a weird error
message.
cards.txt - holds the card’s text, name, mana cost, etc…
all-decks2 - holds
all of the decks that are available
card-pictures.txt - used to download
the card pictures
down.gif - used by the deck editor
up.gif - used by
the deck editor
common.txt - used to create booster packs for
sealed and draft
uncommon.txt
rare.txt
How to update MTG Forge
If you
have added cards to MTG Forge and you wanted to distribute a new version, just
update all of the classes in the file “run-forge.jar” Open the jar file
with a program like Winzip.
The file
“run-forge.exe” runs the class Gui_NewGame in “run-forge.jar” The file
“run-forge.exe” is just a Java wrapper that was created using JSmooth, http://jsmooth.sourceforge.net/
Below
are excerpts from my blog describing how to program cards.
MTG Forge Overview
Trying to
understand a whole programming project is difficult and almost impossible
without copious amounts of documentation.
I’m going to try to summarize the major classes MTG Forge uses. The Card class mimics the functions of a real
card, and the Card class exists in your hand, in play, in the graveyard, and
removed from the game. A Card can hold
any number of SpellAbility objects. Each Card has at least one SpellAbility and if the Card has an ability
like Elvish Piper, the Card will have two SpellAbilities added.
One SpellAbility is
the default summon creature spell and the other SpellAbility
is the activated ability that lets you put a creature from your hand into
play.
Every SpellAbility can have an optional mana
cost, the only SpellAbilities that don’t have a mana cost are creatures that have tap abilities. Instants and Sorceries also use the SpellAbility class. SpellAbility has a resolve method that is called after it
is popped off the stack. The CardFactory class makes new cards and copies existing
ones. CardFactory
also adds the SpellAbility objects to the Cards, and
implements SpellAbility’s resolve method. The resolve method for Wrath of God would
destroy everything.
The file
“cards.txt” holds simple cards as well as all the text for Instants and
Sorceries. This file saved time because
I didn’t have to program simple creatures like Glory Seeker. This file also let me specify mana abilities, so creatures like Bird of Paradise are only
implemented in cards.txt, no other code is needed. Flying, haste, fear, and vigilance are also
supported by cards.txt
Some users have added their own cards, which surprised me.
The class
that runs everything is Gui_NewGame. It starts up the user interface and
initializes everything. Every screen is
a separate class and JFrame. These display classes all start with the
abbreviation Gui and are only loosely tied together,
they all read from Constant.Runtime in order to read
which deck you and the computer are using.
I hate not
throwing some code at you. J The code below constructs Serra
Avenger. AllZone
holds all of the zones, in play, graveyard, etc… and can be globally
accessed. Global variables are generally
bad, but they really helped get MTG Forge working. The computer can play regular creature cards
like Serra Avenger using the code below, since it just pays the mana cost and the resolve method works the same for both
the human and computer player.
The
computer checks the canPlay and canPlayAI
methods in SpellAbility to see if it is allowed to
play that card. This specific card does
not have a canPlayAI method, a card like Hex does,
because it targets 6 creatures. The card
below belongs in the human player’s deck, since he both controls and owns the
card. If this card was going into the
computer’s library, the computer would be listed as the owner and controller.
final
Card card = new Card();
card.setName("Serra Avenger");
card.setManaCost("W W");
card.setAttack(3);
card.setDefense(3);
card.addKeyword("Flying");
card.addKeyword("Vigilance");
card.setOwner("Human");
card.setController("Human");
SpellAbility spell = new SpellAbility(SpellAbility.Spell, card)
{
public void
resolve()
{
PlayerZone play =
AllZone.getZone(Constant.Zone.Play, card.getController());
play.add(card);
}
public boolean canPlay()
{
//Serra Avenger cannot be played for the
first three turns of the game
return 3 < AllZone.Phase.getTurn();
}
};
card.addSpellAbility(spell);
Programming Shock
Many cards
and abilities are similar to Shock. In
case you can’t remember, Shock deals 2 damage to target
creature or player. First, Shock
has to be added to the cards.txt file.
Cards.txt holds all of the text that the card displays as well as the mana cost.
Shock
R
Instant
Shock deals
2 damage to target creature or player.
The resolve
method checks to see if the card is still in play, something that we take for
granted in a real-life game. SpellAbility only allows for 1 target to be set, either a
card or a player by the setTargetPlayer(String player) and setTargetCard(Card)
methods. To get the target you call
either getTargetPlayer or getTargetCard.
The SpellAbiliy method setBeforePayMana(Input),
lets the user actually choose the target card or player. CardFactoryUtil has
a bunch of reusable Input classes that gets a user choice. If the spell just targeted a creature you
would use CardFactoryUtil.input_targetCreature.
SpellAbility canPlayAI method returns false if you do not
want the computer to play this spell. A
very simple AI for Shock is below. canPlayAI is called first, and if
it returns true, then chooseTargetAI is called. chooseTargetAI
sets the target for the spell by either calling setTargetCard(Card)
or setTargetPlayer(String player). Some cards like Wrath of God do not have a chooseTargetAI method, because no targets are needed.
This checks
to see that the human player (you) have at least 1 creature in play and sets
that creature as the target. A more
sophisticated AI would try to get the best creature that Shock could kill, like
a 2/2 flyer. The AI could also see if
you had low life, and could alternately target you instead of a creature. The regular AI for Shock tries to kill a
creature with flying or you if your life is low.
//simple AI
for Shock
//should be
put in a SpellAbility object
public boolean canPlayAI()
{
CardList list = CardFactoryUtil.AI_getHumanCreature();
return 0 < list.size();
}
public
void chooseTargetAI()
{
CardList list = CardFactoryUtil.AI_getHumanCreature();
setTargetCard(list.get(0));
}
//Complete
card
final
Card card = new Card();
card.setName(“Shock”);
card.setManaCost("R");
card.setOwner(Constant.Player.Human);
card.setController(Constant.Player.Human);
final SpellAbility spell = new Spell(card)
{
int
damage = 2;
public boolean canPlayAI() {return
false;}
public void
resolve()
{
if(getTargetCard() != null)
{
if(AllZone.GameAction.isCardInPlay(getTargetCard()))
{
Card c = getTargetCard();
c.addDamage(damage);
}
}
else
AllZone.GameAction.getPlayerLife(getTargetPlayer()).subtractLife(damage);
}//resolve()
};//SpellAbility
card.addSpellAbility(spell);
spell.setBeforePayMana(CardFactoryUtil.input_targetCreaturePlayer(spell));
//Shock as
it would be in CardFactory
if(cardName.equals("Shock"))
{
final SpellAbility spell = new Spell(card)
{
//same stuff
};//SpellAbility
//this is cleared because a card is presumed
to be a permanent
//all Sorceries and Instants have to call card.clearSpellAbility()
card.clearSpellAbility();
card.addSpellAbility(spell);
spell.setBeforePayMana(CardFactoryUtil.input_targetCreaturePlayer(spell));
}
Programming Giant Growth
People
seemed to have really enjoyed my entry on “Programming Shock” so I’ll cover
another common card. If you look at the
code, you will see that it works for both Giant Growth and its Planar Chaos
brother Brute Force. Remember at the top
of the CardFactory getCard
method the mana cost and name of the card is already
set.
Giant
Growth and other effects stay around “until end of turn.” End of turn is commonly abbreviated EOT. In order to program EOT effects some bit of
code has to be executed later.
MTG Forge
source code has an interface called Command, which is a
software Design Pattern. The
interface for Command is very simple.
public
interface Command
{
public void
execute();
}
Command is
a generic object that is used to execute arbitrary code at a later time. So when Giant Growth resolves it will give
the target creature +3/+3 and make a Command object that resets the creatures
stats to normal. AllZone.EndOfTurn.addUntil(Command)
executes
all until EOT Commands.
There are really
two types of EOT effects, until EOT and at EOT.
Giant Growth says until EOT and Ball Lightning says at EOT. So Giant Growth would use AllZone.EndOfTurn.addUntil(Command)
and Ball Lightning would use AllZone.EndOfTurn.addAt(Command) This division doesn’t matter in the current
version of MTG Forge, since you cannot stop at EOT. Technically “At EOT” effects are done first,
then you would get priority to play cards/abilities, and then “Until Effects”
wear off.
Unfortunately
the resolve method for Giant Growth is a little messy because a Command object
has to be constructed. Command has to be
inside the resolve method in case Giant Growth is played, and then returned to
your hand, and then played again.
This is
very Java specific and a little complicated.
The array “target” is used because the Command object needs a final
variable. Target simulates a variable
that can be changed, while fulfilling the syntax that Java needs to make the
Command object.
The AI just
makes sure that the computer is attacking with at least one creature and plays
Giant Growth on that creature. Currently
the computer cannot play any spells/abilities during combat, so the computer
will never attack with a smaller creature and then play Giant Growth. The computer will only play Giant Growth
during his main phase.
if(cardName.equals("Giant Growth") || cardName.equals("Brute
Force"))
{
SpellAbility spell = new Spell(card)
{
public
void resolve()
{
final Card[] target
= new Card[1];
final Command untilEOT = new Command()
{
public void
execute()
{
if(AllZone.GameAction.isCardInPlay(target[0]))
{
target[0].setAttack (target[0].getAttack()
- 3);
target[0].setDefense(target[0].getDefense()-
3);
}
}
};
target[0] = getTargetCard();
if(AllZone.GameAction.isCardInPlay(target[0]))
{
target[0].setAttack (target[0].getAttack()
+ 3);
target[0].setDefense(target[0].getDefense()+
3);
AllZone.EndOfTurn.addUntil(untilEOT);
}
}//resolve()
};
spell.setBeforePayMana(CardFactoryUtil.input_targetCreature(spell));
//Instants
and Sorceries should clear because cards are presumed to be permanents
card.clearSpellAbility();
card.addSpellAbility(spell);
}
//AI
removed for clarity, should be put in SpellAbility
public boolean canPlayAI()
{
return getAttacker() != null;
}
public
void chooseTargetAI()
{
setTargetCard(getAttacker());
}
public
Card getAttacker()
{
//target
creature that is going to attack
Combat c = ComputerUtil.getAttackers();
Card[] att = c.getAttackers();
if(att.length != 0)
return att[0];
else
return null;
}
Programming Magic Cards 1
Many people
are interested in how I program cards in MTG Forge. Obviously some of the cards are simple and
some are complicated. Wrath of God is
very simple while Kiki-Jiki, Mirror Breaker is very
complicated. The article “MTG Forge
Overview” showed you Serra Angel which is a typical creature. Today I’m going to show you how I programmed
tap abilities with and without mana costs as well as
how to draw a card and gain life.
The code
showed below is for “Super Card.” It is
an artifact that lets you tap to draw a card or tap and pay 1 to gain a
life. Super Card has 3 SpellAbilities added to the card: 1. so it comes into play
as a permanent, 2. gain 1 life, and 3. draw a card. As you may or not remember, the SpellAbility class is used for all spells and abilities,
hence its name. The life gaining effects
costs 1 mana of any color while the draw card ability
only taps the card. The resolve method
of SpellAbility does the main function of any card or
ability. The card’s controller is the
one who gains life. If your opponent
steals it during the game the effect will still work correctly and you will
still own the card, but your opponent would control it. If it is put into a graveyard, it will go
into its owner’s graveyard.
The setDescription method of SpellAbility
sets the text that is show text box of the card on screen. The setStackDescription,
also a part of SpellAbility, is the text that the
stack displays. These two abilities
require a different argument for the setBeforePayMana
function. The setBeforePayMana
function optionally lets a user do some action before choosing any targets and
before paying for mana, it is the first thing that is
checked when a card is clicked on. The setBeforePayMana function makes the user pay for the mana cost when gaining life, and just taps the card when
drawing another card. You can put
anything into the setBeforePayMana method and I added
“tap abilities” late in development. setBeforePayMana accepts Input
objects. Input objects process all user
input through the mouse and allows the user choose targets. This way of doing abilities and tap abilities
is not every elegant but it works.
AllZone
is a global (static) object that can be accessed from anywhere in the program
and GameAction does some
logical actions like drawing a card, gaining life, discarding, etc... When this card is clicked on the user is
given the choice of playing either of the two abilities.
Card card = new Card();
card.setName("Super Card");
card.setOwner(“Human”);
card.setController(“Human”);
card.setManaCost(“1”);
card.addType(“Artifact”);
card.addSpellAbility(new Spell_Permanent(card));
final SpellAbility a = new Ability_Tap(card,
"1")
{
public void resolve()
{
AllZone.GameAction.getPlayerLife(card.getController()).addLife(1);
setStackDescription(card.getController() +"
gains 1 life.");
}
};
a.setDescription("1, tap: You gain 1
life.");
a.setBeforePayMana(new Input_PayManaCost(a));
card.addSpellAbility(a);
final SpellAbility b = new Ability_Tap(card,
"0")
{
public void
resolve()
{
AllZone.GameAction.drawCard(card.getController());
setStackDescription(card.getController() +"
draws a card");
}
};
b.setDescription("tap: You draw a card.");
b.setBeforePayMana(new Input_NoCost_TapAbility((Ability_Tap) b));
card.addSpellAbility(b);
Programming Life Gain
I wanted to
cover some of the basic abilities that cards do like gaining life, losing life,
drawing a card, etc… AllZone
is a static global variable that can be accessed from anywhere. GameAction does
some high level actions like drawing cards and destroying a creature. The code snippets that I will be going over
usually will go in the resolve method of SpellAbility
objects. These are some common actions
that are done throughout the game.
This is how
you gain life.
PlayerLife
p = AllZone.GameAction.getPlayerLife(Constant.Player.Computer);
p.addLife(2);
Usually I wouldn’t
hardcode the player (human or computer) so the code would look like this.
PlayerLife
p = AllZone.GameAction.getPlayerLife(card.getController());
p.addLife(2);
Subtracting
life is similar.
PlayerLife
p = AllZone.GameAction.getPlayerLife(Constant.Player.Computer);
p.subtractLife(4);
Drawing a
card looks like this. If you wanted to
draw multiple cards, you would use a for loop. AllZone.GameAction.drawCard(Constant.Player.Human);
Whenever a
card is put into a graveyard, AllZone.GameAction.destroy(Card) is used.
AllZone.GameAction.destroy(Card);
Cards like
Strangling Soot will have a SpellAbility.resolve that
looks like this.
AllZone.GameAction.destroy(spell.getTargetCard());
Terror is
similar but says that the card cannot be regenerated. AllZone.GameAction.destroyNoRegeneration(Card);
This
discards a card at random from a player.
AllZone.GameAction.discardRandom(Constant.Player.Computer);
Regular
discard is done this way. This assumes
that the chosen card is already picked and this method just moves the card from
a player’s hand to the graveyard.
AllZone.GameAction.discard(Card);
Sacrificing
a card is similar to discarding, the chosen card is
already picked. This would be called from
the Input class, since it handles all the user input from the mouse. The Input class lets the user choose a card
and then the code below is executed.
AllZone.GameAction.sacrifice(Card);
Shuffling
your library is a common occurrence.
AllZone.GameAction.shuffle(card.getController());
Cards like
Shock just add damage to a card. GameAction.checkStateEffects moves the card to the
graveyard if it is needed.
Card c = spell.getTargetCard();
c.addDamage(2);
Input Explained
Some people
have commented that the Input class is giving them problems and I understand
where they are coming from. All of this
code was never meant to be understood by anyone else, lol. Anyways, the Input class handles all the user
input, which is from a mouse, and is used to set targets for cards. The targets may be either a player or a
card. SpellAbility
setTargetPlayer(String player) and setTargetCard(Card)
actually sets the target that the resolve will use. There is corresponding getTargetPlay
and getTargetCard commands. This limits cards to a single target. Hex has multiple targets, but it is an
exception. Multiple targets can be
programmed but the code is messy.
The method SpellAbility setBeforePayMana(Input) is used to set the Input that will handle getting a
card’s target. The method could have
been named better.
CardFactoryUtil returns common Inputs that are useful.
To target a
player, CardFactoryUtil.input_targetPlayer(SpellAbility spell). Hopefully Ancestral Recall is straightforward
and easy to understand. The computer
always sets himself as the target with the method setChooseTargetAI and canPlayAI
makes sure the computer doesn’t have to discard any cards.
if(cardName.equals("Ancestral Recall"))
{
SpellAbility spell = new Spell(card)
{
public
void resolve()
{
AllZone.GameAction.drawCard(getTargetPlayer());
AllZone.GameAction.drawCard(getTargetPlayer());
AllZone.GameAction.drawCard(getTargetPlayer());
}
public boolean canPlayAI()
{
return AllZone.Computer_Hand.getCards().length <= 5;
}
};
spell.setChooseTargetAI(CardFactoryUtil.AI_targetComputer());
spell.setBeforePayMana(CardFactoryUtil.input_targetPlayer(spell));
card.clearSpellAbility();
card.addSpellAbility(spell);
To target a creature, CardFactoryUtil. input_targetCreature(SpellAbility spell)
Swords to Plowshares removes a creature from play and gives it’s
controller some life.
if(cardName.equals("Swords to Plowshares"))
{
SpellAbility spell = new Spell(card)
{
public
void resolve()
{
if(AllZone.GameAction.isCardInPlay(getTargetCard()))
{
//add life
String player = getTargetCard().getController();
PlayerLife life = AllZone.GameAction.getPlayerLife(player);
life.addLife(getTargetCard().getAttack());
//remove card from play
PlayerZone zone = AllZone.getZone(getTargetCard());
zone.remove(getTargetCard());
}
}//resolve()
public boolean canPlayAI()
{
CardList
creature = new CardList(AllZone.Human_Play.getCards());
creature
= creature.getType("Creature");
return creature.size() != 0 && (AllZone.Phase.getTurn()
> 4);
}
public
void chooseTargetAI()
{
CardList
play = new CardList(AllZone.Human_Play.getCards());
Card target
= CardFactoryUtil.AI_getBestCreature(play);
setTargetCard(target);
}
};//SpellAbility
spell.setBeforePayMana(CardFactoryUtil.input_targetCreature(spell));
card.clearSpellAbility();
card.addSpellAbility(spell);
}
To target anything in the “type” line of a card like Creature, Land,
Goblin, or Forest. CardFactoryUtil.input_targetType(SpellAbility spell, String cardType)
Type can
also be “All” which would let the user target any permanent.
if(cardName.equals("Stone Rain"))
{
final SpellAbility spell = new Spell(card)
{
public
void resolve()
{
Card c = getTargetCard();
if(AllZone.GameAction.isCardInPlay(c))
AllZone.GameAction.destroy(c);
}
};//Spell
card.clearSpellAbility();
card.addSpellAbility(spell);
spell.setChooseTargetAI(CardFactoryUtil.AI_targetType("Land", AllZone.Human_Play));
spell.setBeforePayMana(CardFactoryUtil.input_targetType(spell, "Land"));
}
Stone Rain
also configures the AI by the method
CardFactoryUtil.AI_targetType(String type, PlayerZone
zone)
Type is
like
AllZone.Human_Play – Shock
AllZone.Computer_Play – Giant Growth
AllZone.Computer_Graveyard – Gravedigger
AllZone is
just a global object that holds all of the “zones”, like in play, graveyard,
etc…
The human
player can only target cards in play, so cards like Gravedigger show a window
so a card from the graveyard can be selected.
Dark Banishing has a custom Input that targets a non-black
creature. After a creature is chosen in
the selectCard method, the user has to pay the mana cost which stops the current Input. The SpellAbility of
Dark Banishing was omitted for clarity.
Input
target = new Input()
{
public void
showMessage()
{
AllZone.Display.showMessage("Select target non-black creature for " +spell.getSourceCard());
ButtonUtil.enableOnlyCancel();
}
public void
selectButtonCancel() {stop();}
public void
selectCard(Card card, PlayerZone zone)
{
if((!CardUtil.getColors(card).contains(Constant.Color.Black))
&& card.isCreature()
&& zone.is(Constant.Zone.Play))
{
spell.setTargetCard(card);
stopSetNext(new Input_PayManaCost(spell));
}//if
}//selectCard()
};//Input
spell.setBeforePayMana(target);
Static Abilities
Today, I’m
going to talk about the hardest cards to program. Cards like Glorious Anthem caused me a lot of
headaches.
This is a
definition of static abilities from the rules manual.
You don’t play
and resolve static abilities like the other two ability types. When a permanent
with a static ability comes into play, the ability’s effect simply “turns on.”
It stays on as long as the permanent stays in play. (Static abilities create continuous effects.) Most enchantments have static abilities. For
example, Telepathy reads, “Your opponents play with their hands revealed.” Once
Telepathy is in play, you don’t have to pay a cost to have your opponent reveal
his or her hand. Your opponent’s hand is just kept face up on the table until
Telepathy leaves play.
I call
static ability, state abilities since they depend on the current “state” or
condition of the game. State abilities
include Glorious Anthem, Levitation, Soul Warden, and Castle Raptors. Glorious Anthem gives your creatures +1/+1,
so it changes the state of the game.
Castle Raptors gets a bonus if it is untapped.
In MTG
Forge static effects are processed after you play a land, the computer plays a
land, or a spell/ability is resolved.
The code for Glorious Anthem is a little messy since it keeps track of
old creatures that were already in play.
Glorious Anthem first takes away the +1/+1 from all old cards and then
gives the bonus for each copy of Glorious Anthem in play. The code loops through all copies of Glorious
Anthem in case you and your opponent both have it in play. This code also works correctly if you have
multiple Anthems in play.
The code
below was taken from the class GameActionUtil. It has a method called executeCardStateEffects
that executes all of the effects. Doing
state effects this way has the drawback of separating code from the Card and SpellAbility classes which I would prefer not to. Platinum Angel has a very cool and hard to
program state effect that denies your opponent from winning the game. Check it out in the upcoming 10th
edition core set.
GameAction
also implements some state effects in the method checkStateEffects. Other MTG Forge state effects include sending
a creature to the graveyard if it has sufficient damage. Destroying legendary creatures if two are
into play and checking to see if any player’s life is 0. The cards that have a state effect in MTG
Forge are Baru, Fist of Krosa,
Soul Warden, Essence Warden, Nightmare, Korlash, Heir
to Blackblade, Glorious Anthem, Gaea's
Anthem, Wonder, Anger, Valor, Veteran Armorer,
Radiant,
static
Command Glorious_Anthem = new Command()
{
CardList gloriousAnthemList = new CardList();
public void
execute()
{
CardList list = gloriousAnthemList;
Card c;
//reset all cards in list - aka
"old" cards
for(int i = 0; i < list.size();
i++)
{
c = list.get(i);
c.setAttack(c.getAttack() - 1);
c.setDefense(c.getDefense() - 1);
}
//add +1/+1 to cards
list.clear();
PlayerZone[] zone =
getZone("Glorious Anthem");
//for each zone found add +1/+1 to each card
for(int outer = 0;
outer < zone.length; outer++)
{
CardList creature
= new CardList(zone[outer].getCards());
creature = creature.getType("Creature");
for(int i = 0; i < creature.size();
i++)
{
c = creature.get(i);
c.setAttack(c.getAttack() + 1);
c.setDefense(c.getDefense() + 1);
gloriousAnthemList.add(c);
}//for inner
}//for outer
}//execute()
};//Glorious
Anthem
//returns
all PlayerZones that has at least 1 Glorious Anthem
//if
Computer has 2 Glorious Anthems, AllZone.Computer_Play
will be returned twice
PlayerZone[]
getZone(String cardName)
//method
body removed on purpose