Clean up testing
[troll.git] / src / test / java / TrollTest.java
index e54d413..9112b28 100644 (file)
@@ -1,6 +1,10 @@
 import java.util.Properties;
+import java.util.HashSet;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Map;
+import java.util.Random;
+import java.util.stream.Collectors;
 import java.io.PrintStream;
 
 import static org.junit.Assert.assertEquals;
@@ -10,44 +14,198 @@ import org.junit.Test;
 import com.codingame.gameengine.runner.MultiplayerGameRunner;
 import com.codingame.gameengine.runner.dto.*;
 
-public class TrollTest {
-    @Test
-    public void drawGame() {
+public class TrollTest implements Cloneable {
+
+    // ==================== Game Runner encapsulation
+
+    private int league = 1; // @#$%^&* this parameter is *global* despite API!
+    private Long seed;
+    private Integer roadLength;
+    private Integer initialStones;
+
+    private TrollTest branch() {
+        try { return (TrollTest) clone(); }
+        catch (CloneNotSupportedException e) { throw new InternalError(e); }
+    }
+    private TrollTest setLeague(int l) { league = l; return this; }
+    private TrollTest setSeed(long s) { seed = s; return this; }
+    private TrollTest setRoadLength(int l) { roadLength = l; return this; }
+    private TrollTest setInitialStones(int s) { initialStones = s; return this; }
+
+    private GameResult runGame(String left, String right) {
         MultiplayerGameRunner gameRunner = new MultiplayerGameRunner();
+        gameRunner.setLeagueLevel(league); // mandatory
+        if (seed != null) gameRunner.setSeed(seed); else gameRunner.setSeed(0l);
+
         Properties gameParameters = new Properties();
-        gameParameters.setProperty("roadLength", "6");
-        gameParameters.setProperty("initialStones", "15");
         gameRunner.setGameParameters(gameParameters);
-        gameRunner.setSeed(0l);
-        gameRunner.setLeagueLevel(1);
+        if (roadLength != null)
+            gameParameters.setProperty("roadLength", roadLength.toString());
+        if (initialStones != null)
+            gameParameters.setProperty("initialStones",
+                                       initialStones.toString());
 
-        gameRunner.addAgent(agentOne);
-        gameRunner.addAgent(agentOne);
+        gameRunner.addAgent(left);
+        gameRunner.addAgent(right);
 
-        assertIsDraw(gameRunner.simulate());
+        return gameRunner.simulate();
     }
 
+    // ==================== Single-round game result testing
+
     @Test
-    public void crashGame() {
-        MultiplayerGameRunner gameRunner = new MultiplayerGameRunner();
-        Properties gameParameters = new Properties();
-        gameParameters.setProperty("roadLength", "6");
-        gameParameters.setProperty("initialStones", "15");
-        gameRunner.setGameParameters(gameParameters);
-        gameRunner.setSeed(0l);
-        gameRunner.setLeagueLevel(1);
+    public void drawGame() {
+        assertIsDraw(runGame(agentOne, agentOne));
+    }
+
+    @Test
+    public void defeatGames() {
+        assertIsDefeat(runGame(agentCrash, agentCrash));
+        assertIsDefeat(runGame(agentGarbage, agentGarbage));
+    }
 
-        gameRunner.addAgent(agentCrash);
-        gameRunner.addAgent(agentCrash);
+    @Test
+    public void simpleGames() {
+        // wins by direct reach, no fastforward
+        assertWinLose(agentTwo, agentOne);
+
+        // win by fastforward after loser exhaustion
+        assertWinLose(agentOne, agentAllIn);
+
+        // win despite fastforward after winner exhaustion
+        // (harder to construct :-D )
+        assertWinLose(agent(1,2,2,2,8), agent(3,1,1,1,8));
+    }
+
+    // ==================== Cheating games (league 1 perk)
 
-        assertIsDefeat(gameRunner.simulate());
+    @Test
+    public void cheatingGames() {
+        // win by cheating (works in league 1, which is the default)
+        assertWinLose(agentCheat, agentTwo);
+
+        // league 2 randomizes: we should be able to get a win and a loss
+        branch().setLeague(2).setSeed(0)
+            .setRoadLength(6).setInitialStones(15)
+            .assertWinLose(agentCheat, agentTwo);
+        branch().setLeague(2).setSeed(1)
+            .setRoadLength(6).setInitialStones(15)
+            .assertWinLose(agentTwo, agentCheat);
+
+        // league 3 forbids
+        Random r = new Random();
+        for (int i = 0; i < 16; i++) {
+            branch().setLeague(3).setSeed(r.nextLong())
+                .setRoadLength(6).setInitialStones(15)
+                .assertWinLose(agentTwo, agentCheat);
+        }
     }
 
+    // ==================== Map generation testing
+
+    static private class GameMap {
+        int roadLength;
+        int initialStones;
+        @Override public boolean equals(Object gm) {
+            if (gm instanceof GameMap) {
+                return roadLength == ((GameMap) gm).roadLength
+                    && initialStones == ((GameMap) gm).initialStones;
+            }
+            else return false;
+        }
+        @Override public int hashCode() {
+            return roadLength << 16 | initialStones;
+        }
+    }
+
+    @Test
+    public void maps() {
+        TrollTest test = branch();
+
+        // league 1, expect a single map
+        for (long s = 0; s < 16; s++) {
+            test.setSeed(s);
+            GameMap map = test.measureMap();
+            assertEquals("Level 1 roadLength", 6, map.roadLength);
+            assertEquals("Level 1 initialStones", 15, map.initialStones);
+        }
+
+        // league 2, expect one of four maps
+        test.setLeague(2);
+        HashSet<GameMap> maps = new HashSet<GameMap>();
+        Random r = new Random();
+        for (int i = 0; i < 16; i++) {
+            test.setSeed(r.nextLong());
+            GameMap map = test.measureMap();
+            maps.add(map);
+        }
+        assertEquals("Level 2 has four maps", 4, maps.size());
+
+        // league 3, maps simply have constraints
+        test.setLeague(3);
+        for (long s = 0; s < 16; s++) {
+            test.setSeed(s);
+            GameMap map = test.measureMap();
+            assertTrue("Level 3 road length is at least 6",
+                       map.roadLength >= 6);
+            assertTrue("Level 3 road length is at most 14",
+                       map.roadLength <= 14);
+            assertTrue("Level 3 road length is even",
+                       map.roadLength % 2 == 0);
+            assertTrue("Level 3 stones is at least 15",
+                       map.initialStones >= 15);
+            assertTrue("Level 3 stones is at most 50",
+                       map.initialStones <= 50);
+        }
+    }
+
+    /*
+     * There's currently no way to extract the parameters (and verify
+     * them!) from a game.  So for now we measure it from other
+     * traces.
+     */
+    private GameMap measureMap() {
+        GameMap result = new GameMap();
+
+        // roadLength is twice the number of moves a troll takes
+        // before the end of the game in a
+        // position-independent-strategy game.
+        GameResult game = runGame(agentOne, agentTwo);
+        result.roadLength = 2 * game.summaries.stream()
+            .filter(s -> s.contains("walks"))
+            .collect(Collectors.counting()).intValue();
+
+        // initialStones is the number of times a troll stands still
+        // in a one-stone-throw draw.
+        game = runGame(agentOne, agentOne);
+        result.initialStones = game.summaries.stream()
+            .filter(s -> s.contains("still"))
+            .collect(Collectors.counting()).intValue();
+
+        return result;
+    }
+
+    // ==================== Common test agents
+
     // great thanks to @dbdr for the intense moral support leading to
-    // the following:
-    static String agentOne = "yes 1";
-    static String agentTwo = "yes 2";
-    static String agentCrash = "false";
+    // the following acceptance of failure:
+    static private final String agentOne = "yes 1";
+    static private final String agentTwo = "yes 2";
+    static private final String agentAllIn = "yes 15";
+    static private final String agentCrash = "false"; // SLOW, try and avoid
+    static private final String agentGarbage = "yes assuredly_not_an_int";
+    static private final String agentCheat = agent(-100,25,25,25,25);
+
+    static private String agent(int... tosses) {
+        String cmd = "echo -e ";
+        for (int i = 0; i < tosses.length; i++) {
+            if (i > 0) cmd += "\\n";
+            cmd += tosses[i];
+        }
+        return cmd;
+    }
+
+    // ==================== Debug routines
 
     static void dumpGameResult(PrintStream p, GameResult gameResult) {
         for (AgentDto agent : gameResult.agents) dumpAgent(p, agent);
@@ -61,32 +219,32 @@ public class TrollTest {
         // dumpList(p, "views", gameResult.views);
     }
 
-    static <V> void dumpList(PrintStream p, String tag, List<V> list) {
-        V[] a = (V[]) list.toArray();
-        for (int i = 0; i < a.length; i++) {
+    static private <V> void dumpList(PrintStream p, String tag, List<V> list) {
+        ListIterator i = list.listIterator();
+        while (i.hasNext()) {
             if (tag != null) p.print(tag + " ");
-            p.print(i + ": ");
-            dumpGeneric(p, a[i]);
+            p.print(i.nextIndex() + ": ");
+            dumpGeneric(p, i.next());
         }
     }
 
-    static <K,V> void dumpMap(PrintStream p, String tag, Map<K,V> map) {
+    static private <K,V> void dumpMap(PrintStream p, String tag, Map<K,V> map) {
         for (Map.Entry kv : map.entrySet()) {
             p.print(tag + " for " + kv.getKey() + ": ");
             dumpGeneric(p, kv.getValue());
         }
     }
 
-    static <V> void dumpGeneric(PrintStream p, V v) {
+    static private <E,V> void dumpGeneric(PrintStream p, E v) {
         if (v instanceof List) {
-            dumpList(p, null, (List) v);
+            dumpList(p, null, (List<?>) v);
         }
         else {
             p.println(v);
         }
     }
 
-    static void dumpString(PrintStream p, String tag, String msg) {
+    static private void dumpString(PrintStream p, String tag, String msg) {
         p.println(tag + ": " + msg);
     }
 
@@ -94,6 +252,37 @@ public class TrollTest {
         p.println("[agent] " + agent.agentId + ": " + agent.avatar + " " + agent.index + " " + agent.name);
     }
 
+    // ==================== Test assertions
+
+    private void assertWinLose(String winner, String loser) {
+        assertLeftWin(runGame(winner, loser));
+        assertRightWin(runGame(loser, winner));
+    }
+
+    static void assertLeftWin(GameResult gameResult) {
+        assertLeftWin(gameResult, false);
+    }
+    static void assertLeftWin(GameResult gameResult, boolean strict) {
+        int[] scores = assertTwoScores(gameResult);
+        if (scores == null) return;
+
+        int s1 = scores[0], s2 = scores[1];
+        assertTrue("Left player has higher score than right player", s1 > s2);
+        if (strict) assertTrue("Right player isn't disqualified", s2 >= 0);
+    }
+
+    static void assertRightWin(GameResult gameResult) {
+        assertRightWin(gameResult, false);
+    }
+    static void assertRightWin(GameResult gameResult, boolean strict) {
+        int[] scores = assertTwoScores(gameResult);
+        if (scores == null) return;
+
+        int s1 = scores[0], s2 = scores[1];
+        assertTrue("Right player has higher score than right player", s2 > s1);
+        if (strict) assertTrue("Left player isn't disqualified", s1 >= 0);
+    }
+
     static void assertIsDraw(GameResult gameResult) {
         int[] scores = assertTwoScores(gameResult);
         if (scores == null) return;