]> git.cworth.org Git - empires-server/blob - empires.js
Give the "/events" route a common implementation
[empires-server] / empires.js
1 const express = require("express");
2 const Game = require("./game.js");
3
4 const router = express.Router();
5
6 const GamePhase = {
7   JOIN:    1,
8   REVEAL:  2,
9   CAPTURE: 3,
10   properties: {
11     1: {name: "join"},
12     2: {name: "reveal"},
13     3: {name: "capture"}
14   }
15 };
16
17 /**
18  * Shuffles array in place.
19  * @param {Array} a items An array containing the items.
20  */
21 function shuffle(a) {
22   if (a === undefined)
23     return;
24
25   var j, x, i;
26   for (i = a.length - 1; i > 0; i--) {
27     j = Math.floor(Math.random() * (i + 1));
28     x = a[i];
29     a[i] = a[j];
30     a[j] = x;
31   }
32 }
33
34 class Empires extends Game {
35   constructor(id) {
36     super(id);
37     this._spectators = [];
38     this.next_spectator_id = 1;
39     this._players = [];
40     this.next_player_id = 1;
41     this.characters_to_reveal = null;
42     this.phase = GamePhase.JOIN;
43
44     /* Send a comment to every connected client every 15 seconds. */
45     setInterval(() => {this.broadcast_string(":");}, 15000);
46   }
47
48   add_spectator(name, session_id) {
49     /* Don't add another spectator that matches an existing session. */
50     const existing = this._spectators.findIndex(
51       spectator => spectator.session_id === session_id);
52     if (existing >= 0)
53       return existing.id;
54
55     const new_spectator = {id: this.next_spectator_id,
56                            name: name,
57                            session_id: session_id
58                           };
59     this._spectators.push(new_spectator);
60     this.next_spectator_id++;
61     this.broadcast_event("spectator-join", JSON.stringify(new_spectator));
62
63     return new_spectator.id;
64   }
65
66   remove_spectator(id) {
67     const index = this._spectators.findIndex(spectator => spectator.id === id);
68     this._spectators.splice(index, 1);
69
70     this.broadcast_event("spectator-leave", `{"id": ${id}}`);
71   }
72
73   add_player(name, character) {
74     const new_player = {id: this.next_player_id,
75                        name: name,
76                        character: character,
77                        captures: [],
78                        };
79     this._players.push(new_player);
80     this.next_player_id++;
81     /* The syntax here is using an anonymous function to create a new
82       object from new_player with just the subset of fields that we
83       want. */
84     const player_data = JSON.stringify((({id, name}) => ({id, name}))(new_player));
85     this.broadcast_event("player-join", player_data);
86
87     return new_player;
88   }
89
90   remove_player(id) {
91     const index = this._players.findIndex(player => player.id === id);
92     this._players.splice(index, 1);
93
94     this.broadcast_event("player-leave", `{"id": ${id}}`);
95   }
96
97   reset() {
98     this._players = [];
99     this.characters_to_reveal = null;
100     this.next_player_id = 1;
101
102     this.change_phase(GamePhase.JOIN);
103
104     this.broadcast_event("spectators", "{}");
105     this.broadcast_event("players", "{}");
106   }
107
108   reveal_next() {
109     /* Don't try to reveal anything if we aren't in the reveal phase. */
110     if (this.phase != GamePhase.REVEAL) {
111       clearInterval(this.reveal_interval);
112       return;
113     }
114
115     if (this.reveal_index >= this.characters_to_reveal.length) {
116       clearInterval(this.reveal_interval);
117       this.broadcast_event("character-reveal", '{"character":""}');
118       return;
119     }
120     const character = this.characters_to_reveal[this.reveal_index];
121     this.reveal_index++;
122     const character_data = JSON.stringify({"character":character});
123     this.broadcast_event("character-reveal", character_data);
124   }
125
126   reveal() {
127     this.change_phase(GamePhase.REVEAL);
128
129     if (this.characters_to_reveal === null) {
130       this.characters_to_reveal = [];
131       this.characters_to_reveal = this._players.reduce((characters, player) => {
132         characters.push(player.character);
133         return characters;
134       }, []);
135       shuffle(this.characters_to_reveal);
136     }
137
138     this.reveal_index = 0;
139
140     this.reveal_interval = setInterval(this.reveal_next.bind(this), 3000);
141   }
142
143   start() {
144     this.change_phase(GamePhase.CAPTURE);
145   }
146
147   capture(captor_id, captee_id) {
148     /* TODO: Fix to fail on already-captured players (or to move the
149      * captured player from an old captor to a new—need to clarify in
150      * the API specification which we want here. */
151     let captor = this._players.find(player => player.id === captor_id);
152     captor.captures.push(captee_id);
153
154     this.broadcast_event("capture", `{"captor": ${captor_id}, "captee": ${captee_id}}`);
155   }
156
157   liberate(captee_id) {
158     let captor = this._players.find(player => player.captures.includes(captee_id));
159     captor.captures.splice(captor.captures.indexOf(captee_id), 1);
160   }
161
162   restart() {
163     for (const player of this._players) {
164       player.captures = [];
165     }
166   }
167
168   get characters() {
169     return this._players.map(player => player.character);
170   }
171
172   get empires() {
173     return this._players.map(player => ({id: player.id, captures: player.captures}));
174   }
175
176   get spectators() {
177     /* We return only "id" and "name" here (specifically not session_id!). */
178     return this._spectators.map(spectator => ({
179       id: spectator.id,
180       name: spectator.name
181     }));
182   }
183
184   get players() {
185     return this._players.map(player => ({id: player.id, name: player.name }));
186   }
187
188   game_phase_event_data(old_phase, new_phase) {
189     var old_phase_name;
190     if (old_phase)
191       old_phase_name = GamePhase.properties[old_phase].name;
192     else
193       old_phase_name = "none";
194     const new_phase_name = GamePhase.properties[new_phase].name;
195
196     return `{"old_phase":"${old_phase_name}","new_phase":"${new_phase_name}"}`;
197   }
198
199   /* Inform clients about a phase change. */
200   broadcast_phase_change() {
201     const event_data = this.game_phase_event_data(this.old_phase, this.phase);
202     this.broadcast_event("game-phase", event_data);
203   }
204
205   /* Change game phase and broadcast the change to all clients. */
206   change_phase(phase) {
207     /* Do nothing if there is no actual change happening. */
208     if (phase === this.phase)
209       return;
210
211     this.old_phase = this.phase;
212     this.phase = phase;
213
214     this.broadcast_phase_change();
215   }
216
217   handle_events(request, response) {
218
219     super.handle_events(request, response);
220
221     /* Now that a client has connected, first we need to stream all of
222      * the existing spectators and players (if any). */
223     if (this._spectators.length > 0) {
224       const spectators_json = JSON.stringify(this.spectators);
225       const spectators_data = `event: spectators\ndata: ${spectators_json}\n\n`;
226       response.write(spectators_data);
227     }
228
229     if (this._players.length > 0) {
230       const players_json = JSON.stringify(this.players);
231       const players_data = `event: players\ndata: ${players_json}\n\n`;
232       response.write(players_data);
233     }
234
235     /* And we need to inform the client of the current game phase.
236      *
237      * In fact, we need to cycle through each phase transition from the
238      * beginning so the client can see each.
239      */
240     var old_phase = null;
241     for (var phase = GamePhase.JOIN; phase <= this.phase; phase++) {
242       var event_data = this.game_phase_event_data(old_phase, phase);
243       response.write("event: game-phase\n" + "data: " + event_data + "\n\n");
244       old_phase = phase;
245     }
246   }
247
248 }
249
250 router.post('/spectator', (request, response) => {
251   const game = request.game;
252   var name = request.session.nickname;
253
254   /* If the request includes a name, that overrides the session nickname. */
255   if (request.body.name)
256     name = request.body.name;
257
258   const id = game.add_spectator(name, request.session.id);
259   response.send(JSON.stringify(id));
260 });
261
262 router.delete('/spectator/:id', (request, response) => {
263   const game = request.game;
264   game.remove_spectator(parseInt(request.params.id));
265   response.send();
266 });
267
268 router.post('/register', (request, response) => {
269   const game = request.game;
270   var name = request.session.nickname;;
271
272   /* If the request includes a name, that overrides the session nickname. */
273   if (request.body.name)
274     name = request.body.name;
275
276   const player = game.add_player(name, request.body.character);
277   response.send(JSON.stringify(player.id));
278 });
279
280 router.post('/deregister/:id', (request, response) => {
281   const game = request.game;
282   game.remove_player(parseInt(request.params.id));
283   response.send();
284 });
285
286 router.post('/reveal', (request, response) => {
287   const game = request.game;
288   game.reveal();
289   response.send();
290 });
291
292 router.post('/start', (request, response) => {
293   const game = request.game;
294   game.start();
295   response.send();
296 });
297
298 router.post('/reset', (request, response) => {
299   const game = request.game;
300   game.reset();
301   response.send();
302 });
303
304 router.post('/capture/:captor/:captee', (request, response) => {
305   const game = request.game;
306   game.capture(parseInt(request.params.captor), parseInt(request.params.captee));
307   response.send();
308 });
309
310 router.post('/liberate/:id', (request, response) => {
311   const game = request.game;
312   game.liberate(parseInt(request.params.id));
313   response.send();
314 });
315
316 router.post('/restart', (request, response) => {
317   const game = request.game;
318   game.restart(parseInt(request.params.id));
319     response.send();
320 });
321
322 router.get('/characters', (request, response) => {
323   const game = request.game;
324   response.send(game.characters);
325 });
326
327 router.get('/empires', (request, response) => {
328   const game = request.game;
329   response.send(game.empires);
330 });
331
332 router.get('/spectators', (request, response) => {
333   const game = request.game;
334   response.send(game.spectators);
335 });
336
337 router.get('/players', (request, response) => {
338   const game = request.game;
339   response.send(game.players);
340 });
341
342 exports.router = router;
343 exports.Game = Empires;
344
345 Empires.meta = {
346   name: "Empires",
347   identifier: "empires"
348 };