diff --git a/Mage.Server/UW Control.dck b/Mage.Server/UW Control.dck
new file mode 100644
index 00000000000..3033c885668
--- /dev/null
+++ b/Mage.Server/UW Control.dck
@@ -0,0 +1,29 @@
+NAME:UW Control
+2 [ROE:236] Island
+1 [ROE:235] Island
+1 [ROE:234] Island
+2 [ROE:233] Island
+2 [CON:15] Path to Exile
+3 [ROE:21] Gideon Jura
+1 [CON:11] Martial Coup
+2 [ZEN:9] Day of Judgment
+1 [ZEN:216] Kabira Crossroads
+4 [WWK:31] Jace, the Mind Sculptor
+3 [M10:64] Mind Spring
+3 [WWK:123] Everflowing Chalice
+1 [ROE:232] Plains
+4 [ROE:53] Wall of Omens
+1 [ROE:229] Plains
+1 [ROE:230] Plains
+1 [ROE:231] Plains
+3 [ALA:20] Oblivion Ring
+4 [ZEN:70] Spreading Seas
+4 [WWK:145] Tectonic Edge
+1 [ALA:9] Elspeth, Knight-Errant
+2 [ROE:59] Deprive
+1 [ZEN:220] Misty Rainforest
+4 [WWK:133] Celestial Colonnade
+1 [ZEN:211] Arid Mesa
+4 [M10:226] Glacial Fortress
+1 [WWK:142] Sejiri Steppe
+2 [M10:65] Negate
diff --git a/Mage.Server/pom.xml b/Mage.Server/pom.xml
index 6c85e253441..d746dea46a3 100644
--- a/Mage.Server/pom.xml
+++ b/Mage.Server/pom.xml
@@ -29,6 +29,11 @@
Mage-Sets
${mage-version}
+
+ junit
+ junit
+ 4.8.2
+
com.sun.xml.bind
jaxb-impl
@@ -83,6 +88,11 @@
${project.version}
runtime
+
+ org.jbehave
+ jbehave-core
+ 3.2-beta-1
+
diff --git a/Mage.Server/src/main/resources/log4j.properties b/Mage.Server/src/main/resources/log4j.properties
new file mode 100644
index 00000000000..cdef4d6b9e0
--- /dev/null
+++ b/Mage.Server/src/main/resources/log4j.properties
@@ -0,0 +1,8 @@
+#default levels
+log4j.rootLogger=info, console
+
+#console log
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern=%-5p [%d{yyyy-MM-dd HH:mm [ss:SSS]}] %C{1}[%t]: %m%n
+log4j.appender.console.Threshold=DEBUG
\ No newline at end of file
diff --git a/Mage.Server/src/test/java/mage/server/bdd/MageEmbedder.java b/Mage.Server/src/test/java/mage/server/bdd/MageEmbedder.java
new file mode 100644
index 00000000000..4e27d6791d9
--- /dev/null
+++ b/Mage.Server/src/test/java/mage/server/bdd/MageEmbedder.java
@@ -0,0 +1,73 @@
+package mage.server.bdd;
+
+import org.jbehave.core.configuration.Configuration;
+import org.jbehave.core.configuration.MostUsefulConfiguration;
+import org.jbehave.core.embedder.Embedder;
+import org.jbehave.core.io.CodeLocations;
+import org.jbehave.core.io.LoadFromRelativeFile;
+import org.jbehave.core.io.StoryResourceNotFound;
+import org.jbehave.core.reporters.StoryReporterBuilder;
+import org.jbehave.core.steps.CandidateSteps;
+import org.jbehave.core.steps.InstanceStepsFactory;
+
+import java.io.File;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Contains configuration for working with steps and stories in Mage.
+ *
+ * @author nantuko
+ */
+public class MageEmbedder extends Embedder {
+
+ /**
+ * We meed to extend LoadFromRelativeFile because of bug in replacing slashes
+ *
+ * @author nantuko
+ */
+ private class MageStoryFilePath extends LoadFromRelativeFile {
+
+ private final StoryFilePath[] traversals;
+ private final URL location;
+
+ public MageStoryFilePath(URL location, StoryFilePath... traversals) {
+ super(location, traversals);
+ this.location = location;
+ this.traversals = traversals;
+ }
+
+ @Override
+ public String loadResourceAsText(String resourcePath) {
+ List traversalPaths = new ArrayList();
+ String locationPath = new File(location.getFile()).getAbsolutePath();
+
+ String filePath = locationPath.replace("target\\test-classes", "src/test/java") + "/" + resourcePath;
+ File file = new File(filePath);
+ if (file.exists()) {
+ return loadContent(filePath);
+ }
+
+ throw new StoryResourceNotFound(resourcePath, traversalPaths);
+ }
+
+ }
+
+ @Override
+ public Configuration configuration() {
+ Class> embedderClass = this.getClass();
+ URL codeLocation = CodeLocations.codeLocationFromClass(embedderClass);
+ Configuration configuration = new MostUsefulConfiguration()
+ .useStoryLoader(new MageStoryFilePath(codeLocation, LoadFromRelativeFile.mavenModuleTestStoryFilePath("src/test/java") ))
+ .useStoryReporterBuilder(new StoryReporterBuilder()
+ .withCodeLocation(codeLocation)
+ .withDefaultFormats());
+ return configuration;
+ }
+
+ @Override
+ public List candidateSteps() {
+ return new InstanceStepsFactory(configuration(), new MageSteps()).createCandidateSteps();
+ }
+}
\ No newline at end of file
diff --git a/Mage.Server/src/test/java/mage/server/bdd/MageSteps.java b/Mage.Server/src/test/java/mage/server/bdd/MageSteps.java
new file mode 100644
index 00000000000..7c59f3c5321
--- /dev/null
+++ b/Mage.Server/src/test/java/mage/server/bdd/MageSteps.java
@@ -0,0 +1,38 @@
+package mage.server.bdd;
+
+import org.jbehave.core.annotations.Given;
+import org.jbehave.core.annotations.Then;
+import org.jbehave.core.annotations.When;
+
+/**
+ * Defines Mage BDD steps.
+ *
+ * @author nantuko
+ */
+public class MageSteps {
+
+ @Given("I'm in the game")
+ public void inTheGame() {
+ System.out.println("In the game");
+ }
+
+ @Given("I have an \"$card\" card")
+ public void hasACard(String card) {
+ System.out.println("card: " + card);
+ }
+
+ @Given("phase is $own \"$phase\"")
+ public void hasPhase(String own, String phase) {
+ System.out.println("phase is: " + own + " -> " + phase);
+ }
+
+ @When("I splay \"$card\"")
+ public void playCard(String card) {
+ System.out.println("play a card: " + card);
+ }
+
+ @Then("there is an \"$card\" on $zone")
+ public void playCard(String card, String zone) {
+ System.out.println("checking: " + card + ", zone=" + zone);
+ }
+}
diff --git a/Mage.Server/src/test/java/mage/server/bdd/MageStoryRunner.java b/Mage.Server/src/test/java/mage/server/bdd/MageStoryRunner.java
new file mode 100644
index 00000000000..f8c142aa1de
--- /dev/null
+++ b/Mage.Server/src/test/java/mage/server/bdd/MageStoryRunner.java
@@ -0,0 +1,21 @@
+package mage.server.bdd;
+
+import org.jbehave.core.io.StoryFinder;
+
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+/**
+ * Runs Mage stories (tests)
+ *
+ * @author nantuko
+ */
+public class MageStoryRunner {
+ public static void main(String[] argv) throws Throwable {
+ MageEmbedder embedder = new MageEmbedder();
+ List storyPaths = (new StoryFinder()).findPaths("src/test/java", asList("stories/land.story"), null);
+ System.out.println("Found stories count: " + storyPaths.size());
+ embedder.runStoriesAsPaths(storyPaths);
+ }
+}
diff --git a/Mage.Server/src/test/java/mage/server/bdd/StoryRunPOC.java b/Mage.Server/src/test/java/mage/server/bdd/StoryRunPOC.java
new file mode 100644
index 00000000000..96f2f1fd373
--- /dev/null
+++ b/Mage.Server/src/test/java/mage/server/bdd/StoryRunPOC.java
@@ -0,0 +1,152 @@
+package mage.server.bdd;
+
+import mage.interfaces.MageException;
+import mage.interfaces.Server;
+import mage.interfaces.ServerState;
+import mage.interfaces.callback.CallbackClient;
+import mage.interfaces.callback.CallbackClientDaemon;
+import mage.interfaces.callback.ClientCallback;
+import mage.server.Main;
+import mage.sets.Sets;
+import mage.util.Logging;
+import mage.view.*;
+import org.junit.Test;
+
+import java.rmi.NotBoundException;
+import java.rmi.RemoteException;
+import java.rmi.registry.LocateRegistry;
+import java.rmi.registry.Registry;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Proof of concept of running game from tests.\
+ * Will be removed later when BDD is finished.
+ *
+ * @author nantuko
+ */
+public class StoryRunPOC {
+
+ private static Logger logger = Logging.getLogger(StoryRunPOC.class.getName());
+
+ private static UUID sessionId;
+ private static Server server;
+ private static String userName;
+ private static ServerState serverState;
+ private static CallbackClientDaemon callbackDaemon;
+ private static UUID gameId;
+ private static UUID playerId;
+ private static CardView cardPlayed;
+
+ @Test
+ public void testEmpty() {
+
+ }
+
+ public static void main(String[] argv) throws Exception {
+ String[] args = new String[] {"-testMode=true"};
+ Main.main(args);
+ connect("player", "localhost", 17171);
+ UUID roomId = server.getMainRoomId();
+
+ List playerTypes = new ArrayList();
+ playerTypes.add("Human");
+ playerTypes.add("Computer - default");
+ TableView table = server.createTable(sessionId, roomId, "Two Player Duel", "Limited", playerTypes, null, null);
+ System.out.println("Cards in the deck: " + Sets.loadDeck("UW Control.dck").getCards().size());
+ server.joinTable(sessionId, roomId, table.getTableId(), "Human", Sets.loadDeck("UW Control.dck"));
+ server.joinTable(sessionId, roomId, table.getTableId(), "Computer", Sets.loadDeck("UW Control.dck"));
+ server.startGame(sessionId, roomId, table.getTableId());
+ }
+
+ public static void connect(String userName, String serverName, int port) {
+ try {
+ System.setSecurityManager(null);
+ Registry reg = LocateRegistry.getRegistry(serverName, port);
+ server = (Server) reg.lookup("mage-server");
+ sessionId = server.registerClient(userName, UUID.randomUUID());
+ CallbackClient client = new CallbackClient(){
+ @Override
+ public void processCallback(ClientCallback callback) {
+ logger.info("IN >> " + callback.getMessageId() + " - " + callback.getMethod());
+ try {
+ if (callback.getMethod().equals("startGame")) {
+ UUID[] data = (UUID[]) callback.getData();
+ gameId = data[0];
+ playerId = data[1];
+ server.joinGame(gameId, sessionId);
+ } else if (callback.getMethod().equals("gameInit")) {
+ server.ack("gameInit", sessionId);
+ } else if (callback.getMethod().equals("gameAsk")) {
+ GameClientMessage message = (GameClientMessage) callback.getData();
+ logger.info("ASK >> " + message.getMessage());
+ if (message.getMessage().equals("Do you want to take a mulligan?")) {
+ server.sendPlayerBoolean(gameId, sessionId, false);
+ }
+ } else if (callback.getMethod().equals("gameTarget")) {
+ GameClientMessage message = (GameClientMessage) callback.getData();
+ logger.info("TARGET >> " + message.getMessage() + " >> " + message.getTargets());
+ if (message.getMessage().equals("Select a starting player")) {
+ logger.info(" Sending >> " + playerId);
+ server.sendPlayerUUID(gameId, sessionId, playerId);
+ }
+ } else if (callback.getMethod().equals("gameSelect")) {
+ GameClientMessage message = (GameClientMessage) callback.getData();
+ logger.info("SELECT >> " + message.getMessage());
+ if (!message.getMessage().startsWith("Precombat Main - play spells and sorceries.")) {
+ server.sendPlayerBoolean(gameId, sessionId, false);
+ } else {
+ if (cardPlayed == null) {
+ CardsView cards = message.getGameView().getHand();
+ CardView landToPlay = null;
+ for (CardView card : cards.values()) {
+ //System.out.println(card.getName());
+ if (card.getName().equals("Plains") || card.getName().equals("Island")) {
+ landToPlay = card;
+ }
+ }
+ if (landToPlay != null) {
+ logger.info("Playing " + landToPlay);
+ server.sendPlayerUUID(gameId, sessionId, landToPlay.getId());
+ cardPlayed = landToPlay;
+ } else {
+ logger.warning("Couldn't find land to play");
+ }
+ } else {
+ logger.info("Checking battlefield...");
+ boolean foundPlayer = false;
+ boolean foundLand = false;
+ for (PlayerView player: message.getGameView().getPlayers()) {
+ if (player.getPlayerId().equals(playerId)) {
+ foundPlayer = true;
+ for (PermanentView permanent : player.getBattlefield().values()) {
+ if (permanent.getId().equals(cardPlayed.getId())) {
+ foundLand = true;
+ }
+ }
+ break;
+ }
+ }
+ logger.info(" found player: " + foundPlayer);
+ logger.info(" found land: " + foundLand);
+ System.exit(0);
+ }
+ }
+ }
+ } catch (Exception e) {
+ logger.info(e.getMessage());
+ }
+ }
+ };
+ callbackDaemon = new CallbackClientDaemon(sessionId, client, server);
+ serverState = server.getServerState();
+ } catch (MageException ex) {
+ logger.log(Level.SEVERE, null, ex);
+ } catch (RemoteException ex) {
+ logger.log(Level.SEVERE, "Unable to connect to server - ", ex);
+ } catch (NotBoundException ex) {
+ logger.log(Level.SEVERE, "Unable to connect to server - ", ex);
+ }
+ }
+}