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(); + } + +}