Clean up testing
[troll.git] / src / test / java / TrollTest.java
1 import java.util.Properties;
2 import java.util.HashSet;
3 import java.util.List;
4 import java.util.ListIterator;
5 import java.util.Map;
6 import java.util.Random;
7 import java.util.stream.Collectors;
8 import java.io.PrintStream;
9
10 import static org.junit.Assert.assertEquals;
11 import static org.junit.Assert.*;
12 import org.junit.Test;
13
14 import com.codingame.gameengine.runner.MultiplayerGameRunner;
15 import com.codingame.gameengine.runner.dto.*;
16
17 public class TrollTest implements Cloneable {
18
19     // ==================== Game Runner encapsulation
20
21     private int league = 1; // @#$%^&* this parameter is *global* despite API!
22     private Long seed;
23     private Integer roadLength;
24     private Integer initialStones;
25
26     private TrollTest branch() {
27         try { return (TrollTest) clone(); }
28         catch (CloneNotSupportedException e) { throw new InternalError(e); }
29     }
30     private TrollTest setLeague(int l) { league = l; return this; }
31     private TrollTest setSeed(long s) { seed = s; return this; }
32     private TrollTest setRoadLength(int l) { roadLength = l; return this; }
33     private TrollTest setInitialStones(int s) { initialStones = s; return this; }
34
35     private GameResult runGame(String left, String right) {
36         MultiplayerGameRunner gameRunner = new MultiplayerGameRunner();
37         gameRunner.setLeagueLevel(league); // mandatory
38         if (seed != null) gameRunner.setSeed(seed); else gameRunner.setSeed(0l);
39
40         Properties gameParameters = new Properties();
41         gameRunner.setGameParameters(gameParameters);
42         if (roadLength != null)
43             gameParameters.setProperty("roadLength", roadLength.toString());
44         if (initialStones != null)
45             gameParameters.setProperty("initialStones",
46                                        initialStones.toString());
47
48         gameRunner.addAgent(left);
49         gameRunner.addAgent(right);
50
51         return gameRunner.simulate();
52     }
53
54     // ==================== Single-round game result testing
55
56     @Test
57     public void drawGame() {
58         assertIsDraw(runGame(agentOne, agentOne));
59     }
60
61     @Test
62     public void defeatGames() {
63         assertIsDefeat(runGame(agentCrash, agentCrash));
64         assertIsDefeat(runGame(agentGarbage, agentGarbage));
65     }
66
67     @Test
68     public void simpleGames() {
69         // wins by direct reach, no fastforward
70         assertWinLose(agentTwo, agentOne);
71
72         // win by fastforward after loser exhaustion
73         assertWinLose(agentOne, agentAllIn);
74
75         // win despite fastforward after winner exhaustion
76         // (harder to construct :-D )
77         assertWinLose(agent(1,2,2,2,8), agent(3,1,1,1,8));
78     }
79
80     // ==================== Cheating games (league 1 perk)
81
82     @Test
83     public void cheatingGames() {
84         // win by cheating (works in league 1, which is the default)
85         assertWinLose(agentCheat, agentTwo);
86
87         // league 2 randomizes: we should be able to get a win and a loss
88         branch().setLeague(2).setSeed(0)
89             .setRoadLength(6).setInitialStones(15)
90             .assertWinLose(agentCheat, agentTwo);
91         branch().setLeague(2).setSeed(1)
92             .setRoadLength(6).setInitialStones(15)
93             .assertWinLose(agentTwo, agentCheat);
94
95         // league 3 forbids
96         Random r = new Random();
97         for (int i = 0; i < 16; i++) {
98             branch().setLeague(3).setSeed(r.nextLong())
99                 .setRoadLength(6).setInitialStones(15)
100                 .assertWinLose(agentTwo, agentCheat);
101         }
102     }
103
104     // ==================== Map generation testing
105
106     static private class GameMap {
107         int roadLength;
108         int initialStones;
109         @Override public boolean equals(Object gm) {
110             if (gm instanceof GameMap) {
111                 return roadLength == ((GameMap) gm).roadLength
112                     && initialStones == ((GameMap) gm).initialStones;
113             }
114             else return false;
115         }
116         @Override public int hashCode() {
117             return roadLength << 16 | initialStones;
118         }
119     }
120
121     @Test
122     public void maps() {
123         TrollTest test = branch();
124
125         // league 1, expect a single map
126         for (long s = 0; s < 16; s++) {
127             test.setSeed(s);
128             GameMap map = test.measureMap();
129             assertEquals("Level 1 roadLength", 6, map.roadLength);
130             assertEquals("Level 1 initialStones", 15, map.initialStones);
131         }
132
133         // league 2, expect one of four maps
134         test.setLeague(2);
135         HashSet<GameMap> maps = new HashSet<GameMap>();
136         Random r = new Random();
137         for (int i = 0; i < 16; i++) {
138             test.setSeed(r.nextLong());
139             GameMap map = test.measureMap();
140             maps.add(map);
141         }
142         assertEquals("Level 2 has four maps", 4, maps.size());
143
144         // league 3, maps simply have constraints
145         test.setLeague(3);
146         for (long s = 0; s < 16; s++) {
147             test.setSeed(s);
148             GameMap map = test.measureMap();
149             assertTrue("Level 3 road length is at least 6",
150                        map.roadLength >= 6);
151             assertTrue("Level 3 road length is at most 14",
152                        map.roadLength <= 14);
153             assertTrue("Level 3 road length is even",
154                        map.roadLength % 2 == 0);
155             assertTrue("Level 3 stones is at least 15",
156                        map.initialStones >= 15);
157             assertTrue("Level 3 stones is at most 50",
158                        map.initialStones <= 50);
159         }
160     }
161
162     /*
163      * There's currently no way to extract the parameters (and verify
164      * them!) from a game.  So for now we measure it from other
165      * traces.
166      */
167     private GameMap measureMap() {
168         GameMap result = new GameMap();
169
170         // roadLength is twice the number of moves a troll takes
171         // before the end of the game in a
172         // position-independent-strategy game.
173         GameResult game = runGame(agentOne, agentTwo);
174         result.roadLength = 2 * game.summaries.stream()
175             .filter(s -> s.contains("walks"))
176             .collect(Collectors.counting()).intValue();
177
178         // initialStones is the number of times a troll stands still
179         // in a one-stone-throw draw.
180         game = runGame(agentOne, agentOne);
181         result.initialStones = game.summaries.stream()
182             .filter(s -> s.contains("still"))
183             .collect(Collectors.counting()).intValue();
184
185         return result;
186     }
187
188     // ==================== Common test agents
189
190     // great thanks to @dbdr for the intense moral support leading to
191     // the following acceptance of failure:
192     static private final String agentOne = "yes 1";
193     static private final String agentTwo = "yes 2";
194     static private final String agentAllIn = "yes 15";
195     static private final String agentCrash = "false"; // SLOW, try and avoid
196     static private final String agentGarbage = "yes assuredly_not_an_int";
197     static private final String agentCheat = agent(-100,25,25,25,25);
198
199     static private String agent(int... tosses) {
200         String cmd = "echo -e ";
201         for (int i = 0; i < tosses.length; i++) {
202             if (i > 0) cmd += "\\n";
203             cmd += tosses[i];
204         }
205         return cmd;
206     }
207
208     // ==================== Debug routines
209
210     static void dumpGameResult(PrintStream p, GameResult gameResult) {
211         for (AgentDto agent : gameResult.agents) dumpAgent(p, agent);
212         dumpMap(p, "errors", gameResult.errors);
213         dumpString(p, "failCause", gameResult.failCause);
214         dumpMap(p, "ids", gameResult.ids);
215         dumpString(p, "metadata", gameResult.metadata);
216         dumpMap(p, "outputs", gameResult.outputs);
217         dumpList(p, "summaries", gameResult.summaries);
218         dumpList(p, "tooltips", gameResult.tooltips);
219         // dumpList(p, "views", gameResult.views);
220     }
221
222     static private <V> void dumpList(PrintStream p, String tag, List<V> list) {
223         ListIterator i = list.listIterator();
224         while (i.hasNext()) {
225             if (tag != null) p.print(tag + " ");
226             p.print(i.nextIndex() + ": ");
227             dumpGeneric(p, i.next());
228         }
229     }
230
231     static private <K,V> void dumpMap(PrintStream p, String tag, Map<K,V> map) {
232         for (Map.Entry kv : map.entrySet()) {
233             p.print(tag + " for " + kv.getKey() + ": ");
234             dumpGeneric(p, kv.getValue());
235         }
236     }
237
238     static private <E,V> void dumpGeneric(PrintStream p, E v) {
239         if (v instanceof List) {
240             dumpList(p, null, (List<?>) v);
241         }
242         else {
243             p.println(v);
244         }
245     }
246
247     static private void dumpString(PrintStream p, String tag, String msg) {
248         p.println(tag + ": " + msg);
249     }
250
251     static void dumpAgent(PrintStream p, AgentDto agent) {
252         p.println("[agent] " + agent.agentId + ": " + agent.avatar + " " + agent.index + " " + agent.name);
253     }
254
255     // ==================== Test assertions
256
257     private void assertWinLose(String winner, String loser) {
258         assertLeftWin(runGame(winner, loser));
259         assertRightWin(runGame(loser, winner));
260     }
261
262     static void assertLeftWin(GameResult gameResult) {
263         assertLeftWin(gameResult, false);
264     }
265     static void assertLeftWin(GameResult gameResult, boolean strict) {
266         int[] scores = assertTwoScores(gameResult);
267         if (scores == null) return;
268
269         int s1 = scores[0], s2 = scores[1];
270         assertTrue("Left player has higher score than right player", s1 > s2);
271         if (strict) assertTrue("Right player isn't disqualified", s2 >= 0);
272     }
273
274     static void assertRightWin(GameResult gameResult) {
275         assertRightWin(gameResult, false);
276     }
277     static void assertRightWin(GameResult gameResult, boolean strict) {
278         int[] scores = assertTwoScores(gameResult);
279         if (scores == null) return;
280
281         int s1 = scores[0], s2 = scores[1];
282         assertTrue("Right player has higher score than right player", s2 > s1);
283         if (strict) assertTrue("Left player isn't disqualified", s1 >= 0);
284     }
285
286     static void assertIsDraw(GameResult gameResult) {
287         int[] scores = assertTwoScores(gameResult);
288         if (scores == null) return;
289
290         int s1 = scores[0], s2 = scores[1];
291         assertEquals("Player scores are equal", s1, s2);
292         assertTrue("Player scores are non-negative", s1 >= 0);
293     }
294
295     static void assertIsDefeat(GameResult gameResult) {
296         int[] scores = assertTwoScores(gameResult);
297         if (scores == null) return;
298
299         int s1 = scores[0], s2 = scores[1];
300         assertTrue("First player score is negative", s1 < 0);
301         assertTrue("Second player score is negative", s2 < 0);
302     }
303
304     static int[] assertTwoScores(GameResult gameResult) {
305         Map<Integer,Integer> scores = gameResult.scores;
306         assertEquals("Two scores are reported", scores.size(), 2);
307         assertTrue("Player 0 has a score", scores.containsKey(0));
308         assertTrue("Player 1 has a score", scores.containsKey(1));
309         return new int[] { scores.get(0), scores.get(1) };
310     }
311 }