From 2297f34ce238dc09c6685eb3963b4df04b19628d Mon Sep 17 00:00:00 2001 From: leriomaggio Date: Wed, 8 Sep 2021 21:27:43 +0100 Subject: [PATCH] Added full support to all Deckstats deck exports + bug fix&extra tests Now DeckRecognizer supports all the exports of decks from Deckstats.net. This now also includes card lists grouped by Rarity, CMC, and Mana Colours. A new set of tests have been also added to test for the new non-card token types parsing, as well as a condition with multiple constraints imposed on the deck recogniser at a time. In particular, now all the combinations of constraints (also together) have been tested, and therefore the types of token returned has been adjusted. Signed-off-by: leriomaggio --- .../main/java/forge/deck/DeckRecognizer.java | 288 ++++++----- .../java/forge/deck/DeckRecognizerTest.java | 459 +++++++++++++++++- 2 files changed, 614 insertions(+), 133 deletions(-) diff --git a/forge-core/src/main/java/forge/deck/DeckRecognizer.java b/forge-core/src/main/java/forge/deck/DeckRecognizer.java index 239584dbad4..999d68ff54d 100644 --- a/forge-core/src/main/java/forge/deck/DeckRecognizer.java +++ b/forge-core/src/main/java/forge/deck/DeckRecognizer.java @@ -27,10 +27,7 @@ import forge.item.IPaperCard; import forge.item.PaperCard; import org.apache.commons.lang3.StringUtils; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,9 +36,6 @@ import java.util.regex.Pattern; * DeckRecognizer class. *

