CardForge – Open Source MTG simulator project

Project Summary
Back in the summer of 2010, I began to work on an open source Magic the Gathering simulator project called CardForge. I am a Magic addict, I’ve been playing it since middle school back in 2002. My first set that I bought was Judgement.
Anyways, I thought why not develop something I have a passion in? So I looked up the project, which was hosted on Google Code at the time, joined the forums, and became an active contributer, with the committer name xitongzou.

Cardforge startup screen
Cardforge startup screen
Cardforge Deck Editor
Cardforge Deck Editor
Cardforge Gameplay
Cardforge Gameplay

Technologies Used
CardForge is entirely Java-based and uses Maven for building and dependency management. The simulator currently cannot simulate multiplayer nor 100% optimal AI actions due to the interactions of MTG’s 10000+ cards, but it has a deck editor and a decent single player practice mode.

Responsibilities/Roles
I implemented about 60 of my favorite cards, including Bloodfire Colossus and Molten Hydra.

Project Details
Since the project is open source, I can divulge some of the implementation of the cards. The Card.java file is very big, and contains all the different abilities of each creature.


    //get the text of the abilities of a card
    /**
     * <p>getAbilityText.</p>
     *
     * @return a {@link java.lang.String} object.
     */
    public String getAbilityText() {
        if (isInstant() || isSorcery()) {
            String s = getSpellText();
            StringBuilder sb = new StringBuilder();

            // Give spellText line breaks for easier reading
            sb.append(s.replaceAll("\\\\r\\\\n", "\r\n"));


            // NOTE:
            if (sb.toString().contains(" (NOTE: ")) {
                sb.insert(sb.indexOf("(NOTE: "), "\r\n");
            }
            if (sb.toString().contains("(NOTE: ") && sb.toString().endsWith(".)") && !sb.toString().endsWith("\r\n")) {
                sb.append("\r\n");
            }

            // Add SpellAbilities
            SpellAbility[] sa = getSpellAbility();
            for (int i = 0; i < sa.length; i++) {
                sb.append(sa[i].toString() + "\r\n");
            }

            // Add Keywords
            ArrayList<String> kw = getKeyword();

            // Triggered abilities
            for (Trigger trig : triggers) {
                if (!trig.isSecondary()) {
                    sb.append(trig.toString() + "\r\n");
                }
            }
            
            // static abilities
            for (StaticAbility stAb : staticAbilities) {
            	String stAbD = stAb.toString();
            	if (!stAbD.equals(""))
            		sb.append(stAbD + "\r\n");
            }

            // Ripple + Dredge + Madness + CARDNAME is {color} + Recover.
            for (int i = 0; i < kw.size(); i++) {
                if ((kw.get(i).startsWith("Ripple") && !sb.toString().contains("Ripple"))
                        || (kw.get(i).startsWith("Dredge") && !sb.toString().contains("Dredge"))
                        || (kw.get(i).startsWith("Madness") && !sb.toString().contains("Madness"))
                        || (kw.get(i).startsWith("CARDNAME is ") && !sb.toString().contains("CARDNAME is "))
                        || (kw.get(i).startsWith("Recover") && !sb.toString().contains("Recover"))) {
                    sb.append(kw.get(i).replace(":", " ")).append("\r\n");
                }
            }

            // Changeling + CARDNAME can't be countered. + Cascade + Multikicker
            for (int i = 0; i < kw.size(); i++) {
                if ((kw.get(i).contains("Changeling") && !sb.toString().contains("Changeling"))
                        || (kw.get(i).contains("CARDNAME can't be countered.") && !sb.toString().contains("CARDNAME can't be countered."))
                        || (kw.get(i).contains("Cascade") && !sb.toString().contains("Cascade"))
                        || (kw.get(i).contains("Multikicker") && !sb.toString().contains("Multikicker"))) {
                    sb.append(kw.get(i)).append("\r\n");
                }
            }

            // Storm
            if (hasKeyword("Storm") && !sb.toString().contains("Storm (When you ")) {
                if (sb.toString().endsWith("\r\n\r\n")) {
                    sb.delete(sb.lastIndexOf("\r\n"), sb.lastIndexOf("\r\n") + 3);
                }
                sb.append("Storm (When you cast this spell, copy it for each spell cast before it this turn.");
                if (sb.toString().contains("Target") || sb.toString().contains("target")) {
                    sb.append(" You may choose new targets for the copies.");
                }
                sb.append(")\r\n");
            }

            //Replicate
            for (String keyw : kw) {
                if (keyw.contains("Replicate") && !sb.toString().contains("you paid its replicate cost.")) {
                    if (sb.toString().endsWith("\r\n\r\n")) {
                        sb.delete(sb.lastIndexOf("\r\n"), sb.lastIndexOf("\r\n") + 3);
                    }
                    sb.append(keyw);
                    sb.append(" (When you cast this spell, copy it for each time you paid its replicate cost.");
                    if (sb.toString().contains("Target") || sb.toString().contains("target")) {
                        sb.append(" You may choose new targets for the copies.");
                    }
                    sb.append(")\r\n");
                }
            }

            while (sb.toString().endsWith("\r\n")) {
                sb.delete(sb.lastIndexOf("\r\n"), sb.lastIndexOf("\r\n") + 3);
            }

            return sb.toString().replaceAll("CARDNAME", getName());
        }

        StringBuilder sb = new StringBuilder();
        ArrayList<String> keyword = getUnhiddenKeyword();

        sb.append(keywordsToText(keyword));

        // Give spellText line breaks for easier reading
        sb.append("\r\n");
        sb.append(text.replaceAll("\\\\r\\\\n", "\r\n"));
        sb.append("\r\n");

        /*
         * if(isAura()) {
            // Give spellText line breaks for easier reading
            sb.append(getSpellText().replaceAll("\\\\r\\\\n", "\r\n")).append("\r\n");
        }
        */

        // Triggered abilities
        for (Trigger trig : triggers) {
            if (!trig.isSecondary()) {
                sb.append(trig.toString() + "\r\n");
            }
        }
        
        // static abilities
        for (StaticAbility stAb : staticAbilities) {
        	sb.append(stAb.toString() + "\r\n");
        }

        ArrayList<String> addedManaStrings = new ArrayList<String>();
        SpellAbility[] abilities = getSpellAbility();
        boolean primaryCost = true;
        for (SpellAbility sa : abilities) {
            // only add abilities not Spell portions of cards
            if (!isPermanent())
                continue;

            if (sa instanceof Spell_Permanent && primaryCost && !isAura()) {
                // For Alt costs, make sure to display the cost!
                primaryCost = false;
                continue;
            }

            String sAbility = sa.toString();

            if (sa instanceof Ability_Mana) {
                if (addedManaStrings.contains(sAbility))
                    continue;
                addedManaStrings.add(sAbility);
            }

            if (sa instanceof Spell_Permanent && !isAura()) {
                sb.insert(0, "\r\n");
                sb.insert(0, sAbility);
            } else if (!sAbility.endsWith(getName())) {
                sb.append(sAbility);
                sb.append("\r\n");
                // The test above appears to prevent the card name from showing and therefore it no longer needs to be deleted from the stringbuilder
                //if (sb.toString().endsWith("CARDNAME")) 
                //    sb.replace(sb.toString().lastIndexOf("CARDNAME"), sb.toString().lastIndexOf("CARDNAME") + name.length() - 1, "");
            }
        }

        // NOTE:
        if (sb.toString().contains(" (NOTE: ")) {
            sb.insert(sb.indexOf("(NOTE: "), "\r\n");
        }
        if (sb.toString().contains("(NOTE: ") && sb.toString().contains(".) ")) {
            sb.insert(sb.indexOf(".) ") + 3, "\r\n");
        }

        // replace tripple line feeds with double line feeds
        int start;
        String s = "\r\n\r\n\r\n";
        while (sb.toString().contains(s)) {
            start = sb.lastIndexOf(s);
            if (start < 0 || start >= sb.length())
                break;
            sb.replace(start, start + 4, "\r\n");
        }

        //Remembered cards
        if (rememberedObjects.size() > 0) {
            sb.append("\r\nRemembered: \r\n");
            for (Object o : rememberedObjects) {
                if (o instanceof Card) {
                    Card c = (Card) o;
                    sb.append(c.getName());
                    sb.append("(");
                    sb.append(c.getUniqueNumber());
                    sb.append(")");
                } else
                    sb.append(o.toString());
                sb.append("\r\n");
            }
        }

        return sb.toString().replaceAll("CARDNAME", getName()).trim();
    }//getText()
 public boolean isToken() {
        return token;
    }

