]> git.cworth.org Git - lmno-server/blob - game.js
game: Index players directly by session ID
[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, name, connection) {
5     this.id = id;
6     this.name = name;
7     this.connections = [connection];
8     this.team = "";
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       team: this.team
37     });
38   }
39 }
40
41 /* Base class providing common code for game engine implementations. */
42 class Game {
43   constructor(id) {
44     this.id = id;
45     this.players = [];
46     this.next_player_id = 1;
47     this.teams = [];
48
49     /* Send a comment to every connected client every 15 seconds. */
50     setInterval(() => {this.broadcast_string(":");}, 15000);
51   }
52
53   /* Suport for game meta-data.
54    *
55    * What we want here is an effectively static field that is
56    * accessible through either the class name (SomeGame.meta) or an
57    * instance (some_game.meta). To pull this off we do keep two copies
58    * of the data. But the game classes can just set SomeGame.meta once
59    * and then reference it either way.
60    */
61   static set meta(data) {
62     /* This allows class access (SomeGame.meta) via the get method below. */
63     this._meta = data;
64
65     /* While this allows access via an instance (some_game.meta). */
66     this.prototype.meta = data;
67   }
68
69   static get meta() {
70     return this._meta;
71   }
72
73   add_player(session, connection) {
74     /* First see if we already have a player object for this session. */
75     const existing = this.players[session.id];
76     if (existing) {
77       existing.add_connection(connection);
78       return existing;
79     }
80
81     /* No existing player. Add a new one. */
82     const id = this.next_player_id;
83     const player = new Player(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[session.id] = player;
91     this.next_player_id++;
92
93     return player;
94   }
95
96   /* Drop a connection object from a player, and if it's the last one,
97    * then drop that player from the game's list of players. */
98   remove_player_connection(player, connection) {
99     const remaining = player.remove_connection(connection);
100     if (remaining === 0) {
101       const player_data = JSON.stringify({ id: player.id });
102       this.players.filter(p => p !== player);
103       this.broadcast_event("player-exit", player_data);
104     }
105   }
106
107   /* Send a string to all players */
108   broadcast_string(str) {
109     for (let [session_id, player] of Object.entries(this.players))
110       player.send(str + '\n');
111   }
112
113   /* Send an event to all players.
114    *
115    * An event has both a declared type and a separate data block.
116    * It also ends with two newlines (to mark the end of the event).
117    */
118   broadcast_event(type, data) {
119     this.broadcast_string(`event: ${type}\ndata: ${data}\n`);
120   }
121
122   handle_events(request, response) {
123     /* These headers will keep the connection open so we can stream events. */
124     const headers = {
125       "Content-type": "text/event-stream",
126       "Connection": "keep-alive",
127       "Cache-Control": "no-cache"
128     };
129     response.writeHead(200, headers);
130
131     /* Add this new player. */
132     const player = this.add_player(request.session, response);
133
134     /* And queue up cleanup to be triggered on client close. */
135     request.on('close', () => {
136       this.remove_player_connection(player, response);
137     });
138
139     /* Give the client the game-info event. */
140     const game_info_json = JSON.stringify({
141       id: this.id,
142       url: `${request.protocol}://${request.hostname}/${this.id}`
143     });
144     response.write(`event: game-info\ndata: ${game_info_json}\n\n`);
145
146     /* And the player-info event. */
147     response.write(`event: player-info\ndata: ${player.info_json()}\n\n`);
148
149     /* Finally, if this game class has a "state" property, stream that
150      * current state to the client. */
151     if (this.state) {
152       const state_json = JSON.stringify(this.state);
153       response.write(`event: game-state\ndata: ${state_json}\n\n`);
154     }
155   }
156
157   handle_player(request, response) {
158     const player = this.players[request.session.id];
159     const name = request.body.name;
160     const team = request.body.team;
161     var updated = false;
162     if (! player) {
163       response.sendStatus(404);
164       return;
165     }
166
167     if (name && (player.name !== name)) {
168       player.name = name;
169
170       /* In addition to setting the name within this game's player
171        * object, also set the name in the session. */
172       request.session.nickname = name;
173       request.session.save();
174
175       updated = true;
176     }
177
178     if (team !== null && (player.team !== team) &&
179         (team === "" || this.teams.includes(team)))
180     {
181       player.team = team;
182
183       updated = true;
184     }
185
186     if (updated)
187       this.broadcast_event("player-update", player.info_json());
188
189     response.send("");
190   }
191
192   broadcast_move(move) {
193     this.broadcast_event("move", move);
194   }
195
196 }
197
198 module.exports = Game;