aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/eu/equalparts/cardbase/Cardbase.java197
-rw-r--r--src/eu/equalparts/cardbase/cards/Card.java3
-rw-r--r--src/eu/equalparts/cardbase/cardstorage/ReferenceCardContainer.java50
-rw-r--r--src/eu/equalparts/cardbase/cardstorage/StandaloneCardContainer.java73
-rw-r--r--src/eu/equalparts/cardbase/cli/CardbaseCLI.java2
-rw-r--r--src/eu/equalparts/cardbase/utils/JSON.java6
-rw-r--r--test/eu/equalparts/cardbase/CardbaseTest.java6
-rw-r--r--test/eu/equalparts/cardbase/cardstorage/ReferenceCardContainerTest.java131
-rw-r--r--test/eu/equalparts/cardbase/cardstorage/StandaloneCardContainerSortTest.java (renamed from test/eu/equalparts/cardbase/CardbaseSortTest.java)11
-rw-r--r--test/eu/equalparts/cardbase/cardstorage/StandaloneCardContainerTest.java170
-rw-r--r--test/eu/equalparts/cardbase/decks/DeckTest.java53
-rw-r--r--test/eu/equalparts/cardbase/decks/StatisticsTest.java148
-rw-r--r--test/eu/equalparts/cardbase/decks/deck.cbd1
-rw-r--r--test/testbase.cb2
14 files changed, 446 insertions, 407 deletions
diff --git a/src/eu/equalparts/cardbase/Cardbase.java b/src/eu/equalparts/cardbase/Cardbase.java
index be66127..d17ab2a 100644
--- a/src/eu/equalparts/cardbase/Cardbase.java
+++ b/src/eu/equalparts/cardbase/Cardbase.java
@@ -2,20 +2,14 @@ package eu.equalparts.cardbase;
import java.io.File;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
import java.util.Map;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
-import eu.equalparts.cardbase.cards.Card;
import eu.equalparts.cardbase.cardstorage.StandaloneCardContainer;
-import eu.equalparts.cardbase.comparator.CardComparator;
import eu.equalparts.cardbase.decks.ReferenceDeck;
import eu.equalparts.cardbase.utils.JSON;
@@ -26,31 +20,10 @@ import eu.equalparts.cardbase.utils.JSON;
*/
public class Cardbase extends StandaloneCardContainer {
-// private static class DataContainer {
-// /**
-// * The cards in the cardbase, set in key-value pairs where the key is the card hash,
-// * generated using {makeHash()}.
-// */
-// public Map<Integer, Card> cards;
-// /**
-// * TODO comment
-// */
-// public Map<Integer, Integer> collection;
-
-//
-// public DataContainer() {
-// cards = new HashMap<Integer, Card>();
-// collection = new HashMap<Integer, Integer>();
-// decks = new HashMap<String, ReferenceDeck>();
-// }
-// }
-
-// private DataContainer dataContainer;
-
/**
* The decks which have been saved along with this collection of cards.
*/
- public Map<String, ReferenceDeck> decks;
+ @JsonProperty private Map<String, ReferenceDeck> decks;
/**
* Initialises the cardbase with the contents of a file.
@@ -66,13 +39,6 @@ public class Cardbase extends StandaloneCardContainer {
}
/**
- * Initialises a clean cardbase.
- */
- public Cardbase() {
- super();
- }
-
- /**
* Writes the provided {@code Cardbase} to the provided file in JSON format.
*
* @param file the file to which to write the {@code Cardbase}.
@@ -82,164 +48,7 @@ public class Cardbase extends StandaloneCardContainer {
* @throws JsonMappingException if the data structure given does not generate valid JSON as well?
* @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs.
*/
- public void writeCollection(File outputFile) throws JsonGenerationException, JsonMappingException, IOException {
+ public void write(File outputFile) throws JsonGenerationException, JsonMappingException, IOException {
JSON.mapper.writeValue(outputFile, this);
}
-
- /**
- * Adds a specific amount of a card to the cardbase.
- * If the card is not already in the cardbase, it is added.
- * If it is already present, the count is simply updated.
- *
- * @param cardToAdd the card to be added. The count value
- * of this object is added to the existing count if the card
- * already exists.
- *TODO fix comment
- */
-// public void addCard(Card cardToAdd, int addCount) {
-// Integer hashCode = cardToAdd.hashCode();
-//
-// // ensure that card is in the card map
-// dataContainer.cards.putIfAbsent(hashCode, cardToAdd);
-//
-// // ensure that card is in the collection, with the correct count
-// Integer currentCount = dataContainer.collection.get(hashCode);
-// if (currentCount != null) {
-// dataContainer.collection.replace(hashCode, currentCount + addCount);
-// } else {
-// dataContainer.collection.put(hashCode, addCount);
-// }
-// }
-
- /**
- * Removes a specific amount of a card from the cardbase.
- * If the card is not present in the cardbase, nothing happens.
- * If the card is present in the cardbase, the specified amount is removed.
- * If that amount is equal to or exceeds the count already in the cardbase,
- * the card entry is removed altogether.
- * <br><br>
- * In any case, the value returned is the actual number of cards removed.
- * For example, if 5 Shivan Dragons are in the cardbase and the method is
- * called to remove 10 Shivan Dragons, the {@code Card} representing the
- * Shivan Dragon is removed from the cardbase, and the value returned is 5.
- *
- * @param cardToRemove the card to be removed.
- * @param removeCount the amount of the card to be removed.
- * @return the number of cards actually removed.
- *TODO comment
- */
-// public Integer removeCard(Card cardToRemove, int removeCount) {
-// Integer hashCode = cardToRemove.hashCode();
-// int removed = 0;
-//
-// Integer currentCount = dataContainer.collection.get(hashCode);
-// if (currentCount != null) {
-// if (removeCount >= currentCount) {
-// dataContainer.collection.remove(hashCode);
-// dataContainer.cards.remove(hashCode);
-// removed = currentCount;
-// } else {
-// dataContainer.collection.replace(hashCode, currentCount - removeCount);
-// removed = removeCount;
-// }
-// }
-// return removed;
-// }
-
- /**
- * Returns a card from the cardbase by set code and number.
- * If no such card is in the cardbase, returns null.
- *
- * @param setCode the set to which the requested card belongs.
- * @param number the requested card's set number.
- * @return the requested {@code Card} or null if no card is found.
- */
-// public Card getCard(String setCode, String number) {
-// return dataContainer.cards.get(Card.makeHash(setCode, number));
-// }
-
- /**
- * Returns a card from the cardbase by hash. The card's hash
- * can be generated using {@code Cardbase.makeHash()}.
- * If no such card is in the cardbase, returns null.
- *
- * @param hash the Cardbase hash of the requested card.
- * @return the requested {@code Card} or null if no card is found.
- */
-// protected Card getCardByHash(Integer hash) {
-// return dataContainer.cards.get(hash);
-// }
-
- /**
- * @param fieldName the name of the field by which to sort.
- * @return an unmodifiable collection representing the cardbase sorted in the required order.
- * @throws NoSuchFieldException if the field provided is invalid.
- */
- public Collection<Card> sortByField(String fieldName) throws NoSuchFieldException {
- List<Card> sortedCards = new ArrayList<Card>(getCards());
- sortedCards.sort(new CardComparator(Card.class.getDeclaredField(fieldName)));
- return Collections.unmodifiableCollection(sortedCards);
- }
-
-// public int getCount(Card card) {
-// Integer count = dataContainer.collection.get(Card.makeHash(card.setCode, card.number));
-// return count != null ? count : 0;
-// }
-
-// public List<Card> getMissingCards(StandaloneDeck deckToCheck) {
-// List<Card> missingCards = new ArrayList<Card>();
-// for (Card card : deckToCheck.cards) {
-// Integer hash = card.hashCode();
-// if (cards.containsKey(hash)) {
-// if (cards.get(hash).count < card.count) {
-// Card missingCard = card.clone();
-// missingCard.count = card.count - cards.get(hash).count;
-// missingCards.add(missingCard);
-// }
-// } else {
-// missingCards.add(card);
-// }
-// }
-// return missingCards;
-// }
-
-// public void addStandaloneDeck(StandaloneDeck deckToAdd) {
-// List<Card> missingCards = getMissingCards(deckToAdd);
-// if (missingCards.size() <= 0) {
-// decks.put(deckToAdd.name, new ReferenceDeck(deckToAdd));
-// } else {
-// throw new IllegalArgumentException("The cardbase is missing cards to add this deck.");
-// }
-// }
-
-// public StandaloneDeck exportDeck(String deckName) {
-// ReferenceDeck referenceDeck = decks.get(deckName);
-//
-// if (referenceDeck != null) {
-// StandaloneDeck standaloneDeck = new StandaloneDeck();
-//
-// standaloneDeck.name = referenceDeck.name;
-// standaloneDeck.plains = referenceDeck.plains;
-// standaloneDeck.islands = referenceDeck.islands;
-// standaloneDeck.swamps = referenceDeck.swamps;
-// standaloneDeck.mountains = referenceDeck.mountains;
-// standaloneDeck.forests = referenceDeck.forests;
-//
-// for (Integer cardHash : referenceDeck.cardReferences.keySet()) {
-// Card card = getCardByHash(cardHash);
-// if (card != null) {
-// // must clone otherwise the original count is affected too
-// card = card.clone();
-// card.count = referenceDeck.cardReferences.get(cardHash);
-// standaloneDeck.cards.add(card);
-// } else {
-// throw new IllegalArgumentException("Deck refers to card not in cardbase: " + cardHash);
-// }
-// }
-//
-// return standaloneDeck;
-// } else {
-// throw new IllegalArgumentException("The specified deck does not exist.");
-// }
-// }
}
diff --git a/src/eu/equalparts/cardbase/cards/Card.java b/src/eu/equalparts/cardbase/cards/Card.java
index 9b0975b..37cc13e 100644
--- a/src/eu/equalparts/cardbase/cards/Card.java
+++ b/src/eu/equalparts/cardbase/cards/Card.java
@@ -1,8 +1,11 @@
package eu.equalparts.cardbase.cards;
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+
import eu.equalparts.cardbase.comparator.SpecialFields.DirtyNumber;
import eu.equalparts.cardbase.comparator.SpecialFields.Rarity;
+@JsonAutoDetect
public class Card {
public String name;
diff --git a/src/eu/equalparts/cardbase/cardstorage/ReferenceCardContainer.java b/src/eu/equalparts/cardbase/cardstorage/ReferenceCardContainer.java
new file mode 100644
index 0000000..ccd5508
--- /dev/null
+++ b/src/eu/equalparts/cardbase/cardstorage/ReferenceCardContainer.java
@@ -0,0 +1,50 @@
+package eu.equalparts.cardbase.cardstorage;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import eu.equalparts.cardbase.cards.Card;
+
+public abstract class ReferenceCardContainer {
+
+ @JsonProperty private Map<Integer, Integer> cardReferences;
+
+ public ReferenceCardContainer() {
+ cardReferences = new HashMap<>();
+ }
+
+ public int getCount(Card cardToCount) {
+ int hashCode = cardToCount.hashCode();
+ return cardReferences.containsKey(hashCode) ? cardReferences.get(hashCode) : 0;
+ }
+
+ public void addCard(Card cardToAdd, int count) {
+ int hashCode = cardToAdd.hashCode();
+ if (cardReferences.containsKey(hashCode)) {
+ cardReferences.replace(hashCode, cardReferences.get(hashCode) + count);
+ } else {
+ cardReferences.put(hashCode, count);
+ }
+ }
+
+ public int removeCard(Card cardToRemove, int count) {
+ int hashCode = cardToRemove.hashCode();
+ int removed = 0;
+
+ if (cardReferences.containsKey(hashCode) && count > 0) {
+ int oldCount = cardReferences.get(hashCode);
+
+ if (oldCount > count) {
+ cardReferences.replace(hashCode, oldCount - count);
+ removed = count;
+ } else {
+ cardReferences.remove(hashCode);
+ removed = oldCount;
+ }
+ }
+
+ return removed;
+ }
+}
diff --git a/src/eu/equalparts/cardbase/cardstorage/StandaloneCardContainer.java b/src/eu/equalparts/cardbase/cardstorage/StandaloneCardContainer.java
new file mode 100644
index 0000000..52fc89e
--- /dev/null
+++ b/src/eu/equalparts/cardbase/cardstorage/StandaloneCardContainer.java
@@ -0,0 +1,73 @@
+package eu.equalparts.cardbase.cardstorage;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import eu.equalparts.cardbase.cards.Card;
+import eu.equalparts.cardbase.comparator.CardComparator;
+
+public abstract class StandaloneCardContainer extends ReferenceCardContainer {
+
+ @JsonProperty private Map<Integer, Card> cardData;
+
+ public StandaloneCardContainer() {
+ super();
+ cardData = new HashMap<>();
+ }
+
+ @Override
+ public void addCard(Card cardToAdd, int count) {
+ super.addCard(cardToAdd, count);
+ cardData.putIfAbsent(cardToAdd.hashCode(), cardToAdd);
+ }
+
+ @Override
+ public int removeCard(Card cardToRemove, int count) {
+ int removed = super.removeCard(cardToRemove, count);
+ if (getCount(cardToRemove) <= 0) {
+ cardData.remove(cardToRemove.hashCode());
+ }
+ return removed;
+ }
+
+ /**
+ * Returns a card from the cardbase by set code and number.
+ * If no such card is in the cardbase, returns null.
+ *
+ * @param setCode the set to which the requested card belongs.
+ * @param number the requested card's set number.
+ * @return the requested {@code Card} or null if no card is found.
+ */
+ public Card getCard(String setCode, String number) {
+ return cardData.get(Card.makeHash(setCode, number));
+ }
+
+ /**
+ * This method is intended to allow iteration directly on the list of cards,
+ * while at the same time retaining control over the insert and remove procedures.
+ * The returned {@code List} is a read-only; trying to modify its structure will
+ * result in an {@code UnsupportedOperationException}.
+ *
+ * @return an unmodifiable list of all the cards in the cardbase.
+ */
+ public Collection<Card> getCards() {
+ return Collections.unmodifiableCollection(cardData.values());
+ }
+
+ /**
+ * @param fieldName the name of the field by which to sort.
+ * @return an unmodifiable collection representing the cardbase sorted in the required order.
+ * @throws NoSuchFieldException if the field provided is invalid.
+ */
+ public Collection<Card> sortByField(String fieldName) throws NoSuchFieldException {
+ List<Card> sortedCards = new ArrayList<Card>(getCards());
+ sortedCards.sort(new CardComparator(Card.class.getDeclaredField(fieldName)));
+ return Collections.unmodifiableCollection(sortedCards);
+ }
+}
diff --git a/src/eu/equalparts/cardbase/cli/CardbaseCLI.java b/src/eu/equalparts/cardbase/cli/CardbaseCLI.java
index cbce4f9..7a932d0 100644
--- a/src/eu/equalparts/cardbase/cli/CardbaseCLI.java
+++ b/src/eu/equalparts/cardbase/cli/CardbaseCLI.java
@@ -256,7 +256,7 @@ public final class CardbaseCLI {
} else {
// handle these exceptions locally - they don't necessarily mean the program should exit
try {
- cardbase.writeCollection(outputFile);
+ cardbase.write(outputFile);
// we are now working off outputFile, which may or may not be the same as cardbaseFile at this point
cardbaseFile = outputFile;
System.out.println("Cardbase was saved to \"" + outputFile.getAbsolutePath() + "\". "
diff --git a/src/eu/equalparts/cardbase/utils/JSON.java b/src/eu/equalparts/cardbase/utils/JSON.java
index 17e8392..4a21311 100644
--- a/src/eu/equalparts/cardbase/utils/JSON.java
+++ b/src/eu/equalparts/cardbase/utils/JSON.java
@@ -1,6 +1,7 @@
package eu.equalparts.cardbase.utils;
import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
@@ -30,6 +31,11 @@ public final class JSON {
ObjectMapper objectMapper = new ObjectMapper();
// classes don't necessarily use all json fields
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ // disable auto detection
+ objectMapper.disable(MapperFeature.AUTO_DETECT_CREATORS,
+ MapperFeature.AUTO_DETECT_FIELDS,
+ MapperFeature.AUTO_DETECT_GETTERS,
+ MapperFeature.AUTO_DETECT_IS_GETTERS);
return objectMapper;
}
}
diff --git a/test/eu/equalparts/cardbase/CardbaseTest.java b/test/eu/equalparts/cardbase/CardbaseTest.java
index 1a54298..c9576ce 100644
--- a/test/eu/equalparts/cardbase/CardbaseTest.java
+++ b/test/eu/equalparts/cardbase/CardbaseTest.java
@@ -151,13 +151,13 @@ public class CardbaseTest {
final int testCount = 5;
File testFile = tempFolder.newFile("saveTest.cb");
- uut.writeCollection(testFile);
+ uut.write(testFile);
uut = Cardbase.load(testFile);
assertEquals("Cardbase should contain no cards.", 0, uut.getCards().size());
uut.addCard(testCard, testCount);
- uut.writeCollection(testFile);
+ uut.write(testFile);
uut = Cardbase.load(testFile);
assertEquals("Cardbase should contain 1 card.", 1, uut.getCards().size());
Card card = uut.getCard("M15", "281");
@@ -173,7 +173,7 @@ public class CardbaseTest {
File testFile = tempFolder.newFile("saveTest.cb");
testFile.setWritable(false);
exception.expect(IOException.class);
- uut.writeCollection(testFile);
+ uut.write(testFile);
}
@Test
diff --git a/test/eu/equalparts/cardbase/cardstorage/ReferenceCardContainerTest.java b/test/eu/equalparts/cardbase/cardstorage/ReferenceCardContainerTest.java
new file mode 100644
index 0000000..d233ddf
--- /dev/null
+++ b/test/eu/equalparts/cardbase/cardstorage/ReferenceCardContainerTest.java
@@ -0,0 +1,131 @@
+package eu.equalparts.cardbase.cardstorage;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import eu.equalparts.cardbase.cards.Card;
+
+/**
+ * This test class tests storage-by-reference behaviour only.
+ * For standalone storage behaviour tests, see {@code StandaloneCardContainerTest}.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public class ReferenceCardContainerTest {
+ private ReferenceCardContainer uut;
+ private static Card testCard;
+
+ @Rule
+ public final ExpectedException exception = ExpectedException.none();
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ testCard = mapper.readValue(ReferenceCardContainerTest.class.getResourceAsStream("/shivandragon.json"), Card.class);
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception {
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ uut = new ReferenceCardContainer() {
+ };
+ }
+
+ /***********************************************************************************
+ * Adding card tests, happy path
+ ***********************************************************************************/
+ @Test
+ public void newCardIsAdded() throws Exception {
+ uut.addCard(testCard, 1);
+
+ assertEquals("Container should have contained 1 test card.", 1, uut.getCount(testCard));
+ }
+
+ @Test
+ public void existingCardIsIncremented() throws Exception {
+ uut.addCard(testCard, 2);
+ uut.addCard(testCard, 4);
+
+ assertEquals("Container should have contained 6 test cards.", 6, uut.getCount(testCard));
+ }
+
+ /*
+ * Edge cases
+ */
+ @Test
+ public void cardAddedIsNull() throws Exception {
+ exception.expect(NullPointerException.class);
+ uut.addCard(null, 0);
+ }
+
+ /***********************************************************************************
+ * Removing card tests, happy path
+ ***********************************************************************************/
+ @Test
+ public void cardRemoveCountIsLessThanCardCount() throws Exception {
+ uut.addCard(testCard, 5);
+
+ int removed = uut.removeCard(testCard, 3);
+
+ assertEquals("Card count was not updated correctly.", 2, uut.getCount(testCard));
+ assertEquals("Container reports wrong removed count.", 3, removed);
+ }
+
+ @Test
+ public void cardRemoveCountIsEqualToCardCount() throws Exception {
+ uut.addCard(testCard, 5);
+
+ int removed = uut.removeCard(testCard, 5);
+
+ assertEquals("Card was not removed from container.", 0, uut.getCount(testCard));
+ assertEquals("Container reports wrong removed count.", 5, removed);
+ }
+
+ @Test
+ public void cardRemoveCountIsGreaterThanCardCount() throws Exception {
+ uut.addCard(testCard, 3);
+
+ int removed = uut.removeCard(testCard, 5);
+
+ assertEquals("Card was not removed from container.", 0, uut.getCount(testCard));
+ assertEquals("Container reports wrong removed count.", 3, removed);
+ }
+
+ /*
+ * Edge cases
+ */
+ @Test
+ public void removedCardIsNull() throws Exception {
+ exception.expect(NullPointerException.class);
+ uut.removeCard(null, 0);
+ }
+
+ @Test
+ public void removedCardIsNotInCardbase() throws Exception {
+ int removed = uut.removeCard(testCard, 1);
+
+ assertEquals("Removed count should be 0.", 0, removed);
+ }
+
+ @Test
+ public void removedCountIsLessThanZero() throws Exception {
+ uut.addCard(testCard, 3);
+
+ int removed = uut.removeCard(testCard, -4);
+
+ assertEquals("Card count in container should be unchanged.", 3, uut.getCount(testCard));
+ assertEquals("Container reports wrong removed count.", 0, removed);
+ }
+}
diff --git a/test/eu/equalparts/cardbase/CardbaseSortTest.java b/test/eu/equalparts/cardbase/cardstorage/StandaloneCardContainerSortTest.java
index b0dbd40..2a4a3ba 100644
--- a/test/eu/equalparts/cardbase/CardbaseSortTest.java
+++ b/test/eu/equalparts/cardbase/cardstorage/StandaloneCardContainerSortTest.java
@@ -1,4 +1,4 @@
-package eu.equalparts.cardbase;
+package eu.equalparts.cardbase.cardstorage;
import static org.junit.Assert.assertTrue;
@@ -13,7 +13,6 @@ import org.junit.Test;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
-import eu.equalparts.cardbase.Cardbase;
import eu.equalparts.cardbase.cards.Card;
/**
@@ -22,15 +21,15 @@ import eu.equalparts.cardbase.cards.Card;
* @author Eduardo Pedroni
*
*/
-public class CardbaseSortTest {
+public class StandaloneCardContainerSortTest {
- private Cardbase uut;
+ private StandaloneCardContainer uut;
private static List<Card> testCards;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
ObjectMapper mapper = new ObjectMapper();
- testCards = mapper.readValue(CardbaseSortTest.class.getResourceAsStream("/testcards.json"), new TypeReference<List<Card>>() {});
+ testCards = mapper.readValue(StandaloneCardContainerSortTest.class.getResourceAsStream("/testcards.json"), new TypeReference<List<Card>>() {});
}
@AfterClass
@@ -39,7 +38,7 @@ public class CardbaseSortTest {
@Before
public void setUp() throws Exception {
- uut = new Cardbase();
+ uut = new StandaloneCardContainer() {};
int[] cardCounts = {1, 2, 3, 8, 1, 15, 1, 1};
for (int i = 0; i < testCards.size(); i++) {
uut.addCard(testCards.get(i), cardCounts[i]);
diff --git a/test/eu/equalparts/cardbase/cardstorage/StandaloneCardContainerTest.java b/test/eu/equalparts/cardbase/cardstorage/StandaloneCardContainerTest.java
new file mode 100644
index 0000000..bd1bbfb
--- /dev/null
+++ b/test/eu/equalparts/cardbase/cardstorage/StandaloneCardContainerTest.java
@@ -0,0 +1,170 @@
+package eu.equalparts.cardbase.cardstorage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Field;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import eu.equalparts.cardbase.cards.Card;
+
+public class StandaloneCardContainerTest {
+ private StandaloneCardContainer uut;
+ private static Card testCard;
+
+ @Rule
+ public final ExpectedException exception = ExpectedException.none();
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ testCard = mapper.readValue(StandaloneCardContainerTest.class.getResourceAsStream("/shivandragon.json"), Card.class);
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception {
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ uut = new StandaloneCardContainer() {
+ };
+ }
+
+ /***********************************************************************************
+ * StandaloneCardContainer should extend ReferenceCardContainer
+ ***********************************************************************************/
+ @Test
+ public void classInherits() throws Exception {
+ assertTrue("StandaloneCardContainer should be subclass of ReferenceCardContainer.", uut instanceof ReferenceCardContainer);
+ }
+
+ /***********************************************************************************
+ * Adding card tests, happy path
+ ***********************************************************************************/
+ @Test
+ public void newCardIsAdded() throws Exception {
+ assertNull("Container should not contain the test card to begin with.", uut.getCard(testCard.setCode, testCard.number));
+
+ uut.addCard(testCard, 1);
+
+ assertEquals("Container should contain the test card once it is added.", testCard, uut.getCard(testCard.setCode, testCard.number));
+ }
+
+ @Test
+ public void existingCardIsIncremented() throws Exception {
+ uut.addCard(testCard, 2);
+ uut.addCard(testCard, 4);
+
+ Card addedCard = uut.getCard(testCard.setCode, testCard.number);
+ assertNotNull("Card was not found in cardbase.", addedCard);
+ assertEquals("Card count was not updated correctly.", 6, uut.getCount(addedCard));
+ }
+
+ /*
+ * Edge cases
+ */
+ @Test
+ public void cardAddedIsNull() throws Exception {
+ exception.expect(NullPointerException.class);
+ uut.addCard(null, 0);
+ }
+
+ /***********************************************************************************
+ * Removing card tests, happy path
+ ***********************************************************************************/
+ @Test
+ public void cardIsStillPresentIfRemoveCountIsLessThanCardCount() throws Exception {
+ uut.addCard(testCard, 5);
+
+ uut.removeCard(testCard, 3);
+
+ assertEquals("Card is missing from container.", testCard, uut.getCard(testCard.setCode, testCard.number));
+ }
+
+ @Test
+ public void cardIsRemovedIfRemoveCountIsEqualToCardCount() throws Exception {
+ uut.addCard(testCard, 5);
+
+ uut.removeCard(testCard, 5);
+
+ assertNull("Card is not missing from container.", uut.getCard(testCard.setCode, testCard.number));
+ }
+
+ @Test
+ public void cardIsRemovedIfRemoveCountIsGreaterThanCardCount() throws Exception {
+ uut.addCard(testCard, 3);
+
+ uut.removeCard(testCard, 5);
+
+ assertNull("Card is not missing from container.", uut.getCard(testCard.setCode, testCard.number));
+ }
+
+ /*
+ * Edge cases
+ */
+ @Test
+ public void removedCardIsNull() throws Exception {
+ exception.expect(NullPointerException.class);
+ uut.removeCard(null, 0);
+ }
+
+ @Test
+ public void removedCardIsNotInContainer() throws Exception {
+ assertNull("Card is not initially missing from container.", uut.getCard(testCard.setCode, testCard.number));
+
+ uut.removeCard(testCard, 1);
+
+ assertNull("Card is not missing from container.", uut.getCard(testCard.setCode, testCard.number));
+ }
+
+ @Test
+ public void removedCountIsLessThanZero() throws Exception {
+ uut.addCard(testCard, 3);
+
+ uut.removeCard(testCard, -4);
+
+ assertEquals("Card should not be missing from container.", testCard, uut.getCard(testCard.setCode, testCard.number));
+ }
+
+ /***********************************************************************************
+ * Card getter tests, happy path
+ ***********************************************************************************/
+ @Test
+ public void correctCardIsReturnedByGetter() throws Exception {
+ uut.addCard(testCard, 1);
+
+ Card card = uut.getCard(testCard.setCode, testCard.number);
+
+ for (Field field : Card.class.getFields()) {
+ assertEquals("Field " + field.getName(), field.get(testCard), field.get(card));
+ }
+ }
+
+ @Test
+ public void correctCardCollectionIsReturnedByGetter() throws Exception {
+ uut.addCard(testCard, 1);
+
+ assertTrue("Not all cards were returned by the getter.", uut.getCards().contains(testCard));
+ }
+
+ @Test
+ public void cardCollectionWhenContainerIsEmpty() throws Exception {
+ assertEquals("Returned collection size should have been 0.", 0, uut.getCards().size());
+ }
+
+ @Test
+ public void getCardIsNotInCardbase() throws Exception {
+ assertNull("Method should have returned null", uut.getCard(testCard.setCode, testCard.number));
+ }
+}
diff --git a/test/eu/equalparts/cardbase/decks/DeckTest.java b/test/eu/equalparts/cardbase/decks/DeckTest.java
deleted file mode 100644
index 1c00e5b..0000000
--- a/test/eu/equalparts/cardbase/decks/DeckTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package eu.equalparts.cardbase.decks;
-
-import static org.junit.Assert.*;
-
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import eu.equalparts.cardbase.cards.Card;
-import eu.equalparts.cardbase.decks.ReferenceDeck;
-import eu.equalparts.cardbase.decks.StandaloneDeck;
-
-public class DeckTest {
-
- @BeforeClass
- public static void setUpBeforeClass() throws Exception {
-
- }
-
- @AfterClass
- public static void tearDownAfterClass() throws Exception {
- }
-
- @Before
- public void setUp() throws Exception {
- }
-
- // TODO sort out
-// @Test
-// public void test_createReferenceDeckFromStandaloneDeck() throws Exception {
-// ObjectMapper mapper = new ObjectMapper();
-// StandaloneDeck standaloneDeck = mapper.readValue(getClass().getResourceAsStream("deck.cbd"), StandaloneDeck.class);
-//
-// ReferenceDeck uut = new ReferenceDeck(standaloneDeck);
-//
-// boolean condition = uut.name == standaloneDeck.name &&
-// uut.plains == standaloneDeck.plains &&
-// uut.islands == standaloneDeck.islands &&
-// uut.swamps == standaloneDeck.swamps &&
-// uut.mountains == standaloneDeck.mountains &&
-// uut.forests == standaloneDeck.forests;
-// assertTrue("Metadata was not correctly set.", condition);
-// assertEquals("Wrong number of cards.", uut.cardReferences.size(), standaloneDeck.cards.size());
-// for (Card card : standaloneDeck.cards) {
-// Integer count = uut.cardReferences.get(card.hashCode());
-// assertNotNull("Reference missing in deck.", count);
-// assertEquals("Card count is wrong.", card.count, count);
-// }
-// }
-}
diff --git a/test/eu/equalparts/cardbase/decks/StatisticsTest.java b/test/eu/equalparts/cardbase/decks/StatisticsTest.java
deleted file mode 100644
index 6210f89..0000000
--- a/test/eu/equalparts/cardbase/decks/StatisticsTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package eu.equalparts.cardbase.decks;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import eu.equalparts.cardbase.decks.StandaloneDeck;
-import eu.equalparts.cardbase.decks.Statistics;
-
-public class StatisticsTest {
-
- private static StandaloneDeck uut;
-
- @BeforeClass
- public static void setUpBeforeClass() throws Exception {
- ObjectMapper mapper = new ObjectMapper();
- uut = mapper.readValue(StatisticsTest.class.getResourceAsStream("deck.cbd"), StandaloneDeck.class);
- }
-
- @AfterClass
- public static void tearDownAfterClass() throws Exception {
- }
-
- @Before
- public void setUp() throws Exception {
- }
-
- @Test
- public void test_totalLandCountIsComputedCorrectly() throws Exception {
- int count = Statistics.count(uut, "Land");
-
- assertEquals(23, count);
- }
-
- @Test
- public void test_basicLandCountIsComputedCorrectly() throws Exception {
- int count = Statistics.count(uut, "Basic Land");
-
- assertEquals(20, count);
- }
-
- @Test
- public void test_cardCountIsComputedCorrectly() throws Exception {
- int count = Statistics.count(uut);
-
- assertEquals(60, count);
- }
-
- @Test
- public void test_landPercentageIsComputedCorrectly() throws Exception {
- double percentage = Statistics.calculatePercentage(uut, "Land");
-
- assertTrue("Land percentage should be " + (23.0 / 60.0) + ", is " + percentage, percentage == (23.0 / 60.0));
- }
-
- @Test
- public void test_creaturePercentageIsComputedCorrectly() throws Exception {
- double percentage = Statistics.calculatePercentage(uut, "Creature");
-
- assertTrue("Creature percentage should be " + (24.0 / 60.0) + ", is " + percentage, percentage == (24.0 / 60.0));
- }
-
- @Test
- public void test_creatureCountIsComputedCorrectly() throws Exception {
- int count = Statistics.count(uut, "Creature");
-
- assertEquals(24, count);
- }
-
- @Test
- public void test_sorceryCountIsComputedCorrectly() throws Exception {
- int count = Statistics.count(uut, "Sorcery");
-
- assertEquals(1, count);
- }
-
- @Test
- public void test_instantCountIsComputedCorrectly() throws Exception {
- int count = Statistics.count(uut, "Instant");
-
- assertEquals(6, count);
- }
-
- @Test
- public void test_planeswalkerCountIsComputedCorrectly() throws Exception {
- int count = Statistics.count(uut, "Planeswalker");
-
- assertEquals(0, count);
- }
-
- @Test
- public void test_elfCountIsComputedCorrectly() throws Exception {
- int count = Statistics.count(uut, "Elf");
-
- assertEquals(2, count);
- }
-
- @Test
- public void test_overallCostDistributionIsComputedCorrectly() throws Exception {
- int[] actualCosts = Statistics.computeDistribution(uut);
- int[] expectedCosts = {0, 8, 11, 3, 4, 7, 4};
-
- assertEquals("Array lengths do not match.", expectedCosts.length, actualCosts.length);
- for (int i = 0; i < expectedCosts.length; i++) {
- assertEquals("CMC: " + i, expectedCosts[i], actualCosts[i]);
- }
- }
-
- @Test
- public void test_creatureCostDistributionIsComputedCorrectly() throws Exception {
- int[] actualCosts = Statistics.computeDistribution(uut, "Creature");
- int[] expectedCosts = {0, 3, 6, 2, 2, 7, 4};
-
- assertEquals("Array lengths do not match.", expectedCosts.length, actualCosts.length);
- for (int i = 0; i < expectedCosts.length; i++) {
- assertEquals("CMC: " + i, expectedCosts[i], actualCosts[i]);
- }
- }
-
- @Test
- public void test_instantCostDistributionIsComputedCorrectly() throws Exception {
- int[] actualCosts = Statistics.computeDistribution(uut, "Instant");
- int[] expectedCosts = {0, 2, 4};
-
- assertEquals("Array lengths do not match.", expectedCosts.length, actualCosts.length);
- for (int i = 0; i < expectedCosts.length; i++) {
- assertEquals("CMC: " + i, expectedCosts[i], actualCosts[i]);
- }
- }
-
- @Test
- public void test_planeswalkerCostDistributionIsComputedCorrectly() throws Exception {
- int[] actualCosts = Statistics.computeDistribution(uut, "Planeswalker");
- int[] expectedCosts = {};
-
- assertEquals("Array lengths do not match.", expectedCosts.length, actualCosts.length);
- for (int i = 0; i < expectedCosts.length; i++) {
- assertEquals("CMC: " + i, expectedCosts[i], actualCosts[i]);
- }
- }
-
-} \ No newline at end of file
diff --git a/test/eu/equalparts/cardbase/decks/deck.cbd b/test/eu/equalparts/cardbase/decks/deck.cbd
deleted file mode 100644
index 57d479a..0000000
--- a/test/eu/equalparts/cardbase/decks/deck.cbd
+++ /dev/null
@@ -1 +0,0 @@
-{"name":"Red-green Ramp","plains":0,"islands":0,"swamps":0,"mountains":11,"forests":9,"cards":[{"name":"Titanic Growth","layout":"normal","manaCost":"{1}{G}","cmc":2,"type":"Instant","rarity":"Common","text":"Target creature gets +4/+4 until end of turn.","flavor":"The massive dominate through might. The tiny survive with guile. Beware the tiny who become massive.","artist":"Ryan Pancoast","number":"203","power":null,"toughness":null,"loyalty":null,"multiverseid":383415,"imageName":"titanic growth","watermark":null,"setCode":"M15","imageCode":"m15","count":1},{"name":"Nessian Asp","layout":"normal","manaCost":"{4}{G}","cmc":5,"type":"Creature — Snake","rarity":"Common","text":"Reach\n{6}{G}: Monstrosity 4. (If this creature isn't monstrous, put four +1/+1 counters on it and it becomes monstrous.)","flavor":"It's not the two heads you should fear. It's the four fangs.","artist":"Alex Horley-Orlandelli","number":"164","power":"4","toughness":"5","loyalty":null,"multiverseid":373650,"imageName":"nessian asp","watermark":null,"setCode":"THS","imageCode":"ths","count":2},{"name":"Satyr Hedonist","layout":"normal","manaCost":"{1}{G}","cmc":2,"type":"Creature — Satyr","rarity":"Common","text":"{R}, Sacrifice Satyr Hedonist: Add {R}{R}{R} to your mana pool.","flavor":"\"Any festival you can walk away from wasn't worth attending in the first place.\"","artist":"Chase Stone","number":"174","power":"2","toughness":"1","loyalty":null,"multiverseid":373744,"imageName":"satyr hedonist","watermark":null,"setCode":"THS","imageCode":"ths","count":1},{"name":"Awe for the Guilds","layout":"normal","manaCost":"{2}{R}","cmc":3,"type":"Sorcery","rarity":"Common","text":"Monocolored creatures can't block this turn.","flavor":"When the guilds cooperate, the guildless celebrate their peaceful society. When the guilds clash, the guildless just try to keep out of the way.","artist":"Mathias Kollros","number":"31","power":null,"toughness":null,"loyalty":null,"multiverseid":369053,"imageName":"awe for the guilds","watermark":null,"setCode":"DGM","imageCode":"dgm","count":1},{"name":"Voyaging Satyr","layout":"normal","manaCost":"{1}{G}","cmc":2,"type":"Creature — Satyr Druid","rarity":"Common","text":"{T}: Untap target land.","flavor":"\"None can own the land's bounty. The gods made this world for all to share its riches. And I'm not just saying that because you caught me stealing your fruit.\"","artist":"Tyler Jacobson","number":"182","power":"1","toughness":"2","loyalty":null,"multiverseid":373518,"imageName":"voyaging satyr","watermark":null,"setCode":"THS","imageCode":"ths","count":1},{"name":"Rugged Highlands","layout":"normal","manaCost":null,"cmc":null,"type":"Land","rarity":"Common","text":"Rugged Highlands enters the battlefield tapped.\nWhen Rugged Highlands enters the battlefield, you gain 1 life.\n{T}: Add {R} or {G} to your mana pool.","flavor":null,"artist":"Eytan Zana","number":"240","power":null,"toughness":null,"loyalty":null,"multiverseid":386641,"imageName":"rugged highlands","watermark":null,"setCode":"KTK","imageCode":"ktk","count":1},{"name":"Karametra's Favor","layout":"normal","manaCost":"{1}{G}","cmc":2,"type":"Enchantment — Aura","rarity":"Common","text":"Enchant creature\nWhen Karametra's Favor enters the battlefield, draw a card.\nEnchanted creature has \"{T}: Add one mana of any color to your mana pool.\"","flavor":"The harvest god's cornucopia contains the fruits of the fields, the forest, and beyond.","artist":"Chase Stone","number":"125","power":null,"toughness":null,"loyalty":null,"multiverseid":378497,"imageName":"karametra's favor","watermark":null,"setCode":"BNG","imageCode":"bng","count":1},{"name":"Segmented Krotiq","layout":"normal","manaCost":"{5}{G}","cmc":6,"type":"Creature — Insect","rarity":"Common","text":"Megamorph {6}{G} (You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its megamorph cost and put a +1/+1 counter on it.)","flavor":"The list of things a krotiq eats is as long as the krotiq itself.","artist":"Christopher Moeller","number":"202","power":"6","toughness":"5","loyalty":null,"multiverseid":394684,"imageName":"segmented krotiq","watermark":null,"setCode":"DTK","imageCode":"dtk","count":2},{"name":"Sprinting Warbrute","layout":"normal","manaCost":"{4}{R}","cmc":5,"type":"Creature — Ogre Berserker","rarity":"Common","text":"Sprinting Warbrute attacks each turn if able.\nDash {3}{R} (You may cast this spell for its dash cost. If you do, it gains haste, and it's returned from the battlefield to its owner's hand at the beginning of the next end step.)","flavor":null,"artist":"Lake Hurwitz","number":"157","power":"5","toughness":"4","loyalty":null,"multiverseid":394709,"imageName":"sprinting warbrute","watermark":"Kolaghan","setCode":"DTK","imageCode":"dtk","count":3},{"name":"Searing Blood","layout":"normal","manaCost":"{R}{R}","cmc":2,"type":"Instant","rarity":"Uncommon","text":"Searing Blood deals 2 damage to target creature. When that creature dies this turn, Searing Blood deals 3 damage to the creature's controller.","flavor":"Purphoros's blessing: sometimes a boon, sometimes a bane. Always ablaze.","artist":"Daniel Ljunggren","number":"111","power":null,"toughness":null,"loyalty":null,"multiverseid":378483,"imageName":"searing blood","watermark":null,"setCode":"BNG","imageCode":"bng","count":1},{"name":"Surrak, the Hunt Caller","layout":"normal","manaCost":"{2}{G}{G}","cmc":4,"type":"Legendary Creature — Human Warrior","rarity":"Rare","text":"Formidable — At the beginning of combat on your turn, if creatures you control have total power 8 or greater, target creature you control gains haste until end of turn.","flavor":"\"The greatest honor is to feed Atarka.\"","artist":"Wesley Burt","number":"210","power":"5","toughness":"4","loyalty":null,"multiverseid":394721,"imageName":"surrak, the hunt caller","watermark":"Atarka","setCode":"DTK","imageCode":"dtk","count":1},{"name":"Kolaghan Aspirant","layout":"normal","manaCost":"{1}{R}","cmc":2,"type":"Creature — Human Warrior","rarity":"Common","text":"Whenever Kolaghan Aspirant becomes blocked by a creature, Kolaghan Aspirant deals 1 damage to that creature.","flavor":"She answers the call of the Crave, the desire for battle sated only by bloodshed.","artist":"Aaron Miller","number":"143","power":"2","toughness":"1","loyalty":null,"multiverseid":394608,"imageName":"kolaghan aspirant","watermark":"Kolaghan","setCode":"DTK","imageCode":"dtk","count":1},{"name":"Alpine Grizzly","layout":"normal","manaCost":"{2}{G}","cmc":3,"type":"Creature — Bear","rarity":"Common","text":null,"flavor":"The Temur welcome bears into the clan, fighting alongside them in battle. The relationship dates back to when they labored side by side under Sultai rule.","artist":"John Severin Brassell","number":"127","power":"4","toughness":"2","loyalty":null,"multiverseid":386474,"imageName":"alpine grizzly","watermark":null,"setCode":"KTK","imageCode":"ktk","count":1},{"name":"Valley Dasher","layout":"normal","manaCost":"{1}{R}","cmc":2,"type":"Creature — Human Berserker","rarity":"Common","text":"Haste\nValley Dasher attacks each turn if able.","flavor":"Mardu riders' greatest fear is that a battle might end before their weapons draw blood.","artist":"Matt Stewart","number":"125","power":"2","toughness":"2","loyalty":null,"multiverseid":386712,"imageName":"valley dasher","watermark":"Mardu","setCode":"KTK","imageCode":"ktk","count":1},{"name":"Terra Stomper","layout":"normal","manaCost":"{3}{G}{G}{G}","cmc":6,"type":"Creature — Beast","rarity":"Rare","text":"Terra Stomper can't be countered.\nTrample (If this creature would assign enough damage to its blockers to destroy them, you may have it assign the rest of its damage to defending player or planeswalker.)","flavor":"Its footfalls cause violent earthquakes, hurtling boulders, and unseasonable dust storms.","artist":"Goran Josic","number":"284","power":"8","toughness":"8","loyalty":null,"multiverseid":383173,"imageName":"terra stomper","watermark":null,"setCode":"M15","imageCode":"m15","count":1},{"name":"Vaultbreaker","layout":"normal","manaCost":"{3}{R}","cmc":4,"type":"Creature — Orc Rogue","rarity":"Uncommon","text":"Whenever Vaultbreaker attacks, you may discard a card. If you do, draw a card.\nDash {2}{R} (You may cast this spell for its dash cost. If you do, it gains haste, and it's returned from the battlefield to its owner's hand at the beginning of the next end step.)","flavor":null,"artist":"Wayne Reynolds","number":"117","power":"4","toughness":"2","loyalty":null,"multiverseid":391951,"imageName":"vaultbreaker","watermark":"Mardu","setCode":"FRF","imageCode":"frf","count":1},{"name":"Elvish Mystic","layout":"normal","manaCost":"{G}","cmc":1,"type":"Creature — Elf Druid","rarity":"Common","text":"{T}: Add {G} to your mana pool.","flavor":"\"Life grows everywhere. My kin merely find those places where it grows strongest.\"\n—Nissa Revane","artist":"Wesley Burt","number":"173","power":"1","toughness":"1","loyalty":null,"multiverseid":383229,"imageName":"elvish mystic","watermark":null,"setCode":"M15","imageCode":"m15","count":2},{"name":"Collateral Damage","layout":"normal","manaCost":"{R}","cmc":1,"type":"Instant","rarity":"Common","text":"As an additional cost to cast Collateral Damage, sacrifice a creature.\nCollateral Damage deals 3 damage to target creature or player.","flavor":"It is much easier to create fire than to contain it.","artist":"Ryan Barger","number":"95","power":null,"toughness":null,"loyalty":null,"multiverseid":391811,"imageName":"collateral damage","watermark":null,"setCode":"FRF","imageCode":"frf","count":1},{"name":"Dragon Whisperer","layout":"normal","manaCost":"{R}{R}","cmc":2,"type":"Creature — Human Shaman","rarity":"Mythic Rare","text":"{R}: Dragon Whisperer gains flying until end of turn.\n{1}{R}: Dragon Whisperer gets +1/+0 until end of turn.\nFormidable — {4}{R}{R}: Put a 4/4 red Dragon creature token with flying onto the battlefield. Activate this ability only if creatures you control have total power 8 or greater.","flavor":null,"artist":"Chris Rallis","number":"137","power":"2","toughness":"2","loyalty":null,"multiverseid":394543,"imageName":"dragon whisperer","watermark":"Atarka","setCode":"DTK","imageCode":"dtk","count":1},{"name":"Magma Spray","layout":"normal","manaCost":"{R}","cmc":1,"type":"Instant","rarity":"Common","text":"Magma Spray deals 2 damage to target creature. If that creature would die this turn, exile it instead.","flavor":"The ancient dragon Thraxes sleeps in Purphoros's sacred peak. When he stirs in dreams, so does the mountain.","artist":"Richard Wright","number":"103","power":null,"toughness":null,"loyalty":null,"multiverseid":380452,"imageName":"magma spray","watermark":null,"setCode":"JOU","imageCode":"jou","count":1},{"name":"Hammerhand","layout":"normal","manaCost":"{R}","cmc":1,"type":"Enchantment — Aura","rarity":"Common","text":"Enchant creature\nWhen Hammerhand enters the battlefield, target creature can't block this turn.\nEnchanted creature gets +1/+1 and has haste. (It can attack and {T} no matter when it came under your control.)","flavor":null,"artist":"Tomasz Jedruszek","number":"147","power":null,"toughness":null,"loyalty":null,"multiverseid":383262,"imageName":"hammerhand","watermark":null,"setCode":"M15","imageCode":"m15","count":1},{"name":"Brindle Boar","layout":"normal","manaCost":"{2}{G}","cmc":3,"type":"Creature — Boar","rarity":"Common","text":"Sacrifice Brindle Boar: You gain 4 life.","flavor":"\"Tell the cooks to prepare the fires. Tonight we feast!\"\n—Tolar Wolfbrother, Krosan tracker","artist":"Dave Allsop","number":"167","power":"2","toughness":"2","loyalty":null,"multiverseid":370778,"imageName":"brindle boar","watermark":null,"setCode":"M14","imageCode":"m14","count":1},{"name":"Harbinger of the Hunt","layout":"normal","manaCost":"{3}{R}{G}","cmc":5,"type":"Creature — Dragon","rarity":"Rare","text":"Flying\n{2}{R}: Harbinger of the Hunt deals 1 damage to each creature without flying.\n{2}{G}: Harbinger of the Hunt deals 1 damage to each other creature with flying.","flavor":"An Atarka dragon's exhale cooks what its inhale consumes.","artist":"Aaron Miller","number":"223","power":"5","toughness":"3","loyalty":null,"multiverseid":394591,"imageName":"harbinger of the hunt","watermark":"Atarka","setCode":"DTK","imageCode":"dtk","count":1},{"name":"Lightning Strike","layout":"normal","manaCost":"{1}{R}","cmc":2,"type":"Instant","rarity":"Common","text":"Lightning Strike deals 3 damage to target creature or player.","flavor":"\"The hand of Keranos can be seen in every rumbling storm cloud. Best not to stand where he points.\"\n—Rakleia of Shrine Peak","artist":"Adam Paquette","number":"127","power":null,"toughness":null,"loyalty":null,"multiverseid":373651,"imageName":"lightning strike","watermark":null,"setCode":"THS","imageCode":"ths","count":2},{"name":"Monastery Swiftspear","layout":"normal","manaCost":"{R}","cmc":1,"type":"Creature — Human Monk","rarity":"Uncommon","text":"Haste\nProwess (Whenever you cast a noncreature spell, this creature gets +1/+1 until end of turn.)","flavor":"The calligraphy of combat is written with strokes of sudden blood.","artist":"Steve Argyle","number":"118","power":"1","toughness":"2","loyalty":null,"multiverseid":386608,"imageName":"monastery swiftspear","watermark":"Jeskai","setCode":"KTK","imageCode":"ktk","count":1},{"name":"Evolving Wilds","layout":"normal","manaCost":null,"cmc":null,"type":"Land","rarity":"Common","text":"{T}, Sacrifice Evolving Wilds: Search your library for a basic land card and put it onto the battlefield tapped. Then shuffle your library.","flavor":"Without the interfering hands of civilization, nature will always shape itself to its own needs.","artist":"Steven Belledin","number":"243","power":null,"toughness":null,"loyalty":null,"multiverseid":383235,"imageName":"evolving wilds","watermark":null,"setCode":"M15","imageCode":"m15","count":2},{"name":"Market Festival","layout":"normal","manaCost":"{3}{G}","cmc":4,"type":"Enchantment — Aura","rarity":"Common","text":"Enchant land\nWhenever enchanted land is tapped for mana, its controller adds two mana in any combination of colors to his or her mana pool (in addition to the mana the land produces).","flavor":"Commerce is always the basis for peace.","artist":"Ryan Barger","number":"130","power":null,"toughness":null,"loyalty":null,"multiverseid":380454,"imageName":"market festival","watermark":null,"setCode":"JOU","imageCode":"jou","count":2},{"name":"Lightning Shrieker","layout":"normal","manaCost":"{4}{R}","cmc":5,"type":"Creature — Dragon","rarity":"Common","text":"Flying, trample, haste\nAt the beginning of the end step, Lightning Shrieker's owner shuffles it into his or her library.","flavor":"Dragonslayers learned to keep silent about their deeds after seeing the terrible vengeance wrought by Kolaghan and her brood.","artist":"Slawomir Maniak","number":"106","power":"5","toughness":"5","loyalty":null,"multiverseid":391868,"imageName":"lightning shrieker","watermark":"Kolaghan","setCode":"FRF","imageCode":"frf","count":1},{"name":"Kalonian Tusker","layout":"normal","manaCost":"{G}{G}","cmc":2,"type":"Creature — Beast","rarity":"Uncommon","text":null,"flavor":"\"And all this time I thought we were tracking it.\"\n—Juruk, Kalonian tracker","artist":"Svetlin Velinov","number":"182","power":"3","toughness":"3","loyalty":null,"multiverseid":370700,"imageName":"kalonian tusker","watermark":null,"setCode":"M14","imageCode":"m14","count":1},{"name":"Dragon Mantle","layout":"normal","manaCost":"{R}","cmc":1,"type":"Enchantment — Aura","rarity":"Common","text":"Enchant creature\nWhen Dragon Mantle enters the battlefield, draw a card.\nEnchanted creature has \"{R}: This creature gets +1/+0 until end of turn.\"","flavor":null,"artist":"Anthony Palumbo","number":"119","power":null,"toughness":null,"loyalty":null,"multiverseid":373634,"imageName":"dragon mantle","watermark":null,"setCode":"THS","imageCode":"ths","count":2},{"name":"Shivan Dragon","layout":"normal","manaCost":"{4}{R}{R}","cmc":6,"type":"Creature — Dragon","rarity":"Rare","text":"Flying (This creature can't be blocked except by creatures with flying or reach.)\n{R}: Shivan Dragon gets +1/+0 until end of turn.","flavor":"The undisputed master of the mountains of Shiv.","artist":"Donato Giancola","number":"281","power":"5","toughness":"5","loyalty":null,"multiverseid":383172,"imageName":"shivan dragon","watermark":null,"setCode":"M15","imageCode":"m15","count":1}]} \ No newline at end of file
diff --git a/test/testbase.cb b/test/testbase.cb
index e617fd8..cf06e9f 100644
--- a/test/testbase.cb
+++ b/test/testbase.cb
@@ -1 +1 @@
-{"cards":{"1588769":{"name":"Reverberate","layout":"normal","manaCost":"{R}{R}","cmc":2,"type":"Instant","rarity":"Rare","text":"Copy target instant or sorcery spell. You may choose new targets for the copy.","flavor":"For every action, there is a swifter and more violent reaction.","artist":"jD","number":"152","power":null,"toughness":null,"loyalty":null,"multiverseid":233722,"imageName":"reverberate","watermark":null,"setCode":"M12","imageCode":"m12"},"126251":{"name":"Mighty Leap","layout":"normal","manaCost":"{1}{W}","cmc":2,"type":"Instant","rarity":"Common","text":"Target creature gets +2/+2 and gains flying until end of turn. (It can't be blocked except by creatures with flying or reach.)","flavor":"\"The southern fortress taken by invaders? Heh, sure . . . when elephants fly.\"\n—Brezard Skeinbow, captain of the guard","artist":"rk post","number":"26","power":null,"toughness":null,"loyalty":null,"multiverseid":241989,"imageName":"mighty leap","watermark":null,"setCode":"M12","imageCode":"m12"},"1580419":{"name":"Formless Nurturing","layout":"normal","manaCost":"{3}{G}","cmc":4,"type":"Sorcery","rarity":"Common","text":"Manifest the top card of your library, then put a +1/+1 counter on it. (To manifest a card, put it onto the battlefield face down as a 2/2 creature. Turn it face up any time for its mana cost if it's a creature card.)","flavor":null,"artist":"Cliff Childs","number":"129","power":null,"toughness":null,"loyalty":null,"multiverseid":391837,"imageName":"formless nurturing","watermark":null,"setCode":"FRF","imageCode":"frf"},"1580388":{"name":"Feral Krushok","layout":"normal","manaCost":"{4}{G}","cmc":5,"type":"Creature — Beast","rarity":"Common","text":null,"flavor":"In a stunning act of diplomacy, Yasova Dragonclaw ceded a portion of Temur lands to the Sultai. Her clan protested until they saw she had given the Sultai the breeding grounds of the krushoks. They hadn't realized she had a sense of humor.","artist":"Kev Walker","number":"128","power":"5","toughness":"4","loyalty":null,"multiverseid":391831,"imageName":"feral krushok","watermark":null,"setCode":"FRF","imageCode":"frf"},"1580357":{"name":"Destructor Dragon","layout":"normal","manaCost":"{4}{G}{G}","cmc":6,"type":"Creature — Dragon","rarity":"Uncommon","text":"Flying\nWhen Destructor Dragon dies, destroy target noncreature permanent.","flavor":"After countless attacks on the Salt Road where it passes through the frozen tundra, the Abzan began to refer to the area as Atarka territory rather than Temur lands.","artist":"Peter Mohrbacher","number":"127","power":"4","toughness":"4","loyalty":null,"multiverseid":391818,"imageName":"destructor dragon","watermark":"Atarka","setCode":"FRF","imageCode":"frf"},"127150":{"name":"Siege Mastodon","layout":"normal","manaCost":"{4}{W}","cmc":5,"type":"Creature — Elephant","rarity":"Common","text":null,"flavor":"\"The walls of the wicked will fall before us. Ready the siege engines. We proceed to war!\"\n—General Avitora","artist":"Matt Cavotta","number":"34","power":"3","toughness":"5","loyalty":null,"multiverseid":222635,"imageName":"siege mastodon","watermark":null,"setCode":"M12","imageCode":"m12"}},"collection":{"1588769":1,"126251":1,"1580419":8,"1580388":1,"1580357":1,"127150":2},"decks":{}} \ No newline at end of file
+{"cardData":{"1588769":{"name":"Reverberate","layout":"normal","manaCost":"{R}{R}","cmc":2,"type":"Instant","rarity":"Rare","text":"Copy target instant or sorcery spell. You may choose new targets for the copy.","flavor":"For every action, there is a swifter and more violent reaction.","artist":"jD","number":"152","power":null,"toughness":null,"loyalty":null,"multiverseid":233722,"imageName":"reverberate","watermark":null,"setCode":"M12","imageCode":"m12"},"126251":{"name":"Mighty Leap","layout":"normal","manaCost":"{1}{W}","cmc":2,"type":"Instant","rarity":"Common","text":"Target creature gets +2/+2 and gains flying until end of turn. (It can't be blocked except by creatures with flying or reach.)","flavor":"\"The southern fortress taken by invaders? Heh, sure . . . when elephants fly.\"\n—Brezard Skeinbow, captain of the guard","artist":"rk post","number":"26","power":null,"toughness":null,"loyalty":null,"multiverseid":241989,"imageName":"mighty leap","watermark":null,"setCode":"M12","imageCode":"m12"},"1580419":{"name":"Formless Nurturing","layout":"normal","manaCost":"{3}{G}","cmc":4,"type":"Sorcery","rarity":"Common","text":"Manifest the top card of your library, then put a +1/+1 counter on it. (To manifest a card, put it onto the battlefield face down as a 2/2 creature. Turn it face up any time for its mana cost if it's a creature card.)","flavor":null,"artist":"Cliff Childs","number":"129","power":null,"toughness":null,"loyalty":null,"multiverseid":391837,"imageName":"formless nurturing","watermark":null,"setCode":"FRF","imageCode":"frf"},"1580388":{"name":"Feral Krushok","layout":"normal","manaCost":"{4}{G}","cmc":5,"type":"Creature — Beast","rarity":"Common","text":null,"flavor":"In a stunning act of diplomacy, Yasova Dragonclaw ceded a portion of Temur lands to the Sultai. Her clan protested until they saw she had given the Sultai the breeding grounds of the krushoks. They hadn't realized she had a sense of humor.","artist":"Kev Walker","number":"128","power":"5","toughness":"4","loyalty":null,"multiverseid":391831,"imageName":"feral krushok","watermark":null,"setCode":"FRF","imageCode":"frf"},"1580357":{"name":"Destructor Dragon","layout":"normal","manaCost":"{4}{G}{G}","cmc":6,"type":"Creature — Dragon","rarity":"Uncommon","text":"Flying\nWhen Destructor Dragon dies, destroy target noncreature permanent.","flavor":"After countless attacks on the Salt Road where it passes through the frozen tundra, the Abzan began to refer to the area as Atarka territory rather than Temur lands.","artist":"Peter Mohrbacher","number":"127","power":"4","toughness":"4","loyalty":null,"multiverseid":391818,"imageName":"destructor dragon","watermark":"Atarka","setCode":"FRF","imageCode":"frf"},"127150":{"name":"Siege Mastodon","layout":"normal","manaCost":"{4}{W}","cmc":5,"type":"Creature — Elephant","rarity":"Common","text":null,"flavor":"\"The walls of the wicked will fall before us. Ready the siege engines. We proceed to war!\"\n—General Avitora","artist":"Matt Cavotta","number":"34","power":"3","toughness":"5","loyalty":null,"multiverseid":222635,"imageName":"siege mastodon","watermark":null,"setCode":"M12","imageCode":"m12"}},"cardReferences":{"1588769":1,"126251":1,"1580419":8,"1580388":1,"1580357":1,"127150":2},"decks":{}}