]> git.cworth.org Git - lmno-server/blob - game.js
Add a new player-info event to the stream
[lmno-server] / game.js
1 /* A single player can have multiple connections, (think, multiple
2  * browser windows with a common session cookie). */
3 class Player {
4   constructor(id, session_id, name, connection) {
5     this.id = id;
6     this.session_id = session_id;
7     this.name = name;
8     this.connections = [connection];
9   }
10
11   add_connection(connection) {
12     /* Don't add a duplicate connection if this player already has it. */
13     for (let c of this.connections) {
14       if (c === connection)
15         return;
16     }
17
18     this.connections.push(connection);
19   }
20
21   /* Returns the number of remaining connections after this one is removed. */
22   remove_connection(connection) {
23     this.connections.filter(c => c !== connection);
24     return this.connections.length;
25   }
26
27   /* Send a string to all connections for this player. */
28   send(data) {
29     this.connections.forEach(connection => connection.write(data));
30   }
31
32   info_json() {
33     return JSON.stringify({
34       id: this.id,
35       name: this.name,
36     });
37   }
38 }
39
40 /* Base class providing common code for game engine implementations. */
41 class Game {
42   constructor(id) {
43     this.id = id;
44     this.players = [];
45     this.next_player_id = 1;
46
47     /* Send a comment to every connected client every 15 seconds. */
48     setInterval(() => {this.broadcast_string(":");}, 15000);
49   }
50
51   /* Suport for game meta-data.
52    *
53    * What we want here is an effectively static field that is
54    * accessible through either the class name (SomeGame.meta) or an
55    * instance (some_game.meta). To pull this off we do keep two copies
56    * of the data. But the game classes can just set SomeGame.meta once
57    * and then reference it either way.
58    */
59   static set meta(data) {
60     /* This allows class access (SomeGame.meta) via the get method below. */
61     this._meta = data;
62
63     /* While this allows access via an instance (some_game.meta). */
64     this.prototype.meta = data;
65   }
66
67   static get meta() {
68     return this._meta;
69   }
70
71   add_player(session, connection) {
72     /* First see if we already have a player object for this session. */
73     const existing_index = this.players.findIndex(
74       player => player.session_id === session.id);
75     if (existing_index >= 0) {
76       const player = this.players[existing_index];
77       player.add_connection(connection);
78       return player;
79     }
80
81     /* No existing player. Add a new one. */
82     const id = this.next_player_id;
83     const player = new Player(id, session.id, session.nickname, connection);
84
85     /* Broadcast before adding player to list (to avoid announcing the
86      * new player to itself). */
87     const player_data = JSON.stringify({ id: player.id, name: player.name });
88     this.broadcast_event("player-enter", player_data);
89
90     this.players.push(player);
91     this.next_player_id++;
92
93     return player;
94   }
95
96   find_player(session) {
97     const existing_index = this.players.findIndex(
98       player => player.session_id === session.id);
99     if (existing_index >= 0)
100       return this.players[existing_index];
101
102     return null;
103   }
104
105   /* Drop a connection object from a player, and if it's the last one,
106    * then drop that player from the game's list of players. */
107   remove_player_connection(player, connection) {
108     const remaining = player.remove_connection(connection);
109     if (remaining === 0) {
110       const player_data = JSON.stringify({ id: player.id });
111       this.players.filter(p => p !== player);
112       this.broadcast_event("player-exit", player_data);
113     }
114   }
115
116   /* Send a string to all players */
117   broadcast_string(str) {
118     this.players.forEach(player => player.send(str + '\n'));
119   }
120
121   /* Send an event to all players.
122    *
123    * An event has both a declared type and a separate data block.
124    * It also ends with two newlines (to mark the end of the event).
125    */
126   broadcast_event(type, data) {
127     this.broadcast_string(`event: ${type}\ndata: ${data}\n`);
128   }
129
130   handle_events(request, response) {
131     /* These headers will keep the connection open so we can stream events. */
132     const headers = {
133       "Content-type": "text/event-stream",
134       "Connection": "keep-alive",
135       "Cache-Control": "no-cache"
136     };
137     response.writeHead(200, headers);
138
139     /* Add this new player. */
140     const player = this.add_player(request.session, response);
141
142     /* And queue up cleanup to be triggered on client close. */
143     request.on('close', () => {
144       this.remove_player_connection(player, response);
145     });
146
147     /* Give the client the game-info event. */
148     const game_info_json = JSON.stringify({
149       id: this.id,
150       url: `${request.protocol}://${request.hostname}/${this.id}`
151     });
152     response.write(`event: game-info\ndata: ${game_info_json}\n\n`);
153
154     /* And the player-info event. */
155     response.write(`event: player-info\ndata: ${player.info_json()}\n\n`);
156
157     /* Finally, if this game class has a "state" property, stream that
158      * current state to the client. */
159     if (this.state) {
160       const state_json = JSON.stringify(this.state);
161       response.write(`event: game-state\ndata: ${state_json}\n\n`);
162     }
163   }
164
165   broadcast_move(move) {
166     this.broadcast_event("move", move);
167   }
168
169 }
170
171 module.exports = Game;