diff --git a/forge-core/src/main/java/forge/deck/DeckRecognizer.java b/forge-core/src/main/java/forge/deck/DeckRecognizer.java index ddc8b69191a..bf4b9b7cf82 100644 --- a/forge-core/src/main/java/forge/deck/DeckRecognizer.java +++ b/forge-core/src/main/java/forge/deck/DeckRecognizer.java @@ -22,8 +22,10 @@ import forge.StaticData; import forge.card.CardDb; import forge.card.CardEdition; import forge.card.CardType; +import forge.card.MagicColor; import forge.item.IPaperCard; import forge.item.PaperCard; +import forge.util.Localizer; import org.apache.commons.lang3.StringUtils; import java.util.*; @@ -41,14 +43,21 @@ public class DeckRecognizer { * The Enum TokenType. */ public enum TokenType { + // Card Token Types LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET, CARD_FROM_INVALID_SET, - UNKNOWN_CARD_REQUEST, + // Warning messages + WARNING_MESSAGE, + UNKNOWN_CARD, + UNSUPPORTED_CARD, + UNSUPPORTED_DECK_SECTION, + // No Token UNKNOWN_TEXT, - DECK_NAME, COMMENT, + // Placeholders + DECK_NAME, DECK_SECTION_NAME, CARD_TYPE, CARD_RARITY, @@ -65,9 +74,9 @@ public class DeckRecognizer { * The Class Token. */ public static class Token { - private TokenType type; - private int number; - private String text; + private final TokenType type; + private final int number; + private final String text; // only used for illegal card tokens private LimitedCardType limitedCardType = null; // only used for card tokens @@ -88,34 +97,60 @@ public class DeckRecognizer { return new Token(TokenType.CARD_FROM_NOT_ALLOWED_SET, count, card); } - public static Token InvalidCard(final PaperCard card, final int count) { + public static Token CardInInvalidSet(final PaperCard card, final int count) { return new Token(TokenType.CARD_FROM_INVALID_SET, count, card); } + // WARNING MESSAGES + // ================ public static Token UnknownCard(final String cardName, final String setCode, final int count) { String ttext = setCode == null || setCode.equals("") ? cardName : - String.format("%s (%s)", cardName, setCode); - return new Token(TokenType.UNKNOWN_CARD_REQUEST, count, ttext); + String.format("%s [%s]", cardName, setCode); + return new Token(TokenType.UNKNOWN_CARD, count, ttext); } - public static Token DeckSection(final String sectionName0){ + public static Token UnsupportedCard(final String cardName, final String setCode, final int count) { + String ttext = setCode == null || setCode.equals("") ? cardName : + String.format("%s [%s]", cardName, setCode); + return new Token(TokenType.UNSUPPORTED_CARD, count, ttext); + } + + public static Token WarningMessage(String msg) { + return new Token(TokenType.WARNING_MESSAGE, msg); + } + + /* ================================= + * DECK SECTIONS + * ================================= */ + private static Token UnsupportedDeckSection(final String sectionName){ + return new Token(TokenType.UNSUPPORTED_DECK_SECTION, sectionName); + } + + public static Token DeckSection(final String sectionName0, List allowedDeckSections){ String sectionName = sectionName0.toLowerCase().trim(); + DeckSection matchedSection = null; if (sectionName.equals("side") || sectionName.contains("sideboard") || sectionName.equals("sb")) - return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Sideboard.name()); - if (sectionName.equals("main") || sectionName.contains("card") + matchedSection = DeckSection.Sideboard; + else if (sectionName.equals("main") || sectionName.contains("card") || sectionName.equals("mainboard") || sectionName.equals("deck")) - return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Main.name()); - if (sectionName.equals("avatar")) - return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Avatar.name()); - if (sectionName.equals("commander")) - return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Commander.name()); - if (sectionName.equals("schemes")) - return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Schemes.name()); - if (sectionName.equals("conspiracy")) - return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Conspiracy.name()); - if (sectionName.equals("planes")) - return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Planes.name()); - return null; + matchedSection = DeckSection.Main; + else if (sectionName.equals("avatar")) + matchedSection = DeckSection.Avatar; + else if (sectionName.equals("commander")) + matchedSection = DeckSection.Commander; + else if (sectionName.equals("schemes")) + matchedSection = DeckSection.Schemes; + else if (sectionName.equals("conspiracy")) + matchedSection = DeckSection.Conspiracy; + else if (sectionName.equals("planes")) + matchedSection = DeckSection.Planes; + + if (matchedSection == null) // no match found + return null; + + if (allowedDeckSections != null && !allowedDeckSections.contains(matchedSection)) + return Token.UnsupportedDeckSection(sectionName0); + return new Token(TokenType.DECK_SECTION_NAME, matchedSection.name()); } private Token(final TokenType type1, final int count, final PaperCard tokenCard) { @@ -164,17 +199,21 @@ public class DeckRecognizer { return this.type; } - public final int getNumber() { + public final int getQuantity() { return this.number; } public final DeckSection getTokenSection() { return this.tokenSection; } + public void resetTokenSection(DeckSection referenceDeckSection) { + this.tokenSection = referenceDeckSection != null ? referenceDeckSection : DeckSection.Main; + } + public final LimitedCardType getLimitedCardType() { return this.limitedCardType; } /** - * Filters all tokens having a PaperCard (not null) - * @return true for tokens of tyoe: + * Filters all tokens types that have a PaperCard instance set (not null) + * @return true for tokens of type: * LEGAL_CARD, LIMITED_CARD, CARD_FROM_NOT_ALLOWED_SET and CARD_FROM_INVALID_SET. * False otherwise. */ @@ -186,9 +225,9 @@ public class DeckRecognizer { } /** - * Filters all tokens that will be actually used by the Deck Importer. + * Filters all tokens that will be potentially considered during Deck Import. * @return true if the type of the token is one of: - * LEGAL_CARD, LIMITED, LIMITED_CARD, DECK_NAME; false otherwise. + * LEGAL_CARD, LIMITED_CARD, DECK_NAME; false otherwise. */ public boolean isTokenForDeck() { return (this.type == TokenType.LEGAL_CARD || @@ -196,6 +235,24 @@ public class DeckRecognizer { this.type == TokenType.DECK_NAME); } + /** + * Determines whether current token is a placeholder token for card categories, + * only used for Decklist formatting. + * @return true if the type of the token is one of: + * CARD_RARITY, CARD_CMC, CARD_TYPE, MANA_COLOUR + */ + public boolean isCardPlaceholder(){ + return (this.type == TokenType.CARD_RARITY || + this.type == TokenType.CARD_CMC || + this.type == TokenType.MANA_COLOUR || + this.type == TokenType.CARD_TYPE); + } + + /** Determins if current token is a Deck Section token + * @return true if the type of token is DECK_SECTION_NAMES + */ + public boolean isDeckSection(){ return this.type == TokenType.DECK_SECTION_NAME; } + /** * Generates the key for the current token, which is a hyphenated string including * "Card Name", "Card Edition", "Card's Collector Number", "token-type", and @@ -308,11 +365,13 @@ public class DeckRecognizer { public static final String REX_NOCARD = String.format("^(?
[^a-zA-Z]*)\\s*(?(\\w+[:]\\s*))?(?<%s>[a-zA-Z]+)(?<post>[^a-zA-Z]*)?$", REGRP_TOKEN);
     public static final String REX_CMC = String.format("^(?<pre>[^a-zA-Z]*)\\s*(?<%s>(C(M)?C(\\s)?\\d{1,2}))(?<post>[^\\d]*)?$", REGRP_TOKEN);
     public static final String REX_RARITY = String.format("^(?<pre>[^a-zA-Z]*)\\s*(?<%s>((un)?common|(mythic)?\\s*(rare)?|land))(?<post>[^a-zA-Z]*)?$", REGRP_TOKEN);
-    public static final String REX_COLOUR = String.format("^(?<pre>[^a-zA-Z]*)\\s*(?<%s>(white|blue|black|red|green|colo(u)?rless|multicolo(u)?r))(?<post>[^a-zA-Z]*)?$", REGRP_TOKEN);
+    public static final String REX_MANA = String.format("^(?<pre>[^a-zA-Z]*)\\s*(?<%s>(white|blue|black|red|green|colo(u)?rless|multicolo(u)?r))(?<post>[^a-zA-Z]*)?$", REGRP_TOKEN);
+    public static final String REX_MANA_SYMBOLS = String.format("^(?<pre>[^a-zA-Z]*)\\s*\\{(?<%s>(w|u|b|r|g|c|m))\\}(?<post>[^a-zA-Z]*)?$", REGRP_TOKEN);
     public static final Pattern NONCARD_PATTERN = Pattern.compile(REX_NOCARD, Pattern.CASE_INSENSITIVE);
     public static final Pattern CMC_PATTERN = Pattern.compile(REX_CMC, Pattern.CASE_INSENSITIVE);
     public static final Pattern CARD_RARITY_PATTERN = Pattern.compile(REX_RARITY, Pattern.CASE_INSENSITIVE);
-    public static final Pattern MANA_PATTERN = Pattern.compile(REX_COLOUR, Pattern.CASE_INSENSITIVE);
+    public static final Pattern MANA_PATTERN = Pattern.compile(REX_MANA, Pattern.CASE_INSENSITIVE);
+    public static final Pattern MANA_SYMBOL_PATTERN = Pattern.compile(REX_MANA_SYMBOLS, Pattern.CASE_INSENSITIVE);
 
     public static final String REGRP_SET = "setcode";
     public static final String REGRP_COLLNR = "collnr";
@@ -334,7 +393,7 @@ public class DeckRecognizer {
 
     // 1. Card-Set Request (Amount?, CardName, Set)
     public static final String REX_CARD_SET_REQUEST = String.format(
-            "(%s\\s*:\\s*)?(%s\\s)?\\s*%s\\s*(\\s|\\||\\(|\\[|\\{)%s(\\s|\\)|\\]|\\})?\\s*%s",
+            "(%s\\s*:\\s*)?(%s\\s)?\\s*%s\\s*(\\s|\\||\\(|\\[|\\{)\\s?%s(\\s|\\)|\\]|\\})?\\s*%s",
             REX_DECKSEC_XMAGE, REX_CARD_COUNT, REX_CARD_NAME, REX_SET_CODE, REX_FOIL_MTGGOLDFISH);
     public static final Pattern CARD_SET_PATTERN = Pattern.compile(REX_CARD_SET_REQUEST);
     // 2. Set-Card Request (Amount?, Set, CardName)
@@ -393,14 +452,76 @@ public class DeckRecognizer {
         return cardTypesList.toArray(new CharSequence[0]);
     }
 
-    private Date releaseDateConstraint = null;
     // These parameters are controlled only via setter methods
+    private Date releaseDateConstraint = null;
     private List<String> allowedSetCodes = null;
     private List<String> gameFormatBannedCards = null;
     private List<String> gameFormatRestrictedCards = null;
+    private List<DeckSection> allowedDeckSections = null;
+    private boolean includeBannedAndRestricted = false;
     private DeckFormat deckFormat = null;
     private CardDb.CardArtPreference artPreference = StaticData.instance().getCardArtPreference();  // init as default
 
+    public List<Token> parseCardList(String[] cardList) {
+        List<Token> tokens = new ArrayList<>();
+        DeckSection referenceDeckSectionInParsing = null;  // default
+        
+        for (String line : cardList) {
+            Token token = this.recognizeLine(line, referenceDeckSectionInParsing);
+            if (token == null)
+                continue;
+
+            TokenType tokenType = token.getType();
+            if (!token.isTokenForDeck() && (tokenType != TokenType.DECK_SECTION_NAME) ||
+                    (tokenType == TokenType.LIMITED_CARD && !this.includeBannedAndRestricted)) {
+                // Just bluntly add the token to the list and proceed.
+                tokens.add(token);
+                continue;
+            }
+
+            if (token.getType() == TokenType.DECK_NAME) {
+                tokens.add(0, token);  // always add deck name top of the decklist
+                continue;
+            }
+
+            if (token.getType() == TokenType.DECK_SECTION_NAME) {
+                referenceDeckSectionInParsing = DeckSection.valueOf(token.getText());
+                tokens.add(token);
+                continue;
+            }
+
+            // OK so now the token is either a Legal card or a limited card that has been marked for inclusion
+            DeckSection tokenSection = token.getTokenSection();
+            PaperCard tokenCard = token.getCard();
+
+            if (isAllowed(tokenSection)) {
+                if (!tokenSection.equals(referenceDeckSectionInParsing)) {
+                    Token sectionToken = Token.DeckSection(tokenSection.name(), this.allowedDeckSections);
+                    // just check that last token is stack is a card placeholder.
+                    // In that case, add the new section token before the placeholder
+                    if (!tokens.isEmpty() && tokens.get(tokens.size() - 1).isCardPlaceholder())
+                        tokens.add(tokens.size() - 1, sectionToken);
+                    else
+                        tokens.add(sectionToken);
+                    referenceDeckSectionInParsing = tokenSection;
+                }
+                tokens.add(token);
+                continue;
+            }
+            // So Section and Token have now been already validated in recogniseLine
+            // Therefore, if the Token Section is not allowed in current Editor/Game Format,
+            // the card would not be supported either.
+            Token unsupportedCard = Token.UnsupportedCard(tokenCard.getName(), tokenCard.getEdition(),
+                    token.getQuantity());
+            tokens.add(unsupportedCard);
+        }
+        return tokens;
+    }
+
+    private boolean isAllowed(DeckSection tokenSection) {
+        return this.allowedDeckSections == null || this.allowedDeckSections.contains(tokenSection);
+    }
+
     public Token recognizeLine(final String rawLine, DeckSection referenceSection) {
         if (rawLine == null)
             return null;
@@ -443,7 +564,7 @@ public class DeckRecognizer {
         return line;
     }
 
-    public Token recogniseCardToken(final String text, final DeckSection referenceSection) {
+    public Token recogniseCardToken(final String text, final DeckSection currentDeckSection) {
         String line = text.trim();
         Token uknonwnCardToken = null;
         StaticData data = StaticData.instance();
@@ -460,7 +581,7 @@ public class DeckRecognizer {
             String setCode = getRexGroup(matcher, REGRP_SET);
             String collNo = getRexGroup(matcher, REGRP_COLLNR);
             String foilGr = getRexGroup(matcher, REGRP_FOIL_GFISH);
-            String deckSec = getRexGroup(matcher, REGRP_DECK_SEC_XMAGE_STYLE);
+            String deckSecFromCardLine = getRexGroup(matcher, REGRP_DECK_SEC_XMAGE_STYLE);
             boolean isFoil = foilGr != null;
             int cardCount = ccount != null ? Integer.parseInt(ccount) : 1;
             // if any, it will be tried to convert specific collector number to art index (useful for lands).
@@ -471,7 +592,6 @@ public class DeckRecognizer {
             } catch (NumberFormatException ex) {
                 artIndex = IPaperCard.NO_ART_INDEX;
             }
-            DeckSection tokenSection = getTokenSection(deckSec, referenceSection);
             if (setCode != null) {
                 // Ok Now we're sure the cardName is correct. Now check for setCode
                 CardEdition edition = StaticData.instance().getEditions().get(setCode);
@@ -487,7 +607,7 @@ public class DeckRecognizer {
                 PaperCard pc = data.getCardFromSet(cardName, edition, collectorNumber, artIndex, isFoil);
                 if (pc != null)
                     // ok so the card has been found - let's see if there's any restriction on the set
-                    return checkAndSetCardToken(pc, edition, cardCount, tokenSection);
+                    return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine, currentDeckSection);
                 // UNKNOWN card as in the Counterspell|FEM case
                 return Token.UnknownCard(cardName, setCode, cardCount);
             }
@@ -506,44 +626,57 @@ public class DeckRecognizer {
 
             if (pc != null) {
                 CardEdition edition = StaticData.instance().getCardEdition(pc.getEdition());
-                return checkAndSetCardToken(pc, edition, cardCount, tokenSection);
+                return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine, currentDeckSection);
             }
         }
         return uknonwnCardToken;  // either null or unknown card
     }
 
-    private Token checkAndSetCardToken(PaperCard pc, CardEdition edition, int cardCount, DeckSection tokenSection) {
+    private Token checkAndSetCardToken(PaperCard pc, CardEdition edition, int cardCount,
+                                       String deckSecFromCardLine, DeckSection referenceSection) {
         // Note: Always Check Allowed Set First to avoid accidentally importing invalid cards
         // e.g. Banned Cards from not-allowed sets!
         if (IsIllegalInFormat(edition.getCode()))
             // Mark as illegal card
             return Token.NotAllowedCard(pc, cardCount);
 
+        if (isNotCompliantWithReleaseDateRestrictions(edition))
+            return Token.CardInInvalidSet(pc, cardCount);
+
+        DeckSection tokenSection = getTokenSection(deckSecFromCardLine, referenceSection, pc);
         if (isBannedInFormat(pc))
             return Token.LimitedCard(pc, cardCount, tokenSection, LimitedCardType.BANNED);
 
         if (isRestrictedInFormat(pc, cardCount))
             return Token.LimitedCard(pc, cardCount, tokenSection, LimitedCardType.RESTRICTED);
 
-        if (isNotCompliantWithReleaseDateRestrictions(edition))
-            return Token.InvalidCard(pc, cardCount);
-
         return Token.LegalCard(pc, cardCount, tokenSection);
     }
 
-    private DeckSection getTokenSection(String deckSec, DeckSection currentDeckSection){
-        if (deckSec == null)
-            return currentDeckSection == null ? DeckSection.Main : currentDeckSection;
-        switch (deckSec.toUpperCase().trim()){
-            case "MB":
-                return DeckSection.Main;
-            case "SB":
-                return DeckSection.Sideboard;
-            case "CM":
-                return DeckSection.Commander;
-            default:
-                return DeckSection.Main;
+    // This would save tons of time in parsing Input + would also allow to return UnsupportedCardTokens beforehand
+    private DeckSection getTokenSection(String deckSec, DeckSection currentDeckSection, PaperCard card){
+        if (deckSec != null) {
+            DeckSection cardSection;
+            switch (deckSec.toUpperCase().trim()) {
+                case "MB":
+                    cardSection = DeckSection.Main;
+                    break;
+                case "SB":
+                    cardSection = DeckSection.Sideboard;
+                    break;
+                case "CM":
+                    cardSection = DeckSection.Commander;
+                    break;
+                default:
+                    cardSection = DeckSection.matchingSection(card);
+                    break;
+            }
+            if (cardSection.validate(card))
+                return cardSection;
         }
+        if (currentDeckSection != null && currentDeckSection.validate(card))
+            return currentDeckSection;
+        return DeckSection.matchingSection(card);
     }
 
     private boolean hasGameFormatConstraints() {
@@ -610,7 +743,7 @@ public class DeckRecognizer {
     public Token recogniseNonCardToken(final String text) {
         if (isDeckSectionName(text)) {
             String tokenText = nonCardTokenMatch(text);
-            return Token.DeckSection(tokenText);
+            return Token.DeckSection(tokenText, this.allowedDeckSections);
         }
         if (isCardCMC(text)){
             String tokenText = cardCMCTokenMatch(text);
@@ -627,7 +760,7 @@ public class DeckRecognizer {
             return new Token(TokenType.CARD_TYPE, tokenText);
         }
         if(isManaToken(text)){
-            String tokenText = manaTokenMatch(text);
+            String tokenText = getManaTokenMatch(text);
             return new Token(TokenType.MANA_COLOUR, tokenText);
         }
         if (isDeckName(text)) {
@@ -703,9 +836,53 @@ public class DeckRecognizer {
             return null;
         String line = lineAsIs.trim();
         Matcher manaMatcher = MANA_PATTERN.matcher(line);
-        if (!manaMatcher.matches())
+        Matcher manaSymbolMatcher = MANA_SYMBOL_PATTERN.matcher(line);
+        if (!manaMatcher.matches() && !manaSymbolMatcher.matches())
             return null;
-        return manaMatcher.group(REGRP_TOKEN);
+        if (manaMatcher.matches())
+            return manaMatcher.group(REGRP_TOKEN);
+        return manaSymbolMatcher.group(REGRP_TOKEN);
+    }
+
+    private static String getManaTokenMatch(final String lineAsIs){
+        String matchedText = manaTokenMatch(lineAsIs);
+        String tokenText = "%s %s";
+        switch (matchedText.toLowerCase()) {
+            case MagicColor.Constant.WHITE:
+            case "w":
+                return String.format(tokenText, Localizer.getInstance().getMessage("lblWhite"),
+                                     MagicColor.Color.WHITE.getSymbol());
+
+            case MagicColor.Constant.BLUE:
+            case "u":
+                return String.format(tokenText, Localizer.getInstance().getMessage("lblBlue"),
+                        MagicColor.Color.BLUE.getSymbol());
+
+            case MagicColor.Constant.BLACK:
+            case "b":
+                return String.format(tokenText, Localizer.getInstance().getMessage("lblBlack"),
+                        MagicColor.Color.BLACK.getSymbol());
+
+            case MagicColor.Constant.RED:
+            case "r":
+                return String.format(tokenText, Localizer.getInstance().getMessage("lblRed"),
+                        MagicColor.Color.RED.getSymbol());
+
+            case MagicColor.Constant.GREEN:
+            case "g":
+                return String.format(tokenText, Localizer.getInstance().getMessage("lblGreen"),
+                        MagicColor.Color.GREEN.getSymbol());
+
+            case MagicColor.Constant.COLORLESS:
+            case "c":
+                return String.format(tokenText, Localizer.getInstance().getMessage("lblColorless"),
+                        MagicColor.Color.COLORLESS.getSymbol());
+
+            default: // Multicolour
+                return String.format(tokenText, Localizer.getInstance().getMessage("lblMulticolor"),
+                        "{M}");
+        }
+
     }
 
     public static boolean isDeckName(final String lineAsIs) {
@@ -756,4 +933,8 @@ public class DeckRecognizer {
     }
 
     public void setArtPreference(CardDb.CardArtPreference artPref){ this.artPreference = artPref; }
+
+    public void setAllowedDeckSections(List<DeckSection> deckSections){ this.allowedDeckSections = deckSections; }
+
+    public void forceImportBannedAndRestrictedCards() { this.includeBannedAndRestricted = true; }
 }
diff --git a/forge-gui-desktop/src/test/java/forge/deck/DeckRecognizerTest.java b/forge-gui-desktop/src/test/java/forge/deck/DeckRecognizerTest.java
index a7099e3c8db..115ed81382c 100644
--- a/forge-gui-desktop/src/test/java/forge/deck/DeckRecognizerTest.java
+++ b/forge-gui-desktop/src/test/java/forge/deck/DeckRecognizerTest.java
@@ -4,6 +4,7 @@ import forge.StaticData;
 import forge.card.CardDb;
 import forge.card.CardEdition;
 import forge.card.ForgeCardMockTestCase;
+import forge.card.MagicColor;
 import forge.item.IPaperCard;
 import forge.item.PaperCard;
 import forge.deck.DeckRecognizer.Token;
@@ -52,6 +53,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
      * - Deck Name
      * - Card Type
      * - Deck Section
+     * - Mana Colour
      * ======================================
      */
 
@@ -301,11 +303,59 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
             assertFalse(DeckRecognizer.isCardCMC(line), "Fail on "+line);
     }
 
+    @Test void testManaSymbolsMatches(){
+        Pattern manaSymbolPattern = DeckRecognizer.MANA_SYMBOL_PATTERN;
+
+        List<MagicColor.Color> colours = Arrays.asList(MagicColor.Color.COLORLESS, MagicColor.Color.BLACK,
+                MagicColor.Color.BLUE, MagicColor.Color.GREEN, MagicColor.Color.RED, MagicColor.Color.GREEN);
+
+        for (MagicColor.Color color : colours){
+            String matchingManaSymbol = color.getSymbol();
+            Matcher manaSymbolMatcher = manaSymbolPattern.matcher(matchingManaSymbol);
+            assertTrue(manaSymbolMatcher.matches(), "Failed on : " + matchingManaSymbol);
+        }
+        // Lowercase
+        for (MagicColor.Color color : colours){
+            String matchingManaSymbol = color.getSymbol().toLowerCase();
+            Matcher manaSymbolMatcher = manaSymbolPattern.matcher(matchingManaSymbol);
+            assertTrue(manaSymbolMatcher.matches(), "Failed on : " + matchingManaSymbol);
+        }
+
+        // No Brackets - SO expected to fail matching
+        for (MagicColor.Color color : colours){
+            String matchingManaSymbol = color.getSymbol().toLowerCase().substring(1, color.getSymbol().length());
+            Matcher manaSymbolMatcher = manaSymbolPattern.matcher(matchingManaSymbol);
+            assertFalse(manaSymbolMatcher.matches(), "Failed on : " + matchingManaSymbol);
+        }
+
+        // Test Multi-Colour
+        Matcher manaSymbolMatcher = manaSymbolPattern.matcher("{m}");
+        assertTrue(manaSymbolMatcher.matches());
+
+        manaSymbolMatcher = manaSymbolPattern.matcher("{M}");
+        assertTrue(manaSymbolMatcher.matches());
+
+        manaSymbolMatcher = manaSymbolPattern.matcher("m");
+        assertFalse(manaSymbolMatcher.matches());
+
+        manaSymbolMatcher = manaSymbolPattern.matcher("ubwm");
+        assertFalse(manaSymbolMatcher.matches());
+    }
+
     @Test void testManaTokenMatch(){
+        DeckRecognizer recognizer = new DeckRecognizer();
         String[] cmcTokens = new String[] {"Blue", "red", "White", "// Black", "       //Colorless----", "(green)",
                 "// Multicolor", "// MultiColour"};
-        for (String line : cmcTokens)
+
+        String[] expectedTokenText = new String[] {"{U}", "{R}", "{W}", "{B}",
+        "{C}", "{G}", "{M}", "{M}"};
+        for (int i = 0; i < cmcTokens.length; i++) {
+            String line = cmcTokens[i];
             assertTrue(DeckRecognizer.isManaToken(line), "Fail on " + line);
+            Token manaToken = recognizer.recogniseNonCardToken(line);
+            assertNotNull(manaToken);
+            assertTrue(manaToken.getText().endsWith(expectedTokenText[i]));
+        }
 
         String[] nonCMCtokens = new String[] {"blues", "red more words", "mainboard"};
         for (String line : nonCMCtokens)
@@ -314,8 +364,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
 
     /*=============================
     * TEST RECOGNISE NON-CARD LINES
-    * =============================
-    */
+    * ============================= */
     @Test void testMatchNonCardLine(){
         DeckRecognizer recognizer = new DeckRecognizer();
 
@@ -324,98 +373,97 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.CARD_TYPE);
         assertEquals(t.getText(), "Lands");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         // Test Token Types
         t = recognizer.recogniseNonCardToken("//Land");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.CARD_RARITY);
         assertEquals(t.getText(), "Land");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("[Main]");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.DECK_SECTION_NAME);
         assertEquals(t.getText(), "Main");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("## Mainboard (75)");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.DECK_SECTION_NAME);
         assertEquals(t.getText(), "Main");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("### Artifact (3)");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.CARD_TYPE);
         assertEquals(t.getText(), "Artifact");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("Enchantments");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.CARD_TYPE);
         assertEquals(t.getText(), "Enchantments");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("//Name: Artifacts from DeckStats.net");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.DECK_NAME);
         assertEquals(t.getText(), "Artifacts from DeckStats");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("Name: OLDSCHOOL 93-94 Red Green Aggro by Zombies with JetPack");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.DECK_NAME);
         assertEquals(t.getText(), "OLDSCHOOL 93-94 Red Green Aggro by Zombies with JetPack");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("CMC0");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.CARD_CMC);
         assertEquals(t.getText(), "CMC0");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("CC1");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.CARD_CMC);
         assertEquals(t.getText(), "CC1");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("//Common");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.CARD_RARITY);
         assertEquals(t.getText(), "Common");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("(mythic rare)");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.CARD_RARITY);
         assertEquals(t.getText(), "mythic rare");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("//Blue");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.MANA_COLOUR);
