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
    /**
     * 

getAbilityText.

* * @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 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 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 addedManaStrings = new ArrayList(); 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

Comments

2 responses to “CardForge – Open Source MTG simulator project”

  1. ravager2000 Avatar

    My robots deck is the best

  2. Darkmaster006 Avatar
    Darkmaster006

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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.