* - * @author Forge - * @version $Id: DeckRecognizer.java 10499 2011-09-17 15:08:47Z Max mtg $ - * */ public class DeckRecognizer { /** @@ -52,11 +46,14 @@ public class DeckRecognizer { ILLEGAL_CARD_REQUEST, INVALID_CARD_REQUEST, UNKNOWN_CARD_REQUEST, - DECK_NAME, - DECK_SECTION_NAME, - COMMENT, UNKNOWN_TEXT, - CARD_TYPE + DECK_NAME, + COMMENT, + DECK_SECTION_NAME, + CARD_TYPE, + CARD_RARITY, + CARD_CMC, + MANA_COLOUR } /** @@ -74,23 +71,29 @@ public class DeckRecognizer { public static Token IllegalCard(final String cardName, final String setCode, final int count) { String ttext = setCode == null || setCode.equals("") ? cardName : - String.format("%s [%s]", cardName, setCode); + String.format("%s (%s)", cardName, setCode); return new Token(null, TokenType.ILLEGAL_CARD_REQUEST, count, ttext); } public static Token InvalidCard(final String cardName, final String setCode, final int count) { String ttext = setCode == null || setCode.equals("") ? cardName : - String.format("%s [%s]", cardName, setCode); + String.format("%s (%s)", cardName, setCode); return new Token(null, TokenType.INVALID_CARD_REQUEST, count, ttext); } 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); + String.format("%s (%s)", cardName, setCode); return new Token(null, TokenType.UNKNOWN_CARD_REQUEST, count, ttext); } - public static Token DeckSection(final String sectionName){ + public static Token DeckSection(final String sectionName0){ + String sectionName = sectionName0.toLowerCase(); + if (sectionName.equals("side") || sectionName.contains("sideboard")) + return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Sideboard.name()); + 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")) @@ -99,11 +102,6 @@ public class DeckRecognizer { 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("side") || sectionName.contains("sideboard")) - return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Sideboard.name()); - if (sectionName.equals("main") || sectionName.contains("card") - || sectionName.equals("mainboard") || sectionName.equals("deck")) - return new Token(TokenType.DECK_SECTION_NAME, DeckSection.Main.name()); return null; } @@ -148,8 +146,6 @@ public class DeckRecognizer { private static final String LINE_COMMENT_DELIMITER_OR_MD_HEADER = "#"; private static final String ASTERISK = "* "; // Note the blank space after asterisk! -// private static final Pattern EDITION_AFTER_CARD_NAME = Pattern.compile("([\\d]{1,2})?\\s*([a-zA-Z',\\/\\-\\s]+)\\s*((\\(|\\[)([a-zA-Z0-9]{3})(\\)|\\])\\s*)?([\\d]{1,3})?"); - // Core Matching Patterns (initialised in Constructor) public static final String REGRP_DECKNAME = "deckName"; public static final String REX_DECK_NAME = @@ -157,9 +153,15 @@ public class DeckRecognizer { REGRP_DECKNAME); public static final Pattern DECK_NAME_PATTERN = Pattern.compile(REX_DECK_NAME, Pattern.CASE_INSENSITIVE); - public static final String REGRP_NOCARD = "token"; - public static final String REX_NOCARD = String.format("^(?
[^a-zA-Z]*)\\s*(?(\\w+[:]\\s*))?(?<%s>[a-zA-Z]+)(?<post>[^a-zA-Z]*)?$", REGRP_NOCARD);
+    public static final String REGRP_TOKEN = "token";
+    public static final String REX_NOCARD = String.format("^(?<pre>[^a-zA-Z]*)\\s*(?<title>(\\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|colorless))(?<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 String REGRP_SET = "setcode";
     public static final String REGRP_COLLNR = "collnr";
@@ -170,61 +172,69 @@ public class DeckRecognizer {
     public static final String REX_SET_CODE = String.format("(?<%s>[a-zA-Z0-9_]{2,7})", REGRP_SET);
     public static final String REX_COLL_NUMBER = String.format("(?<%s>\\*?[0-9A-Z]+\\S?[A-Z]*)", REGRP_COLLNR);
     public static final String REX_CARD_COUNT = String.format("(?<%s>[\\d]{1,2})(?<mult>x)?", REGRP_CARDNO);
-
     // EXTRA
     public static final String REGRP_FOIL_GFISH = "foil";
     private static final String REX_FOIL_MTGGOLDFISH = String.format(
             "(?<%s>\\(F\\))?", REGRP_FOIL_GFISH);
-
     // 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",
             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)
     public static final String REX_SET_CARD_REQUEST = String.format(
             "(%s\\s)?\\s*(\\(|\\[|\\{)?%s(\\s+|\\)|\\]|\\}|\\|)\\s*%s\\s*%s\\s*",
             REX_CARD_COUNT, REX_SET_CODE, REX_CARD_NAME, REX_FOIL_MTGGOLDFISH);
     public static final Pattern SET_CARD_PATTERN = Pattern.compile(REX_SET_CARD_REQUEST);
-
     // 3. Full-Request (Amount?, CardName, Set, Collector Number|Art Index) - MTGArena Format
     public static final String REX_FULL_REQUEST_CARD_SET = String.format(
             "(%s\\s)?\\s*%s\\s*(\\||\\(|\\[|\\{|\\s)%s(\\s|\\)|\\]|\\})?\\s+%s\\s*%s\\s*",
             REX_CARD_COUNT, REX_CARD_NAME, REX_SET_CODE, REX_COLL_NUMBER, REX_FOIL_MTGGOLDFISH);
     public static final Pattern CARD_SET_COLLNO_PATTERN = Pattern.compile(REX_FULL_REQUEST_CARD_SET);
-
     // 4. Full-Request (Amount?, Set, CardName, Collector Number|Art Index) - Alternative for flexibility
     public static final String REX_FULL_REQUEST_SET_CARD = String.format(
             "^(%s\\s)?\\s*(\\(|\\[|\\{)?%s(\\s+|\\)|\\]|\\}|\\|)\\s*%s\\s+%s\\s*%s$",
             REX_CARD_COUNT, REX_SET_CODE, REX_CARD_NAME, REX_COLL_NUMBER, REX_FOIL_MTGGOLDFISH);
     public static final Pattern SET_CARD_COLLNO_PATTERN = Pattern.compile(REX_FULL_REQUEST_SET_CARD);
-
     // 5. (MTGGoldfish mostly) (Amount?, Card Name, <Collector Number>, Set)
     public static final String REX_FULL_REQUEST_CARD_COLLNO_SET = String.format(
             "^(%s\\s)?\\s*%s\\s+(\\<%s\\>)\\s*(\\(|\\[|\\{)?%s(\\s+|\\)|\\]|\\}|\\|)\\s*%s$",
             REX_CARD_COUNT, REX_CARD_NAME, REX_COLL_NUMBER, REX_SET_CODE, REX_FOIL_MTGGOLDFISH);
     public static final Pattern CARD_COLLNO_SET_PATTERN = Pattern.compile(REX_FULL_REQUEST_CARD_COLLNO_SET);
-
     // 6. XMage format (Amount?, [Set:Collector Number] Card Name)
     public static final String REX_FULL_REQUEST_XMAGE = String.format(
             "^(%s\\s)?\\s*(\\[)?%s:%s(\\])\\s+%s\\s*%s$",
             REX_CARD_COUNT, REX_SET_CODE, REX_COLL_NUMBER, REX_CARD_NAME, REX_FOIL_MTGGOLDFISH);
     public static final Pattern SET_COLLNO_CARD_XMAGE_PATTERN = Pattern.compile(REX_FULL_REQUEST_XMAGE);
-
     // 7. Card-Only Request (Amount?)
     public static final String REX_CARDONLY = String.format(
             "(%s\\s)?\\s*%s\\s*%s", REX_CARD_COUNT, REX_CARD_NAME, REX_FOIL_MTGGOLDFISH);
     public static final Pattern CARD_ONLY_PATTERN = Pattern.compile(REX_CARDONLY);
 
-
     // CoreTypes (to recognise Tokens of type CardType
-    private static CharSequence[] CARD_TYPES = allCardTypes();
-
+    private static final CharSequence[] CARD_TYPES = allCardTypes();
     private static final CharSequence[] DECK_SECTION_NAMES = {"avatar", "commander",
             "schemes", "conspiracy", "planes", "deck",
             "main", "card", "mainboard", "side", "sideboard"};
 
+    private static CharSequence[] allCardTypes(){
+        List<String> cardTypesList = new ArrayList<>();
+        // CoreTypesNames
+        List<CardType.CoreType> coreTypes = Lists.newArrayList(CardType.CoreType.values());
+        for (CardType.CoreType coreType : coreTypes)
+            cardTypesList.add(coreType.name().toLowerCase());
+        // Manual Additions:
+        // NOTE: "sorceries" is also included as it can be found in exported deck, even if it's incorrect.
+        // Example: https://deckstats.net/decks/70852/556925-artifacts/en - see Issue 1010
+        cardTypesList.add("sorceries");  // Sorcery is the only name with different plural form
+        cardTypesList.add("aura");  // in case.
+        cardTypesList.add("mana");  // "Mana" (see Issue 1010)
+        cardTypesList.add("spell");
+        cardTypesList.add("other spell");
+        cardTypesList.add("planeswalker");
+        return cardTypesList.toArray(new CharSequence[cardTypesList.size()]);
+    }
+
     private final CardDb db;
     private final CardDb altDb;
     private Date releaseDateConstraint = null;
@@ -244,32 +254,33 @@ public class DeckRecognizer {
             return null;
 
         final char smartQuote = (char) 8217;
-        String line = rawLine.trim().replace(smartQuote, '\'');
+        String refLine = rawLine.trim().replace(smartQuote, '\'');
         // Remove any link (e.g. Markdown Export format from TappedOut)
-        line = purgeAllLinks(line);
+        refLine = purgeAllLinks(refLine);
 
-        if (StringUtils.startsWith(line, LINE_COMMENT_DELIMITER_OR_MD_HEADER))
-            line = line.replaceAll(LINE_COMMENT_DELIMITER_OR_MD_HEADER, "");
+        String line;
+        if (StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER))
+            line = refLine.replaceAll(LINE_COMMENT_DELIMITER_OR_MD_HEADER, "");
+        else
+            line = refLine.trim();  // Remove any trailing formatting
 
         // Some websites export split card names with a single slash. Replace with double slash.
         line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // ");
-        line = line.trim();  // Remove any trailing formattings
-        if (StringUtils.startsWith(line, DOUBLE_SLASH))
-            line = line.substring(2);  // In this case, we are sure to support split cards
-
-        if (StringUtils.startsWith(line, ASTERISK))
+        if (StringUtils.startsWith(line, ASTERISK))  // markdown lists (tappedout md export)
             line = line.substring(2);
 
         // In some format, cards in the Sideboard have an SB: prefix.
         // We won't support that as it is, due to how the recognition process works, so a section must be
         // specified (e.g. Sideboard or Side) to allow that those cards will be imported in sideboard.
         if (StringUtils.startsWith(line.trim(), "SB:"))
-            line = StringUtils.replace(line, "SB:", "").trim();
+            //refLine = StringUtils.replace(refLine, "SB:", "").trim();
+            return new Token(TokenType.COMMENT, 0, line);
 
         Token result = recogniseCardToken(line);
         if (result == null)
             result = recogniseNonCardToken(line);
-        return result != null ? result : new Token(TokenType.UNKNOWN_TEXT, 0, line);
+        return result != null ? result : StringUtils.startsWith(refLine, DOUBLE_SLASH) || StringUtils.startsWith(refLine, LINE_COMMENT_DELIMITER_OR_MD_HEADER) ?
+                new Token(TokenType.COMMENT, 0, refLine) : new Token(TokenType.UNKNOWN_TEXT, 0, refLine);
     }
 
     public static String purgeAllLinks(String line){
@@ -288,9 +299,6 @@ public class DeckRecognizer {
     public Token recogniseCardToken(final String text) {
         String line = text.trim();
         Token uknonwnCardToken = null;
-
-        // TODO: recognize format: http://topdeck.ru/forum/index.php?showtopic=12711
-        // @leriomaggio: DONE!
         List<Matcher> cardMatchers = getRegExMatchers(line);
         for (Matcher matcher : cardMatchers) {
             String cardName = getRexGroup(matcher, REGRP_CARD);
@@ -313,7 +321,7 @@ public class DeckRecognizer {
             int artIndex;
             try {
                 artIndex = Integer.parseInt(collectorNumber);
-            } catch (NumberFormatException ex){
+            } catch (NumberFormatException ex) {
                 artIndex = IPaperCard.NO_ART_INDEX;
             }
 
@@ -344,19 +352,31 @@ public class DeckRecognizer {
             }
             // ok so we can simply ignore everything but card name - as set code does not exist
             // At this stage, we know the card name exists in the DB so a Card MUST be found
-            // unless it is illegal for current format.
-            // In that case, an illegalCard token will be returned!
-            PaperCard pc = this.getCardFromSupportedEditions(cr.cardName, cr.isFoil);
-            if (pc != null){
-                if (isIllegalCardInDeckFormat(pc))
+            // unless it is illegal for current format or invalid with selected date.
+            PaperCard pc = null;
+            if (hasGameFormatConstraints()) {
+                Predicate<PaperCard> filter = (Predicate<PaperCard>) this.db.isLegal(this.allowedSetCodes);
+                pc = this.getCardFromSupportedEditions(cr.cardName, cr.isFoil, filter);
+            }
+            if (pc == null)
+                pc = this.getCardFromSupportedEditions(cr.cardName, cr.isFoil, null);
+
+            if (pc != null) {
+                if (isIllegalSetInGameFormat(pc.getEdition()) || isIllegalCardInDeckFormat(pc))
                     return Token.IllegalCard(pc.getName(), pc.getEdition(), cardCount);
+                CardEdition edition = StaticData.instance().getCardEdition(pc.getEdition());
+                if (isNotCompliantWithReleaseDateRestrictions(edition))
+                    return Token.InvalidCard(pc.getName(), pc.getEdition(), cardCount);
                 return Token.KnownCard(pc, cardCount);
             }
-            return Token.IllegalCard(cardName, "", cardCount);
         }
         return uknonwnCardToken;  // either null or unknown card
     }
 
+    private boolean hasGameFormatConstraints() {
+        return this.allowedSetCodes != null && this.allowedSetCodes.size() > 0;
+    }
+
     private String getRexGroup(Matcher matcher, String groupName){
         String rexGroup;
         try{
@@ -368,7 +388,7 @@ public class DeckRecognizer {
     }
 
     private boolean isIllegalCardInDeckFormat(PaperCard pc) {
-        return this.deckFormat != null && !deckFormat.isLegalCard(pc);
+        return this.deckFormat != null && !this.deckFormat.isLegalCard(pc);
     }
 
     private boolean isIllegalSetInGameFormat(String setCode) {
@@ -438,79 +458,119 @@ public class DeckRecognizer {
         return result;
     }
 
-    private PaperCard getCardFromSupportedEditions(final String cardName, boolean isFoil){
-        Predicate<PaperCard> filter = null;
-        if (this.allowedSetCodes != null && this.allowedSetCodes.size() > 0)
-            filter = (Predicate<PaperCard>) this.db.isLegal(this.allowedSetCodes);
+    private PaperCard getCardFromSupportedEditions(final String cardName, boolean isFoil,
+                                                   Predicate<PaperCard> filter){
         String reqInfo = CardDb.CardRequest.compose(cardName, isFoil);
+        CardDb targetDb = this.db.contains(cardName) ? this.db : this.altDb;
         PaperCard result;
-        if (this.releaseDateConstraint != null){
-            result = this.db.getCardFromEditionsReleasedBefore(reqInfo,
+        if (this.releaseDateConstraint != null) {
+            result = targetDb.getCardFromEditionsReleasedBefore(reqInfo,
                     this.releaseDateConstraint, filter);
             if (result == null)
-                result = this.altDb.getCardFromEditionsReleasedBefore(reqInfo,
-                        this.releaseDateConstraint, filter);
-        }
-        else {
-            result = this.db.getCardFromEditions(reqInfo, filter);
-            if (result == null)
-                result = this.altDb.getCardFromEditions(reqInfo, filter);
+                result = targetDb.getCardFromEditions(reqInfo, filter);
         }
+        else
+            result = targetDb.getCardFromEditions(reqInfo, filter);
         return result;
     }
 
     public Token recogniseNonCardToken(final String text) {
         if (isDeckSectionName(text)) {
-            String tokenText = getNonCardTokenText(text.toLowerCase().trim());
+            String tokenText = nonCardTokenMatch(text);
             return Token.DeckSection(tokenText);
         }
-        if (isCardType(text)) {
-            String tokenText = getNonCardTokenText(text);
+        if (isCardRarity(text)){
+            String tokenText = cardRarityTokenMatch(text);
+            return new Token(TokenType.CARD_RARITY, tokenText);
+        }
+        if (isCardCMC(text)){
+            String tokenText = cardCMCTokenMatch(text);
+            return new Token(TokenType.CARD_CMC, tokenText);
+        }
+        if (isCardType(text)){
+            String tokenText = nonCardTokenMatch(text);
             return new Token(TokenType.CARD_TYPE, tokenText);
         }
+        if(isManaToken(text)){
+            String tokenText = manaTokenMatch(text);
+            return new Token(TokenType.MANA_COLOUR, tokenText);
+        }
         if (isDeckName(text)) {
-            String deckName = getDeckName(text);
+            String deckName = deckNameMatch(text);
             return new Token(TokenType.DECK_NAME, deckName);
         }
         return null;
     }
 
-    private static String getNonCardTokenText(final String line){
-        Matcher noncardMatcher = NONCARD_PATTERN.matcher(line);
-        if (!noncardMatcher.matches())
-            return "";
-        return noncardMatcher.group(REGRP_NOCARD);
-    }
-
-    private static CharSequence[] allCardTypes(){
-        List<String> cardTypesList = new ArrayList<>();
-        // CoreTypesNames
-        List<CardType.CoreType> coreTypes = Lists.newArrayList(CardType.CoreType.values());
-        for (CardType.CoreType coreType : coreTypes)
-            cardTypesList.add(coreType.name().toLowerCase());
-        // Manual Additions:
-        // NOTE: "sorceries" is also included as it can be found in exported deck, even if it's incorrect.
-        // Example: https://deckstats.net/decks/70852/556925-artifacts/en - see Issue 1010
-        cardTypesList.add("sorceries");  // Sorcery is the only name with different plural form
-        cardTypesList.add("aura");  // in case.
-        cardTypesList.add("mana");  // "Mana" (see Issue 1010)
-        cardTypesList.add("spell");
-        cardTypesList.add("other spell");
-        cardTypesList.add("planeswalker");
-        return cardTypesList.toArray(new CharSequence[cardTypesList.size()]);
-    }
-
-    // NOTE: Card types recognition is ONLY used for style formatting in the Import Editor
-    // This won't affect the import process of cards in any way !-)
+    /* -----------------------------------------------------------------------------
+    Note: Card types, CMC, and Rarity Tokens are **only** used for style formatting
+    in the Import Editor. This won't affect the import process in any way.
+    The use of this token has been borrowed by Deckstats.net format export.
+    ----------------------------------------------------------------------------- */
     public static boolean isCardType(final String lineAsIs) {
-        if (lineAsIs == null)
+        String nonCardToken = nonCardTokenMatch(lineAsIs);
+        if (nonCardToken == null)
             return false;
-        String line = lineAsIs.toLowerCase().trim();
+        return StringUtils.containsAny(nonCardToken.toLowerCase(), CARD_TYPES);
+    }
+
+    public static boolean isCardRarity(final String lineAsIs){
+        return cardRarityTokenMatch(lineAsIs) != null;
+    }
+
+    public static boolean isCardCMC(final String lineAsIs) {
+        return cardCMCTokenMatch(lineAsIs) != null;
+    }
+
+    public static boolean isManaToken(final String lineAsIs) {
+        return manaTokenMatch(lineAsIs) != null;
+    }
+
+    public static boolean isDeckSectionName(final String lineAsIs) {
+        String nonCardToken = nonCardTokenMatch(lineAsIs);
+        if (nonCardToken == null)
+            return false;
+        return StringUtils.equalsAnyIgnoreCase(nonCardToken, DECK_SECTION_NAMES);
+    }
+
+    private static String nonCardTokenMatch(final String lineAsIs){
+        if (lineAsIs == null)
+            return null;
+        String line = lineAsIs.trim();
         Matcher noncardMatcher = NONCARD_PATTERN.matcher(line);
         if (!noncardMatcher.matches())
-            return false;
-        String nonCardToken = noncardMatcher.group(REGRP_NOCARD);
-        return StringUtils.containsAny(nonCardToken, CARD_TYPES);
+            return null;
+        return noncardMatcher.group(REGRP_TOKEN);
+    }
+
+    private static String cardRarityTokenMatch(final String lineAsIs){
+        if (lineAsIs == null)
+            return null;
+        String line = lineAsIs.trim();
+        Matcher cardRarityMatcher = CARD_RARITY_PATTERN.matcher(line);
+        if (!cardRarityMatcher.matches())
+            return null;
+        return cardRarityMatcher.group(REGRP_TOKEN);
+    }
+
+    private static String cardCMCTokenMatch(final String lineAsIs){
+        if (lineAsIs == null)
+            return null;
+        String line = lineAsIs.trim();
+        Matcher cardCMCmatcher = CMC_PATTERN.matcher(line);
+        if (!cardCMCmatcher.matches())
+            return null;
+        return cardCMCmatcher.group(REGRP_TOKEN);
+    }
+
+    private static String manaTokenMatch(final String lineAsIs){
+        if (lineAsIs == null)
+            return null;
+        String line = lineAsIs.trim();
+        Matcher manaMatcher = MANA_PATTERN.matcher(line);
+        if (!manaMatcher.matches())
+            return null;
+        return manaMatcher.group(REGRP_TOKEN);
     }
 
     public static boolean isDeckName(final String lineAsIs) {
@@ -518,11 +578,10 @@ public class DeckRecognizer {
             return false;
         final String line = lineAsIs.trim();
         final Matcher deckNameMatcher = DECK_NAME_PATTERN.matcher(line);
-        boolean matches = deckNameMatcher.matches();
-        return matches;
+        return deckNameMatcher.matches();
     }
 
-    public static String getDeckName(final String text) {
+    public static String deckNameMatch(final String text) {
         if (text == null)
             return "";
         String line = text.trim();
@@ -532,17 +591,6 @@ public class DeckRecognizer {
         return "";
     }
 
-    public static boolean isDeckSectionName(final String text) {
-        if (text == null)
-            return false;
-        String line = text.toLowerCase().trim();
-        Matcher noncardMatcher = NONCARD_PATTERN.matcher(line);
-        if (!noncardMatcher.matches())
-            return false;
-        String nonCardToken = noncardMatcher.group(REGRP_NOCARD);
-        return StringUtils.equalsAnyIgnoreCase(nonCardToken, DECK_SECTION_NAMES);
-    }
-
     public void setDateConstraint(int year, int month) {
         Calendar ca = Calendar.getInstance();
         ca.set(year, month, 1);
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 820f6954b5a..adc6a8a0177 100644
--- a/forge-gui-desktop/src/test/java/forge/deck/DeckRecognizerTest.java
+++ b/forge-gui-desktop/src/test/java/forge/deck/DeckRecognizerTest.java
@@ -130,35 +130,35 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertTrue(deckNameMatcher.matches());
         assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
         assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "Red Green Aggro");
 
         matchingDeckName = "Name: Red Green Aggro";
         deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
         assertTrue(deckNameMatcher.matches());
         assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
         assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "Red Green Aggro");
 
         matchingDeckName = "Name:Red Green Aggro";
         deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
         assertTrue(deckNameMatcher.matches());
         assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
         assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "Red Green Aggro");
 
         matchingDeckName = "Name:       Red Green Aggro";
         deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
         assertTrue(deckNameMatcher.matches());
         assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
         assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "Red Green Aggro");
 
         matchingDeckName = "Deck:Red Green Aggro";
         deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
         assertTrue(deckNameMatcher.matches());
         assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
         assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "Red Green Aggro");
 
         // Case Insensitive
         matchingDeckName = "deck: Red Green Aggro";
