aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEduardo Pedroni <e.pedroni91@gmail.com>2015-06-05 15:37:51 +0200
committerEduardo Pedroni <e.pedroni91@gmail.com>2015-06-05 15:37:51 +0200
commit0465829c25ef43ba9ba898dff01ebaa5106fd1f8 (patch)
tree4b6a15e99294c6f3c12376c0d280b73387cd9613
parent1f0159bcf903c0f422ab47b07cd296da1e816f87 (diff)
It works! Reading in a file, writing to the same file or a different existing file, glancing, perusing, adding and removing cards by number
-rw-r--r--help45
-rw-r--r--src/eu/equalparts/cardbase/data/Card.java4
-rw-r--r--src/eu/equalparts/cardbase/data/CardBase.java18
-rw-r--r--src/eu/equalparts/cardbase/data/CardBaseManager.java68
-rw-r--r--src/eu/equalparts/cardbase/data/Deck.java10
-rw-r--r--src/eu/equalparts/cardbase/data/MetaCardSet.java31
-rw-r--r--src/eu/equalparts/cardbase/query/IO.java46
-rw-r--r--src/eu/equalparts/cardbase/query/Test.java1
-rw-r--r--src/eu/equalparts/cardbase/standalone/CardBaseCLI.java198
-rw-r--r--testbase1
10 files changed, 358 insertions, 64 deletions
diff --git a/help b/help
new file mode 100644
index 0000000..8fc3f8d
--- /dev/null
+++ b/help
@@ -0,0 +1,45 @@
+You are using Cardbasecli (Cardbase command line interface).
+This tool is designed to facilitate adding and removing new cards as they are added to or removed from the physical collection.
+
+
+Load a Cardbase
+
+To use, first make sure to load the cardbase by passing it as the first argument to the program, e.g. cardbasecli ~/mycardbase.cb. To start a new clean cardbase, simply run cardbasecli.
+
+
+Adding Cards
+
+To add cards, first use "set" to enter the code of the set to which the card belongs, e.g. to add 2015 Core Set cards, enter "set M15". To see a list of all valid set codes, type "sets".
+Once a set is selected, simply type the card's set number and it will be added to the collection. You may add more of the same card by typing in the same set number multiple times, or by specifying the quantity following the set number.
+
+For example, to add 3 M15 edition Shivan Dragons, do:
+
+> set m15
+> 281 3
+
+
+Removing Cards
+
+To remove cards, first choose a set and then type "remove" followed by the card's set number. To remove two "copies" of the same card, add the desired count after the card number.
+
+For example, to remove 2 of your 3 M15 Shivan Dragons, do:
+
+> set m15
+> remove 281 2
+
+
+Viewing the Cardbase
+
+Cardbasecli also offers some very basic commands to view the current state of the cardbase: glance, peruse and diff.
+
+glance: prints a list showing the amount of each card in the cardbase, by name.
+peruse: prints the same list as glance, but with more information about each card.
+diff: prints a concise list of added and remove cards and amounts, *since the last write*.
+
+Peruse optionally accepts a card number as an argument. If present, the card's entire information set is printed.
+
+
+Committing Changes and Exiting
+
+Any changes made on this tool must be manually written to the file. If you make a mistake, simply exit and your cardbase will not be modified. To write changes, simply type "write".
+To exit, simply type "exit". \ No newline at end of file
diff --git a/src/eu/equalparts/cardbase/data/Card.java b/src/eu/equalparts/cardbase/data/Card.java
index 8e25d56..1bf6a75 100644
--- a/src/eu/equalparts/cardbase/data/Card.java
+++ b/src/eu/equalparts/cardbase/data/Card.java
@@ -28,8 +28,8 @@ public class Card {
public String border;
public String watermark;
- // Not part of JSON, will be set later
+ // Not part of upstream JSON
public String setCode;
- public String setName;
+ public Integer count;
} \ No newline at end of file
diff --git a/src/eu/equalparts/cardbase/data/CardBase.java b/src/eu/equalparts/cardbase/data/CardBase.java
index 70ae0ea..abdd423 100644
--- a/src/eu/equalparts/cardbase/data/CardBase.java
+++ b/src/eu/equalparts/cardbase/data/CardBase.java
@@ -4,9 +4,21 @@ import java.util.ArrayList;
public class CardBase {
- private ArrayList<Card> cards;
- private ArrayList<Deck> decks;
-
+ public ArrayList<Card> cards = new ArrayList<>();
+ public ArrayList<Deck> decks = new ArrayList<>();
+ /**
+ * @param setCode
+ * @param number
+ * @return the card if found, else null.
+ */
+ public Card getCardByNumber(String setCode, String number) {
+ for (Card card : cards) {
+ if (card.setCode.equals(setCode) && card.number.equals(number))
+ return card;
+ }
+
+ return null;
+ }
}
diff --git a/src/eu/equalparts/cardbase/data/CardBaseManager.java b/src/eu/equalparts/cardbase/data/CardBaseManager.java
index acf5a3a..0baf9d7 100644
--- a/src/eu/equalparts/cardbase/data/CardBaseManager.java
+++ b/src/eu/equalparts/cardbase/data/CardBaseManager.java
@@ -2,6 +2,7 @@ package eu.equalparts.cardbase.data;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Iterator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
@@ -11,19 +12,17 @@ import eu.equalparts.cardbase.query.IO;
public class CardBaseManager {
private ArrayList<MetaCardSet> metaSets;
+ public CardBase cardBase;
- public static void main(String... args) {
-
- }
-
/**
* Parse a cardbase file and create an associated CardBase object.
* @throws IOException
* @throws JsonMappingException
* @throws JsonParseException
*/
- public CardBaseManager(File cardbase) throws JsonParseException, JsonMappingException, IOException {
+ public CardBaseManager(File cardBaseFile) throws JsonParseException, JsonMappingException, IOException {
metaSets = IO.getAllMetaSets();
+ cardBase = IO.readCardBase(cardBaseFile);
}
/**
@@ -34,10 +33,69 @@ public class CardBaseManager {
*/
public CardBaseManager() throws JsonParseException, JsonMappingException, IOException {
metaSets = IO.getAllMetaSets();
+ cardBase = new CardBase();
}
public ArrayList<MetaCardSet> getAllMetaSets() {
return metaSets;
}
+ /**
+ * Add 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 newCard
+ * @param count
+ */
+ public void addCard(Card newCard, Integer count) {
+ Card card = cardBase.getCardByNumber(newCard.setCode, newCard.number);
+ if (card != null) {
+ card.count += count;
+ } else {
+ newCard.count = count;
+ cardBase.cards.add(newCard);
+ }
+ }
+
+ /**
+ * Remove 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 card, 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.
+ *
+ * @param remove
+ * @param count
+ */
+ public void removeCard(Card remove, Integer count) {
+ Card card = cardBase.getCardByNumber(remove.setCode, remove.number);
+ if (card != null) {
+ if (card.count <= count) {
+ cardBase.cards.remove(card);
+ } else {
+ card.count -= count;
+ }
+ }
+ }
+
+ /**
+ * @return an iterator to the cards in the cardbase.
+ */
+ public Iterator<Card> cardIterator() {
+ return cardBase.cards.iterator();
+ }
+
+ /**
+ * Return a card from the cardBase by setCode and number.
+ * If no such card is in the cardbase, return null.
+ *
+ * @param code
+ * @param string
+ * @return
+ */
+ public Card getCard(String code, String number) {
+ return cardBase.getCardByNumber(code, number);
+ }
}
diff --git a/src/eu/equalparts/cardbase/data/Deck.java b/src/eu/equalparts/cardbase/data/Deck.java
index 50f1129..b5fe402 100644
--- a/src/eu/equalparts/cardbase/data/Deck.java
+++ b/src/eu/equalparts/cardbase/data/Deck.java
@@ -1,5 +1,15 @@
package eu.equalparts.cardbase.data;
+import java.util.HashMap;
+
public class Deck {
+ /**
+ * Cards stored in key-value pairs of [multiverse ID, amount].
+ */
+ public HashMap<Integer, Integer> cards;
+
+ public Deck() {
+
+ }
}
diff --git a/src/eu/equalparts/cardbase/data/MetaCardSet.java b/src/eu/equalparts/cardbase/data/MetaCardSet.java
index 4ddf228..3b7d4fd 100644
--- a/src/eu/equalparts/cardbase/data/MetaCardSet.java
+++ b/src/eu/equalparts/cardbase/data/MetaCardSet.java
@@ -2,34 +2,9 @@ package eu.equalparts.cardbase.data;
public class MetaCardSet {
- private String name = "";
- private String code = "";
- private String releaseDate = "";
-
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public String getCode() {
- return code;
- }
-
- public void setCode(String code) {
- this.code = code;
- }
-
- public String getReleaseDate() {
- return releaseDate;
- }
-
- public void setReleaseDate(String releaseDate) {
- this.releaseDate = releaseDate;
- }
+ public String name = "";
+ public String code = "";
+ public String releaseDate = "";
@Override
public String toString() {
diff --git a/src/eu/equalparts/cardbase/query/IO.java b/src/eu/equalparts/cardbase/query/IO.java
index 65a3a34..9b81b20 100644
--- a/src/eu/equalparts/cardbase/query/IO.java
+++ b/src/eu/equalparts/cardbase/query/IO.java
@@ -2,6 +2,7 @@ package eu.equalparts.cardbase.query;
import java.io.File;
import java.io.IOException;
+import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
@@ -21,21 +22,56 @@ public class IO {
public static final String BASE_URL = "http://mtgjson.com/json/";
public static final String SETS_URL = BASE_URL + "SetList.json";
- private static final ObjectMapper mapper = new ObjectMapper();
+ private static final ObjectMapper mapper = createMapper();
- public static CardSet getCardSet(String setId) {
- return new CardSet();
+ private static ObjectMapper createMapper() {
+ ObjectMapper om = new ObjectMapper();
+ om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ return om;
}
- public static ArrayList<MetaCardSet> getAllMetaSets() throws JsonParseException, JsonMappingException, IOException {
- //mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ /**
+ * @param code
+ * @return the actual cardset (containing cards).
+ * @throws IOException
+ * @throws MalformedURLException
+ * @throws JsonMappingException
+ * @throws JsonParseException
+ */
+ public static CardSet getCardSet(String code) throws JsonParseException, JsonMappingException, MalformedURLException, IOException {
+ return mapper.readValue(new URL(BASE_URL + code + ".json"), CardSet.class);
+ }
+
+ /**
+ * @return a list of metadata for every available set.
+ * @throws JsonParseException
+ * @throws JsonMappingException
+ * @throws IOException
+ */
+ public static ArrayList<MetaCardSet> getAllMetaSets() throws JsonParseException, JsonMappingException, IOException {
return mapper.readValue(new URL(SETS_URL), new TypeReference<ArrayList<MetaCardSet>>() {});
}
+ /**
+ * @param file
+ * @return a CardBase object equivalent to the given file.
+ * @throws JsonParseException
+ * @throws JsonMappingException
+ * @throws IOException
+ */
public static CardBase readCardBase(File file) throws JsonParseException, JsonMappingException, IOException {
return mapper.readValue(file, CardBase.class);
}
+ /**
+ * Writes the provided CardBase to the provided file in JSON format.
+ *
+ * @param file
+ * @param cardBase
+ * @throws JsonGenerationException
+ * @throws JsonMappingException
+ * @throws IOException
+ */
public static void writeCardBase(File file, CardBase cardBase) throws JsonGenerationException, JsonMappingException, IOException {
mapper.writeValue(file, cardBase);
}
diff --git a/src/eu/equalparts/cardbase/query/Test.java b/src/eu/equalparts/cardbase/query/Test.java
index 2050c76..e466c55 100644
--- a/src/eu/equalparts/cardbase/query/Test.java
+++ b/src/eu/equalparts/cardbase/query/Test.java
@@ -59,7 +59,6 @@ public class Test {
for (CardSet set : sets.values()) {
for (Card card : set.cards) {
card.setCode = set.code;
- card.setName = set.name;
// System.out.println(set.getName() + ": " + card.getName());
allCards.add(card);
diff --git a/src/eu/equalparts/cardbase/standalone/CardBaseCLI.java b/src/eu/equalparts/cardbase/standalone/CardBaseCLI.java
index 2806a9e..d0503ac 100644
--- a/src/eu/equalparts/cardbase/standalone/CardBaseCLI.java
+++ b/src/eu/equalparts/cardbase/standalone/CardBaseCLI.java
@@ -4,22 +4,32 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Scanner;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
+import eu.equalparts.cardbase.data.Card;
import eu.equalparts.cardbase.data.CardBaseManager;
+import eu.equalparts.cardbase.data.CardSet;
import eu.equalparts.cardbase.data.MetaCardSet;
+import eu.equalparts.cardbase.query.IO;
/**
* This provides a lightweight CLI for interacting with cardbase files.
*
*/
public class CardBaseCLI {
-
- private static String selectedSet = "";
+
+ private static CardSet selectedSet = null;
+ private static HashMap<String, CardSet> setCache = new HashMap<String, CardSet>();
private static CardBaseManager cbm;
private static boolean exit = false;
+ private static String help = "No help file was found";
+ private static File cardBaseFile;
/**
* Execute the interface.
@@ -33,18 +43,35 @@ public class CardBaseCLI {
try {
// construct the cardbase
if (args.length > 0) {
- System.out.println("Building cardbase from " + args[0]);
- cbm = new CardBaseManager(new File(args[0]));
+ cardBaseFile = new File(args[0]);
+ if (cardBaseFile.exists() && cardBaseFile.isFile() && cardBaseFile.canRead()) {
+ System.out.println("Loading cardbase from " + args[0]);
+ cbm = new CardBaseManager(cardBaseFile);
+ } else {
+ System.out.println(args[0] + " appears to be invalid");
+ System.exit(0);
+ }
} else {
- System.out.println("No cardbase file was provided, initialising a clean cardbase");
- cbm = new CardBaseManager();
+ //System.out.println("No cardbase file was provided, initialising a clean cardbase");
+ System.out.println("Loading testbase for debugging purposes");
+ cardBaseFile = new File("testbase");
+ cbm = new CardBaseManager(cardBaseFile);
}
// initialise necessary components
System.out.println("Fetching card sets from upstream");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+ System.out.println("Loading externals");
+ File helpFile = new File("help");
+ if (helpFile.exists() && helpFile.canRead()) {
+ help = new Scanner(helpFile).useDelimiter("\\Z").next();
+ } else {
+ System.out.println("Help file is not available, I hope you know how to use the program!");
+ }
+
while (!exit) {
+ System.out.print(selectedSet == null ? "> " : selectedSet.code + " > ");
String rawInput = br.readLine().trim().toLowerCase();
String[] commands = rawInput.split("[ \t]+");
@@ -64,21 +91,51 @@ public class CardBaseCLI {
}
- private static void parse(String[] commands) {
+ private static void parse(String[] commands) throws JsonParseException, JsonMappingException, MalformedURLException, IOException {
if (commands.length > 0) {
switch (commands[0]) {
/*
- * Exit procedures
+ * Show help
*/
- case "exit":
- exit = true;
+ case "help":
+ System.out.println(help);
break;
+
/*
- * Show help (externalised?)
+ * Write current CardBase to file
*/
- case "help":
- System.out.println("google it");
+ case "write":
+ if (commands.length > 1) {
+ File output = new File(commands[1]);
+ if (output.exists()) {
+ if(output.isFile()) {
+ if (output.canWrite()) {
+ IO.writeCardBase(output, cbm.cardBase);
+ cardBaseFile = output;
+ System.out.println("Cardbase saved to " + output.getAbsolutePath());
+ } else {
+ System.out.println(commands[1] + " cannot be written to, nothing was written");
+ }
+ } else {
+ System.out.println(commands[1] + " is not a file, nothing was written");
+ }
+ } else {
+ System.out.println(commands[1] + " does not exist, nothing was written");
+ }
+ } else if (cardBaseFile != null) {
+ System.out.println("Writing to " + cardBaseFile.getAbsolutePath());
+ } else {
+ System.out.println("Please provide a file name");
+ }
break;
+
+ /*
+ * Exit procedures
+ */
+ case "exit":
+ exit = true;
+ break;
+
/*
* Print a list of valid set codes
*/
@@ -87,31 +144,132 @@ public class CardBaseCLI {
System.out.println(mcs);
}
break;
+
/*
* Select a set, any card numbers provided will be fetched from that set
*/
case "set":
for (MetaCardSet mcs : cbm.getAllMetaSets()) {
- if (mcs.getCode().equalsIgnoreCase(commands[1])) {
- System.out.println("Selected set: " + mcs.getName());
- selectedSet = mcs.getCode();
+ if (mcs.code.equalsIgnoreCase(commands[1])) {
+ // if the set is cached, use that
+ if (setCache.containsKey(mcs.code)) {
+ selectedSet = setCache.get(mcs.code);
+ } else {
+ selectedSet = IO.getCardSet(mcs.code);
+ setCache.put(mcs.code, selectedSet);
+ }
+ System.out.println("Selected set: " + mcs.name);
return;
}
}
System.out.println(commands[1] + " does not correspond to any set (use \"sets\" to see all valid set codes)");
break;
+
/*
- * Write current CardBase to file
+ * Print a brief list of the complete cardbase.
*/
- case "write":
+ case "glance":
+ for (Iterator<Card> i = cbm.cardIterator(); i.hasNext();) {
+ printGlance(i.next());
+ }
+ break;
+
+ /*
+ * Print a detailed information of a single card or the whole cardbase.
+ */
+ case "peruse":
+ if (commands.length > 1) {
+ if (selectedSet != null) {
+ Card card = cbm.getCard(selectedSet.code, commands[1]);
+ if (card != null) {
+ printPerusal(card);
+ } else {
+ System.out.println("Card not in cardbase");
+ }
+ } else {
+ System.out.println("Please select a set before perusing a specific card");
+ }
+ } else {
+ for (Iterator<Card> i = cbm.cardIterator(); i.hasNext();) {
+ printPerusal(i.next());
+ }
+ }
+ break;
+
+ /*
+ * Remove one or more cards
+ */
+ case "remove":
+ if (selectedSet != null) {
+ if (commands.length > 1) {
+ Card remove = selectedSet.getCardByNumber(commands[1]);
+ if (remove != null) {
+ Integer count = 1;
+ if (commands.length > 2 && commands[2].matches("[0-9]+")) {
+ count = Integer.valueOf(commands[2]);
+ if (count <= 0) {
+ System.out.println("Can't remove " + count + " cards");
+ return;
+ }
+ }
+ cbm.removeCard(remove, count);
+ System.out.println("Removed " + count + "x " + remove.name);
+ } else {
+ System.out.println(commands[1] + " does not correspond to a card in " + selectedSet.name);
+ }
+ } else {
+ System.out.println("Please specify a card number to remove");
+ }
+ } else {
+ System.out.println("Select a set before removing cards.");
+ }
break;
+
/*
- * Try to parse a card number and get it from the set
+ * Add one or more cards
*/
default:
+ if (selectedSet != null) {
+ Card newCard = selectedSet.getCardByNumber(commands[0]);
+ if (newCard != null) {
+ Integer count = 1;
+ if (commands.length > 1 && commands[1].matches("[0-9]+")) {
+ count = Integer.valueOf(commands[1]);
+ if (count <= 0) {
+ System.out.println("Can't add " + count + " cards");
+ return;
+ }
+ }
+ newCard.setCode = selectedSet.code;
+ cbm.addCard(newCard, count);
+ System.out.println("Added " + count + "x " + newCard.name);
+ } else {
+ System.out.println(commands[0] + " does not correspond to a card in " + selectedSet.name);
+ }
+ } else {
+ System.out.println("Select a set before adding cards.");
+ }
break;
}
}
}
-
+
+ private static void printPerusal(Card card) {
+ printGlance(card);
+ if (card.type != null) System.out.println("\t" + card.type);
+ if (card.manaCost != null) System.out.println("\tCost: " + card.manaCost);
+ if (card.power != null && card.toughness != null) System.out.println("\t" + card.power + "/" + card.toughness);
+ if (card.loyalty != null) System.out.println("\tLoyalty: " + card.loyalty);
+
+ if (card.text != null) System.out.println("\t" + card.text.replaceAll("\n", "\n\t"));
+ if (card.flavor != null) System.out.println("\t" + card.flavor.replaceAll("\n", "\n\t"));
+
+ if (card.rarity != null) System.out.println("\t" + card.rarity);
+ if (card.multiverseid != null) System.out.println("\tMID: " + card.multiverseid);
+ if (card.artist != null) System.out.println("\tIllus. " + card.artist);
+ }
+
+ private static void printGlance(Card card) {
+ System.out.println(String.format("%1$-4d %2$s (%3$s, %4$s)", card.count, card.name, card.setCode, card.number));
+ }
}
diff --git a/testbase b/testbase
new file mode 100644
index 0000000..4e1e6f4
--- /dev/null
+++ b/testbase
@@ -0,0 +1 @@
+{"cards":[{"layout":"normal","name":"Divination","names":null,"manaCost":"{2}{U}","cmc":3,"colors":["Blue"],"type":"Sorcery","supertypes":null,"types":["Sorcery"],"subtypes":null,"rarity":"Common","text":"Draw two cards.","flavor":"\"The key to unlocking this puzzle is within you.\"\n—Doriel, mentor of Mistral Isle","artist":"Howard Lyon","number":"52","power":null,"toughness":null,"loyalty":null,"multiverseid":383227,"variations":null,"imageName":"divination","border":null,"watermark":null,"setCode":"M15","count":1},{"layout":"normal","name":"Soulmender","names":null,"manaCost":"{W}","cmc":1,"colors":["White"],"type":"Creature — Human Cleric","supertypes":null,"types":["Creature"],"subtypes":["Human","Cleric"],"rarity":"Common","text":"{T}: You gain 1 life.","flavor":"\"Healing is more art than magic. Well, there is still quite a bit of magic.\"","artist":"James Ryman","number":"35","power":"1","toughness":"1","loyalty":null,"multiverseid":383393,"variations":null,"imageName":"soulmender","border":null,"watermark":null,"setCode":"M15","count":1},{"layout":"normal","name":"Amphin Pathmage","names":null,"manaCost":"{3}{U}","cmc":4,"colors":["Blue"],"type":"Creature — Salamander Wizard","supertypes":null,"types":["Creature"],"subtypes":["Salamander","Wizard"],"rarity":"Common","text":"{2}{U}: Target creature can't be blocked this turn.","flavor":"\"There are those who do not believe in the existence of the amphin. This seems somehow to be of their own design.\"\n—Gor Muldrak, Cryptohistories","artist":"Mark Winters","number":"45","power":"3","toughness":"2","loyalty":null,"multiverseid":383183,"variations":null,"imageName":"amphin pathmage","border":null,"watermark":null,"setCode":"M15","count":1},{"layout":"normal","name":"Seek the Horizon","names":null,"manaCost":"{3}{G}","cmc":4,"colors":["Green"],"type":"Sorcery","supertypes":null,"types":["Sorcery"],"subtypes":null,"rarity":"Uncommon","text":"Search your library for up to three basic land cards, reveal them, and put them into your hand. Then shuffle your library.","flavor":"The Temur call the flickering lights the Path of Whispers, believing that they lead the way to ancestral knowledge.","artist":"Min Yum","number":"150","power":null,"toughness":null,"loyalty":null,"multiverseid":386659,"variations":null,"imageName":"seek the horizon","border":null,"watermark":null,"setCode":"KTK","count":3},{"layout":"normal","name":"Bitter Revelation","names":null,"manaCost":"{3}{B}","cmc":4,"colors":["Black"],"type":"Sorcery","supertypes":null,"types":["Sorcery"],"subtypes":null,"rarity":"Common","text":"Look at the top four cards of your library. Put two of them into your hand and the rest into your graveyard. You lose 2 life.","flavor":"\"Here you lie then, Ugin. The corpses of worlds will join you in the tomb.\"\n—Sorin Markov","artist":"Viktor Titov","number":"65","power":null,"toughness":null,"loyalty":null,"multiverseid":386489,"variations":null,"imageName":"bitter revelation","border":null,"watermark":null,"setCode":"KTK","count":2}],"decks":[]} \ No newline at end of file