-        assertEquals(t.getText(), "Blue");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getText(), String.format("%s {U}", ForgeCardMockTestCase.MOCKED_LOCALISED_STRING));
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("(Colorless)");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.MANA_COLOUR);
-        assertEquals(t.getText(), "Colorless");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getText(), String.format("%s {C}", ForgeCardMockTestCase.MOCKED_LOCALISED_STRING));
+        assertEquals(t.getQuantity(), 0);
 
         t = recognizer.recogniseNonCardToken("//Planes");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.DECK_SECTION_NAME);
         assertEquals(t.getText(), "Planes");
-        assertEquals(t.getNumber(), 0);
+        assertEquals(t.getQuantity(), 0);
     }
 
     /*=============================
      * TEST RECOGNISE CARD LINES
-     * =============================
-     */
+     * =============================*/
 
     // === Card-Set Pattern Request
     @Test void testValidMatchCardSetLine(){
@@ -890,7 +938,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         PaperCard tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertTrue(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "TMP");
@@ -902,7 +950,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertFalse(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "TMP");
@@ -914,7 +962,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertFalse(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "TMP");
@@ -926,7 +974,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertFalse(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "TMP");
@@ -938,7 +986,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertFalse(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "TMP");
@@ -950,7 +998,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertFalse(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "TMP");
@@ -962,7 +1010,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertFalse(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "TMP");
@@ -977,7 +1025,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertFalse(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "VMA");
@@ -988,7 +1036,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertTrue(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "VMA");
@@ -999,7 +1047,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertTrue(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "VMA");
@@ -1014,7 +1062,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         PaperCard tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertEquals(tokenCard.getName(), "Counterspell");
         assertEquals(tokenCard.getEdition(), "ICE");
 
@@ -1026,7 +1074,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertEquals(tokenCard.getName(), "Counterspell");
         assertEquals(tokenCard.getEdition(), "MH2");
 
@@ -1047,7 +1095,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertTrue(cardToken.isCardToken());
         PaperCard tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertTrue(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "TMP");
@@ -1178,7 +1226,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         PaperCard tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 20);
+        assertEquals(cardToken.getQuantity(), 20);
         assertEquals(tokenCard.getName(), "Mountain");
         assertEquals(tokenCard.getEdition(), "MIR");
         assertEquals(tokenCard.getArtIndex(), 3);
