aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEduardo Pedroni <e.pedroni91@gmail.com>2015-06-24 18:58:15 +0200
committerEduardo Pedroni <e.pedroni91@gmail.com>2015-06-24 18:58:15 +0200
commitbf29c3d96160eeaa64f60a512a0a9f4d7c85167a (patch)
treed748a0fa4016c188b3d4be5bdd719261b40655b0
parent59174889aba82c64ae63e0a5622d6cbcab63dc39 (diff)
Mulling over the generic comparator issue
-rw-r--r--README.md3
-rw-r--r--src/eu/equalparts/cardbase/Cardbase.java19
-rw-r--r--src/eu/equalparts/cardbase/cli/CardbaseCLI.java20
-rw-r--r--src/eu/equalparts/cardbase/comparator/CardComparator.java126
-rw-r--r--src/eu/equalparts/cardbase/comparator/ComparatorDelegates.java39
-rw-r--r--src/eu/equalparts/cardbase/comparator/SpecialFields.java8
-rw-r--r--src/eu/equalparts/cardbase/comparators/CardComparators.java76
-rw-r--r--src/eu/equalparts/cardbase/data/Card.java3
-rw-r--r--test/eu/equalparts/test/cardbase/CardbaseSortTest.java169
-rw-r--r--test/eu/equalparts/test/cardbase/testbase.cb1
-rw-r--r--test/eu/equalparts/test/cardbase/testcards.json1
-rw-r--r--test/eu/equalparts/test/ui/CardbaseCLITest.java (renamed from test/eu/equalparts/test/ui/CLITest.java)2
12 files changed, 374 insertions, 93 deletions
diff --git a/README.md b/README.md
index 629718f..18f1a05 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,9 @@
# cardbase
-The primary purpose of cardbase is to create a digital representation of your own MTG collection. A cardbase file (.cb) is simply a JSON file containing an array of card objects. The card data structure used is a superset of that used in [MTG JSON](http://mtgjson.com/), and in fact that is where most of the data comes from.
+The primary purpose of cardbase is to create a digital representation of your own MTG collection. A cardbase file (.cb) is simply a JSON file containing a list of card objects and associated unique keys. The card data structure used is derived from that used in [MTG JSON](http://mtgjson.com/), and in fact that is where most of the data comes from right now.
To-do list:
* Build basic GUI to visualise the cards
-* Allow sorting by field
* Basic deck building - decks can be built and stored
* Statistical deck analysis - cost distribution, etc
diff --git a/src/eu/equalparts/cardbase/Cardbase.java b/src/eu/equalparts/cardbase/Cardbase.java
index 211f865..9245870 100644
--- a/src/eu/equalparts/cardbase/Cardbase.java
+++ b/src/eu/equalparts/cardbase/Cardbase.java
@@ -2,9 +2,11 @@ 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.core.JsonGenerationException;
@@ -12,11 +14,12 @@ import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
+import eu.equalparts.cardbase.comparator.CardComparator;
import eu.equalparts.cardbase.data.Card;
import eu.equalparts.cardbase.utils.JSON;
/**
- * Provides a variety of utility methods to interact with the loaded cardbase.
+ * Provides a variety of utility methods to interact with an optionally loaded cardbase.
*
* @author Eduardo Pedroni
*/
@@ -32,7 +35,6 @@ public class Cardbase {
* information to be printed to the console.
*/
public static final boolean DEBUG = System.getenv("CB_DEBUG") != null;
-
/**
* Used in the hash generation.
*/
@@ -134,12 +136,22 @@ public class Cardbase {
}
/**
+ * @param field 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> sort(String field) throws NoSuchFieldException {
+ List<Card> sortedCards = new ArrayList<Card>(cards.values());
+ sortedCards.sort(new CardComparator(Card.class.getDeclaredField(field)));
+ return Collections.unmodifiableCollection(sortedCards);
+ }
+
+ /**
* 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.
- * key
* @return the requested {@code Card} or null if no card is found.
*/
public Card getCard(String setCode, String number) {
@@ -152,7 +164,6 @@ public class Cardbase {
* 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.
*/
public Card getCardFromHash(String hash) {
diff --git a/src/eu/equalparts/cardbase/cli/CardbaseCLI.java b/src/eu/equalparts/cardbase/cli/CardbaseCLI.java
index 7a2e581..6a7f013 100644
--- a/src/eu/equalparts/cardbase/cli/CardbaseCLI.java
+++ b/src/eu/equalparts/cardbase/cli/CardbaseCLI.java
@@ -96,15 +96,15 @@ public class CardbaseCLI {
*
* @param args a list of arguments. Only the first argument is used, as a cardbase JSON.
*/
- private CardbaseCLI(String... args) {
+ public CardbaseCLI(String... args) {
System.out.println("Welcome to Cardbase CLI!");
// set debug flag if we are debugging
if (Cardbase.DEBUG) System.out.println("Debug mode is on.");
- // make the CardbaseManager
- if (args.length > 0) {
- File cardbaseFile = new File(args[0]);
+ // make the Cardbase
+ if (args != null && args.length > 0) {
+ cardbaseFile = new File(args[0]);
if (cardbaseFile.exists() && cardbaseFile.isFile() && cardbaseFile.canRead()) {
System.out.println("Loading cardbase from \"" + args[0] + "\".");
try {
@@ -124,7 +124,7 @@ public class CardbaseCLI {
System.exit(1);
}
} else {
- System.out.println(args[0] + " appears to be invalid.");
+ System.out.println(args[0] + " appears to be an invalid path.");
System.exit(0);
}
} else {
@@ -202,7 +202,7 @@ public class CardbaseCLI {
public void write(String[] args) {
File outputFile;
// user-provided file overrides everything else
- if (args.length > 0) {
+ if (args != null && args.length > 0) {
outputFile = new File(sanitiseFileName(args[0]));
} else {
outputFile = cardbaseFile;
@@ -261,7 +261,7 @@ public class CardbaseCLI {
* @param args the code of the chosen set.
*/
public void set(String[] args) {
- if (args.length > 0) {
+ if (args != null && args.length > 0) {
try {
selectedSet = MTGUniverse.getFullCardSet(args[0]);
// if the set code is invalid, null is returned
@@ -306,7 +306,7 @@ public class CardbaseCLI {
*/
public void peruse(String[] args) {
// if a card is specified, peruse only that
- if (args.length > 0) {
+ if (args != null && args.length > 0) {
if (selectedSet != null) {
Card card = cardbase.getCard(selectedSet.code, args[0]);
if (card != null) {
@@ -352,7 +352,7 @@ public class CardbaseCLI {
*/
public void remove(String[] args) {
if (selectedSet != null) {
- if (args.length > 0) {
+ if (args != null && args.length > 0) {
Card cardToRemove = selectedSet.getCardByNumber(args[0]);
if (cardToRemove != null) {
Integer count = 1;
@@ -391,7 +391,7 @@ public class CardbaseCLI {
Card cardToAdd = selectedSet.getCardByNumber(number);
if (cardToAdd != null) {
Integer count = 1;
- if (args.length > 0 && args[0].matches("[0-9]+")) {
+ if (args != null && args.length > 0 && args[0].matches("[0-9]+")) {
count = Integer.valueOf(args[0]);
if (count <= 0) {
System.out.println("Can't add " + count + " cards.");
diff --git a/src/eu/equalparts/cardbase/comparator/CardComparator.java b/src/eu/equalparts/cardbase/comparator/CardComparator.java
new file mode 100644
index 0000000..78200b8
--- /dev/null
+++ b/src/eu/equalparts/cardbase/comparator/CardComparator.java
@@ -0,0 +1,126 @@
+package eu.equalparts.cardbase.comparator;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Comparator;
+import java.util.function.BiFunction;
+
+import eu.equalparts.cardbase.Cardbase;
+import eu.equalparts.cardbase.data.Card;
+
+/**
+ * I'm new to this reflection business, so bear with me.
+ * <br><br>
+ * The idea here is to avoid having to write one class
+ * for each comparable field in {@code Card}. The program
+ * can dynamically instantiate them as cards are compared
+ * by different fields.
+ * <br><br>
+ * This class uses reflection to determine if the specified
+ * field is comparable with itself upon construction, and throws
+ * an {@code IllegalArgumentException} if that is not the case.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+@SuppressWarnings({"rawtypes", "unchecked"})
+public class CardComparator implements Comparator<Card> {
+
+ public enum Order {
+ NATURAL, REVERSE;
+ }
+
+ /**
+ * The field being compared.
+ */
+ private Field fieldToCompare;
+
+ private BiFunction<Comparable, Comparable, Integer> comparisonDelegate = this::defaultComparison;
+
+ /**
+ * Creates a new comparator for the specified field only. This class
+ * will only be constructed successfully if the field comes from
+ * {@code Card} and can be compared to itself (i.e. implements
+ * {@code Comparable<T>} where T is its own type.
+ * <br>
+ * For reference, {@code String} and {@code Integer} are both self comparable.
+ *
+ * @param fieldToCompare the field this comparator will use to compare cards, as declared in {@code Card}.
+ */
+ public CardComparator(Field fieldToCompare) {
+ if (fieldToCompare.getDeclaringClass().equals(Card.class) &&
+ isSelfComparable(fieldToCompare.getType())) {
+
+ this.fieldToCompare = fieldToCompare;
+
+ // if annotated with a special comparator, set the comparison delegate here
+ this.comparisonDelegate = ComparatorDelegates::compareManaCost;
+
+ } else {
+ System.out.println(fieldToCompare.isAccessible());
+ System.out.println(fieldToCompare.getDeclaringClass().equals(Card.class));
+ System.out.println(isSelfComparable(fieldToCompare.getType()));
+ throw new IllegalArgumentException("The field provided is not valid.");
+ }
+ }
+
+ @Override
+ public int compare(Card o1, Card o2) {
+ /*
+ * we've already checked that the field is self comparable,
+ * so we are now free to cast to whatever type it is and compare.
+ */
+ try {
+ Comparable field1 = (Comparable) fieldToCompare.get(o1);
+ Comparable field2 = (Comparable) fieldToCompare.get(o2);
+
+ return comparisonDelegate.apply(field1, field2);
+ } catch (IllegalArgumentException e) {
+ System.out.println("Error: class Card does not define field" + fieldToCompare.getName() + ".");
+ if (Cardbase.DEBUG) e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ System.out.println("Error: field " + fieldToCompare.getName() + " in Card is not visible.");
+ if (Cardbase.DEBUG) e.printStackTrace();
+ }
+
+ // fallback, this shouldn't happen
+ return 0;
+ }
+
+ /**
+ * The standard comparison operation, which uses the field's own {@code compareTo()}
+ * method.
+ *
+ * @param field1 the first object to be compared.
+ * @param field2 the second object to be compared.
+ * @return a negative integer, zero, or a positive integer as the
+ * first argument is less than, equal to, or greater than the
+ * second.
+ */
+ private int defaultComparison(Comparable field1, Comparable field2) {
+ return field1.compareTo(field2);
+ }
+
+ /**
+ * Use reflection to determine if the specified class can be compared with itself.
+ *
+ * @param type the type to analyse.
+ * @return true if the type can be compared to itself using {@code compareTo()}, false otherwise.
+ */
+ private boolean isSelfComparable(Class<?> type) {
+
+ // go through all interfaces implemented by this class
+ for (Type implementedInterface : type.getGenericInterfaces()) {
+ // check if any parameterised interface found is "Comparable"
+ if (implementedInterface instanceof ParameterizedType) {
+ ParameterizedType genericInterface = (ParameterizedType) implementedInterface;
+ if (genericInterface.getRawType().equals(Comparable.class)) {
+ // check that the type argument of comparable is the same as the field type itself
+ return genericInterface.getActualTypeArguments()[0].equals(type);
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/eu/equalparts/cardbase/comparator/ComparatorDelegates.java b/src/eu/equalparts/cardbase/comparator/ComparatorDelegates.java
new file mode 100644
index 0000000..5736381
--- /dev/null
+++ b/src/eu/equalparts/cardbase/comparator/ComparatorDelegates.java
@@ -0,0 +1,39 @@
+package eu.equalparts.cardbase.comparator;
+
+
+abstract class ComparatorDelegates {
+
+ private ComparatorDelegates() {}
+
+ public static Integer compareManaCost(Comparable<String> field1, Comparable<String> field2) {
+ // avoid casting syntax nightmare
+ String mc1 = (String) field1, mc2 = (String) field2;
+
+ // first by number of colours
+ int mc1count = 0, mc2count = 0;
+ if (mc1.contains("W")) mc1count++;
+ if (mc1.contains("U")) mc1count++;
+ if (mc1.contains("B")) mc1count++;
+ if (mc1.contains("R")) mc1count++;
+ if (mc1.contains("G")) mc1count++;
+
+ if (mc2.contains("W")) mc2count++;
+ if (mc2.contains("U")) mc2count++;
+ if (mc2.contains("B")) mc2count++;
+ if (mc2.contains("R")) mc2count++;
+ if (mc2.contains("G")) mc2count++;
+
+ if (mc1count != mc2count)
+ return (mc1count < mc2count) ? -1 : ((mc1count == mc2count) ? 0 : 1);
+
+ // next by colour wheel
+
+ return 0;
+ }
+
+}
+/*
+ * first by number of colours
+ * next by colour wheel: white > blue > black > red > green
+ * next by cmc
+ */ \ No newline at end of file
diff --git a/src/eu/equalparts/cardbase/comparator/SpecialFields.java b/src/eu/equalparts/cardbase/comparator/SpecialFields.java
new file mode 100644
index 0000000..8ecc82c
--- /dev/null
+++ b/src/eu/equalparts/cardbase/comparator/SpecialFields.java
@@ -0,0 +1,8 @@
+package eu.equalparts.cardbase.comparator;
+
+
+public class SpecialFields {
+
+ public @interface ManaCost {}
+
+}
diff --git a/src/eu/equalparts/cardbase/comparators/CardComparators.java b/src/eu/equalparts/cardbase/comparators/CardComparators.java
deleted file mode 100644
index 99f7ded..0000000
--- a/src/eu/equalparts/cardbase/comparators/CardComparators.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package eu.equalparts.cardbase.comparators;
-
-import java.util.Comparator;
-
-import eu.equalparts.cardbase.data.Card;
-
-public final class CardComparators {
-
- private CardComparators() {}
-
- public enum Order {
- NATURAL, REVERSE;
- }
-
- public static class NameComparator implements Comparator<Card> {
-
- private Order order = Order.NATURAL;
-
- public NameComparator() {}
-
- public NameComparator(Order order) {
- this.order = order;
- }
-
- @Override
- public int compare(Card o1, Card o2) {
- if (order == Order.NATURAL) {
- return o1.name.compareTo(o2.name);
- } else {
- return o2.name.compareTo(o1.name);
- }
-
- }
- }
-
- public static class LayoutComparator implements Comparator<Card> {
-
- private Order order = Order.NATURAL;
-
- public LayoutComparator() {}
-
- public LayoutComparator(Order order) {
- this.order = order;
- }
-
- @Override
- public int compare(Card o1, Card o2) {
- if (order == Order.NATURAL) {
- return o1.layout.compareTo(o2.layout);
- } else {
- return o2.layout.compareTo(o1.layout);
- }
- }
- }
-
- public static class ManaCostComparator implements Comparator<Card> {
-
- private Order order = Order.NATURAL;
-
- public ManaCostComparator() {}
-
- public ManaCostComparator(Order order) {
- this.order = order;
- }
-
- @Override
- public int compare(Card o1, Card o2) {
- if (order == Order.NATURAL) {
- return o1.manaCost.compareTo(o2.manaCost);
- } else {
- return o2.manaCost.compareTo(o1.manaCost);
- }
- }
- }
-
-}
diff --git a/src/eu/equalparts/cardbase/data/Card.java b/src/eu/equalparts/cardbase/data/Card.java
index 2810564..7202660 100644
--- a/src/eu/equalparts/cardbase/data/Card.java
+++ b/src/eu/equalparts/cardbase/data/Card.java
@@ -1,9 +1,12 @@
package eu.equalparts.cardbase.data;
+import eu.equalparts.cardbase.comparator.SpecialFields.ManaCost;
+
public class Card {
public String name;
public String layout;
+ @ManaCost
public String manaCost;
public Integer cmc;
public String type;
diff --git a/test/eu/equalparts/test/cardbase/CardbaseSortTest.java b/test/eu/equalparts/test/cardbase/CardbaseSortTest.java
new file mode 100644
index 0000000..261451a
--- /dev/null
+++ b/test/eu/equalparts/test/cardbase/CardbaseSortTest.java
@@ -0,0 +1,169 @@
+package eu.equalparts.test.cardbase;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import eu.equalparts.cardbase.Cardbase;
+import eu.equalparts.cardbase.data.Card;
+import eu.equalparts.cardbase.utils.JSON;
+
+/**
+ * Tests the sorting functionality.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public class CardbaseSortTest {
+
+ private Cardbase cardbase;
+ private static List<Card> testCards;
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception {
+ testCards = JSON.mapper.readValue(CardbaseSortTest.class.getResourceAsStream("testcards.json"), new TypeReference<List<Card>>() {});
+ }
+
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception {
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ cardbase = new Cardbase();
+ for (Card card : testCards) {
+ cardbase.addCard(card, 1);
+ }
+ }
+
+ @Test
+ public void test_sortByName() throws Exception {
+
+ Collection<Card> sortedCards = cardbase.sort("name");
+
+ int i = 0;
+ String[] names = {"Callow Jushi",
+ "Coerced Confession",
+ "Disrupting Shoal",
+ "Khalni Hydra",
+ "Nightmare",
+ "Shivan Dragon",
+ "Sorin Markov",
+ "Ugin's Construct" };
+
+ for (Card card : sortedCards) {
+ assertTrue(card.name + " should have been " + names[i] + ", i = " + i, card.name.equals(names[i]));
+ i++;
+ }
+ }
+
+ @Test
+ public void test_sortByLayout() throws Exception {
+
+ Collection<Card> sortedCards = cardbase.sort("layout");
+
+ int i = 0;
+ String[] layouts = {"flip",
+ "normal",
+ "normal",
+ "normal",
+ "normal",
+ "normal",
+ "normal",
+ "normal" };
+
+ for (Card card : sortedCards) {
+ assertTrue(card.layout + " should have been " + layouts[i] + ", i = " + i, card.layout.equals(layouts[i]));
+ i++;
+ }
+ }
+
+ @Test
+ public void test_sortByManaCost() throws Exception {
+
+
+
+// Collection<Card> sortedCards = cardbase.sort("name");
+//
+// int i = 0;
+// String[] layouts = {"flip",
+// "normal",
+// "normal",
+// "normal",
+// "normal",
+// "normal",
+// "normal",
+// "normal" };
+//
+// for (Card card : sortedCards) {
+// assertTrue(card.layout + " should have been " + layouts[i] + ", i = " + i, card.layout.equals(layouts[i]));
+// i++;
+// }
+ fail("todo");
+ }
+
+ @Test
+ public void test_sortByCMC() throws Exception {
+
+ Collection<Card> sortedCards = cardbase.sort("cmc");
+
+ int i = 0;
+ Integer[] cmcs = {2, 3, 4, 5, 6, 6, 6, 8};
+
+ for (Card card : sortedCards) {
+ assertTrue(card.cmc + " should have been " + cmcs[i] + ", i = " + i, card.cmc.equals(cmcs[i]));
+ i++;
+ }
+ }
+
+ @Test
+ public void test_sortByType() throws Exception {
+
+ Collection<Card> sortedCards = cardbase.sort("type");
+
+ int i = 0;
+ String[] types = {"Artifact Creature — Construct",
+ "Creature — Dragon",
+ "Creature — Human Wizard",
+ "Creature — Hydra",
+ "Creature — Nightmare Horse",
+ "Instant — Arcane",
+ "Planeswalker — Sorin",
+ "Sorcery"};
+
+ for (Card card : sortedCards) {
+ assertTrue(card.type + " should have been " + types[i] + ", i = " + i, card.type.equals(types[i]));
+ i++;
+ }
+ }
+
+ @Test
+ public void test_sortByRarity() throws Exception {
+
+ Collection<Card> sortedCards = cardbase.sort("rarity");
+
+ int i = 0;
+ String[] rarities = {"Uncommon",
+ "Uncommon",
+ "Uncommon",
+ "Rare",
+ "Rare",
+ "Rare",
+ "Mythic Rare",
+ "Mythic Rare"};
+
+ for (Card card : sortedCards) {
+ assertTrue(card.rarity + " should have been " + rarities[i] + ", i = " + i, card.rarity.equals(rarities[i]));
+ i++;
+ }
+ }
+}
diff --git a/test/eu/equalparts/test/cardbase/testbase.cb b/test/eu/equalparts/test/cardbase/testbase.cb
new file mode 100644
index 0000000..89b3bfb
--- /dev/null
+++ b/test/eu/equalparts/test/cardbase/testbase.cb
@@ -0,0 +1 @@
+{"M15276":{"name":"Nightmare","layout":"normal","manaCost":"{5}{B}","cmc":6,"type":"Creature — Nightmare Horse","rarity":"Rare","text":"Flying (This creature can't be blocked except by creatures with flying or reach.)\nNightmare's power and toughness are each equal to the number of Swamps you control.","flavor":"The thunder of its hooves beats dreams into despair.","artist":"Vance Kovacs","number":"276","power":"*","toughness":"*","loyalty":null,"multiverseid":383168,"imageName":"nightmare","border":null,"watermark":null,"setCode":"M15","imageCode":"m15","count":1},"FRF164":{"name":"Ugin's Construct","layout":"normal","manaCost":"{4}","cmc":4,"type":"Artifact Creature — Construct","rarity":"Uncommon","text":"When Ugin's Construct enters the battlefield, sacrifice a permanent that's one or more colors.","flavor":"While trapping the Eldrazi on Zendikar, Ugin learned little from Sorin, but he gleaned the rudiments of lithomancy from Nahiri.","artist":"Peter Mohrbacher","number":"164","power":"4","toughness":"5","loyalty":null,"multiverseid":391949,"imageName":"ugin's construct","border":null,"watermark":null,"setCode":"FRF","imageCode":"frf","count":1},"M12109":{"name":"Sorin Markov","layout":"normal","manaCost":"{3}{B}{B}{B}","cmc":6,"type":"Planeswalker — Sorin","rarity":"Mythic Rare","text":"+2: Sorin Markov deals 2 damage to target creature or player and you gain 2 life.\n−3: Target opponent's life total becomes 10.\n−7: You control target player during that player's next turn.","flavor":null,"artist":"Michael Komarck","number":"109","power":null,"toughness":null,"loyalty":4,"multiverseid":238330,"imageName":"sorin markov","border":null,"watermark":null,"setCode":"M12","imageCode":"m12","count":1},"ROE192":{"name":"Khalni Hydra","layout":"normal","manaCost":"{G}{G}{G}{G}{G}{G}{G}{G}","cmc":8,"type":"Creature — Hydra","rarity":"Mythic Rare","text":"Khalni Hydra costs {G} less to cast for each green creature you control.\nTrample","flavor":"\"In ages past, bargains were struck and promises were made. Now we must collect on our debt. Begin the hymns.\"\n—Moruul, Khalni druid","artist":"Todd Lockwood","number":"192","power":"8","toughness":"8","loyalty":null,"multiverseid":193551,"imageName":"khalni hydra","border":null,"watermark":null,"setCode":"ROE","imageCode":"roe","count":1},"BOK33":{"name":"Disrupting Shoal","layout":"normal","manaCost":"{X}{U}{U}","cmc":2,"type":"Instant — Arcane","rarity":"Rare","text":"You may exile a blue card with converted mana cost X from your hand rather than pay Disrupting Shoal's mana cost.\nCounter target spell if its converted mana cost is X.","flavor":null,"artist":"Scott M. Fischer","number":"33","power":null,"toughness":null,"loyalty":null,"multiverseid":74128,"imageName":"disrupting shoal","border":null,"watermark":null,"setCode":"BOK","imageCode":"bok","count":1},"M15281":{"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","border":null,"watermark":null,"setCode":"M15","imageCode":"m15","count":1},"BOK31a":{"name":"Callow Jushi","layout":"flip","manaCost":"{1}{U}{U}","cmc":3,"type":"Creature — Human Wizard","rarity":"Uncommon","text":"Whenever you cast a Spirit or Arcane spell, you may put a ki counter on Callow Jushi.\nAt the beginning of the end step, if there are two or more ki counters on Callow Jushi, you may flip it.","flavor":null,"artist":"Tsutomu Kawade","number":"31a","power":"2","toughness":"2","loyalty":null,"multiverseid":74489,"imageName":"callow jushi","border":null,"watermark":null,"setCode":"BOK","imageCode":"bok","count":1},"GTC217":{"name":"Coerced Confession","layout":"normal","manaCost":"{4}{U/B}","cmc":5,"type":"Sorcery","rarity":"Uncommon","text":"Target player puts the top four cards of his or her library into his or her graveyard. You draw a card for each creature card put into that graveyard this way.","flavor":"\"Ask the right questions in the right way and truth is inevitable.\"\n—Lazav","artist":"Mathias Kollros","number":"217","power":null,"toughness":null,"loyalty":null,"multiverseid":366408,"imageName":"coerced confession","border":null,"watermark":"Dimir","setCode":"GTC","imageCode":"gtc","count":1}} \ No newline at end of file
diff --git a/test/eu/equalparts/test/cardbase/testcards.json b/test/eu/equalparts/test/cardbase/testcards.json
new file mode 100644
index 0000000..71c8683
--- /dev/null
+++ b/test/eu/equalparts/test/cardbase/testcards.json
@@ -0,0 +1 @@
+[{"name":"Callow Jushi","layout":"flip","manaCost":"{1}{U}{U}","cmc":3,"type":"Creature — Human Wizard","rarity":"Uncommon","text":"Whenever you cast a Spirit or Arcane spell, you may put a ki counter on Callow Jushi.\nAt the beginning of the end step, if there are two or more ki counters on Callow Jushi, you may flip it.","flavor":null,"artist":"Tsutomu Kawade","number":"31a","power":"2","toughness":"2","loyalty":null,"multiverseid":74489,"imageName":"callow jushi","border":null,"watermark":null,"setCode":"BOK","imageCode":"bok","count":null},{"name":"Coerced Confession","layout":"normal","manaCost":"{4}{U/B}","cmc":5,"type":"Sorcery","rarity":"Uncommon","text":"Target player puts the top four cards of his or her library into his or her graveyard. You draw a card for each creature card put into that graveyard this way.","flavor":"\"Ask the right questions in the right way and truth is inevitable.\"\n—Lazav","artist":"Mathias Kollros","number":"217","power":null,"toughness":null,"loyalty":null,"multiverseid":366408,"imageName":"coerced confession","border":null,"watermark":"Dimir","setCode":"GTC","imageCode":"gtc","count":null},{"name":"Khalni Hydra","layout":"normal","manaCost":"{G}{G}{G}{G}{G}{G}{G}{G}","cmc":8,"type":"Creature — Hydra","rarity":"Mythic Rare","text":"Khalni Hydra costs {G} less to cast for each green creature you control.\nTrample","flavor":"\"In ages past, bargains were struck and promises were made. Now we must collect on our debt. Begin the hymns.\"\n—Moruul, Khalni druid","artist":"Todd Lockwood","number":"192","power":"8","toughness":"8","loyalty":null,"multiverseid":193551,"imageName":"khalni hydra","border":null,"watermark":null,"setCode":"ROE","imageCode":"roe","count":null},{"name":"Nightmare","layout":"normal","manaCost":"{5}{B}","cmc":6,"type":"Creature — Nightmare Horse","rarity":"Rare","text":"Flying (This creature can't be blocked except by creatures with flying or reach.)\nNightmare's power and toughness are each equal to the number of Swamps you control.","flavor":"The thunder of its hooves beats dreams into despair.","artist":"Vance Kovacs","number":"276","power":"*","toughness":"*","loyalty":null,"multiverseid":383168,"imageName":"nightmare","border":null,"watermark":null,"setCode":"M15","imageCode":"m15","count":null},{"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","border":null,"watermark":null,"setCode":"M15","imageCode":"m15","count":null},{"name":"Disrupting Shoal","layout":"normal","manaCost":"{X}{U}{U}","cmc":2,"type":"Instant — Arcane","rarity":"Rare","text":"You may exile a blue card with converted mana cost X from your hand rather than pay Disrupting Shoal's mana cost.\nCounter target spell if its converted mana cost is X.","flavor":null,"artist":"Scott M. Fischer","number":"33","power":null,"toughness":null,"loyalty":null,"multiverseid":74128,"imageName":"disrupting shoal","border":null,"watermark":null,"setCode":"BOK","imageCode":"bok","count":null},{"name":"Sorin Markov","layout":"normal","manaCost":"{3}{B}{B}{B}","cmc":6,"type":"Planeswalker — Sorin","rarity":"Mythic Rare","text":"+2: Sorin Markov deals 2 damage to target creature or player and you gain 2 life.\n−3: Target opponent's life total becomes 10.\n−7: You control target player during that player's next turn.","flavor":null,"artist":"Michael Komarck","number":"109","power":null,"toughness":null,"loyalty":4,"multiverseid":238330,"imageName":"sorin markov","border":null,"watermark":null,"setCode":"M12","imageCode":"m12","count":null},{"name":"Ugin's Construct","layout":"normal","manaCost":"{4}","cmc":4,"type":"Artifact Creature — Construct","rarity":"Uncommon","text":"When Ugin's Construct enters the battlefield, sacrifice a permanent that's one or more colors.","flavor":"While trapping the Eldrazi on Zendikar, Ugin learned little from Sorin, but he gleaned the rudiments of lithomancy from Nahiri.","artist":"Peter Mohrbacher","number":"164","power":"4","toughness":"5","loyalty":null,"multiverseid":391949,"imageName":"ugin's construct","border":null,"watermark":null,"setCode":"FRF","imageCode":"frf","count":null}] \ No newline at end of file
diff --git a/test/eu/equalparts/test/ui/CLITest.java b/test/eu/equalparts/test/ui/CardbaseCLITest.java
index 4a7ebac..ad81fb9 100644
--- a/test/eu/equalparts/test/ui/CLITest.java
+++ b/test/eu/equalparts/test/ui/CardbaseCLITest.java
@@ -8,7 +8,7 @@ import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
-public class CLITest {
+public class CardbaseCLITest {
@BeforeClass
public static void setUpBeforeClass() throws Exception {