1 module netorcai.message;
2 
3 import std.json;
4 import std.exception;
5 
6 import netorcai.json_util;
7 import netorcai.metaprotocol_version;
8 
9 /// Stores information about one player
10 struct PlayerInfo
11 {
12     int playerID; /// The player unique identifier (in [0..nbPlayers[)
13     string nickname; /// The player nickname
14     string remoteAddress; /// The player socket remote address
15     bool isConnected; /// Whether the player is currently connected or not
16 }
17 
18 /// Parses a player information (in GAME_STARTS and GAME_ENDS messages)
19 PlayerInfo parsePlayerInfo(JSONValue o)
20 {
21     PlayerInfo info;
22     info.playerID = o["player_id"].getInt;
23     info.nickname = o["nickname"].str;
24     info.remoteAddress = o["remote_address"].str;
25     info.isConnected = o["is_connected"].getBool;
26 
27     return info;
28 }
29 unittest
30 {
31     immutable string s = `{
32       "player_id": 0,
33       "nickname": "jugador",
34       "remote_address": "127.0.0.1:59840",
35       "is_connected": true
36     }`;
37 
38     const PlayerInfo pinfo = parseJSON(s).parsePlayerInfo;
39     assert(pinfo.playerID == 0);
40     assert(pinfo.nickname == "jugador");
41     assert(pinfo.remoteAddress == "127.0.0.1:59840");
42     assert(pinfo.isConnected == true);
43 }
44 
45 /// Parses several player information (in GAME_STARTS and GAME_ENDS messages)
46 PlayerInfo[] parsePlayersInfo(JSONValue[] array)
47 {
48     PlayerInfo[] infos;
49     infos.length = array.length;
50 
51     foreach (i, o; array)
52     {
53         infos[i] = o.parsePlayerInfo;
54     }
55 
56     return infos;
57 }
58 unittest
59 {
60     immutable string s = `[
61       {
62         "player_id": 0,
63         "nickname": "jugador",
64         "remote_address": "127.0.0.1:59840",
65         "is_connected": true
66       },
67       {
68         "player_id": 1,
69         "nickname": "bot",
70         "remote_address": "127.0.0.1:59842",
71         "is_connected": false
72       }
73     ]`;
74 
75     const PlayerInfo[] pinfos = parseJSON(s).array.parsePlayersInfo;
76     assert(pinfos.length == 2);
77 
78     assert(pinfos[0].playerID == 0);
79     assert(pinfos[0].nickname == "jugador");
80     assert(pinfos[0].remoteAddress == "127.0.0.1:59840");
81     assert(pinfos[0].isConnected == true);
82 
83     assert(pinfos[1].playerID == 1);
84     assert(pinfos[1].nickname == "bot");
85     assert(pinfos[1].remoteAddress == "127.0.0.1:59842");
86     assert(pinfos[1].isConnected == false);
87 }
88 
89 /// Content of a LOGIN_ACK metaprotocol message
90 struct LoginAckMessage
91 {
92     string metaprotocolVersion; /// netorcai's metaprotocol version
93 }
94 
95 /// Parses a LOGIN_ACK metaprotocol message
96 LoginAckMessage parseLoginAckMessage(JSONValue o)
97 {
98     LoginAckMessage m;
99 
100     m.metaprotocolVersion = o["metaprotocol_version"].str;
101     if (m.metaprotocolVersion != metaprotocolVersion)
102     {
103         import std.stdio;
104         writefln("Warning: netorcai uses version '%s' while netorcai-client-d uses '%s'",
105             m.metaprotocolVersion, metaprotocolVersion);
106     }
107 
108     return m;
109 }
110 
111 /// Content of a GAME_STARTS metaprotocol message
112 struct GameStartsMessage
113 {
114     int playerID; /// Caller's player identifier. players: [0..nbPlayers+nbSpecialPlayers[. visu: -1
115     int nbPlayers; /// Number of players in the game
116     int nbSpecialPlayers; /// Number of special players in the game
117     int nbTurnsMax; /// Maximum number of turns. Game can finish before it
118     double msBeforeFirstTurn; /// Time before the first TURN is sent (in ms)
119     double msBetweenTurns; /// Time between two consecutive TURNs (in ms)
120     PlayerInfo[] playersInfo; /// (only for visus) Information about the players
121     JSONValue initialGameState; /// Game-dependent object.
122 }
123 
124 /// Parses a GAME_STARTS metaprotocol message
125 GameStartsMessage parseGameStartsMessage(JSONValue o)
126 {
127     GameStartsMessage m;
128 
129     m.playerID = o["player_id"].getInt;
130     m.nbPlayers = o["nb_players"].getInt;
131     m.nbSpecialPlayers = o["nb_special_players"].getInt;
132     m.nbTurnsMax = o["nb_turns_max"].getInt;
133     m.msBeforeFirstTurn = o["milliseconds_before_first_turn"].getDouble;
134     m.msBetweenTurns = o["milliseconds_between_turns"].getDouble;
135     m.initialGameState = o["initial_game_state"].object;
136     m.playersInfo = o["players_info"].array.parsePlayersInfo;
137 
138     return m;
139 }
140 unittest
141 {
142     immutable string s = `{
143       "message_type": "GAME_STARTS",
144       "player_id": 0,
145       "players_info": [
146         {
147           "player_id": 0,
148           "nickname": "jugador",
149           "remote_address": "127.0.0.1:59840",
150           "is_connected": true
151         }
152       ],
153       "nb_players": 4,
154       "nb_special_players": 0,
155       "nb_turns_max": 100,
156       "milliseconds_before_first_turn": 1000,
157       "milliseconds_between_turns": 1000,
158       "initial_game_state": {}
159     }`;
160 
161     const GameStartsMessage m = parseJSON(s).parseGameStartsMessage;
162     assert(m.playerID == 0);
163     assert(m.playersInfo.length == 1);
164     assert(m.playersInfo[0] == `{"player_id":0,"nickname":"jugador",
165         "remote_address":"127.0.0.1:59840",
166         "is_connected":true}`.parseJSON.parsePlayerInfo);
167     assert(m.nbPlayers == 4);
168     assert(m.nbSpecialPlayers == 0);
169     assert(m.nbTurnsMax == 100);
170     assert(m.msBeforeFirstTurn == 1000);
171     assert(m.msBetweenTurns == 1000);
172     assert(m.initialGameState.object.length == 0);
173 }
174 
175 /// Content of a GAME_ENDS metaprotocol message
176 struct GameEndsMessage
177 {
178     int winnerPlayerID; /// Unique identifier of the player that won the game. Or -1.
179     JSONValue gameState; /// Game-dependent object.
180 }
181 
182 /// Parses a GAME_ENDS metaprotocol message
183 GameEndsMessage parseGameEndsMessage(JSONValue o)
184 {
185     GameEndsMessage m;
186     m.winnerPlayerID = o["winner_player_id"].getInt;
187     m.gameState = o["game_state"].object;
188 
189     return m;
190 }
191 unittest
192 {
193     immutable string s = `{
194       "message_type": "GAME_ENDS",
195       "winner_player_id": 0,
196       "game_state": {}
197     }`;
198 
199     const GameEndsMessage m = s.parseJSON.parseGameEndsMessage;
200     assert(m.winnerPlayerID == 0);
201     assert(m.gameState.object.length == 0);
202 }
203 
204 /// Content of a TURN metaprotocol message
205 struct TurnMessage
206 {
207     int turnNumber; /// In [0..nbTurnsMax[
208     PlayerInfo[] playersInfo; /// (only for visus) Information about the players
209     JSONValue gameState; /// Game-dependent object.
210 }
211 
212 /// Parses a TURN metaprotocol message
213 TurnMessage parseTurnMessage(JSONValue o)
214 {
215     TurnMessage m;
216     m.turnNumber = o["turn_number"].getInt;
217     m.playersInfo = o["players_info"].array.parsePlayersInfo;
218     m.gameState = o["game_state"].object;
219 
220     return m;
221 }
222 unittest
223 {
224     immutable string s = `{
225       "message_type": "TURN",
226       "turn_number": 0,
227       "game_state": {},
228       "players_info": [
229         {
230           "player_id": 0,
231           "nickname": "jugador",
232           "remote_address": "127.0.0.1:59840",
233           "is_connected": true
234         }
235       ]
236     }`;
237 
238     const TurnMessage m = s.parseJSON.parseTurnMessage;
239     assert(m.turnNumber == 0);
240     assert(m.gameState.object.length == 0);
241     assert(m.playersInfo == `[{"player_id":0,"nickname":"jugador",
242         "remote_address":"127.0.0.1:59840",
243         "is_connected":true}]`.parseJSON.array.parsePlayersInfo);
244 }
245 
246 /// Content of a DO_INIT metaprotocol message
247 struct DoInitMessage
248 {
249     int nbPlayers; /// The number of players of the game
250     int nbSpecialPlayers; /// Number of special players in the game
251     int nbTurnsMax; /// The maximum number of turns of the game
252 }
253 
254 /// Parses a DO_INIT metaprotocol message
255 DoInitMessage parseDoInitMessage(JSONValue o)
256 {
257     DoInitMessage m;
258     m.nbPlayers = o["nb_players"].getInt;
259     m.nbSpecialPlayers = o["nb_special_players"].getInt;
260     m.nbTurnsMax = o["nb_turns_max"].getInt;
261 
262     return m;
263 }
264 unittest
265 {
266     immutable string s = `{
267       "message_type": "DO_INIT",
268       "nb_players": 4,
269       "nb_special_players": 0,
270       "nb_turns_max": 100
271     }`;
272 
273     immutable DoInitMessage m = s.parseJSON.parseDoInitMessage;
274     assert(m.nbPlayers == 4);
275     assert(m.nbSpecialPlayers == 0);
276     assert(m.nbTurnsMax == 100);
277 }
278 
279 /// Convenient struct for the player actions of a DO_TURN metaprotocol message
280 struct PlayerActions
281 {
282     int playerID; /// The identifier of the player that issued the actions
283     int turnNumber; /// The turn number the actions come from
284     JSONValue actions; /// The actions themselves
285 }
286 
287 /// Parses the playerActions field of a DO_TURN metaprotocol message
288 PlayerActions parsePlayerActions(JSONValue o)
289 {
290     PlayerActions pa;
291     pa.playerID = o["player_id"].getInt;
292     pa.turnNumber = o["turn_number"].getInt;
293     pa.actions = o["actions"].array;
294 
295     return pa;
296 }
297 unittest
298 {
299     immutable string s = `{
300       "player_id": 2,
301       "turn_number": 4,
302       "actions": []
303     }`;
304 
305     immutable PlayerActions pa = s.parseJSON.parsePlayerActions;
306     assert(pa.playerID == 2);
307     assert(pa.turnNumber == 4);
308     assert(pa.actions.array.length == 0);
309 }
310 
311 /// Content of a DO_TURN metaprotocol message
312 struct DoTurnMessage
313 {
314     PlayerActions[] playerActions; /// The ordered list of player actions
315 }
316 
317 /// Parses a DO_TURN metaprotocol message
318 DoTurnMessage parseDoTurnMessage(JSONValue v)
319 {
320     DoTurnMessage m;
321 
322     auto actions = v["player_actions"].array;
323     m.playerActions.length = actions.length;
324 
325     foreach (i, o; actions)
326     {
327         m.playerActions[i] = o.parsePlayerActions;
328     }
329 
330     return m;
331 }
332 unittest
333 {
334     immutable string s = `{
335       "message_type": "DO_TURN",
336       "player_actions": [
337         {
338           "player_id": 1,
339           "turn_number": 2,
340           "actions": []
341         },
342         {
343           "player_id": 0,
344           "turn_number": 3,
345           "actions": [ 4 ]
346         }
347       ]
348     }`;
349 
350     DoTurnMessage m = s.parseJSON.parseDoTurnMessage;
351     assert(m.playerActions.length == 2);
352     assert(m.playerActions[0].playerID == 1);
353     assert(m.playerActions[0].turnNumber == 2);
354     assert(m.playerActions[0].actions.array.length == 0);
355     assert(m.playerActions[1].playerID == 0);
356     assert(m.playerActions[1].turnNumber == 3);
357     assert(m.playerActions[1].actions.array.length == 1);
358     assert(m.playerActions[1].actions.array[0].getInt == 4);
359 }