@@ -1194,7 +1242,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         PaperCard tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertEquals(tokenCard.getName(), "Auspicious Ancestor");
         assertEquals(tokenCard.getEdition(), "MIR");
         assertEquals(tokenCard.getArtIndex(), 1);
@@ -1209,7 +1257,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         Token cardToken = recognizer.recogniseCardToken(requestLine, null);
         assertNotNull(cardToken);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 3);
+        assertEquals(cardToken.getQuantity(), 3);
         PaperCard card = cardToken.getCard();
         assertEquals(card.getName(), "Jayemdae Tome");
         assertEquals(card.getEdition(), "LEB");
@@ -1222,7 +1270,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(cardToken);
         assertNull(cardToken.getCard());
         assertNull(cardToken.getTokenSection());
-        assertEquals(cardToken.getType(), TokenType.UNKNOWN_CARD_REQUEST);
+        assertEquals(cardToken.getType(), TokenType.UNKNOWN_CARD);
     }
 
     /*=================================
@@ -1234,7 +1282,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         String lineRequest = "2x Counterspell FEM";
         Token cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
-        assertEquals(cardToken.getType(), TokenType.UNKNOWN_CARD_REQUEST);
+        assertEquals(cardToken.getType(), TokenType.UNKNOWN_CARD);
         assertNull(cardToken.getCard());
     }
 
@@ -1244,7 +1292,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         String lineRequest = "2x Counterspell BOU";
         Token cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
-        assertEquals(cardToken.getType(), TokenType.UNKNOWN_CARD_REQUEST);
+        assertEquals(cardToken.getType(), TokenType.UNKNOWN_CARD);
         assertNull(cardToken.getCard());
         assertNull(cardToken.getTokenSection());
     }
@@ -1263,7 +1311,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         PaperCard ancestralCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(ancestralCard.getName(), "Ancestral Recall");
         assertEquals(StaticData.instance().getCommonCards().getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
         assertEquals(ancestralCard.getEdition(), "2ED");
@@ -1276,7 +1324,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         PaperCard counterSpellCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(counterSpellCard.getName(), "Counterspell");
         assertEquals(counterSpellCard.getEdition(), "MMQ");
     }
@@ -1293,7 +1341,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         PaperCard counterSpellCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(counterSpellCard.getName(), "Counterspell");
         assertEquals(counterSpellCard.getEdition(), "MH2");
 
@@ -1317,7 +1365,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertNotNull(cardToken.getTokenSection());
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         PaperCard tc = cardToken.getCard();
@@ -1330,7 +1378,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertNotNull(cardToken.getTokenSection());
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         tc = cardToken.getCard();
@@ -1348,7 +1396,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         PaperCard tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertTrue(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "VMA");
@@ -1359,7 +1407,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertTrue(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "LEA");
@@ -1371,7 +1419,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         tokenCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(tokenCard.getName(), "Power Sink");
         assertFalse(tokenCard.isFoil());
         assertEquals(tokenCard.getEdition(), "LEA");
@@ -1411,7 +1459,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
         PaperCard ancestralCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(ancestralCard.getName(), "Ancestral Recall");
         assertEquals(StaticData.instance().getCommonCards().getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
         assertEquals(ancestralCard.getEdition(), "VMA");
@@ -1444,7 +1492,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.CARD_FROM_NOT_ALLOWED_SET);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertNull(cardToken.getTokenSection());
         assertEquals(cardToken.getCard().getName(), "Bloodstained Mire");
         assertEquals(cardToken.getCard().getEdition(), "ONS");
@@ -1456,7 +1504,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getType(), TokenType.LIMITED_CARD);
         assertEquals(cardToken.getLimitedCardType(), DeckRecognizer.LimitedCardType.BANNED);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertNotNull(cardToken.getTokenSection());
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertEquals(cardToken.getCard().getName(), "Bloodstained Mire");
@@ -1491,7 +1539,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Ancestral Recall");
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(cardToken.getCard().getEdition(), "VMA");
 
         cardRequest = "4x Ancestral Recall";
@@ -1502,7 +1550,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Ancestral Recall");
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(cardToken.getCard().getEdition(), "VMA");
     }
 
@@ -1525,7 +1573,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Viashino Sandstalker");
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(cardToken.getCard().getEdition(), "MB1");
 
         cardRequest = "4x Viashino Sandstalker";
@@ -1536,7 +1584,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Viashino Sandstalker");
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(cardToken.getCard().getEdition(), "MB1");
 
         // Requesting now what will be a Banned card later in this test
@@ -1547,7 +1595,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Squandered Resources");
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(cardToken.getCard().getEdition(), "VIS");
 
         // == ALLOWED SETS ONLY
@@ -1560,7 +1608,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Viashino Sandstalker");
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(cardToken.getCard().getEdition(), "VIS");
 
         // == BANNED CARDS ONLY
@@ -1573,7 +1621,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Viashino Sandstalker");
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(cardToken.getCard().getEdition(), "MB1");
 
         cardRequest = "Squandered Resources";
@@ -1585,7 +1633,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Squandered Resources");
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(cardToken.getCard().getEdition(), "VIS");
 
         // ALLOWED SET CODES AND RESTRICTED
@@ -1599,7 +1647,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Viashino Sandstalker");
-        assertEquals(cardToken.getNumber(), 4);
+        assertEquals(cardToken.getQuantity(), 4);
         assertEquals(cardToken.getCard().getEdition(), "VIS");
     }
 
@@ -1617,7 +1665,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         PaperCard tc = cardToken.getCard();
         assertEquals(tc.getName(), "Lightning Dragon");
         assertEquals(tc.getEdition(), "VMA");
@@ -1632,7 +1680,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         tc = cardToken.getCard();
         assertEquals(tc.getName(), "Lightning Dragon");
         assertEquals(tc.getEdition(), "USG");
@@ -1642,7 +1690,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
         tc = cardToken.getCard();
         assertEquals(tc.getName(), "Lightning Dragon");
@@ -1654,14 +1702,14 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.CARD_FROM_INVALID_SET);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
 
         lineRequest = "2x Lightning Dragon";
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.CARD_FROM_INVALID_SET);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getText(), "Lightning Dragon [USG] #202");
 
@@ -1674,14 +1722,14 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.CARD_FROM_NOT_ALLOWED_SET);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
 
         lineRequest = "2x Lightning Dragon";
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.CARD_FROM_NOT_ALLOWED_SET);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
 
         // Now relaxing date constraint but removing USG from allowed sets
@@ -1692,7 +1740,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
         tc = cardToken.getCard();
         assertEquals(tc.getName(), "Lightning Dragon");
@@ -1710,7 +1758,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         PaperCard tc = cardToken.getCard();
         assertEquals(tc.getName(), "Flash");
         assertEquals(tc.getEdition(), "A25");
@@ -1732,7 +1780,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
         tc = cardToken.getCard();
         assertEquals(tc.getName(), "Cancel");
@@ -1742,7 +1790,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.CARD_FROM_INVALID_SET);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getText(), "Cancel [M21] #46");
     }
@@ -1758,7 +1806,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         PaperCard tc = cardToken.getCard();
         assertEquals(tc.getName(), "Flash");
         assertEquals(tc.getEdition(), "A25");
@@ -1779,7 +1827,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
         tc = cardToken.getCard();
         assertEquals(tc.getName(), "Femeref Knight");
@@ -1789,7 +1837,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertNotNull(cardToken.getCard());
         tc = cardToken.getCard();
         assertEquals(tc.getName(), "Incinerate");
@@ -1799,7 +1847,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LIMITED_CARD);  // violating Deck format
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getText(), "Noble Elephant [MIR] #30");
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
@@ -1810,7 +1858,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         cardToken = recognizer.recogniseCardToken(lineRequest, null);
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.CARD_FROM_NOT_ALLOWED_SET);  // violating Game format
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getText(), "Incinerate [ICE] #194");
     }
@@ -1826,7 +1874,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNotNull(cardToken);
         assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
         assertNotNull(cardToken.getCard());
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         PaperCard tc = cardToken.getCard();
         assertEquals(tc.getName(), "Flash");
         assertEquals(tc.getEdition(), "A25");
@@ -1879,6 +1927,286 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getText(), "Buried Alive [WTH] #63");
     }
 
+    /*==================================
+     * TEST RECOGNISE CARD EXTRA FORMATS
+     * =================================*/
+
+    // === MTG Goldfish
+    @Test void testFoilRequestInMTGGoldfishExportFormat(){
+        String mtgGoldfishRequest = "18 Forest <254> [THB]";
+        Pattern target = DeckRecognizer.CARD_COLLNO_SET_PATTERN;
+        Matcher matcher = target.matcher(mtgGoldfishRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "18");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Forest");  // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "THB");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "254");
+        assertNull(matcher.group(DeckRecognizer.REGRP_FOIL_GFISH));
+
+        mtgGoldfishRequest = "18 Forest <254> [THB] (F)";
+        matcher = target.matcher(mtgGoldfishRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "18");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Forest"); // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "THB");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "254");
+        assertNotNull(matcher.group(DeckRecognizer.REGRP_FOIL_GFISH));
+
+        mtgGoldfishRequest = "18 Forest [THB]";
+        matcher = target.matcher(mtgGoldfishRequest);
+        assertFalse(matcher.matches());
+
+        mtgGoldfishRequest = "18 [THB] Forest";
+        matcher = target.matcher(mtgGoldfishRequest);
+        assertFalse(matcher.matches());
+
+        mtgGoldfishRequest = "18 Forest [THB] (F)";
+        matcher = target.matcher(mtgGoldfishRequest);
+        assertFalse(matcher.matches());
+
+        mtgGoldfishRequest = "18 [THB] Forest (F)";
+        matcher = target.matcher(mtgGoldfishRequest);
+        assertFalse(matcher.matches());
+
+        mtgGoldfishRequest = "18 Forest 254 [THB] (F)";
+        matcher = target.matcher(mtgGoldfishRequest);
+        assertFalse(matcher.matches());
+
+        mtgGoldfishRequest = "18 Forest 254 [THB]";
+        matcher = target.matcher(mtgGoldfishRequest);
+        assertFalse(matcher.matches());
+
+        mtgGoldfishRequest = "18 [THB] Forest 254";
+        matcher = target.matcher(mtgGoldfishRequest);
+        assertFalse(matcher.matches());
+    }
+
+    @Test void testCardRecognisedMTGGoldfishFormat(){
+        DeckRecognizer recognizer = new DeckRecognizer();
+        assertEquals(StaticData.instance().getCommonCards().getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+
+        String lineRequest = "4 Aspect of Hydra [BNG] (F)";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest, null);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        PaperCard aspectOfHydraCard = cardToken.getCard();
+        assertEquals(cardToken.getQuantity(), 4);
+        assertEquals(aspectOfHydraCard.getName(), "Aspect of Hydra");
+        assertEquals(aspectOfHydraCard.getEdition(), "BNG");
+        assertTrue(aspectOfHydraCard.isFoil());
+
+        lineRequest = "18 Forest <254> [THB] (F)";
+        cardToken = recognizer.recogniseCardToken(lineRequest, null);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        PaperCard forestCard = cardToken.getCard();
+        assertEquals(cardToken.getQuantity(), 18);
+        assertEquals(forestCard.getName(), "Forest");
+        assertEquals(forestCard.getEdition(), "THB");
+        assertTrue(forestCard.isFoil());
+    }
+
+    // === TappedOut Markdown Format
+    @Test void testPurgeLinksInLineRequests(){
+        String line = "* 1 [Ancestral Recall](http://tappedout.nethttp://tappedout.net/mtg-card/ancestral-recall/)";
+        String expected = "* 1 [Ancestral Recall]";
+        assertEquals(DeckRecognizer.purgeAllLinks(line), expected);
+
+        line = "1 [Ancestral Recall](http://tappedout.nethttp://tappedout.net/mtg-card/ancestral-recall/)";
+        expected = "1 [Ancestral Recall]";
+        assertEquals(DeckRecognizer.purgeAllLinks(line), expected);
+    }
+
+    @Test void testCardNameEntryInMarkDownExportFromTappedOut(){
+        DeckRecognizer recognizer = new DeckRecognizer();
+        assertEquals(StaticData.instance().getCommonCards().getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+
+        String line = "* 1 [Ancestral Recall](http://tappedout.nethttp://tappedout.net/mtg-card/ancestral-recall/)";
+
+        Token token = recognizer.recognizeLine(line, null);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.LEGAL_CARD);
+        assertEquals(token.getQuantity(), 1);
+        assertNotNull(token.getCard());
+        PaperCard ancestralRecallCard = token.getCard();
+        assertEquals(ancestralRecallCard.getName(), "Ancestral Recall");
+        assertEquals(ancestralRecallCard.getEdition(), "VMA");
+    }
+
+    // === XMage Format
+    @Test void testMatchCardRequestXMageFormat(){
+        String xmageFormatRequest = "1 [LRW:51] Amoeboid Changeling";
+        Pattern target = DeckRecognizer.SET_COLLNO_CARD_XMAGE_PATTERN;
+        Matcher matcher = target.matcher(xmageFormatRequest);
+        assertTrue(matcher.matches());
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Amoeboid Changeling");  // TRIM
+        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "LRW");
+        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "51");
+        assertNull(matcher.group(DeckRecognizer.REGRP_FOIL_GFISH));
+
+        // Test that this line matches only with this pattern
+
+        target = DeckRecognizer.CARD_SET_PATTERN;
+        matcher = target.matcher(xmageFormatRequest);
+        assertFalse(matcher.matches());
+
+        target = DeckRecognizer.SET_CARD_PATTERN;
+        matcher = target.matcher(xmageFormatRequest);
+        assertFalse(matcher.matches());
+
+        target = DeckRecognizer.CARD_SET_COLLNO_PATTERN;
+        matcher = target.matcher(xmageFormatRequest);
+        assertFalse(matcher.matches());
+
+        target = DeckRecognizer.SET_CARD_COLLNO_PATTERN;
+        matcher = target.matcher(xmageFormatRequest);
+        assertFalse(matcher.matches());
+
+        target = DeckRecognizer.CARD_COLLNO_SET_PATTERN;
+        matcher = target.matcher(xmageFormatRequest);
+        assertFalse(matcher.matches());
+
+        target = DeckRecognizer.CARD_ONLY_PATTERN;
+        matcher = target.matcher(xmageFormatRequest);
+        assertFalse(matcher.matches());
+    }
+
+    @Test void testRecognizeCardTokenInXMageFormatRequest(){
+        DeckRecognizer recognizer = new DeckRecognizer();
+
+        String xmageFormatRequest = "1 [LRW:51] Amoeboid Changeling";
+        Token xmageCardToken = recognizer.recogniseCardToken(xmageFormatRequest, null);
+        assertNotNull(xmageCardToken);
+        assertEquals(xmageCardToken.getType(), TokenType.LEGAL_CARD);
+        assertEquals(xmageCardToken.getQuantity(), 1);
+        assertNotNull(xmageCardToken.getCard());
+        PaperCard acCard = xmageCardToken.getCard();
+        assertEquals(acCard.getName(), "Amoeboid Changeling");
+        assertEquals(acCard.getEdition(), "LRW");
+        assertEquals(acCard.getCollectorNumber(), "51");
+    }
+
+    /*=================================
+     * TEST CARD TOKEN SECTION MATCHING
+     * ================================ */
+    @Test void testCardTokenIsAssignedToCorrectDeckSection(){
+        DeckRecognizer recognizer = new DeckRecognizer();
+
+        String cardRequest = "2x Counterspell |TMP";
+        Token cardToken = recognizer.recogniseCardToken(cardRequest, null);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getCard().getName(), "Counterspell");
+        assertEquals(cardToken.getCard().getEdition(), "TMP");
+        assertEquals(cardToken.getQuantity(), 2);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Main);
+
+        cardToken = recognizer.recogniseCardToken(cardRequest, DeckSection.Main);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getQuantity(), 2);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Main);
+
+        cardToken = recognizer.recogniseCardToken(cardRequest, DeckSection.Sideboard);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getQuantity(), 2);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Sideboard);
+
+        // Setting Deck Section in Card Request now
+        cardRequest = "SB: 2x Counterspell|TMP";
+        cardToken = recognizer.recogniseCardToken(cardRequest, null);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getCard().getName(), "Counterspell");
+        assertEquals(cardToken.getCard().getEdition(), "TMP");
+        assertEquals(cardToken.getQuantity(), 2);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Sideboard);
+
+        cardToken = recognizer.recogniseCardToken(cardRequest, DeckSection.Main);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getQuantity(), 2);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Sideboard);
+    }
+
+    @Test void testCardSectionIsAdaptedToCardRegardlessOfCurrentSection(){
+        DeckRecognizer recognizer = new DeckRecognizer();
+
+        String cardRequest = "2x All in good time";  // Scheme Card
+        Token cardToken = recognizer.recogniseCardToken(cardRequest, null);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getCard().getName(), "All in Good Time");
+        assertEquals(cardToken.getQuantity(), 2);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Schemes);
+
+        cardToken = recognizer.recogniseCardToken(cardRequest, DeckSection.Main);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getCard().getName(), "All in Good Time");
+        assertEquals(cardToken.getQuantity(), 2);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Schemes);
+    }
+
+    @Test void testCardSectionIsAdpatedToCardRegardlessOfSectionInCardRequest(){
+        DeckRecognizer recognizer = new DeckRecognizer();
+
+        String cardRequest = "CM: 4x Incinerate";  // Incinerate in Commander Section
+        Token cardToken = recognizer.recogniseCardToken(cardRequest, null);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getCard().getName(), "Incinerate");
+        assertEquals(cardToken.getQuantity(), 4);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Main);
+
+        // Current Deck Section is Sideboard, so Side should be used as replacing Deck Section
+        cardToken = recognizer.recogniseCardToken(cardRequest, DeckSection.Sideboard);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getCard().getName(), "Incinerate");
+        assertEquals(cardToken.getQuantity(), 4);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Sideboard);
+    }
+
+    @Test void testDeckSectionTokenValidationAlsoAppliesToNonLegalCards(){
+        DeckRecognizer recognizer = new DeckRecognizer();
+        recognizer.setGameFormatConstraint(null, Collections.singletonList("Incinerate"), null);
+
+        String cardRequest = "CM: 4x Incinerate";  // Incinerate in Commander Section
+        Token cardToken = recognizer.recogniseCardToken(cardRequest, null);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LIMITED_CARD);
+        assertNotNull(cardToken.getLimitedCardType());
+        assertEquals(cardToken.getLimitedCardType(), DeckRecognizer.LimitedCardType.BANNED);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getCard().getName(), "Incinerate");
+        assertEquals(cardToken.getQuantity(), 4);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Main);
+
+        // Current Deck Section is Sideboard, so Side should be used as replacing Deck Section
+        cardToken = recognizer.recogniseCardToken(cardRequest, DeckSection.Sideboard);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LIMITED_CARD);
+        assertNotNull(cardToken.getLimitedCardType());
+        assertEquals(cardToken.getLimitedCardType(), DeckRecognizer.LimitedCardType.BANNED);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getCard().getName(), "Incinerate");
+        assertEquals(cardToken.getQuantity(), 4);
+        assertEquals(cardToken.getTokenSection(), DeckSection.Sideboard);
+    }
 
     /*===============
      * TEST TOKEN-KEY
@@ -1892,7 +2220,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Viashino Sandstalker");
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(cardToken.getCard().getEdition(), "MB1");
 
         // Token Key
@@ -1927,7 +2255,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getLimitedCardType(), DeckRecognizer.LimitedCardType.RESTRICTED);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Viashino Sandstalker");
-        assertEquals(cardToken.getNumber(), 2);
+        assertEquals(cardToken.getQuantity(), 2);
         assertEquals(cardToken.getCard().getEdition(), "VIS");
 
         // Token Key
@@ -1952,7 +2280,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getLimitedCardType(), DeckRecognizer.LimitedCardType.BANNED);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Squandered Resources");
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(cardToken.getCard().getEdition(), "VIS");
 
         // Token Key
@@ -2034,7 +2362,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         line = "2x Counterspell FEM";
         lineToken = recognizer.recognizeLine(line, null);
         assertNotNull(lineToken);
-        assertEquals(lineToken.getType(), TokenType.UNKNOWN_CARD_REQUEST);
+        assertEquals(lineToken.getType(), TokenType.UNKNOWN_CARD);
         assertFalse(lineToken.isCardToken());
         assertFalse(lineToken.isTokenForDeck());
         assertNull(lineToken.getCard());
@@ -2091,7 +2419,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(cardToken.getTokenSection(), DeckSection.Main);
         assertNotNull(cardToken.getCard());
         assertEquals(cardToken.getCard().getName(), "Viashino Sandstalker");
-        assertEquals(cardToken.getNumber(), 1);
+        assertEquals(cardToken.getQuantity(), 1);
         assertEquals(cardToken.getCard().getEdition(), "MB1");
 
         // Token Key
@@ -2117,173 +2445,11 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(newTokenKey.limitedType, tokenKey.limitedType);
     }
 
-    /*==================================
-     * TEST RECOGNISE CARD EXTRA FORMATS
-     * =================================
-     */
-
-    // === MTG Goldfish
-    @Test void testFoilRequestInMTGGoldfishExportFormat(){
-        String mtgGoldfishRequest = "18 Forest <254> [THB]";
-        Pattern target = DeckRecognizer.CARD_COLLNO_SET_PATTERN;
-        Matcher matcher = target.matcher(mtgGoldfishRequest);
-        assertTrue(matcher.matches());
-        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "18");
-        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Forest");  // TRIM
-        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "THB");
-        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "254");
-        assertNull(matcher.group(DeckRecognizer.REGRP_FOIL_GFISH));
-
-        mtgGoldfishRequest = "18 Forest <254> [THB] (F)";
-        matcher = target.matcher(mtgGoldfishRequest);
-        assertTrue(matcher.matches());
-        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "18");
-        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Forest"); // TRIM
-        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "THB");
-        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "254");
-        assertNotNull(matcher.group(DeckRecognizer.REGRP_FOIL_GFISH));
-
-        mtgGoldfishRequest = "18 Forest [THB]";
-        matcher = target.matcher(mtgGoldfishRequest);
-        assertFalse(matcher.matches());
-
-        mtgGoldfishRequest = "18 [THB] Forest";
-        matcher = target.matcher(mtgGoldfishRequest);
-        assertFalse(matcher.matches());
-
-        mtgGoldfishRequest = "18 Forest [THB] (F)";
-        matcher = target.matcher(mtgGoldfishRequest);
-        assertFalse(matcher.matches());
-
-        mtgGoldfishRequest = "18 [THB] Forest (F)";
-        matcher = target.matcher(mtgGoldfishRequest);
-        assertFalse(matcher.matches());
-
-        mtgGoldfishRequest = "18 Forest 254 [THB] (F)";
-        matcher = target.matcher(mtgGoldfishRequest);
-        assertFalse(matcher.matches());
-
-        mtgGoldfishRequest = "18 Forest 254 [THB]";
-        matcher = target.matcher(mtgGoldfishRequest);
-        assertFalse(matcher.matches());
-
-        mtgGoldfishRequest = "18 [THB] Forest 254";
-        matcher = target.matcher(mtgGoldfishRequest);
-        assertFalse(matcher.matches());
-    }
-
-    @Test void testCardRecognisedMTGGoldfishFormat(){
-        DeckRecognizer recognizer = new DeckRecognizer();
-        assertEquals(StaticData.instance().getCommonCards().getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
-
-        String lineRequest = "4 Aspect of Hydra [BNG] (F)";
-        Token cardToken = recognizer.recogniseCardToken(lineRequest, null);
-        assertNotNull(cardToken);
-        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
-        assertNotNull(cardToken.getCard());
-        PaperCard aspectOfHydraCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 4);
-        assertEquals(aspectOfHydraCard.getName(), "Aspect of Hydra");
-        assertEquals(aspectOfHydraCard.getEdition(), "BNG");
-        assertTrue(aspectOfHydraCard.isFoil());
-
-        lineRequest = "18 Forest <254> [THB] (F)";
-        cardToken = recognizer.recogniseCardToken(lineRequest, null);
-        assertNotNull(cardToken);
-        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD);
-        assertNotNull(cardToken.getCard());
-        PaperCard forestCard = cardToken.getCard();
-        assertEquals(cardToken.getNumber(), 18);
-        assertEquals(forestCard.getName(), "Forest");
-        assertEquals(forestCard.getEdition(), "THB");
-        assertTrue(forestCard.isFoil());
-    }
-
-    // === TappedOut Markdown Format
-    @Test void testPurgeLinksInLineRequests(){
-        String line = "* 1 [Ancestral Recall](http://tappedout.nethttp://tappedout.net/mtg-card/ancestral-recall/)";
-        String expected = "* 1 [Ancestral Recall]";
-        assertEquals(DeckRecognizer.purgeAllLinks(line), expected);
-
-        line = "1 [Ancestral Recall](http://tappedout.nethttp://tappedout.net/mtg-card/ancestral-recall/)";
-        expected = "1 [Ancestral Recall]";
-        assertEquals(DeckRecognizer.purgeAllLinks(line), expected);
-    }
-
-    @Test void testCardNameEntryInMarkDownExportFromTappedOut(){
-        DeckRecognizer recognizer = new DeckRecognizer();
-        assertEquals(StaticData.instance().getCommonCards().getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
-
-        String line = "* 1 [Ancestral Recall](http://tappedout.nethttp://tappedout.net/mtg-card/ancestral-recall/)";
-
-        Token token = recognizer.recognizeLine(line, null);
-        assertNotNull(token);
-        assertEquals(token.getType(), TokenType.LEGAL_CARD);
-        assertEquals(token.getNumber(), 1);
-        assertNotNull(token.getCard());
-        PaperCard ancestralRecallCard = token.getCard();
-        assertEquals(ancestralRecallCard.getName(), "Ancestral Recall");
-        assertEquals(ancestralRecallCard.getEdition(), "VMA");
-    }
-
-    // === XMage Format
-    @Test void testMatchCardRequestXMageFormat(){
-        String xmageFormatRequest = "1 [LRW:51] Amoeboid Changeling";
-        Pattern target = DeckRecognizer.SET_COLLNO_CARD_XMAGE_PATTERN;
-        Matcher matcher = target.matcher(xmageFormatRequest);
-        assertTrue(matcher.matches());
-        assertEquals(matcher.group(DeckRecognizer.REGRP_CARDNO), "1");
-        assertEquals(matcher.group(DeckRecognizer.REGRP_CARD), "Amoeboid Changeling");  // TRIM
-        assertEquals(matcher.group(DeckRecognizer.REGRP_SET), "LRW");
-        assertEquals(matcher.group(DeckRecognizer.REGRP_COLLNR), "51");
-        assertNull(matcher.group(DeckRecognizer.REGRP_FOIL_GFISH));
-
-        // Test that this line matches only with this pattern
-
-        target = DeckRecognizer.CARD_SET_PATTERN;
-        matcher = target.matcher(xmageFormatRequest);
-        assertFalse(matcher.matches());
-
-        target = DeckRecognizer.SET_CARD_PATTERN;
-        matcher = target.matcher(xmageFormatRequest);
-        assertFalse(matcher.matches());
-
-        target = DeckRecognizer.CARD_SET_COLLNO_PATTERN;
-        matcher = target.matcher(xmageFormatRequest);
-        assertFalse(matcher.matches());
-
-        target = DeckRecognizer.SET_CARD_COLLNO_PATTERN;
-        matcher = target.matcher(xmageFormatRequest);
-        assertFalse(matcher.matches());
-
-        target = DeckRecognizer.CARD_COLLNO_SET_PATTERN;
-        matcher = target.matcher(xmageFormatRequest);
-        assertFalse(matcher.matches());
-
-        target = DeckRecognizer.CARD_ONLY_PATTERN;
-        matcher = target.matcher(xmageFormatRequest);
-        assertFalse(matcher.matches());
-    }
-
-    @Test void testRecognizeCardTokenInXMageFormatRequest(){
-        DeckRecognizer recognizer = new DeckRecognizer();
-
-        String xmageFormatRequest = "1 [LRW:51] Amoeboid Changeling";
-        Token xmageCardToken = recognizer.recogniseCardToken(xmageFormatRequest, null);
-        assertNotNull(xmageCardToken);
-        assertEquals(xmageCardToken.getType(), TokenType.LEGAL_CARD);
-        assertEquals(xmageCardToken.getNumber(), 1);
-        assertNotNull(xmageCardToken.getCard());
-        PaperCard acCard = xmageCardToken.getCard();
-        assertEquals(acCard.getName(), "Amoeboid Changeling");
-        assertEquals(acCard.getEdition(), "LRW");
-        assertEquals(acCard.getCollectorNumber(), "51");
-    }
-
     /*====================================
-     * TEST RECOGNISE LINES (MIXED inputs)
-     * ===================================
-     */
+     * TEST PARSE INPUT
+     * ==================================*/
+
+    // === MIXED inputs ===
     @Test void testRecognizeLines(){
         DeckRecognizer recognizer = new DeckRecognizer();
 
@@ -2362,7 +2528,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(token.getType(), TokenType.LEGAL_CARD);
         assertNotNull(token.getText());
         assertNotNull(token.getCard());
-        assertEquals(token.getNumber(), 4);
+        assertEquals(token.getQuantity(), 4);
 
         lineRequest = "### Instant (14)";
         token = recognizer.recognizeLine(lineRequest, null);