]> git.cworth.org Git - empires-server/commitdiff
game: Rename Game.clients to Game.players, combining multiple connections
authorCarl Worth <cworth@cworth.org>
Fri, 5 Jun 2020 12:31:15 +0000 (05:31 -0700)
committerCarl Worth <cworth@cworth.org>
Fri, 5 Jun 2020 14:28:57 +0000 (07:28 -0700)
Previously we were simply storing an array of "clients", one for every
request to the /events endpoint.

Now, the array of "players" is similar, but there is only one item in
the array for each unique session ID, but where each one may have
multiple "connections", (for a case where a player connects multiple
times with the same session ID).

In this commit we're not making large changes to the Empires class to
take advantage of this new functionality, (for example, it already has
unique session identification as part of its "spectators"
notion). Instead, we make a minimal change Empires so that it doesn't
step on the base "players" property.

In the future, we'll be able to port Empires forward to use this
base-class functionality and in the process delete some code from the
Empires class.

empires.js
game.js

index 744224e6b68f30a7abfe4bed34df96ceda81651f..5b9df829e252e9ef42b9ee4d8338007fb1463bc3 100644 (file)
@@ -176,7 +176,14 @@ class Empires extends Game {
     }));
   }
 
-  get players() {
+  /* The base class recently acquired Game.players which works like
+   * Empires.spectators, (and meanwhile the Empires._players
+   * functionality could perhaps be reworked into
+   * Game.players[].team). Until we do that rework, lets use
+   * .registered_players as the getter for the Empires-specific
+   * ._players property to avoid mixing it up with the distinct
+   * Game.players property. */
+  get registered_players() {
     return this._players.map(player => ({id: player.id, name: player.name }));
   }
 
@@ -222,7 +229,7 @@ class Empires extends Game {
     }
 
     if (this._players.length > 0) {
-      const players_json = JSON.stringify(this.players);
+      const players_json = JSON.stringify(this.registered_players);
       const players_data = `event: players\ndata: ${players_json}\n\n`;
       response.write(players_data);
     }
@@ -334,7 +341,7 @@ router.get('/spectators', (request, response) => {
 
 router.get('/players', (request, response) => {
   const game = request.game;
-  response.send(game.players);
+  response.send(game.registered_players);
 });
 
 Empires.meta = {
diff --git a/game.js b/game.js
index 56ee56c8fa5e013991f676abfbbd8b51889994ff..baf1e557df502cc28f775f3540fa5f14d55102c8 100644 (file)
--- a/game.js
+++ b/game.js
@@ -1,9 +1,48 @@
+/* A single player can have multiple connections, (think, multiple
+ * browser windows with a common session cookie). */
+class Player {
+  constructor(id, session_id, name, connection) {
+    this.id = id;
+    this.session_id = session_id;
+    this.name = name;
+    this.connections = [connection];
+  }
+
+  add_connection(connection) {
+    /* Don't add a duplicate connection if this player already has it. */
+    for (let c of this.connections) {
+      if (c === connection)
+        return;
+    }
+
+    this.connections.push(connection);
+  }
+
+  /* Returns the number of remaining connections after this one is removed. */
+  remove_connection(connection) {
+    this.connections.filter(c => c !== connection);
+    return this.connections.length;
+  }
+
+  /* Send a string to all connections for this player. */
+  send(data) {
+    this.connections.forEach(connection => connection.write(data));
+  }
+
+  info_json() {
+    return JSON.stringify({
+      id: this.id,
+      name: this.name,
+    });
+  }
+}
+
 /* Base class providing common code for game engine implementations. */
 class Game {
   constructor(id) {
     this.id = id;
-    this.clients = [];
-    this.next_client_id = 1;
+    this.players = [];
+    this.next_player_id = 1;
 
     /* Send a comment to every connected client every 15 seconds. */
     setInterval(() => {this.broadcast_string(":");}, 15000);
@@ -29,25 +68,57 @@ class Game {
     return this._meta;
   }
 
-  add_client(response) {
-    const id = this.next_client_id;
-    this.clients.push({id: id,
-                       response: response});
-    this.next_client_id++;
+  add_player(session, connection) {
+    /* First see if we already have a player object for this session. */
+    const existing_index = this.players.findIndex(
+      player => player.session_id === session.id);
+    if (existing_index >= 0) {
+      const player = this.players[existing_index];
+      player.add_connection(connection);
+      return player;
+    }
+
+    /* No existing player. Add a new one. */
+    const id = this.next_player_id;
+    const player = new Player(id, session.id, session.nickname, connection);
+
+    /* Broadcast before adding player to list (to avoid announcing the
+     * new player to itself). */
+    const player_data = JSON.stringify({ id: player.id, name: player.name });
+    this.broadcast_event("player-enter", player_data);
+
+    this.players.push(player);
+    this.next_player_id++;
+
+    return player;
+  }
+
+  find_player(session) {
+    const existing_index = this.players.findIndex(
+      player => player.session_id === session.id);
+    if (existing_index >= 0)
+      return this.players[existing_index];
 
-    return id;
+    return null;
   }
 
-  remove_client(id) {
-    this.clients = this.clients.filter(client => client.id !== id);
+  /* Drop a connection object from a player, and if it's the last one,
+   * then drop that player from the game's list of players. */
+  remove_player_connection(player, connection) {
+    const remaining = player.remove_connection(connection);
+    if (remaining === 0) {
+      const player_data = JSON.stringify({ id: player.id });
+      this.players.filter(p => p !== player);
+      this.broadcast_event("player-exit", player_data);
+    }
   }
 
-  /* Send a string to all clients */
+  /* Send a string to all players */
   broadcast_string(str) {
-    this.clients.forEach(client => client.response.write(str + '\n'));
+    this.players.forEach(player => player.send(str + '\n'));
   }
 
-  /* Send an event to all clients.
+  /* Send an event to all players.
    *
    * An event has both a declared type and a separate data block.
    * It also ends with two newlines (to mark the end of the event).
@@ -65,12 +136,12 @@ class Game {
     };
     response.writeHead(200, headers);
 
-    /* Add this new client to our list of clients. */
-    const id = this.add_client(response);
+    /* Add this new player. */
+    const player = this.add_player(request.session, response);
 
     /* And queue up cleanup to be triggered on client close. */
     request.on('close', () => {
-      this.remove_client(id);
+      this.remove_player_connection(player, response);
     });
 
     /* Give the client the game-info event. */