@@ -166,7 +166,7 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertTrue(deckNameMatcher.matches());
         assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
         assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Red Green Aggro");
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Red Green Aggro");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "Red Green Aggro");
 
         // Forge deck format
         matchingDeckName = "Name=Sliver Overlord (Commander)";
@@ -174,38 +174,38 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertTrue(deckNameMatcher.matches());
         assertTrue(DeckRecognizer.isDeckName(matchingDeckName));
         assertEquals(deckNameMatcher.group(DeckRecognizer.REGRP_DECKNAME), "Sliver Overlord (Commander)");
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "Sliver Overlord (Commander)");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "Sliver Overlord (Commander)");
 
         // Failing Cases
         matchingDeckName = ":Red Green Aggro";
         deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
         assertFalse(deckNameMatcher.matches());
         assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "");
 
         matchingDeckName = "Red Green Aggro";
         deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
         assertFalse(deckNameMatcher.matches());
         assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "");
 
         matchingDeckName = "Name-Red Green Aggro";
         deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
         assertFalse(deckNameMatcher.matches());
         assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "");
 
         matchingDeckName = "Deck.Red Green Aggro";
         deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
         assertFalse(deckNameMatcher.matches());
         assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "");
 
         matchingDeckName = ":Red Green Aggro";
         deckNameMatcher = deckNamePattern.matcher(matchingDeckName);
         assertFalse(deckNameMatcher.matches());
         assertFalse(DeckRecognizer.isDeckName(matchingDeckName));
