diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..18d70f0
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/.project b/.project
new file mode 100644
index 0000000..44a8490
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+
+
+ thunderstone
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/src/com/mehtank/thunderstone/api/GameEventListener.java b/src/com/mehtank/thunderstone/api/GameEventListener.java
new file mode 100644
index 0000000..ece8ce2
--- /dev/null
+++ b/src/com/mehtank/thunderstone/api/GameEventListener.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.api;
+
+import com.mehtank.thunderstone.engine.GameEvent;
+
+public interface GameEventListener {
+ public void gameEvent(GameEvent event);
+}
diff --git a/src/com/mehtank/thunderstone/api/GameInterface.java b/src/com/mehtank/thunderstone/api/GameInterface.java
new file mode 100644
index 0000000..cb74e7d
--- /dev/null
+++ b/src/com/mehtank/thunderstone/api/GameInterface.java
@@ -0,0 +1,16 @@
+package com.mehtank.thunderstone.api;
+
+import java.util.Random;
+
+import com.mehtank.thunderstone.engine.Card;
+import com.mehtank.thunderstone.engine.GameEvent;
+
+public interface GameInterface {
+ public Random getRand();
+
+ public void trash(Card c);
+ public void broadcastEvent(GameEvent event);
+
+ public void info(String s);
+ public void debug(String s);
+}
diff --git a/src/com/mehtank/thunderstone/api/LogReader.java b/src/com/mehtank/thunderstone/api/LogReader.java
new file mode 100644
index 0000000..bc0db44
--- /dev/null
+++ b/src/com/mehtank/thunderstone/api/LogReader.java
@@ -0,0 +1,5 @@
+package com.mehtank.thunderstone.api;
+
+public interface LogReader {
+ void log(String s);
+}
diff --git a/src/com/mehtank/thunderstone/api/PlayHandler.java b/src/com/mehtank/thunderstone/api/PlayHandler.java
new file mode 100644
index 0000000..c927a56
--- /dev/null
+++ b/src/com/mehtank/thunderstone/api/PlayHandler.java
@@ -0,0 +1,10 @@
+package com.mehtank.thunderstone.api;
+
+import com.mehtank.thunderstone.comms.GameQuery;
+
+public interface PlayHandler {
+
+ String getName();
+ GameQuery query(GameQuery p);
+
+}
diff --git a/src/com/mehtank/thunderstone/api/PlayerInterface.java b/src/com/mehtank/thunderstone/api/PlayerInterface.java
new file mode 100644
index 0000000..5092eaa
--- /dev/null
+++ b/src/com/mehtank/thunderstone/api/PlayerInterface.java
@@ -0,0 +1,24 @@
+package com.mehtank.thunderstone.api;
+
+import com.mehtank.thunderstone.engine.Card;
+import com.mehtank.thunderstone.engine.SelectCardOptions;
+
+public interface PlayerInterface {
+ public String getName();
+
+ public Card[] getHand();
+ public int getHandSize();
+ public int getDiscardSize();
+ public int getDeckSize();
+
+ public void gain(Card card);
+ public void discard(Card card);
+ public void discardHand();
+
+ public Card drawFromDeck();
+ public void draw();
+ public void draw(int numCards);
+
+ public Card pickACard(SelectCardOptions sco, Card[] hand);
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/CardGroup.java b/src/com/mehtank/thunderstone/cards/CardGroup.java
new file mode 100644
index 0000000..d545c11
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/CardGroup.java
@@ -0,0 +1,56 @@
+package com.mehtank.thunderstone.cards;
+
+import java.util.ArrayList;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import com.mehtank.thunderstone.engine.Card;
+import com.mehtank.thunderstone.engine.Strings;
+
+public class CardGroup {
+ String type;
+ String name;
+ String title;
+ ArrayList cards = new ArrayList();
+
+ public CardGroup(Element e, Cards allCards) {
+ type = e.getAttribute("type");
+ name = e.getAttribute("name");
+ title = e.getAttribute("title");
+
+ NodeList cardNodes = e.getElementsByTagName("card");
+ for (int i = 0; i < cardNodes.getLength(); i++) {
+ Node node = cardNodes.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ Card card = allCards.getCard(element.getAttribute("name"));
+ card.addAttribute("type", type);
+ card.addAttribute("grouptitle", title);
+ cards.add(card);
+ }
+ }
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public ArrayList getCards() {
+ return cards;
+ }
+
+ public boolean equals(String s) {
+ String cName = Strings.getCanonicalName(name);
+ return (cName.equals(Strings.getCanonicalName(s)));
+ }
+}
diff --git a/src/com/mehtank/thunderstone/cards/Cards.java b/src/com/mehtank/thunderstone/cards/Cards.java
new file mode 100644
index 0000000..d6abfa0
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/Cards.java
@@ -0,0 +1,149 @@
+package com.mehtank.thunderstone.cards;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Random;
+import java.util.TreeMap;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import com.mehtank.thunderstone.engine.Card;
+import com.mehtank.thunderstone.engine.CardPile;
+import com.mehtank.thunderstone.engine.XMLUtils;
+
+public class Cards {
+ private String set = "";
+ private ArrayList cards = new ArrayList();
+ private ArrayList groups = new ArrayList();
+
+ public String getSet() {
+ return set;
+ }
+ public Card[] getCards() {
+ return cards.toArray(new Card[0]);
+ }
+
+ public Cards() {
+ loadCards();
+ }
+
+ public Card getCard(String name) {
+ for (Card c : cards)
+ if (c.equals(name))
+ return c.copy();
+ return null;
+ }
+
+ public CardPile getCardPile(String name) {
+ return getCardPile(name, -1);
+ }
+
+ public CardPile getCardPile(String name, int count) {
+ Card c = getCard(name);
+ if (c == null)
+ return null;
+
+ if (count < 0) count = c.getCount();
+
+ CardPile p = new CardPile(name);
+ for (int i = 0; i < count; i++)
+ c.copy().moveTo(p);
+ return p;
+ }
+
+ public CardPile getHeroPile(String name) {
+ return getGroupPile(name, "level");
+ }
+ public CardPile getMonsterPile(String name) {
+ return getGroupPile(name, "xp");
+ }
+
+ public CardPile getGroupPile(String name, String sort) {
+ for (CardGroup g : groups) {
+ if (g.equals(name)) {
+ CardPile p = new CardPile(g.getTitle());
+ TreeMap hash = new TreeMap();
+ for (Card c : g.getCards())
+ hash.put(c.getInt(sort) + new Random().nextFloat(), c); // XXX is this robust enough?
+ for (Card c : hash.values())
+ for (int i = 0; i < c.getCount() ; i++)
+ c.copy().moveTo(p);
+ return p;
+ }
+ }
+ return null;
+ }
+
+ private void loadCards() {
+ InputStream xmlStream = this.getClass().getResourceAsStream("cards.xml");
+ DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder dBuilder;
+ try {
+ dBuilder = dbFactory.newDocumentBuilder();
+ } catch (ParserConfigurationException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return;
+ }
+ Document doc;
+ try {
+ doc = dBuilder.parse(xmlStream);
+ } catch (SAXException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return;
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return;
+ }
+ Element root = doc.getDocumentElement();
+ root.normalize();
+
+ String rootname = root.getNodeName();
+
+ if (!rootname.equals("cards"))
+ return;
+
+ set = root.getAttribute("set");
+
+ for (Element e : XMLUtils.getElementListByTagName(root, "card"))
+ makeCard(e);
+ for (Element e : XMLUtils.getElementListByTagName(root, "cardgroup"))
+ groups.add(new CardGroup(e, this));
+ }
+
+ private void makeCard(Element element) {
+ String name = element.getAttribute("name");
+ String classname = this.getClass().getPackage().getName() + "." + name;
+ Class> c = null;
+ try {
+ c = Class.forName(classname);
+ } catch (ClassNotFoundException e) {
+ return;
+ }
+
+ if (c == null)
+ return;
+
+ Card card;
+ try {
+ card = (Card) c.newInstance();
+ } catch (InstantiationException e) {
+ return;
+ } catch (IllegalAccessException e) {
+ return;
+ }
+
+ card.setXML(element);
+ cards.add(card);
+ }
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/ArchdukeOfPain.java b/src/com/mehtank/thunderstone/cards/base/ArchdukeOfPain.java
new file mode 100644
index 0000000..96d62c1
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/ArchdukeOfPain.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.base;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class ArchdukeOfPain extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/Barkeep.java b/src/com/mehtank/thunderstone/cards/base/Barkeep.java
new file mode 100644
index 0000000..4713973
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/Barkeep.java
@@ -0,0 +1,28 @@
+package com.mehtank.thunderstone.cards.base;
+
+import com.mehtank.thunderstone.engine.Card;
+import com.mehtank.thunderstone.engine.Context;
+import com.mehtank.thunderstone.engine.Effect;
+
+public class Barkeep extends Card {
+ public Barkeep() {
+ super();
+
+ final Card me = this;
+
+ addEffect("plusbuy", new Effect() {
+ @Override
+ public boolean effect(Context context) {
+ context.addBuys(1);
+ return true;
+ }});
+
+ addEffect("plusgold", new Effect() {
+ @Override
+ public boolean effect(Context context) {
+ context.getGame().trash(me);
+ context.addGold(2);
+ return true;
+ }});
+ }
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/Cards.java b/src/com/mehtank/thunderstone/cards/base/Cards.java
new file mode 100644
index 0000000..76f4d0d
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/Cards.java
@@ -0,0 +1,5 @@
+package com.mehtank.thunderstone.cards.base;
+
+public class Cards extends com.mehtank.thunderstone.cards.Cards {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/Grudgebeast.java b/src/com/mehtank/thunderstone/cards/base/Grudgebeast.java
new file mode 100644
index 0000000..6caf622
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/Grudgebeast.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.base;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class Grudgebeast extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/Succubus.java b/src/com/mehtank/thunderstone/cards/base/Succubus.java
new file mode 100644
index 0000000..55f5efb
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/Succubus.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.base;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class Succubus extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/TheUnchained.java b/src/com/mehtank/thunderstone/cards/base/TheUnchained.java
new file mode 100644
index 0000000..13cb6cf
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/TheUnchained.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.base;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class TheUnchained extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/ThyrianKnight.java b/src/com/mehtank/thunderstone/cards/base/ThyrianKnight.java
new file mode 100644
index 0000000..130cc5e
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/ThyrianKnight.java
@@ -0,0 +1,53 @@
+package com.mehtank.thunderstone.cards.base;
+
+import com.mehtank.thunderstone.api.PlayerInterface;
+import com.mehtank.thunderstone.engine.Card;
+import com.mehtank.thunderstone.engine.Context;
+import com.mehtank.thunderstone.engine.Effect;
+import com.mehtank.thunderstone.engine.GameEvent;
+import com.mehtank.thunderstone.engine.SelectCardOptions;
+import com.mehtank.thunderstone.engine.Strings;
+
+public class ThyrianKnight extends Card {
+
+ public ThyrianKnight() {
+ super();
+
+ addEffect("militiagain", new Effect() {
+ @Override
+ public boolean effect(Context context) {
+ // TODO: implement
+ return false;
+ }
+ });
+
+ addEffect("destroyfood", new Effect() {
+ @Override
+ public boolean effect(Context context) {
+ PlayerInterface currentPlayer = context.getCurrentPlayer();
+
+ SelectCardOptions sco = new SelectCardOptions()
+ .fromHand()
+ .hasParameter("trait", "food")
+ .isPassable()
+ .to(Strings.toDestroy);
+
+ Card[] foodCards = sco.filter(currentPlayer.getHand());
+
+ if (foodCards.length == 0)
+ return false;
+
+ Card c = currentPlayer.pickACard(sco, foodCards);
+
+ if (c != null) {
+ context.getGame().broadcastEvent(new GameEvent(GameEvent.Type.TRASHES, currentPlayer).setCard(c));
+ context.getGame().trash(c);
+ context.addAttack(2);
+ return true;
+ }
+
+ return false;
+ }
+ });
+ }
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/ThyrianLord.java b/src/com/mehtank/thunderstone/cards/base/ThyrianLord.java
new file mode 100644
index 0000000..241cad4
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/ThyrianLord.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.base;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class ThyrianLord extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/ThyrianSquire.java b/src/com/mehtank/thunderstone/cards/base/ThyrianSquire.java
new file mode 100644
index 0000000..0d06c0f
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/ThyrianSquire.java
@@ -0,0 +1,45 @@
+package com.mehtank.thunderstone.cards.base;
+
+import com.mehtank.thunderstone.api.PlayerInterface;
+import com.mehtank.thunderstone.engine.Card;
+import com.mehtank.thunderstone.engine.Context;
+import com.mehtank.thunderstone.engine.Effect;
+import com.mehtank.thunderstone.engine.GameEvent;
+import com.mehtank.thunderstone.engine.SelectCardOptions;
+import com.mehtank.thunderstone.engine.Strings;
+
+public class ThyrianSquire extends Card {
+
+ public ThyrianSquire() {
+ super();
+
+ addEffect("destroyfood", new Effect() {
+ @Override
+ public boolean effect(Context context) {
+ PlayerInterface currentPlayer = context.getCurrentPlayer();
+
+ SelectCardOptions sco = new SelectCardOptions()
+ .fromHand()
+ .hasParameter("trait", "food")
+ .isPassable()
+ .to(Strings.toDestroy);
+
+ Card[] foodCards = sco.filter(currentPlayer.getHand());
+
+ if (foodCards.length == 0)
+ return false;
+
+ Card c = currentPlayer.pickACard(sco, foodCards);
+
+ if (c != null) {
+ context.getGame().broadcastEvent(new GameEvent(GameEvent.Type.TRASHES, currentPlayer).setCard(c));
+ context.getGame().trash(c);
+ context.addAttack(2);
+ return true;
+ }
+
+ return false;
+ }});
+
+ }
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/Tormentor.java b/src/com/mehtank/thunderstone/cards/base/Tormentor.java
new file mode 100644
index 0000000..764c08c
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/Tormentor.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.base;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class Tormentor extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/base/cards.xml b/src/com/mehtank/thunderstone/cards/base/cards.xml
new file mode 100644
index 0000000..82dbabb
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/base/cards.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/com/mehtank/thunderstone/cards/common/Cards.java b/src/com/mehtank/thunderstone/cards/common/Cards.java
new file mode 100644
index 0000000..9800838
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/common/Cards.java
@@ -0,0 +1,5 @@
+package com.mehtank.thunderstone.cards.common;
+
+public class Cards extends com.mehtank.thunderstone.cards.Cards {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/common/Dagger.java b/src/com/mehtank/thunderstone/cards/common/Dagger.java
new file mode 100644
index 0000000..16b256d
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/common/Dagger.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.common;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class Dagger extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/common/Disease.java b/src/com/mehtank/thunderstone/cards/common/Disease.java
new file mode 100644
index 0000000..d55dcd2
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/common/Disease.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.common;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class Disease extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/common/IronRations.java b/src/com/mehtank/thunderstone/cards/common/IronRations.java
new file mode 100644
index 0000000..b7c4dcd
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/common/IronRations.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.common;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class IronRations extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/common/Militia.java b/src/com/mehtank/thunderstone/cards/common/Militia.java
new file mode 100644
index 0000000..ac33a58
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/common/Militia.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.common;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class Militia extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/common/StoneOfMystery.java b/src/com/mehtank/thunderstone/cards/common/StoneOfMystery.java
new file mode 100644
index 0000000..070df1b
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/common/StoneOfMystery.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.common;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class StoneOfMystery extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/common/Torch.java b/src/com/mehtank/thunderstone/cards/common/Torch.java
new file mode 100644
index 0000000..1d1abdb
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/common/Torch.java
@@ -0,0 +1,7 @@
+package com.mehtank.thunderstone.cards.common;
+
+import com.mehtank.thunderstone.engine.Card;
+
+public class Torch extends Card {
+
+}
diff --git a/src/com/mehtank/thunderstone/cards/common/cards.xml b/src/com/mehtank/thunderstone/cards/common/cards.xml
new file mode 100644
index 0000000..82d6ccd
--- /dev/null
+++ b/src/com/mehtank/thunderstone/cards/common/cards.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/com/mehtank/thunderstone/comms/GameQuery.java b/src/com/mehtank/thunderstone/comms/GameQuery.java
new file mode 100644
index 0000000..7704455
--- /dev/null
+++ b/src/com/mehtank/thunderstone/comms/GameQuery.java
@@ -0,0 +1,54 @@
+package com.mehtank.thunderstone.comms;
+
+import com.mehtank.thunderstone.engine.Game.GameState;
+
+public class GameQuery {
+ public enum QueryType {
+ GETNAME, NAME,
+ GETCARD, CARD,
+ GETOPTION, OPTION,
+ ORDERCARDS, CARDORDER,
+
+ GAMEEVENT,
+ }
+
+ public QueryType t; // type of query
+ public QueryType r; // type of response requested; null if no response necessary
+
+ public String s;
+ public boolean b;
+ public int i;
+ public Object o;
+ public GameState gs;
+
+ public GameQuery(QueryType t, QueryType r) {
+ this.t = t;
+ this.r = r;
+ }
+
+ public GameQuery setState(GameState gs) {
+ this.gs = gs;
+ return this;
+ }
+
+ public GameQuery setType(QueryType r) {
+ this.t = r;
+ return this;
+ }
+ public GameQuery setString(String s) {
+ this.s = s;
+ return this;
+ }
+ public GameQuery setBoolean(boolean b) {
+ this.b = b;
+ return this;
+ }
+ public GameQuery setInteger(int i) {
+ this.i = i;
+ return this;
+ }
+ public GameQuery setObject (Object o) {
+ this.o = o;
+ return this;
+ }
+}
diff --git a/src/com/mehtank/thunderstone/engine/Card.java b/src/com/mehtank/thunderstone/engine/Card.java
new file mode 100644
index 0000000..6fba449
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/Card.java
@@ -0,0 +1,130 @@
+package com.mehtank.thunderstone.engine;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import org.w3c.dom.Element;
+
+import com.mehtank.thunderstone.engine.Effect.EffectType;
+
+public class Card {
+ Element xmlDefinition;
+ String name;
+
+ CardPile parent;
+
+ public String getName() { return name; }
+
+ public String getStr(String s) { return xmlDefinition.getAttribute(s); };
+ public String getTitle() { return getStr("title"); }
+ public String getText() { return getStr("text"); }
+ public String getType() { return getStr("type"); }
+ public String getTrait() { return getStr("trait"); }
+
+ public Integer getInt(String s) { return XMLUtils.getIntFromXML(xmlDefinition, s); }
+ public int getCount() { return getInt("count"); }
+ public Integer getCost() { return getInt("cost"); }
+ public Integer getGold() { return getInt("gold"); }
+ public Integer getWeight() { return getInt("weight"); }
+ public Integer getStrength() { return getInt("strength"); }
+ public Integer getLight() { return getInt("light"); }
+ public Integer getLevel() { return getInt("level"); }
+ public Integer getVP() { return getInt("vp"); }
+ public Integer getXP() { return getInt("xp"); }
+ public Integer getHealth() { return getInt("health"); }
+
+ HashMap effects = new HashMap();
+
+ public Card () {}
+
+ public Card (Element e) {
+ setXML(e);
+ }
+
+ public void setXML(Element e) {
+ xmlDefinition = e;
+
+ name = e.getAttribute("name");
+ }
+
+ public void addEffect(String name, Effect effect) {
+ effects.put(name, effect);
+ }
+
+ public ArrayList getEffects(EffectType type) {
+ ArrayList es = new ArrayList();
+
+ for (Element e : XMLUtils.getElementListByTagName(xmlDefinition, "effect")) {
+ try {
+ EffectType eType = EffectType.valueOf(e.getAttribute("type").toUpperCase());
+ if (eType.equals(type)) {
+ Effect effect = effects.get(e.getAttribute("name"));
+ if (effect == null)
+ continue;
+ effect.setText(e.getAttribute("text"));
+ es.add(effect);
+ }
+ } catch (IllegalArgumentException ex) {
+ // XXX ignore?
+ }
+ }
+
+ return es;
+ }
+
+ public void moveTo(CardPile cardlist) {
+ if (cardlist == null)
+ return;
+
+ if (parent != null)
+ parent.remove(this);
+ parent = cardlist;
+ parent.add(this);
+ }
+ public void moveTo(CardPile cardlist, int index) {
+ if (parent != null)
+ parent.remove(this);
+ parent = cardlist;
+ parent.add(index, this);
+ }
+
+ public boolean equals(Card c) {
+ return (name.equals(c.getName()));
+ }
+ public boolean equals(String s) {
+ String cName = Strings.getCanonicalName(name);
+ return (cName.equals(Strings.getCanonicalName(s)));
+ }
+
+ public Card copy() {
+ Card c = null;
+ try {
+ c = this.getClass().getDeclaredConstructor().newInstance();
+ c.setXML(xmlDefinition);
+ } catch (InstantiationException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return c;
+ }
+
+ public void addAttribute(String name, String value) {
+ xmlDefinition.setAttribute(name, value);
+ }
+}
diff --git a/src/com/mehtank/thunderstone/engine/CardPile.java b/src/com/mehtank/thunderstone/engine/CardPile.java
new file mode 100644
index 0000000..2c0dae6
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/CardPile.java
@@ -0,0 +1,28 @@
+package com.mehtank.thunderstone.engine;
+
+import java.util.ArrayList;
+
+public class CardPile extends ArrayList {
+ private static final long serialVersionUID = 6916512102368602030L;
+
+ private String name;
+
+ String getName() {
+ return name;
+ }
+
+ public CardPile(String name) {
+ super();
+ this.name = name;
+ }
+
+ public Card[] toArray() {
+ return toArray(new Card[0]);
+ }
+
+ public Card top() {
+ if (size() > 0)
+ return get(0);
+ return null;
+ }
+}
diff --git a/src/com/mehtank/thunderstone/engine/Context.java b/src/com/mehtank/thunderstone/engine/Context.java
new file mode 100644
index 0000000..3011b8e
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/Context.java
@@ -0,0 +1,40 @@
+package com.mehtank.thunderstone.engine;
+
+import com.mehtank.thunderstone.api.GameInterface;
+import com.mehtank.thunderstone.api.PlayerInterface;
+
+public class Context {
+
+ private Game game;
+ private Player currentPlayer;
+ private int gold = 0;
+ private int attack = 0;
+ private int magic = 0;
+ private int buys = 1;
+
+ public Context(Game game, Player currentPlayer) {
+ this.game = game;
+ this.currentPlayer = currentPlayer;
+ }
+
+ public GameInterface getGame() { return game; }
+ public PlayerInterface getCurrentPlayer() { return currentPlayer; }
+ public int getGold() { return gold; }
+ public int getBuys() { return buys; }
+ public int getAttack() { return attack; }
+ public int getMagic() { return magic; }
+
+ public void addGold(int gold) {
+ this.gold += gold;
+ }
+ public void addBuys(int buys) {
+ this.buys += buys;
+ }
+ public void addAttack(int attack) {
+ this.attack += attack;
+ }
+ public void addMagic(int magic) {
+ this.magic += magic;
+ }
+
+}
diff --git a/src/com/mehtank/thunderstone/engine/Effect.java b/src/com/mehtank/thunderstone/engine/Effect.java
new file mode 100644
index 0000000..394a656
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/Effect.java
@@ -0,0 +1,30 @@
+package com.mehtank.thunderstone.engine;
+
+public abstract class Effect {
+ public enum EffectType {
+ VILLAGE,
+ DUNGEON,
+ REPEATVILLAGE,
+ REPEATDUNGEON,
+ VILLAGEDUNGEON,
+ ALWAYSVILLAGE,
+ ALWAYSDUNGEON,
+ BATTLE,
+ BREACH}
+
+ String text = "";
+ String success = "";
+ String failure = "";
+
+
+ public Effect setText(String text) { this.text = text; return this; }
+ public Effect setSuccess(String success) { this.success = success; return this; }
+ public Effect setFailure(String failure) { this.failure = failure; return this; }
+
+ public String getText() { return text; }
+ public String getSuccess() { return success; }
+ public String getFailure() { return failure; }
+
+ public abstract boolean effect(Context context);
+
+}
diff --git a/src/com/mehtank/thunderstone/engine/Game.java b/src/com/mehtank/thunderstone/engine/Game.java
new file mode 100644
index 0000000..aeb881b
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/Game.java
@@ -0,0 +1,380 @@
+package com.mehtank.thunderstone.engine;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Random;
+
+import com.mehtank.thunderstone.api.GameEventListener;
+import com.mehtank.thunderstone.api.GameInterface;
+import com.mehtank.thunderstone.api.LogReader;
+import com.mehtank.thunderstone.api.PlayHandler;
+import com.mehtank.thunderstone.cards.Cards;
+import com.mehtank.thunderstone.engine.Effect.EffectType;
+import com.mehtank.thunderstone.engine.Logger.LogLevel;
+import com.mehtank.thunderstone.engine.Player.PlayerState;
+import com.mehtank.thunderstone.engine.Tableau.TableauState;
+
+public class Game implements GameInterface, GameEventListener, LogReader {
+ private long randSeed;
+ private Random rand;
+ private Logger logger = new Logger();
+
+// private GameOptions options;
+ private Tableau table;
+
+ private static Cards common = new com.mehtank.thunderstone.cards.common.Cards();
+
+ private ArrayList players = new ArrayList();
+ private int currentPlayerIndex = 0;
+ private Player currentPlayer;
+ private int turnCount = 0;
+
+ private Context context;
+
+ private boolean gameOver = false;
+ private ArrayList listeners = new ArrayList();
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Setup and initialization
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ public Game(Tableau t, PlayHandler[] ps) {
+ newGame(t, ps, new GameOptions());
+ }
+ public Game(Tableau t, PlayHandler[] ps, GameOptions o) {
+ newGame(t, ps, o);
+ }
+
+ private void newGame(Tableau t, PlayHandler[] ps, GameOptions o) {
+ logger.addReader(LogLevel.GAME, this);
+ logger.addReader(LogLevel.INFO, this);
+ logger.addReader(LogLevel.DEBUG, this);
+ listeners.add(this);
+
+ randSeed = System.currentTimeMillis();
+ debug("Starting seed = " + randSeed);
+
+ rand = new Random(randSeed);
+
+// this.options = o;
+ this.table = t;
+
+ // TODO: validate t, ps
+ setupTable();
+ setupPlayers(ps);
+ }
+
+ private void setupTable() {
+ info("Setting up table");
+ table.addVillagePile(common.getCardPile("Dagger"));
+ table.addVillagePile(common.getCardPile("IronRations"));
+ table.addVillagePile(common.getCardPile("Torch"));
+ table.addVillagePile(common.getCardPile("Militia"));
+
+ table.fillDungeon();
+ }
+ private void setupPlayers(PlayHandler[] ps) {
+ info("Setting up players");
+ logger.indent();
+ for (PlayHandler p : ps) {
+ info("Creating player " + p.getName());
+ logger.indent();
+ Player player = new Player(p, this);
+ listeners.add(player);
+
+ info("Dealing cards");
+ for (int i = 0; i < 2; i++) {
+ player.gain(table.getCardFromPile("Dagger"));
+ player.gain(table.getCardFromPile("IronRations"));
+ player.gain(table.getCardFromPile("Torch"));
+ }
+ for (int i = 0; i < 6; i++) {
+ player.gain(table.getCardFromPile("Militia"));
+ }
+ info("Drawing starting hand");
+ player.draw(6);
+
+ players.add(player);
+ logger.unindent();
+ }
+ info("Seating players");
+ Collections.shuffle(players);
+ logger.unindent();
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Play order
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ public void play() {
+ broadcastEvent(new GameEvent(GameEvent.Type.STARTGAME, null));
+ while (!gameOver ) {
+ currentPlayer = players.get(currentPlayerIndex);
+ broadcastEvent(new GameEvent(GameEvent.Type.STARTTURN, currentPlayer));
+ context = new Context(this, currentPlayer);
+
+ // TODO - Reveal Hand
+
+ Integer i = currentPlayer.selectOption(Strings.chooseAction, new String[] {Strings.visitVillage, Strings.enterDungeon, Strings.rest});
+ if (i == null) i = 2;
+ if (i.equals(0)) { // visit village
+ broadcastEvent(new GameEvent(GameEvent.Type.VISITSVILLAGE, currentPlayer));
+ logger.indent();
+ doVillage();
+ } else if (i.equals(1)) { // enter dungeon
+ broadcastEvent(new GameEvent(GameEvent.Type.ENTERSDUNGEON, currentPlayer));
+ logger.indent();
+ doDungeon();
+ } else { // rest or error
+ broadcastEvent(new GameEvent(GameEvent.Type.RESTS, currentPlayer));
+ logger.indent();
+ doRest();
+ }
+ logger.unindent();
+
+ info("Cleaning up");
+ logger.indent();
+ currentPlayer.discardHand();
+ currentPlayer.draw(6);
+ logger.unindent();
+
+ currentPlayerIndex = (currentPlayerIndex + 1) % players.size();
+ turnCount ++;
+ }
+ }
+
+ private void doVillage() {
+ // Optional village effects
+ pickEffects(EffectType.VILLAGE, EffectType.REPEATVILLAGE);
+
+ // Mandatory village effects
+ mandatoryEffects(EffectType.ALWAYSVILLAGE);
+
+ // Tally money
+ for (Card c: currentPlayer.getHand())
+ if (c.getGold() != null)
+ context.addGold(c.getGold());
+
+ // Pick cards to buy
+ while (context.getBuys() > 0) {
+ int maxCost = context.getGold();
+ SelectCardOptions sco = new SelectCardOptions()
+ .fromVillage()
+ .isPassable()
+ .maxCost(maxCost)
+ .to(Strings.toBuy);
+
+ Card c = currentPlayer.pickACard(sco, table.getBuyableCards(maxCost));
+
+ if (c == null)
+ break;
+
+ Card bought = table.getCardFromPile(c);
+
+ // TODO: handle errors better
+ if (bought == null)
+ continue;
+ if (bought.getCost() > maxCost)
+ continue;
+
+ broadcastEvent(new GameEvent(GameEvent.Type.BUYS, currentPlayer).setCard(bought));
+ currentPlayer.gain(bought);
+
+ context.addGold(-bought.getCost());
+ context.addBuys(-1);
+ }
+
+ // TODO - Upgrade heroes
+ }
+
+ private void doDungeon() {
+ // Optional dungeon effects
+ pickEffects(EffectType.DUNGEON, EffectType.REPEATDUNGEON);
+
+ // TODO - Equip heroes
+ // Mandatory dungeon effects
+ mandatoryEffects(EffectType.ALWAYSDUNGEON);
+
+ // Select monster
+ Card[] monsters = table.getMonstersToFight();
+ int monsterRank = pickMonster(monsters);
+ Card monster = monsters[monsterRank];
+
+ // TODO - Calculate total attack and defense
+
+ // Resolve battle effects
+ for (Effect e : monster.getEffects(EffectType.BATTLE)) // XXX choose order?
+ doEffect(e);
+
+ // TODO - Determine winner and move monster
+ // TODO - Receive spoils
+ // Shift monster cards / refill hall and resolve breach
+ for (Effect e : table.fillDungeon()) // XXX choose order?
+ doEffect(e);
+ }
+
+ int pickMonster(Card[] monsters) {
+ ArrayList opts = new ArrayList();
+ for (int i = 0; i < monsters.length; i++)
+ if (monsters[i] != null)
+ opts.add(Strings.tableauStateDungeonRank + " " + i + ": " + monsters[i].getTitle());
+ Integer i = currentPlayer.selectOption(Strings.chooseMonster, opts.toArray(new String[0]));
+
+ if (i == null || i.intValue() < 0 || i.intValue() >= monsters.length || monsters[i.intValue()] == null)
+ return 1; // XXX error!
+
+ return i.intValue();
+ }
+
+ private void doRest() {
+ // pick a card to destroy
+ SelectCardOptions sco = new SelectCardOptions()
+ .fromHand()
+ .isPassable()
+ .to(Strings.toDestroy);
+
+ Card c = currentPlayer.pickACard(sco, currentPlayer.getHand());
+
+ if (c != null) {
+ broadcastEvent(new GameEvent(GameEvent.Type.TRASHES, currentPlayer).setCard(c));
+ trash(c);
+ }
+ }
+
+ void pickEffects(EffectType once, EffectType repeat) {
+ ArrayList effects = new ArrayList();
+ for (Card c: currentPlayer.getHand())
+ effects.addAll(c.getEffects(once));
+
+ ArrayList repeatEffects = new ArrayList();
+ for (Card c: currentPlayer.getHand())
+ repeatEffects.addAll(c.getEffects(repeat));
+
+ while (effects.size() + repeatEffects.size() > 0) {
+ ArrayList opts = new ArrayList();
+ for (Effect e : effects)
+ opts.add(e.getText());
+ for (Effect e : repeatEffects)
+ opts.add(e.getText());
+
+ Integer i = currentPlayer.selectOption(Strings.chooseEffect, opts.toArray(new String[0]));
+ if (i == null) break;
+
+ Effect e;
+ if (i < effects.size())
+ e = effects.remove(i.intValue());
+ else
+ e = repeatEffects.get(i - effects.size());
+ doEffect(e);
+ }
+ }
+
+ void mandatoryEffects(EffectType type) {
+ ArrayList mandatoryEffects = new ArrayList();
+ for (Card c: currentPlayer.getHand())
+ mandatoryEffects.addAll(c.getEffects(type));
+ for (Effect e : mandatoryEffects) // XXX choose order?
+ doEffect(e);
+ }
+
+ void doEffect(Effect e) {
+ e.effect(context);
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Game Mechanics [ GameInterface ]
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ @Override
+ public void broadcastEvent(GameEvent event) {
+ for (GameEventListener l : listeners )
+ l.gameEvent(event);
+ }
+
+ public void trash(Card c) {
+ table.trash(c);
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Debug and logging [ GameInterface, GameEventListener, LogReader ]
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ @Override
+ public Random getRand() {
+ return rand;
+ }
+ @Override
+ public void info(String s) {
+ logger.log(LogLevel.INFO, s);
+ }
+ @Override
+ public void debug(String s) {
+ logger.log(LogLevel.DEBUG, s);
+ }
+
+ @Override
+ public void gameEvent(GameEvent event) {
+ String str = "";
+ if (event.getPlayer() != null) str += "<" + event.getPlayer().getName() + "> ";
+ str += event.getType().toString();
+ if (event.getCard() != null) str += " (" + event.getPlayer().getName() + ")";
+ if (event.getStr() != null) str += ": " + event.getStr();
+
+ logger.log(LogLevel.GAME, str);
+ }
+
+ @Override
+ public void log(String s) {
+ System.out.println(s);
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // State
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ public class GameState {
+ PlayerState[] playerStates;
+ TableauState tableauState;
+ Card[] hand;
+
+ int turnCount;
+ int currentPlayer;
+
+ public GameState(Game g) {
+ playerStates = new PlayerState[g.players.size()];
+ int i=0;
+ for (Player p : g.players)
+ playerStates[i++] = p.new PlayerState(p);
+ currentPlayer = g.currentPlayerIndex;
+ turnCount = g.turnCount;
+ tableauState = g.table.getState();
+ }
+
+ public GameState setHand(Card[] hand) {
+ this.hand = hand; return this;
+ }
+
+ public String toString() {
+ String s = tableauState.toString() + "\n";
+
+ s += Strings.playerStateHeader + "\n";
+ int i=0;
+ for (PlayerState ps : playerStates) {
+ if (i++ == currentPlayer)
+ s += "->";
+ else
+ s += " ";
+ s += ps.toString() + "\n";
+ }
+ if (hand != null) {
+ s += "\n" + Strings.currentHand + "\n";
+ for (Card c : hand)
+ s += c.getTitle() + "\n";
+ }
+
+ return s;
+ }
+ }
+
+ public GameState getState() {
+ return new GameState(this);
+ }
+
+}
diff --git a/src/com/mehtank/thunderstone/engine/GameEvent.java b/src/com/mehtank/thunderstone/engine/GameEvent.java
new file mode 100644
index 0000000..b5299c4
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/GameEvent.java
@@ -0,0 +1,56 @@
+package com.mehtank.thunderstone.engine;
+
+import com.mehtank.thunderstone.api.PlayerInterface;
+
+public class GameEvent {
+ public enum Type {
+ STARTGAME, // A new game is starting, called at the start of each game when multiple are played
+
+ STARTTURN, // Player begins a turn
+
+ VISITSVILLAGE, // Player visits village
+ ENTERSDUNGEON, // Player enters dungeon
+ RESTS, // Player rests
+
+ BUYS, // Buying a card in the buy phase.
+
+ RESHUFFLES, // Discard pile shuffled to create a new deck for one of the players
+
+ OBTAINS, // Card was obtained by a player through an effect of an action
+ TRASHES, // Card removed from the game
+ REVEALS, // Card revealed by an action
+
+ ENDTURN, // Player's turn ends
+
+ GAMEOVER, // Game completed
+ }
+
+ private Type type; // type of event
+ private PlayerInterface who; // Player who generated the event
+ private Card card; // optional, depending on event type
+ private String str; // optional, depending on event type
+
+ public GameEvent(Type type, PlayerInterface currentPlayer) {
+ this.type = type;
+ this.who = currentPlayer;
+ }
+ public GameEvent setString(String str) {
+ this.str = str; return this;
+ }
+ public GameEvent setCard(Card card) {
+ this.card = card; return this;
+ }
+
+ public Type getType() {
+ return type;
+ }
+ public PlayerInterface getPlayer() {
+ return who;
+ }
+ public Card getCard() {
+ return card;
+ }
+ public String getStr() {
+ return str;
+ }
+}
diff --git a/src/com/mehtank/thunderstone/engine/GameOptions.java b/src/com/mehtank/thunderstone/engine/GameOptions.java
new file mode 100644
index 0000000..582804e
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/GameOptions.java
@@ -0,0 +1,10 @@
+package com.mehtank.thunderstone.engine;
+
+public class GameOptions {
+ public boolean keepUndefeatedMonster = false;
+
+ public GameOptions keepUndefeatedMonster() {
+ this.keepUndefeatedMonster = true; return this;
+ }
+
+}
diff --git a/src/com/mehtank/thunderstone/engine/Logger.java b/src/com/mehtank/thunderstone/engine/Logger.java
new file mode 100644
index 0000000..f5b08c2
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/Logger.java
@@ -0,0 +1,52 @@
+package com.mehtank.thunderstone.engine;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.Map;
+
+import com.mehtank.thunderstone.api.LogReader;
+
+public class Logger {
+
+ public enum LogLevel {GAME, INFO, DEBUG};
+
+ private static final String TAB = " ";
+ private int indentLevel = 0;
+
+ private Map bullet = new EnumMap(LogLevel.class);
+ private Map> readers = new EnumMap>(LogLevel.class);
+
+ public Logger() {
+ bullet.put(LogLevel.GAME, "* ");
+ bullet.put(LogLevel.INFO, "> ");
+ bullet.put(LogLevel.DEBUG, "! ");
+ }
+
+ public void addReader(LogLevel l, LogReader r) {
+ if (readers.get(l) == null)
+ readers.put(l, new ArrayList());
+ readers.get(l).add(r);
+ }
+
+ public void indent() {
+ indentLevel++;
+ }
+ public void unindent() {
+ if (indentLevel > 0)
+ indentLevel--;
+ }
+
+ public void log(LogLevel l, String s) {
+ for (LogReader r : readers.get(l))
+ r.log(bullet.get(l) + tabify(s));
+ }
+
+ private String tabify(String s) {
+ String r = "";
+ for (int i = 0; i < indentLevel; i++) {
+ r += TAB;
+ }
+ return r+s;
+ }
+
+}
diff --git a/src/com/mehtank/thunderstone/engine/Player.java b/src/com/mehtank/thunderstone/engine/Player.java
new file mode 100644
index 0000000..f94cf9e
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/Player.java
@@ -0,0 +1,131 @@
+package com.mehtank.thunderstone.engine;
+
+import com.mehtank.thunderstone.api.GameEventListener;
+import com.mehtank.thunderstone.api.PlayHandler;
+import com.mehtank.thunderstone.api.PlayerInterface;
+import com.mehtank.thunderstone.comms.GameQuery;
+
+public class Player extends PlayerQuerys implements PlayerInterface, GameEventListener {
+
+ private PlayHandler playHandler;
+ private Game game;
+
+ private CardPile hand = new CardPile(Strings.playerHandPile);
+ private CardPile discard = new CardPile(Strings.playerDiscardPile);
+ private CardPile deck = new CardPile(Strings.playerDeckPile);
+
+ public Player(PlayHandler p, Game game) {
+ this.playHandler = p;
+ this.game = game;
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Internal actions
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ private void replenishDeck() {
+ while (discard.size() > 0) {
+ int i = game.getRand().nextInt(discard.size());
+ discard.get(i).moveTo(deck);
+ }
+
+ GameEvent event = new GameEvent(GameEvent.Type.RESHUFFLES, this);
+ game.broadcastEvent(event);
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Game event notifications [ GameEventListener ]
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ @Override
+ public void gameEvent(GameEvent event) {
+ // TODO Auto-generated method stub
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Player interactions [ PlayerInterface ]
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ @Override
+ public String getName() {
+ return playHandler.getName();
+ }
+
+ @Override
+ public Card[] getHand() {
+ return hand.toArray();
+ }
+ @Override
+ public int getHandSize() { return hand.size(); }
+ @Override
+ public int getDiscardSize() { return discard.size(); }
+ @Override
+ public int getDeckSize() { return deck.size(); }
+
+ @Override
+ public void gain(Card card) {
+ discard(card);
+ }
+ @Override
+ public void discard(Card card) {
+ card.moveTo(discard);
+ }
+ @Override
+ public void discardHand() {
+ while (hand.size() > 0)
+ discard(hand.get(0));
+ }
+
+ @Override
+ public Card drawFromDeck() {
+ if (getDeckSize() == 0)
+ replenishDeck();
+ if (getDeckSize() == 0)
+ return null;
+
+ return deck.get(0);
+ }
+
+ @Override
+ public void draw() {
+ Card c = drawFromDeck();
+ if (c != null)
+ c.moveTo(hand);
+ }
+ @Override
+ public void draw(int numCards) {
+ for (int i = 0; i < numCards; i++)
+ draw();
+ }
+
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Player queries
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ @Override
+ public GameQuery query(GameQuery q) {
+ return playHandler.query(q.setState(game.getState().setHand(getHand())));
+ }
+
+
+ public class PlayerState {
+ String name = "";
+ int handSize = 0;
+ int deckSize = 0;
+ int discardSize = 0;
+
+ public PlayerState(Player p) {
+ name = p.getName();
+ handSize = p.getHandSize();
+ deckSize = p.getDeckSize();
+ discardSize = p.getDiscardSize();
+ }
+
+ public String toString() {
+ String s = name;
+ s += " < ";
+ s += Strings.playerDeckSize + deckSize + " , ";
+ s += Strings.playerHandSize + handSize + " , ";
+ s += Strings.playerDiscardSize + discardSize + " >";
+
+ return s;
+ }
+ }
+
+}
diff --git a/src/com/mehtank/thunderstone/engine/PlayerQuerys.java b/src/com/mehtank/thunderstone/engine/PlayerQuerys.java
new file mode 100644
index 0000000..0af5d4f
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/PlayerQuerys.java
@@ -0,0 +1,104 @@
+package com.mehtank.thunderstone.engine;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import com.mehtank.thunderstone.comms.GameQuery;
+import com.mehtank.thunderstone.comms.GameQuery.QueryType;
+
+public abstract class PlayerQuerys {
+
+ public Card pickACard(SelectCardOptions sco, Card[] allcards) {
+ sco.exactly(1);
+
+ Card[] cs = pickCards(sco, allcards);
+ if (cs == null)
+ return null;
+ if (cs.length > 0)
+ for (Card c : allcards)
+ if (c == cs[0])
+ return c;
+ return null;
+ }
+
+ public Card[] pickCards(SelectCardOptions sco, Card[] allcards) {
+ GameQuery p = new GameQuery(QueryType.GETCARD, QueryType.CARD)
+ .setObject(sco);
+
+ p = query(p);
+ if (p == null)
+ return null;
+ if (p.t != QueryType.CARD)
+ return null;
+ if (p.o instanceof Card[] || p.o instanceof String[]) {
+ String[] selected;
+ if (p.o instanceof Card[]) {
+ ArrayList a = new ArrayList();
+ for (Card c : (Card[]) p.o)
+ a.add(c.getName());
+ selected = a.toArray(new String[0]);
+ } else
+ selected = (String[])p.o;
+
+ ArrayList ret = new ArrayList();
+ ArrayList all = new ArrayList(Arrays.asList(allcards));
+ for (int i = 0; i < selected.length; i++) {
+ for (int j = 0; j < all.size(); j++) {
+ if (all.get(j).equals(selected[i])) {
+ ret.add(all.get(j));
+ all.remove(j);
+ break;
+ }
+ }
+ }
+ return ret.toArray(new Card[0]);
+ }
+
+ return null;
+ }
+
+ public Integer selectOption(String header, String[] s) {
+ GameQuery p = new GameQuery(QueryType.GETOPTION, QueryType.OPTION)
+ .setString(header)
+ .setObject(s);
+ p = query(p);
+ if (p == null)
+ return null;
+ if (p.t != QueryType.OPTION)
+ return null;
+ if (p.i < 0 || p.i >= s.length)
+ return null;
+
+ return p.i;
+ }
+
+ public Integer[] orderCards(String header, Card[] cards) {
+ GameQuery p = new GameQuery(QueryType.ORDERCARDS, QueryType.CARDORDER)
+ .setString(header)
+ .setObject(cards);
+
+ p = query(p);
+ if (p == null)
+ return null;
+ else
+ try {
+ return (Integer[])p.o;
+ } catch (ClassCastException e) {
+ return null;
+ }
+ }
+
+ public boolean selectBoolean(String header, String strTrue, String strFalse) {
+ String [] s = new String [] {strTrue, strFalse};
+ Integer i = selectOption(header, s);
+ if (i == null)
+ return false;
+
+ if (i.equals(0))
+ return true;
+
+ return false;
+ }
+
+ public abstract GameQuery query(GameQuery p);
+}
\ No newline at end of file
diff --git a/src/com/mehtank/thunderstone/engine/SelectCardOptions.java b/src/com/mehtank/thunderstone/engine/SelectCardOptions.java
new file mode 100644
index 0000000..3b1dccc
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/SelectCardOptions.java
@@ -0,0 +1,94 @@
+package com.mehtank.thunderstone.engine;
+
+import java.util.ArrayList;
+
+public class SelectCardOptions {
+ public String header = "";
+
+ enum fromLocations {HAND, VILLAGE, DUNGEON, HEROES, ALL};
+
+ fromLocations from = fromLocations.ALL;
+ int maxCost = -1;
+ int minCost = -1;
+ int count = 1;
+ boolean exactly = true;
+ boolean isPassable = false;
+ boolean ordered = false;
+ ArrayList parameters = new ArrayList ();
+
+ // Prompt
+ public SelectCardOptions to(String s) {header = s; return this;}
+
+ // Location of card
+ public SelectCardOptions fromHand() {from = fromLocations.HAND; return this;}
+ public SelectCardOptions fromVillage() {from = fromLocations.VILLAGE; return this;}
+
+ // Number of cards
+ public SelectCardOptions exactly(int c) {count = c; return this;}
+ public SelectCardOptions atMost(int c) {count = c; exactly = false; return this;}
+
+ // Return options
+ public SelectCardOptions isPassable() {isPassable = true; return this;}
+ public SelectCardOptions ordered() {ordered = true; return this;}
+
+ // Filters
+ public SelectCardOptions maxCost(int c) {maxCost = c; return this;}
+ public SelectCardOptions minCost(int c) {minCost = c; return this;}
+ public SelectCardOptions hasParameter(String key, String value) { parameters.add(new String[] {key, value}); return this; }
+
+ public boolean isValid(Card c) {
+ if ((maxCost >= 0) && (c.getCost() > maxCost)) return false;
+ if ((minCost >= 0) && (c.getCost() < minCost)) return false;
+ for (String[] param : parameters) {
+ try {
+ if (!c.getStr(param[0]).equals(param[1]))
+ return false;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public String toString() {
+ // TODO: move strings to Strings
+
+ String str = "Select ";
+
+ if (exactly) str += "exactly ";
+ else str += "up to ";
+
+ str += count + " ";
+
+ str += "cards ";
+
+ switch (from) {
+ case HAND:
+ str += Strings.fromHand;
+ break;
+ case VILLAGE:
+ str += Strings.fromVillage;
+ break;
+ }
+
+ if (ordered) str += "in order ";
+
+ if (minCost >= 0 || maxCost >= 0) {
+ str += "that costs ";
+ if (minCost >= 0) str += "at least �" + minCost + " ";
+ if (maxCost >= 0) str += "at most �" + maxCost + " ";
+ }
+
+ str += "to " + header;
+ if (isPassable) str += " (or pass)";
+ return str;
+ }
+
+ public Card[] filter(Card[] input) {
+ ArrayList output = new ArrayList();
+ for (Card c : input)
+ if (isValid(c))
+ output.add(c);
+ return output.toArray(new Card[0]);
+ }
+}
\ No newline at end of file
diff --git a/src/com/mehtank/thunderstone/engine/Strings.java b/src/com/mehtank/thunderstone/engine/Strings.java
new file mode 100644
index 0000000..9d33d27
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/Strings.java
@@ -0,0 +1,47 @@
+package com.mehtank.thunderstone.engine;
+
+public class Strings {
+ public static final String chooseAction = "Choose your action";
+
+ public static final String visitVillage = "Visit the village";
+ public static final String enterDungeon = "Enter the dungeon";
+ public static final String rest = "Rest";
+
+ public static final String pickOneCard = "Select a card ";
+
+ public static final String fromHand = "from your hand ";
+ public static final String fromVillage = "from the village ";
+
+ public static final String toDestroy = "to destroy.";
+ public static final String toBuy = "to buy.";
+
+ public static final String playerHandPile = "Hand";
+ public static final String playerDeckPile = "Deck";
+ public static final String playerDiscardPile = "Discard";
+ public static final String trashPile = "Trash";
+
+ public static final String tableauStateHeader = "*** Tableau ***";
+ public static final String tableauStateDungeon = "Dungeon: ";
+ public static final String tableauStateDungeonRank = "Rank";
+ public static final String tableauStateDungeonDeck = "Deck";
+ public static final String tableauStateHeroes = "Heroes: ";
+ public static final String tableauStateVillage = "Village: ";
+ public static final String tableauStateTrash = "Trash";
+
+ public static final String playerStateHeader = "*** Players ***";
+ public static final String playerHandSize = "*";
+ public static final String playerDeckSize = "=";
+ public static final String playerDiscardSize = "#";
+
+ public static final String currentHand = "*** Current Hand ***";
+
+ public static final String chooseEffect = "Choose an effect";
+
+ public static final String chooseMonster = "Choose a monster to fight";
+
+
+ public static String getCanonicalName(String orig) {
+ String s = new String(orig);
+ return s.replaceAll("\\W","").toLowerCase();
+ }
+}
diff --git a/src/com/mehtank/thunderstone/engine/Tableau.java b/src/com/mehtank/thunderstone/engine/Tableau.java
new file mode 100644
index 0000000..e0f8046
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/Tableau.java
@@ -0,0 +1,204 @@
+package com.mehtank.thunderstone.engine;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.TreeMap;
+
+import com.mehtank.thunderstone.engine.Effect.EffectType;
+
+public class Tableau {
+ private static final int dungeonDeck = 4;
+
+ private CardPile[] dungeon = new CardPile[dungeonDeck + 1];
+
+ private ArrayList heroes = new ArrayList();
+ private ArrayList village = new ArrayList();
+ private ArrayList other = new ArrayList();
+ private CardPile trash = new CardPile(Strings.trashPile);
+
+ private ArrayList allPiles = new ArrayList();
+
+ public Tableau() {
+ for (int i = 0; i < dungeonDeck; i++) {
+ dungeon[i] = new CardPile("Dungeon rank " + i);
+ allPiles.add(dungeon[i]);
+ }
+ dungeon[dungeonDeck] = new CardPile("Dungeon deck");
+ }
+
+ public Card[] getBuyableCards() {
+ return getBuyableCards(Integer.MAX_VALUE);
+ }
+
+ public Card[] getBuyableCards(int maxCost) {
+ ArrayList all = new ArrayList();
+
+ all.addAll(getBuyableCards(heroes, maxCost));
+ all.addAll(getBuyableCards(village, maxCost));
+ return all.toArray(new Card[0]);
+ }
+
+ public ArrayList getBuyableCards(ArrayList piles, int maxCost) {
+ ArrayList all = new ArrayList();
+
+ for (CardPile p : piles)
+ if (p.top() != null)
+ if (p.top().getCost() <= maxCost)
+ all.add(p.top());
+
+ return all;
+ }
+
+ public void addHeroPile(CardPile p) {
+ if (p == null) return;
+ heroes.add(p);
+ allPiles.add(p);
+ }
+ public void addVillagePile(CardPile p) {
+ if (p == null) return;
+ village.add(p);
+ allPiles.add(p);
+ }
+ public void addOtherPile(CardPile p) {
+ if (p == null) return;
+ other.add(p);
+ allPiles.add(p);
+ }
+ public void addDungeonPile(CardPile monsterPile) {
+ while (monsterPile.size() > 0)
+ monsterPile.get(0).moveTo(dungeon[dungeonDeck]);
+ Collections.shuffle(dungeon[dungeonDeck]);
+ }
+
+ private boolean advanceDungeon() {
+ for (int i = 1; i < dungeonDeck; i++) {
+ if (dungeon[i].size() > 0)
+ continue;
+ boolean changed = false;
+ for (int j = i; j < dungeonDeck; j++) {
+ if (dungeon[j+1].size() > 0) {
+ dungeon[j+1].get(0).moveTo(dungeon[j]);
+ changed = true;
+ }
+ }
+ return changed;
+ }
+ return false;
+ }
+
+ public ArrayList fillDungeon() {
+ boolean breached = (dungeon[1].size() == 0);
+
+ while (advanceDungeon());
+
+ if (breached && dungeon[1].size() > 0)
+ return dungeon[1].get(0).getEffects(EffectType.BREACH);
+
+ return new ArrayList();
+ }
+
+ public Card[] getMonstersToFight() {
+ Card[] monsters = new Card[dungeonDeck];
+
+ for (int i = 0; i < dungeonDeck; i++)
+ if (dungeon[i].size() > 0)
+ monsters[i] = dungeon[i].get(0);
+
+ return monsters;
+ }
+
+ public Card getCardFromPile(String s) {
+ for (CardPile p : allPiles)
+ if (p.size() > 0)
+ if (p.getName().equals(s))
+ return p.top();
+ return null;
+ }
+
+ public Card getCardFromPile(Card c) {
+ for (CardPile p : allPiles)
+ if (p.size() > 0)
+ if (c.equals(p.top()))
+ return p.top();
+ return null;
+ }
+
+ public void trash(Card c) {
+ c.moveTo(trash);
+ }
+
+ public static class TableauState {
+ public Card[] dungeon;
+ public int dungeonDeck;
+ LinkedHashMap> heroes = new LinkedHashMap>();
+ LinkedHashMap village = new LinkedHashMap();
+ public int trash;
+
+ public TableauState (Tableau t) {
+ dungeon = new Card[t.dungeon.length - 1];
+ for (int i=0; i < dungeon.length; i++) {
+ dungeon[i] = t.dungeon[i].top();
+ }
+ dungeonDeck = t.dungeon[t.dungeon.length - 1].size();
+ for (CardPile p : t.heroes) {
+ if (p == null)
+ continue;
+ if (p.size() == 0)
+ continue;
+ TreeMap hero = new TreeMap();
+ for (Card c : p) {
+ if (hero.containsKey(c.getLevel()))
+ hero.put(c.getLevel(), 1 + hero.get(c.getLevel()));
+ else
+ hero.put(c.getLevel(), 1);
+ }
+ heroes.put(p.top(), hero);
+ }
+ for (CardPile p : t.village)
+ if (p != null && p.size() > 0)
+ village.put(p.top(), p.size());
+
+ trash = t.trash.size();
+ }
+
+ public String toString() {
+ String state = Strings.tableauStateHeader + "\n";
+
+ state += Strings.tableauStateDungeon + "\n";
+ for (int i=0; i < dungeon.length; i++) {
+ state += " " + Strings.tableauStateDungeonRank + " " + i + ": ";
+ if (dungeon[i] == null)
+ state += "\n";
+ else
+ state += dungeon[i].getTitle() + "\n";
+ }
+ state += " " + Strings.tableauStateDungeonDeck + " (";
+ state += dungeonDeck + ")\n";
+
+ state += Strings.tableauStateHeroes + "\n";
+ for (Card c : heroes.keySet()) {
+ state += " " + c.getTitle() + " ( ";
+ for (Integer i : heroes.get(c).values())
+ state += i + " ";
+ state += ")\n";
+ }
+
+ state += Strings.tableauStateVillage + "\n";
+ for (Card c : village.keySet()) {
+ state += " " + c.getTitle() + " (";
+ state += village.get(c) + ")\n";
+ }
+
+ state += Strings.tableauStateTrash + " (";
+ state += trash + ")\n";
+
+ return state;
+ }
+ }
+
+ public TableauState getState() {
+ return new TableauState(this);
+ }
+
+}
diff --git a/src/com/mehtank/thunderstone/engine/XMLUtils.java b/src/com/mehtank/thunderstone/engine/XMLUtils.java
new file mode 100644
index 0000000..0a79c92
--- /dev/null
+++ b/src/com/mehtank/thunderstone/engine/XMLUtils.java
@@ -0,0 +1,29 @@
+package com.mehtank.thunderstone.engine;
+
+import java.util.ArrayList;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+public class XMLUtils {
+ public static Integer getIntFromXML(Element e, String attribute) {
+ String s = e.getAttribute(attribute);
+ if (s == null || s.isEmpty())
+ return null;
+ return Integer.parseInt(s);
+ }
+
+ public static Element[] getElementListByTagName(Element e, String name) {
+ NodeList cardNodes = e.getElementsByTagName(name);
+ ArrayList elements = new ArrayList();
+ for (int i = 0; i < cardNodes.getLength(); i++) {
+ Node node = cardNodes.item(i);
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+ elements.add(element);
+ }
+ }
+ return elements.toArray(new Element[0]);
+ }
+}
diff --git a/src/com/mehtank/thunderstone/test/CliPlayer.java b/src/com/mehtank/thunderstone/test/CliPlayer.java
new file mode 100644
index 0000000..9ec6d41
--- /dev/null
+++ b/src/com/mehtank/thunderstone/test/CliPlayer.java
@@ -0,0 +1,74 @@
+package com.mehtank.thunderstone.test;
+
+import java.io.IOException;
+
+import com.mehtank.thunderstone.api.PlayHandler;
+import com.mehtank.thunderstone.comms.GameQuery;
+import com.mehtank.thunderstone.engine.SelectCardOptions;
+
+public class CliPlayer implements PlayHandler {
+ String name = "Clyde";
+
+ public CliPlayer(String name) {
+ super();
+ this.name = name;
+ }
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public GameQuery query(GameQuery p) {
+ // TODO Auto-generated method stub
+ System.out.println(p.gs.toString());
+ System.out.println("------------------------------------------------");
+ if (p.s != null) System.out.println(p.s);
+ switch (p.t) {
+ case GETOPTION:
+ return getOption(p);
+ case GETCARD:
+ return getCard(p);
+ }
+ return null;
+ }
+
+ private GameQuery getOption(GameQuery p) {
+ int i = 0;
+ String[] opts = (String[]) p.o;
+ for (String s : opts)
+ System.out.println(i++ + ": " + s);
+ System.out.print("? ");
+ try {
+ i = Integer.parseInt(readString());
+ } catch (Exception e) {
+ i = -1;
+ };
+ return new GameQuery(p.r, null).setInteger(i);
+ }
+
+ private GameQuery getCard(GameQuery p) {
+ SelectCardOptions sco = (SelectCardOptions) p.o;
+ System.out.println(sco.toString());
+
+ System.out.print("? ");
+ String choice = readString();
+ return new GameQuery(p.r, null).setObject(new String[] {choice});
+ }
+
+ static String readString() {
+ StringBuilder sb = new StringBuilder();
+ try {
+ do {
+ int input = System.in.read();
+ if (input != -1 && input != 10 && input != 13)
+ sb.append((char) input);
+ } while (System.in.available() != 0);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return sb.toString();
+ }
+
+}
diff --git a/src/com/mehtank/thunderstone/test/GameTest.java b/src/com/mehtank/thunderstone/test/GameTest.java
new file mode 100644
index 0000000..2494f52
--- /dev/null
+++ b/src/com/mehtank/thunderstone/test/GameTest.java
@@ -0,0 +1,26 @@
+package com.mehtank.thunderstone.test;
+
+import com.mehtank.thunderstone.api.PlayHandler;
+import com.mehtank.thunderstone.cards.Cards;
+import com.mehtank.thunderstone.engine.Game;
+import com.mehtank.thunderstone.engine.Tableau;
+
+public class GameTest {
+
+ public static void main(String[] args) {
+
+ Cards baseCards = new com.mehtank.thunderstone.cards.base.Cards();
+
+ Tableau t = new Tableau();
+
+ t.addHeroPile(baseCards.getHeroPile("Thyrian"));
+ t.addVillagePile(baseCards.getCardPile("barkeep"));
+ t.addDungeonPile(baseCards.getMonsterPile("Abyssal"));
+
+ PlayHandler p1 = new CliPlayer("Player D");
+ PlayHandler p2 = new CliPlayer("Player T");
+ Game g = new Game(t, new PlayHandler[] {p1, p2});
+ g.play();
+ }
+
+}