From: Carl Worth Date: Sun, 17 May 2020 19:26:42 +0000 (-0700) Subject: Rename server.js to empires.js X-Git-Url: https://git.cworth.org/git?p=empires-server;a=commitdiff_plain;h=a44b9dd5ed5bc7a221655bddbf07dcb498cac90d Rename server.js to empires.js We have multiple express-based server apps implemented here now, so the name "server.js" is now ambiguous. We rename it "empires.js" to make clear that its an implementation only of the empires game. --- diff --git a/empires.js b/empires.js new file mode 100644 index 0000000..b0fc79e --- /dev/null +++ b/empires.js @@ -0,0 +1,307 @@ +const express = require("express"); +const cors = require("cors"); +const body_parser = require("body-parser"); + +const app = express(); +app.use(cors()); + +const GameState = { + JOIN: 1, + REVEAL: 2, + CAPTURE: 3, + properties: { + 1: {name: "join"}, + 2: {name: "reveal"}, + 3: {name: "capture"} + } +}; + +/** + * Shuffles array in place. + * @param {Array} a items An array containing the items. + */ +function shuffle(a) { + if (a === undefined) + return; + + var j, x, i; + for (i = a.length - 1; i > 0; i--) { + j = Math.floor(Math.random() * (i + 1)); + x = a[i]; + a[i] = a[j]; + a[j] = x; + } +} + +class Game { + constructor() { + this._players = []; + this.characters_to_reveal = null; + this.next_player_id = 1; + this.clients = []; + this.next_client_id = 1; + this.state = GameState.JOIN; + + /* Send a comment to every connected client every 15 seconds. */ + setInterval(() => {this.broadcast_string(":");}, 15000); + } + + add_player(name, character) { + const new_player = {id: this.next_player_id, + name: name, + character: character, + captures: [], + }; + this._players.push(new_player); + this.next_player_id++; + /* The syntax here is using an anonymous function to create a new + object from new_player with just the subset of fields that we + want. */ + const player_data = JSON.stringify((({id, name}) => ({id, name}))(new_player)); + this.broadcast_event("player-join", player_data); + } + + remove_player(id) { + const index = this._players.findIndex(player => player.id === id); + this._players.splice(index, 1); + + this.broadcast_event("player-leave", `{"id": ${id}}`); + } + + reset() { + this._players = []; + this.characters_to_reveal = null; + this.next_player_id = 1; + + this.change_state(GameState.JOIN); + + this.broadcast_event("players", "{}"); + } + + reveal_next() { + if (this.reveal_index >= this.characters_to_reveal.length) { + clearInterval(this.reveal_interval); + this.broadcast_event("character-reveal", '{"character":""}'); + return; + } + const character = this.characters_to_reveal[this.reveal_index]; + this.reveal_index++; + const character_data = JSON.stringify({"character":character}); + this.broadcast_event("character-reveal", character_data); + } + + reveal() { + this.change_state(GameState.REVEAL); + + if (this.characters_to_reveal === null) { + this.characters_to_reveal = []; + this.characters_to_reveal = this._players.reduce((characters, player) => { + characters.push(player.character); + return characters; + }, []); + shuffle(this.characters_to_reveal); + } + + this.reveal_index = 0; + + this.reveal_interval = setInterval(this.reveal_next.bind(this), 3000); + } + + start() { + this.change_state(GameState.CAPTURE); + } + + capture(captor_id, captee_id) { + /* TODO: Fix to fail on already-captured players (or to move the + * captured player from an old captor to a new—need to clarify in + * the API specification which we want here. */ + let captor = this._players.find(player => player.id === captor_id); + captor.captures.push(captee_id); + + this.broadcast_event("capture", `{"captor": ${captor_id}, "captee": ${captee_id}}`); + } + + liberate(captee_id) { + let captor = this._players.find(player => player.captures.includes(captee_id)); + captor.captures.splice(captor.captures.indexOf(captee_id), 1); + } + + restart() { + for (const player of this._players) { + player.captures = []; + } + } + + get characters() { + return this._players.map(player => player.character); + } + + get empires() { + return this._players.map(player => ({id: player.id, captures: player.captures})); + } + + get players() { + return this._players.map(player => ({id: player.id, name: player.name })); + } + + add_client(response) { + const id = this.next_client_id; + this.clients.push({id: id, + response: response}); + this.next_client_id++; + + return id; + } + + remove_client(id) { + this.clients = this.clients.filter(client => client.id !== id); + } + + /* Send a string to all clients */ + broadcast_string(str) { + this.clients.forEach(client => client.response.write(str + '\n')); + } + + /* Send an event to all clients. + * + * 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). + */ + broadcast_event(type, data) { + this.broadcast_string(`event: ${type}\ndata: ${data}\n`); + } + + game_state_event_data(old_state, new_state) { + var old_state_name; + if (old_state) + old_state_name = GameState.properties[old_state].name; + else + old_state_name = "none"; + const new_state_name = GameState.properties[new_state].name; + + return `{"old_state":"${old_state_name}","new_state":"${new_state_name}"}`; + } + + /* Inform clients about a state change. */ + broadcast_state_change() { + const event_data = this.game_state_event_data(this.old_state, this.state); + this.broadcast_event("game-state", event_data); + } + + /* Change game state and broadcast the change to all clients. */ + change_state(state) { + /* Do nothing if there is no actual change happening. */ + if (state === this.state) + return; + + this.old_state = this.state; + this.state = state; + + this.broadcast_state_change(); + } +} + +const game = new Game(); + +app.use(body_parser.urlencoded({ extended: false })); +app.use(body_parser.json()); + +function handle_events(request, response) { + /* These headers will keep the connection open so we can stream events. */ + const headers = { + "Content-type": "text/event-stream", + "Connection": "keep-alive", + "Cache-Control": "no-cache" + }; + response.writeHead(200, headers); + + /* Now that a client has connected, first we need to stream all of + * the existing players (if any). */ + if (game._players.length > 0) { + const players_json = JSON.stringify(game.players); + const players_data = `event: players\ndata: ${players_json}\n\n`; + response.write(players_data); + } + + /* And we need to inform the client of the current game state. + * + * In fact, we need to cycle through each state transition from the + * beginning so the client can see each. + */ + var old_state = null; + for (var state = GameState.JOIN; state <= game.state; state++) { + var event_data = game.game_state_event_data(old_state, state); + response.write("event: game-state\n" + "data: " + event_data + "\n\n"); + old_state = state; + } + + /* Add this new client to our list of clients. */ + const id = game.add_client(response); + + /* And queue up cleanup to be triggered on client close. */ + request.on('close', () => { + game.remove_client(id); + }); +} + +app.get('/', (request, response) => { + response.send('Hello World!'); +}); + +app.post('/register', (request, response) => { + game.add_player(request.body.name, request.body.character); + response.send(); +}); + +app.post('/deregister/:id', (request, response) => { + game.remove_player(parseInt(request.params.id)); + response.send(); +}); + +app.post('/reveal', (request, response) => { + game.reveal(); + response.send(); +}); + +app.post('/start', (request, response) => { + game.start(); + response.send(); +}); + +app.post('/reset', (request, response) => { + game.reset(); + response.send(); +}); + +app.post('/capture/:captor/:captee', (request, response) => { + game.capture(parseInt(request.params.captor), parseInt(request.params.captee)); + response.send(); +}); + +app.post('/liberate/:id', (request, response) => { + game.liberate(parseInt(request.params.id)); + response.send(); +}); + +app.post('/restart', (request, response) => { + game.restart(parseInt(request.params.id)); + response.send(); +}); + +app.get('/characters', (request, response) => { + response.send(game.characters); +}); + +app.get('/empires', (request, response) => { + response.send(game.empires); +}); + +app.get('/players', (request, response) => { + response.send(game.players); +}); + +app.get('/events', handle_events); + +app.listen(3000, function () { + console.log('Empires server listening on localhost:3000'); +}); diff --git a/server.js b/server.js deleted file mode 100644 index b0fc79e..0000000 --- a/server.js +++ /dev/null @@ -1,307 +0,0 @@ -const express = require("express"); -const cors = require("cors"); -const body_parser = require("body-parser"); - -const app = express(); -app.use(cors()); - -const GameState = { - JOIN: 1, - REVEAL: 2, - CAPTURE: 3, - properties: { - 1: {name: "join"}, - 2: {name: "reveal"}, - 3: {name: "capture"} - } -}; - -/** - * Shuffles array in place. - * @param {Array} a items An array containing the items. - */ -function shuffle(a) { - if (a === undefined) - return; - - var j, x, i; - for (i = a.length - 1; i > 0; i--) { - j = Math.floor(Math.random() * (i + 1)); - x = a[i]; - a[i] = a[j]; - a[j] = x; - } -} - -class Game { - constructor() { - this._players = []; - this.characters_to_reveal = null; - this.next_player_id = 1; - this.clients = []; - this.next_client_id = 1; - this.state = GameState.JOIN; - - /* Send a comment to every connected client every 15 seconds. */ - setInterval(() => {this.broadcast_string(":");}, 15000); - } - - add_player(name, character) { - const new_player = {id: this.next_player_id, - name: name, - character: character, - captures: [], - }; - this._players.push(new_player); - this.next_player_id++; - /* The syntax here is using an anonymous function to create a new - object from new_player with just the subset of fields that we - want. */ - const player_data = JSON.stringify((({id, name}) => ({id, name}))(new_player)); - this.broadcast_event("player-join", player_data); - } - - remove_player(id) { - const index = this._players.findIndex(player => player.id === id); - this._players.splice(index, 1); - - this.broadcast_event("player-leave", `{"id": ${id}}`); - } - - reset() { - this._players = []; - this.characters_to_reveal = null; - this.next_player_id = 1; - - this.change_state(GameState.JOIN); - - this.broadcast_event("players", "{}"); - } - - reveal_next() { - if (this.reveal_index >= this.characters_to_reveal.length) { - clearInterval(this.reveal_interval); - this.broadcast_event("character-reveal", '{"character":""}'); - return; - } - const character = this.characters_to_reveal[this.reveal_index]; - this.reveal_index++; - const character_data = JSON.stringify({"character":character}); - this.broadcast_event("character-reveal", character_data); - } - - reveal() { - this.change_state(GameState.REVEAL); - - if (this.characters_to_reveal === null) { - this.characters_to_reveal = []; - this.characters_to_reveal = this._players.reduce((characters, player) => { - characters.push(player.character); - return characters; - }, []); - shuffle(this.characters_to_reveal); - } - - this.reveal_index = 0; - - this.reveal_interval = setInterval(this.reveal_next.bind(this), 3000); - } - - start() { - this.change_state(GameState.CAPTURE); - } - - capture(captor_id, captee_id) { - /* TODO: Fix to fail on already-captured players (or to move the - * captured player from an old captor to a new—need to clarify in - * the API specification which we want here. */ - let captor = this._players.find(player => player.id === captor_id); - captor.captures.push(captee_id); - - this.broadcast_event("capture", `{"captor": ${captor_id}, "captee": ${captee_id}}`); - } - - liberate(captee_id) { - let captor = this._players.find(player => player.captures.includes(captee_id)); - captor.captures.splice(captor.captures.indexOf(captee_id), 1); - } - - restart() { - for (const player of this._players) { - player.captures = []; - } - } - - get characters() { - return this._players.map(player => player.character); - } - - get empires() { - return this._players.map(player => ({id: player.id, captures: player.captures})); - } - - get players() { - return this._players.map(player => ({id: player.id, name: player.name })); - } - - add_client(response) { - const id = this.next_client_id; - this.clients.push({id: id, - response: response}); - this.next_client_id++; - - return id; - } - - remove_client(id) { - this.clients = this.clients.filter(client => client.id !== id); - } - - /* Send a string to all clients */ - broadcast_string(str) { - this.clients.forEach(client => client.response.write(str + '\n')); - } - - /* Send an event to all clients. - * - * 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). - */ - broadcast_event(type, data) { - this.broadcast_string(`event: ${type}\ndata: ${data}\n`); - } - - game_state_event_data(old_state, new_state) { - var old_state_name; - if (old_state) - old_state_name = GameState.properties[old_state].name; - else - old_state_name = "none"; - const new_state_name = GameState.properties[new_state].name; - - return `{"old_state":"${old_state_name}","new_state":"${new_state_name}"}`; - } - - /* Inform clients about a state change. */ - broadcast_state_change() { - const event_data = this.game_state_event_data(this.old_state, this.state); - this.broadcast_event("game-state", event_data); - } - - /* Change game state and broadcast the change to all clients. */ - change_state(state) { - /* Do nothing if there is no actual change happening. */ - if (state === this.state) - return; - - this.old_state = this.state; - this.state = state; - - this.broadcast_state_change(); - } -} - -const game = new Game(); - -app.use(body_parser.urlencoded({ extended: false })); -app.use(body_parser.json()); - -function handle_events(request, response) { - /* These headers will keep the connection open so we can stream events. */ - const headers = { - "Content-type": "text/event-stream", - "Connection": "keep-alive", - "Cache-Control": "no-cache" - }; - response.writeHead(200, headers); - - /* Now that a client has connected, first we need to stream all of - * the existing players (if any). */ - if (game._players.length > 0) { - const players_json = JSON.stringify(game.players); - const players_data = `event: players\ndata: ${players_json}\n\n`; - response.write(players_data); - } - - /* And we need to inform the client of the current game state. - * - * In fact, we need to cycle through each state transition from the - * beginning so the client can see each. - */ - var old_state = null; - for (var state = GameState.JOIN; state <= game.state; state++) { - var event_data = game.game_state_event_data(old_state, state); - response.write("event: game-state\n" + "data: " + event_data + "\n\n"); - old_state = state; - } - - /* Add this new client to our list of clients. */ - const id = game.add_client(response); - - /* And queue up cleanup to be triggered on client close. */ - request.on('close', () => { - game.remove_client(id); - }); -} - -app.get('/', (request, response) => { - response.send('Hello World!'); -}); - -app.post('/register', (request, response) => { - game.add_player(request.body.name, request.body.character); - response.send(); -}); - -app.post('/deregister/:id', (request, response) => { - game.remove_player(parseInt(request.params.id)); - response.send(); -}); - -app.post('/reveal', (request, response) => { - game.reveal(); - response.send(); -}); - -app.post('/start', (request, response) => { - game.start(); - response.send(); -}); - -app.post('/reset', (request, response) => { - game.reset(); - response.send(); -}); - -app.post('/capture/:captor/:captee', (request, response) => { - game.capture(parseInt(request.params.captor), parseInt(request.params.captee)); - response.send(); -}); - -app.post('/liberate/:id', (request, response) => { - game.liberate(parseInt(request.params.id)); - response.send(); -}); - -app.post('/restart', (request, response) => { - game.restart(parseInt(request.params.id)); - response.send(); -}); - -app.get('/characters', (request, response) => { - response.send(game.characters); -}); - -app.get('/empires', (request, response) => { - response.send(game.empires); -}); - -app.get('/players', (request, response) => { - response.send(game.players); -}); - -app.get('/events', handle_events); - -app.listen(3000, function () { - console.log('Empires server listening on localhost:3000'); -});