-        assertEquals(DeckRecognizer.getDeckName(matchingDeckName), "");
+        assertEquals(DeckRecognizer.deckNameMatch(matchingDeckName), "");
     }
 
     @Test void testMatchDeckSectionNames(){
@@ -249,6 +249,43 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
             assertTrue(DeckRecognizer.isCardType(entry), "Fail on " + entry);
     }
 
+    @Test void testOnlyContainingCardTypeWontMatchCardTypeToken(){
+        String[] nonCardTypes = new String[] {"Spell collection", "instants list",
+                "creatures elves", "land list"};
+        for (String nonCardTypeTokens : nonCardTypes)
+            assertFalse(DeckRecognizer.isCardType(nonCardTypeTokens), "Fail on "+nonCardTypeTokens);
+    }
+
+    @Test void testRarityTypeTokenMatch(){
+        String[] rarityTokens = new String[] {"Common", "uncommon", "rare", "mythic", "mythic rare", "land"};
+        for (String line : rarityTokens)
+            assertTrue(DeckRecognizer.isCardRarity(line), "Fail on "+line);
+
+        String[] nonRarityTokens = new String[] {"Common cards", "uncommon cards", "mythics", "rares", "lands"};
+        for (String line : nonRarityTokens)
+            assertFalse(DeckRecognizer.isCardRarity(line), "Fail on "+line);
+    }
+
+    @Test void testCMCTokenMatch(){
+        String[] cmcTokens = new String[] {"CC0", "CMC2", "CMC11", "cc3"};
+        for (String line : cmcTokens)
+            assertTrue(DeckRecognizer.isCardCMC(line), "Fail on "+line);
+
+        String[] nonCMCtokens = new String[] {"cc", "CMC", "cc322", "cmc111"};
+        for (String line : nonCMCtokens)
+            assertFalse(DeckRecognizer.isCardCMC(line), "Fail on "+line);
+    }
+
+    @Test void testManaTokenMatch(){
+        String[] cmcTokens = new String[] {"Blue", "red", "White", "// Black", "       //Colorless----", "(green)"};
+        for (String line : cmcTokens)
+            assertTrue(DeckRecognizer.isManaToken(line), "Fail on " + line);
+
+        String[] nonCMCtokens = new String[] {"blues", "red more words", "mainboard"};
+        for (String line : nonCMCtokens)
+            assertFalse(DeckRecognizer.isManaToken(line), "Fail on "+line);
+    }
+
     /*=============================
     * TEST RECOGNISE NON-CARD LINES
     * =============================
@@ -266,6 +303,13 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(t.getText(), "Lands");
         assertEquals(t.getNumber(), 0);
 
+        // Test Token Types
+        t = recognizer.recogniseNonCardToken("//Land");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.CARD_RARITY);
+        assertEquals(t.getText(), "Land");
+        assertEquals(t.getNumber(), 0);
+
         t = recognizer.recogniseNonCardToken("[Main]");
         assertNotNull(t);
         assertEquals(t.getType(), TokenType.DECK_SECTION_NAME);
@@ -301,6 +345,42 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(t.getType(), TokenType.DECK_NAME);
         assertEquals(t.getText(), "OLDSCHOOL 93-94 Red Green Aggro by Zombies with JetPack");
         assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("CMC0");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.CARD_CMC);
+        assertEquals(t.getText(), "CMC0");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("CC1");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.CARD_CMC);
+        assertEquals(t.getText(), "CC1");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("//Common");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.CARD_RARITY);
+        assertEquals(t.getText(), "Common");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("(mythic rare)");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.CARD_RARITY);
+        assertEquals(t.getText(), "mythic rare");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("//Blue");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.MANA_COLOUR);
+        assertEquals(t.getText(), "Blue");
+        assertEquals(t.getNumber(), 0);
+
+        t = recognizer.recogniseNonCardToken("(Colorless)");
+        assertNotNull(t);
+        assertEquals(t.getType(), TokenType.MANA_COLOUR);
+        assertEquals(t.getText(), "Colorless");
+        assertEquals(t.getNumber(), 0);
     }
 
     /*=============================
@@ -696,7 +776,6 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
     }
 
     @Test void testInvalidMatchFullSetCardRequest(){
-        System.out.println(DeckRecognizer.REX_FULL_REQUEST_SET_CARD);
         // NOTE: this will be matcher by another pattern
         String invalidRequest = "1 Power Sink TMP";  // missing collector number
         Matcher matcher = DeckRecognizer.SET_CARD_COLLNO_PATTERN.matcher(invalidRequest);
@@ -1242,6 +1321,274 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertNull(cardToken.getCard());
     }
 
+    @Test void testCardMatchWithDateANDGameFormatConstraints(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        // Baseline - no constraints
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+        String lineRequest = "2x Lightning Dragon";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getNumber(), 2);
+        PaperCard tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Lightning Dragon");
+        assertEquals(tc.getEdition(), "VMA");
+
+        recognizer.setDateConstraint(2000, 0);  // Jan 2000
+        // Setting Fantasy Constructed Game Format: Urza's Block Format (no promo)
+        List<String> allowedSets = Arrays.asList("USG", "ULG", "UDS");
+        recognizer.setGameFormatConstraint(allowedSets);
+
+        lineRequest = "2x Lightning Dragon|USG";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getNumber(), 2);
+        tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Lightning Dragon");
+        assertEquals(tc.getEdition(), "USG");
+
+        // Relaxing Constraint on Set
+        lineRequest = "2x Lightning Dragon";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNotNull(cardToken.getCard());
+        tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Lightning Dragon");
+        assertEquals(tc.getEdition(), "USG");  // the latest available within set requested
+
+        // Now setting a tighter date constraint
+        recognizer.setDateConstraint(1998, 0);  // Jan 1998
+        lineRequest = "2x Lightning Dragon|USG";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.INVALID_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNull(cardToken.getCard());
+
+        lineRequest = "2x Lightning Dragon";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.INVALID_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Lightning Dragon (USG)");
+
+        // Now relaxing date constraint but removing USG from allowed sets
+        // VMA release date: 2014-06-16
+        recognizer.setDateConstraint(2015, 0);  // This will match VMA
+        recognizer.setGameFormatConstraint(Arrays.asList("ULG", "UDS"));
+
+        lineRequest = "2x Lightning Dragon|USG";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNull(cardToken.getCard());
+
+        lineRequest = "2x Lightning Dragon";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNull(cardToken.getCard());
+
+        // Now relaxing date constraint but removing USG from allowed sets
+        // VMA release date: 2014-06-16
+        recognizer.setDateConstraint(2015, 0);  // This will match VMA
+        recognizer.setGameFormatConstraint(Arrays.asList("VMA", "ULG", "UDS"));
+        lineRequest = "2x Lightning Dragon";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNotNull(cardToken.getCard());
+        tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Lightning Dragon");
+        assertEquals(tc.getEdition(), "VMA");
+    }
+
+    @Test void testCardMatchWithDateANDdeckFormatConstraints(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        // Baseline - no constraints
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+
+        String lineRequest = "Flash";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getNumber(), 1);
+        PaperCard tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Flash");
+        assertEquals(tc.getEdition(), "A25");
+
+        recognizer.setDateConstraint(2012, 0);  // Jan 2012
+        recognizer.setDeckFormatConstraint(DeckFormat.TinyLeaders);
+
+        lineRequest = "Flash";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Flash (6ED)");
+
+        lineRequest = "2x Cancel";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNotNull(cardToken.getCard());
+        tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Cancel");
+        assertEquals(tc.getEdition(), "M12");  // the latest within date constraint
+
+        lineRequest = "2x Cancel|M21";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.INVALID_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Cancel (M21)");
+    }
+
+    @Test void testCardMatchWithGameANDdeckFormatConstraints(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        // Baseline - no constraints
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+
+        String lineRequest = "Flash";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getNumber(), 1);
+        PaperCard tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Flash");
+        assertEquals(tc.getEdition(), "A25");
+
+        recognizer.setGameFormatConstraint(Arrays.asList("MIR", "VIS", "WTH"));
+        recognizer.setDeckFormatConstraint(DeckFormat.TinyLeaders);
+
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Flash (MIR)");
+
+        lineRequest = "2x Femeref Knight";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNotNull(cardToken.getCard());
+        tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Femeref Knight");
+        assertEquals(tc.getEdition(), "MIR");
+
+        lineRequest = "2x Incinerate";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertEquals(cardToken.getNumber(), 2);
+        assertNotNull(cardToken.getCard());
+        tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Incinerate");
+        assertEquals(tc.getEdition(), "MIR");
+
+        lineRequest = "Noble Elephant";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);  // violating Deck format
+        assertEquals(cardToken.getNumber(), 1);
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Noble Elephant (MIR)");
+
+        lineRequest = "Incinerate|ICE";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);  // violating Game format
+        assertEquals(cardToken.getNumber(), 1);
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Incinerate (ICE)");
+    }
+
+    @Test void testCardMatchWitDateANDgameANDdeckFormatConstraints(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        // Baseline - no constraints
+        assertEquals(db.getCardArtPreference(), CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS);
+
+        String lineRequest = "Flash";
+        Token cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getNumber(), 1);
+        PaperCard tc = cardToken.getCard();
+        assertEquals(tc.getName(), "Flash");
+        assertEquals(tc.getEdition(), "A25");
+
+        recognizer.setGameFormatConstraint(Arrays.asList("MIR", "VIS", "WTH"));
+        recognizer.setDeckFormatConstraint(DeckFormat.TinyLeaders);
+        recognizer.setDateConstraint(1999, 2);  // March '99
+
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Flash (MIR)");
+
+        lineRequest = "Ardent Militia";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.ILLEGAL_CARD_REQUEST);  // illegal in deck format
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Ardent Militia (WTH)");  // within set constraints
+
+        lineRequest = "Buried Alive|UMA";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.INVALID_CARD_REQUEST);  // illegal in game format
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Buried Alive (UMA)");  // within set constraints
+
+        lineRequest = "Buried Alive";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.LEGAL_CARD_REQUEST);  // illegal in deck format
+        assertNotNull(cardToken.getCard());
+        assertEquals(cardToken.getCard().getName(), "Buried Alive");
+        assertEquals(cardToken.getCard().getEdition(), "WTH");  // within set constraints
+
+        recognizer.setDateConstraint(1997, 2);  // March '97 - before WTH
+        lineRequest = "Buried Alive";
+        cardToken = recognizer.recogniseCardToken(lineRequest);
+        assertNotNull(cardToken);
+        assertEquals(cardToken.getType(), TokenType.INVALID_CARD_REQUEST);
+        assertNull(cardToken.getCard());
+        assertEquals(cardToken.getText(), "Buried Alive (WTH)");
+    }
+
     /*==================================
      * TEST RECOGNISE CARD EXTRA FORMATS
      * =================================
@@ -1413,4 +1760,90 @@ public class DeckRecognizerTest extends ForgeCardMockTestCase {
         assertEquals(acCard.getEdition(), "LRW");
         assertEquals(acCard.getCollectorNumber(), "51");
     }
+
+    /*====================================
+     * TEST RECOGNISE LINES (MIXED inputs)
+     * ===================================
+     */
+    @Test void testRecognizeLines(){
+        StaticData magicDb = FModel.getMagicDb();
+        CardDb db = magicDb.getCommonCards();
+        CardDb altDb = magicDb.getVariantCards();
+        DeckRecognizer recognizer = new DeckRecognizer(db, altDb);
+
+        String lineRequest = "// MainBoard";
+        Token token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.DECK_SECTION_NAME);
+        assertEquals(token.getText(), "Main");
+
+        lineRequest = "## Sideboard (15)";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.DECK_SECTION_NAME);
+        assertEquals(token.getText(), "Sideboard");
+
+        lineRequest = "Normal Text";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.UNKNOWN_TEXT);
+        assertEquals(token.getText(), "Normal Text");
+
+        lineRequest = "//Creatures";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.CARD_TYPE);
+        assertEquals(token.getText(), "Creatures");
+
+        lineRequest = "//Lands";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.CARD_TYPE);
+        assertEquals(token.getText(), "Lands");
+
+        lineRequest = "//Land";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.CARD_RARITY);
+        assertEquals(token.getText(), "Land");
+
+        lineRequest = "//Creatures with text";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.COMMENT);
+        assertEquals(token.getText(), "//Creatures with text");
+
+        lineRequest = "SB:Ancestral Recall";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.COMMENT);
+        assertEquals(token.getText(), "SB:Ancestral Recall");
+
+        lineRequest = "Ancestral Recall";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNull(token.getText());
+        assertNotNull(token.getCard());
+
+        lineRequest = "* 4 [Counterspell](http://tappedout.nethttp://tappedout.net/mtg-card/counterspell/)";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.LEGAL_CARD_REQUEST);
+        assertNull(token.getText());
+        assertNotNull(token.getCard());
+        assertEquals(token.getNumber(), 4);
+
+        lineRequest = "### Instant (14)";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.CARD_TYPE);
+        assertEquals(token.getText(), "Instant");
+
+        lineRequest = "### General line as comment";
+        token = recognizer.recognizeLine(lineRequest);
+        assertNotNull(token);
+        assertEquals(token.getType(), TokenType.COMMENT);
+        assertEquals(token.getText(), "### General line as comment");
+    }
 }