From 4c810ccb16ea6cfcdcb7f507aea2affc2c36f163 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Fri, 5 Jun 2020 05:31:15 -0700 Subject: [PATCH] game: Rename Game.clients to Game.players, combining multiple connections 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 | 13 +++++-- game.js | 103 ++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 97 insertions(+), 19 deletions(-) diff --git a/empires.js b/empires.js index 744224e..5b9df82 100644 --- a/empires.js +++ b/empires.js @@ -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 56ee56c..baf1e55 100644 --- 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. */ -- 2.43.0