Some example of card implementations in the Creature CardFactory:


        //*************** START *********** START **************************
        else if (cardName.equals("Rhys the Redeemed")) {

            Cost abCost = new Cost("4 GW GW T", card.getName(), true);
            final Ability_Activated copyTokens1 = new Ability_Activated(card, abCost, null) {
                private static final long serialVersionUID = 6297992502069547478L;

                @Override
                public void resolve() {
                    CardList allTokens = AllZoneUtil.getCreaturesInPlay(card.getController());
                    allTokens = allTokens.filter(AllZoneUtil.token);

                    int multiplier = AllZoneUtil.getDoublingSeasonMagnitude(card.getController());

                    for (int i = 0; i < allTokens.size(); i++) {
                        Card c = allTokens.get(i);
                        for (int j = 0; j < multiplier; j++)
                            copyToken(c);
                    }
                }

                public void copyToken(Card token) {
                    Card copy = new Card();
                    copy.setName(token.getName());
                    copy.setImageName(token.getImageName());

                    copy.setOwner(token.getController());
                    copy.setController(token.getController());
                    copy.setManaCost(token.getManaCost());
                    copy.setColor(token.getColor());
                    copy.setToken(true);
                    copy.setType(token.getType());
                    copy.setBaseAttack(token.getBaseAttack());
                    copy.setBaseDefense(token.getBaseDefense());

                    AllZone.getGameAction().moveToPlay(copy);
                }

                @Override
                public boolean canPlayAI() {
                    CardList allTokens = AllZoneUtil.getCreaturesInPlay(AllZone.getComputerPlayer());
                    allTokens = allTokens.filter(AllZoneUtil.token);

                    return allTokens.size() >= 2;
                }
            };

            card.addSpellAbility(copyTokens1);
            copyTokens1.setDescription(abCost + "For each creature token you control, put a token that's a copy of that creature onto the battlefield.");
            StringBuilder sb = new StringBuilder();
            sb.append(card.getName()).append(" - For each creature token you control, put a token that's a copy of that creature onto the battlefield.");
            copyTokens1.setStackDescription(sb.toString());
        }//*************** END ************ END **************************
 //*************** START *********** START **************************
        else if (cardName.equals("Treva, the Renewer")) {
            final Player player = card.getController();

            final Ability ability2 = new Ability(card, "2 W") {
                @Override
                public void resolve() {
                    int lifeGain = 0;
                    if (card.getController().isHuman()) {
                        String choices[] = {"white", "blue", "black", "red", "green"};
                        Object o = GuiUtils.getChoiceOptional("Select Color: ", choices);
                        Log.debug("Treva, the Renewer", "Color:" + o);
                        lifeGain = CardFactoryUtil.getNumberOfPermanentsByColor((String) o);

                    } else {
                        CardList list = AllZoneUtil.getCardsInPlay();
                        String color = CardFactoryUtil.getMostProminentColor(list);
                        lifeGain = CardFactoryUtil.getNumberOfPermanentsByColor(color);
                    }

                    card.getController().gainLife(lifeGain, card);
                }

                @Override
                public boolean canPlay() {
                    //this is set to false, since it should only TRIGGER
                    return false;
                }
            };// ability2
            //card.clearSpellAbility();
            card.addSpellAbility(ability2);

            StringBuilder sb2 = new StringBuilder();
            sb2.append(card.getName()).append(" - ").append(player);
            sb2.append(" gains life equal to permanents of the chosen color.");
            ability2.setStackDescription(sb2.toString());
        }//*************** END ************ END **************************

Sources/Repo:
Cardforge is open source.
The website is http://cardforge.org,
Releases can be found here,
The discussion board is here,
and the documentation is here.
Github repo is Here

  • My robots deck is the best

  • Darkmaster006

    Good project, I am thrilled by this card game even though I have only seen glimpses of it.