├── resources └── screen_recording.gif ├── docker-start.sh ├── server ├── package.json ├── config │ └── config.js ├── index.js ├── tests │ └── snake_test.js ├── api │ └── socket_api.js ├── snake │ └── snake.js └── package-lock.json ├── .gitignore ├── docker-nginx.conf ├── LICENSE ├── README.md ├── Dockerfile └── client ├── index.html └── snake_client.js /resources/screen_recording.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yunzhu-li/multiplayer-snake/master/resources/screen_recording.gif -------------------------------------------------------------------------------- /docker-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -euo pipefail 3 | 4 | # Replace server uri and path 5 | sed -i "s|var server_uri.*|var server_uri = '$SERVER_URI';|" /app/client/snake_client.js 6 | sed -i "s|var socket_io_path.*|var socket_io_path = '$SOCKET_IO_PATH';|" /app/client/snake_client.js 7 | 8 | nginx 9 | exec node /app/server/ 10 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snake", 3 | "version": "0.0.3", 4 | "description": "A multiplayer snake game.", 5 | "dependencies": { 6 | "socket.io": "^2.4.0" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:yunzhu-li/multiplayer-snake.git" 11 | }, 12 | "scripts": { 13 | "start": "node ." 14 | }, 15 | "jshintConfig": { 16 | "esversion": 6 17 | }, 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS 40 | .DS_Store 41 | -------------------------------------------------------------------------------- /docker-nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | pid /run/nginx.pid; 4 | 5 | events { 6 | worker_connections 1024; 7 | } 8 | 9 | http { 10 | include /etc/nginx/mime.types; 11 | default_type application/octet-stream; 12 | 13 | sendfile on; 14 | server_tokens off; 15 | 16 | access_log off; 17 | error_log /dev/stderr; 18 | 19 | server { 20 | listen 8000; 21 | server_name _; 22 | 23 | # HTML client 24 | location / { 25 | root /app/client; 26 | index index.html; 27 | } 28 | 29 | location = /status { 30 | add_header Content-Type text/plain; 31 | return 200 'ok'; 32 | } 33 | 34 | # socket.io forwarding 35 | location /socket.io/ { 36 | proxy_pass http://localhost:3000; 37 | proxy_http_version 1.1; 38 | proxy_set_header Upgrade $http_upgrade; 39 | proxy_set_header Connection "upgrade"; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Yunzhu Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | snake 2 | ----- 3 | A network snake game synchronization made with nodejs and socket.io. 4 | 5 | A **live demo** is available at: [https://apps.yunzhu.li/snake](https://apps.yunzhu.li/snake/) 6 | 7 | ![Screen Recording](resources/screen_recording.gif) 8 | 9 | Branches 10 | ----- 11 | 12 | `master` - For low-latency networks (<100ms RTT). Simple and robust. 13 | 14 | `rollback_and_prediction` - For high-latency networks. Using `rollback & prediction` non-blocking synchronization. 15 | 16 | Run Your Own Copy 17 | ----- 18 | This application is available as a docker image. 19 | 20 | - Make sure you have access to `docker`. 21 | 22 | - Run: 23 | ``` 24 | docker run -d --rm -p 8000:8000 -e SERVER_URI="http://:8000" yunzhu/snake 25 | ``` 26 | 27 | - Access `http://:8000` in your browser. 28 | 29 | > The 2 environment variables `SERVER_URI` and `SOCKET_IO_PATH` (optional) are used to configure the socket.io connection info for the client page served by the container. 30 | > 31 | > The container always serves client page at `/` and socket.io connection at `/socket.io/`. 32 | > 33 | > See: https://socket.io/docs/client-api/#new-manager-url-options 34 | -------------------------------------------------------------------------------- /server/config/config.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 Yunzhu Li 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Configurations 24 | var config = {}; 25 | 26 | // Listening port 27 | config.port = 3000; 28 | 29 | // Number of rooms 30 | config.numRooms = 3; 31 | 32 | // Size of board 33 | config.boardSize = 50; 34 | 35 | module.exports.config = config; 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2016 Yunzhu Li 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | FROM node:8-alpine 24 | 25 | # Install nginx 26 | RUN apk --no-cache add nginx 27 | 28 | # Copy application & configuration 29 | COPY . /app/ 30 | COPY docker-nginx.conf /etc/nginx/nginx.conf 31 | 32 | # Install node packages 33 | RUN npm install /app/server/ 34 | 35 | # Default socket.io path 36 | ENV SOCKET_IO_PATH /socket.io 37 | 38 | # Run start script 39 | CMD ["/app/docker-start.sh"] 40 | 41 | # Default port 8000 42 | EXPOSE 8000 43 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 Yunzhu Li 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Get configuration 24 | var config = require('./config/config.js'); 25 | 26 | // Handle signals 27 | process.on('SIGTERM', function() { 28 | process.exit(0); 29 | }); 30 | 31 | process.on('SIGINT', function() { 32 | process.exit(0); 33 | }); 34 | 35 | // Start socket API service 36 | var SocketAPI = require('./api/socket_api.js'); 37 | socket_api = new SocketAPI(); 38 | socket_api.startService(config.config.port, config.config.numRooms, config.config.boardSize); 39 | -------------------------------------------------------------------------------- /server/tests/snake_test.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 Yunzhu Li 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // snake module test 24 | var chai = require('chai'); 25 | var expect = chai.expect; 26 | var Snake = require('../snake/snake.js'); 27 | 28 | describe('snake', function() { 29 | var snake, numRooms, boardSize; 30 | var roomId, playerName; 31 | 32 | beforeEach(function() { 33 | boardSize = Math.floor(Math.random() * 100 + 30); 34 | playerName = Math.random().toString(36).substring(2, 10); 35 | }); 36 | 37 | describe('startPlayer()', function() { 38 | it('Should create a player and generate an ID', function() { 39 | snake = new Snake(boardSize); 40 | expect(snake.startPlayer()).to.equal(1); 41 | expect(snake.startPlayer()).to.equal(2); 42 | expect(snake.startPlayer()).to.equal(3); 43 | }); 44 | }); 45 | 46 | describe('keyStroke()', function() { 47 | it('Should check and take a keystroke', function() { 48 | snake = new Snake(boardSize); 49 | snake.startPlayer(); 50 | expect(snake.keyStroke(1)).to.equal(false); 51 | expect(snake.keyStroke(0, {keycode: 0})).to.equal(false); 52 | expect(snake.keyStroke(1, {keycode: 1})).to.equal(true); 53 | }); 54 | }); 55 | 56 | describe('endPlayer()', function() { 57 | it('Should delete a player', function() { 58 | snake = new Snake(boardSize); 59 | snake.startPlayer(); 60 | snake.startPlayer(); 61 | expect(snake.keyStroke(1, {keycode: 1})).to.equal(true); 62 | expect(snake.endPlayer(1)).to.equal(true); 63 | expect(snake.keyStroke(1, {keycode: 1})).to.equal(false); 64 | expect(snake.keyStroke(2, {keycode: 1})).to.equal(true); 65 | expect(snake.endPlayer(1)).to.equal(false); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 27 | 28 | 29 | Snake 30 | 31 | 32 | 33 | 50 | 51 | 52 | 53 |
54 |
55 | 56 |
57 |
58 |
59 |

Snake

60 | GitHub 61 |
62 | You died.   63 | 64 |
65 |
66 |
67 | 68 |
69 |
70 | 1. Enter your name (optional) 71 | 72 |
73 | 2. Join a room! 74 | 75 | 76 | 77 | 78 | 79 |
RoomPlayersAction
80 |
81 |
82 |
83 |
84 | 85 |
86 |
87 |

Status

88 |
89 |
    90 |
91 |
92 |
93 |
94 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | 104 | 105 | -------------------------------------------------------------------------------- /server/api/socket_api.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 Yunzhu Li 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Snake socket API 24 | "use strict"; 25 | class SocketAPI { 26 | 27 | /** 28 | * Initializes snake socket API and game instance. 29 | * @param {Number} port - port to listen on 30 | * @param {Number} numRooms - number of rooms 31 | * @param {Number} boardSize - board size 32 | */ 33 | startService(port, numRooms, boardSize) { 34 | var Snake = require('../snake/snake.js'); 35 | var socket_io = require('socket.io')(); 36 | 37 | // Init each room 38 | this.rooms = []; 39 | for (var roomID = 0; roomID < numRooms; roomID++) { 40 | var room = {}; 41 | room.id = roomID; 42 | 43 | // Create snake instance 44 | room.snake = new Snake(boardSize); 45 | room.snake.setGameEventListener(this._gameEvent.bind(this)); 46 | 47 | // Reverse reference back to room 48 | room.snake.room = room; 49 | 50 | // Maps playerID to socket 51 | room.sockets = {}; 52 | this.rooms.push(room); 53 | } 54 | 55 | // Init socket.io 56 | socket_io.on('connection', this._onConnection.bind(this)); 57 | socket_io.listen(port); 58 | } 59 | 60 | /** 61 | * Handles new connection event. 62 | * @param {socket} socket - socket instance 63 | */ 64 | _onConnection(socket) { 65 | // Mark socket as not started 66 | socket.gameStarted = false; 67 | 68 | // Socket.io events 69 | socket.on('_ping', function() { 70 | socket.emit('_ping_ack'); 71 | }.bind(this)); 72 | 73 | // list_rooms (List all rooms) 74 | socket.on('list_rooms', function() { 75 | var list = []; 76 | for (var roomID in this.rooms) { 77 | var sockets = this.rooms[roomID].sockets; 78 | list.push({id: roomID, num_players: Object.keys(sockets).length}); 79 | } 80 | socket.emit('room_list', list); 81 | }.bind(this)); 82 | 83 | // start - a player joins 84 | socket.on('start', function(data) { 85 | // Cancel if already started 86 | if (socket.gameStarted) return; 87 | 88 | // Remove previous socket reference if exists 89 | this._removeSocket(socket); 90 | 91 | var roomID = data[0]; 92 | var playerName = data[1]; 93 | 94 | var room = this.rooms[roomID]; 95 | if (typeof room === 'undefined') return; 96 | 97 | var snake = room.snake; 98 | var playerID = snake.startPlayer(); 99 | 100 | // Assign player information 101 | socket.gameStarted = true; 102 | socket.roomID = roomID; 103 | socket.playerID = playerID; 104 | socket.playerName = playerName; 105 | 106 | // Add socket to set and map from playerID 107 | room.sockets[playerID] = socket; 108 | 109 | // Notify client 110 | socket.emit('started', playerID); 111 | 112 | // Broadcast join message 113 | this._sendRoomMessage(roomID, playerID, playerName, ' joined.'); 114 | }.bind(this)); 115 | 116 | // keystroke - player presses a key 117 | socket.on('keystroke', function(data) { 118 | if (!socket.gameStarted) return; 119 | if (typeof data === 'undefined') return; 120 | 121 | // Find room (game instance) 122 | var roomID = socket.roomID; 123 | var room = this.rooms[roomID]; 124 | if (typeof room === 'undefined') return; 125 | 126 | // Pass data 127 | room.snake.keyStroke(socket.playerID, data); 128 | }.bind(this)); 129 | 130 | // disconnect - player disconnects 131 | socket.on('disconnect', function() { 132 | this._removeSocket(socket); 133 | 134 | // End player if it's active 135 | if (socket.gameStarted) { 136 | var room = this.rooms[socket.roomID]; 137 | room.snake.endPlayer(socket.playerID); 138 | } 139 | }.bind(this)); 140 | } 141 | 142 | /** 143 | * Removes socket from all tracking data structures. 144 | * @param {socket} socket - socket to be removed 145 | */ 146 | _removeSocket(socket) { 147 | if (typeof socket.playerID === 'undefined') return; 148 | var room = this.rooms[socket.roomID]; 149 | delete room.sockets[socket.playerID]; 150 | } 151 | 152 | /** 153 | * Broadcasts message to all players in a room. 154 | * @param {Number} roomID - room ID 155 | * @param {Number} playerID - player ID 156 | * @param {string} playerName - player name 157 | * @param {string} message - message 158 | */ 159 | _sendRoomMessage(roomID, playerID, playerName, message) { 160 | var room = this.rooms[roomID]; 161 | if (typeof room === 'undefined') return; 162 | for (var k in room.sockets) { 163 | var s = room.sockets[k]; 164 | s.emit('message', [playerID, playerName, message]); 165 | } 166 | } 167 | 168 | /** 169 | * Handles game events. 170 | * @param {Snake} snake - snack instance 171 | * @param {string} event - event name 172 | * @param data - event data 173 | */ 174 | _gameEvent(snake, event, data) { 175 | var playerID, room, socket; 176 | 177 | if (event == 'state') { 178 | // Game state update 179 | for (playerID in snake.room.sockets) { 180 | socket = snake.room.sockets[playerID]; 181 | socket.emit('state', data); 182 | } 183 | } else if (event == 'player_delete') { 184 | // Player removal 185 | playerID = data; 186 | room = snake.room; 187 | socket = room.sockets[playerID]; 188 | if (typeof socket !== 'undefined') { 189 | // Broadcast to all players in the same room 190 | this._sendRoomMessage(room.id, playerID, room.sockets[playerID].playerName, ' died.'); 191 | 192 | // Notify client 193 | socket.emit('ended'); 194 | socket.gameStarted = false; 195 | } 196 | } 197 | } 198 | } 199 | 200 | module.exports = SocketAPI; 201 | -------------------------------------------------------------------------------- /client/snake_client.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 Yunzhu Li 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Snake game client 24 | 25 | /** 26 | * Initializes SnakeClient 27 | */ 28 | function SnakeClient() { 29 | 30 | // DOM elements 31 | this.div_settings = $('#div_settings'); 32 | this.txt_player_name = $('#txt_player_name'); 33 | this.tbody_rooms = $('#tbody_rooms'); 34 | this.canvas_board = $('#canvas_board'); 35 | this.ctx_board = $('#canvas_board')[0].getContext('2d'); 36 | this.panel_status = $('#panel_status'); 37 | this.ul_messages = $('#ul_messages'); 38 | this.div_restart = $('#div_restart'); 39 | 40 | // Game data 41 | this.playerColors = ['#2196F3', '#FF5722', '#607D8B', '#E91E63', 42 | '#9C27B0', '#795548', '#009688', '#4CAF50']; 43 | this.keyMap = {37: 0, 38: 1, 39: 2, 40: 3, 65: 0, 87: 1, 68: 2, 83: 3}; 44 | 45 | this.gameStarted = false; 46 | 47 | // Init socket.io 48 | this.initSocket(); 49 | 50 | // Keystroke handler 51 | document.onkeydown = function(e) { 52 | e = e || window.event; 53 | 54 | if (!this.gameStarted) return; 55 | 56 | // Map key codes 57 | var keyCode = this.keyMap[e.keyCode]; 58 | if (typeof keyCode === 'undefined') return; 59 | 60 | // If accepted, send to server 61 | this.socket.emit('keystroke', {keycode: keyCode}); 62 | }.bind(this); 63 | } 64 | 65 | /** 66 | * Initializes socket.io. 67 | */ 68 | SnakeClient.prototype.initSocket = function() { 69 | 70 | // Connect 71 | var server_uri = 'http://127.0.0.1:8000'; 72 | var socket_io_path = '/socket.io'; 73 | var socket = io(server_uri, { path: socket_io_path, reconnectionAttempts: 3 }); 74 | 75 | this.socket = socket; 76 | this.updateStatusPanel('#FF9800', 'Connecting'); 77 | 78 | // Connected 79 | socket.on('connect', function() { 80 | this.updateStatusPanel('#00C853', 'Connected'); 81 | 82 | // Request room list 83 | socket.emit('list_rooms'); 84 | 85 | // Measure latency 86 | setTimeout(this.measureLatency.bind(this), 100); 87 | }.bind(this)); 88 | 89 | // Disconnected 90 | socket.on('disconnect', function() { 91 | this.updateStatusPanel('#F44336', 'Disconnected'); 92 | location.reload(); 93 | }.bind(this)); 94 | 95 | socket.on('reconnect_failed', function() { 96 | this.updateStatusPanel('#F44336', 'Connection failed'); 97 | }.bind(this)); 98 | 99 | // Receives ping_ack 100 | socket.on('_ping_ack', function() { 101 | // Calculate rtt 102 | var rtt = Date.now() - this.pingTimestamp; 103 | 104 | // Display rtt 105 | if (this.gameStarted) { 106 | this.updateStatusPanel(this.playerColor(this.playerID), this.playerName + ' (' + rtt + ' ms)'); 107 | } else { 108 | this.updateStatusPanel('#00C853', 'Connected (' + rtt + ' ms)'); 109 | } 110 | 111 | // Measure latency again in 2 seconds 112 | setTimeout(this.measureLatency.bind(this), 2000); 113 | }.bind(this)); 114 | 115 | // Receives room list 116 | socket.on('room_list', function(data) { 117 | // Empty table first 118 | this.tbody_rooms.empty(); 119 | 120 | // Process list 121 | var list = data; 122 | for (var i in list) { 123 | var room = list[i]; 124 | var row = ''; 125 | row += '' + room.id + ''; 126 | row += '' + room.num_players + ''; 127 | row += 'Join'; 128 | this.tbody_rooms.append(row); 129 | } 130 | }.bind(this)); 131 | 132 | // Game started 133 | socket.on('started', function(data) { 134 | this.playerID = data; 135 | 136 | // Update UI 137 | this.div_settings.hide(); 138 | this.div_restart.hide(); 139 | this.canvas_board.show(); 140 | this.ul_messages.empty(); 141 | this.updateStatusPanel(this.playerColor(this.playerID), this.playerName); 142 | 143 | // Mark as game started 144 | this.gameStarted = true; 145 | }.bind(this)); 146 | 147 | // Game ended 148 | socket.on('ended', function() { 149 | this.div_restart.show(); 150 | }.bind(this)); 151 | 152 | // Game state update 153 | socket.on('state', function(data) { 154 | this.renderBoard(data.board); 155 | }.bind(this)); 156 | 157 | // Receives message 158 | socket.on('message', function(data) { 159 | this.addMessage(data[0], data[1], data[2]); 160 | }.bind(this)); 161 | }; 162 | 163 | /** 164 | * Measures network latency 165 | */ 166 | SnakeClient.prototype.measureLatency = function() { 167 | this.socket.emit('_ping'); 168 | this.pingTimestamp = Date.now(); 169 | }; 170 | 171 | /** 172 | * Starts game. 173 | * @param {Number} roomID - room ID 174 | */ 175 | SnakeClient.prototype.startGame = function(roomID) { 176 | // Get player name 177 | if (typeof this.playerName === 'undefined') 178 | this.playerName = this.txt_player_name.val(); 179 | 180 | if (this.playerName.length <= 0) 181 | this.playerName = this.randomPlayerName(); 182 | 183 | // Send room ID 184 | this.roomID = roomID; 185 | this.socket.emit('start', [roomID, this.playerName]); 186 | }; 187 | 188 | /** 189 | * Restarts game. 190 | */ 191 | SnakeClient.prototype.restartGame = function() { 192 | this.startGame(this.roomID); 193 | }; 194 | 195 | /** 196 | * Generates random player name. 197 | */ 198 | SnakeClient.prototype.randomPlayerName = function() { 199 | return Math.random().toString(36).substring(2, 9); 200 | }; 201 | 202 | /** 203 | * Gets color for a player ID. 204 | * @param {Number} playerID - player ID 205 | */ 206 | SnakeClient.prototype.playerColor = function(playerID) { 207 | return this.playerColors[playerID % this.playerColors.length]; 208 | }; 209 | 210 | /** 211 | * Renders board on canvas. 212 | */ 213 | SnakeClient.prototype.renderBoard = function(board) { 214 | for(var r = 0; r < 50; r++) { 215 | for(var c = 0; c < 50; c++) { 216 | var playerID = board[r][c]; 217 | 218 | var fillColor = '#DDD'; 219 | if (playerID > 0) { 220 | fillColor = this.playerColor(playerID); 221 | } else if (playerID < 0) { 222 | fillColor = '#555'; 223 | } 224 | 225 | this.ctx_board.fillStyle = fillColor; 226 | this.ctx_board.fillRect(c * 12, r * 12, 11, 11); 227 | } 228 | } 229 | }; 230 | 231 | /** 232 | * Updates status panel. 233 | * @param {string} color - color of the square before message 234 | * @param {string} message - message 235 | */ 236 | SnakeClient.prototype.updateStatusPanel = function(color, message) { 237 | var text = '
' + message; 238 | this.panel_status.html(text); 239 | }; 240 | 241 | /** 242 | * Adds a message to the status panel. 243 | * @param {Number} playerID - player ID 244 | * @param {string} playerName - player name 245 | * @param {string} message - message 246 | */ 247 | SnakeClient.prototype.addMessage = function(playerID, playerName, message) { 248 | // Random message element ID 249 | var msgElemID = Math.random().toString(10).substring(2, 10); 250 | 251 | // Generate element 252 | var text = '' + playerName + ' '; 253 | text += message; 254 | this.ul_messages.append('
  • ' + text + '
  • '); 255 | 256 | // Removes message after 2 seconds 257 | setTimeout(function() {$('#' + msgElemID).remove();}, 2000); 258 | }; 259 | -------------------------------------------------------------------------------- /server/snake/snake.js: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2016 Yunzhu Li 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | // Snake game logic 24 | "use strict"; 25 | class Snake { 26 | /** 27 | * Initializes game instance. 28 | * @param {Number} broadSize - board size 29 | */ 30 | constructor(broadSize) { 31 | // Check arguments 32 | broadSize = Number(broadSize); 33 | if (broadSize < 10) throw new Error('Invalid board size'); 34 | this.boardSize = broadSize; 35 | 36 | // Initialize game data 37 | this.players = {}; 38 | this.nextPlayerID = 1; 39 | this.board = this._createBoard(); 40 | this.directions = this._createBoard(); 41 | 42 | // Spawn foods 43 | for (var i = 0; i < 5; i++) this._spawnFood(); 44 | 45 | // Start updating 46 | this._startGameTimer(); 47 | } 48 | 49 | setGameEventListener(listener) { 50 | this._gameEventListener = listener; 51 | return true; 52 | } 53 | 54 | /** 55 | * Creates and starts a new player. 56 | */ 57 | startPlayer() { 58 | // Create and add player 59 | var player = {}; 60 | player.id = this.nextPlayerID; 61 | this.nextPlayerID++; 62 | this.players[player.id] = player; 63 | 64 | // Spawn snake for player 65 | this._spawnSnake(player); 66 | return player.id; 67 | } 68 | 69 | /** 70 | * Ends a player. 71 | * @param {Number} playerID - player ID 72 | */ 73 | endPlayer(playerID) { 74 | return this._deletePlayer(playerID); 75 | } 76 | 77 | /** 78 | * Handles key strokes from players. 79 | * @param {Number} playerID - player ID 80 | * @param {Number} data - {keycode: (0: Left, 1: Up, 2: Right, 3: Down)} 81 | */ 82 | keyStroke(playerID, data) { 83 | // Check data 84 | if (typeof data === 'undefined' || 85 | typeof data.keycode === 'undefined') return false; 86 | 87 | var keyCode = data.keycode; 88 | 89 | keyCode = Number(keyCode); 90 | if (keyCode < 0 || keyCode >= 4) return false; 91 | 92 | // Find player 93 | var player = this.players[playerID]; 94 | if (typeof player === 'undefined') return false; 95 | 96 | // Prevent 2 direction changes in 1 frame 97 | if (player.directionLock) return false; 98 | 99 | // Prevent changing to reverse-direction (0 <-> 2, 1 <-> 3) 100 | if (Math.abs(this.directions[player.head[0]][player.head[1]] - keyCode) % 2 === 0) return false; 101 | 102 | // Change head direction 103 | this.directions[player.head[0]][player.head[1]] = keyCode; 104 | 105 | // Lock direction for current frame 106 | player.directionLock = true; 107 | return true; 108 | } 109 | 110 | /** 111 | * Allocates an game board filled with zeros. 112 | */ 113 | _createBoard() { 114 | var board = new Array(this.boardSize); 115 | for (var r = 0; r < this.boardSize; r++) { 116 | board[r] = new Array(this.boardSize); 117 | for (var c = 0; c < this.boardSize; c++) { 118 | board[r][c] = 0; 119 | } 120 | } 121 | return board; 122 | } 123 | 124 | /** 125 | * Spawns a snake for a player. 126 | * @param {Number} player - player 127 | */ 128 | _spawnSnake(player) { 129 | // Find location to spawn 130 | while (true) { 131 | // Random location within a range 132 | var r = Math.floor((Math.random() * (this.boardSize - 10))); 133 | var c = Math.floor((Math.random() * this.boardSize)); 134 | 135 | // Find space for snake 136 | var found = true; 137 | for (var len = 0; len < 5; len++) { 138 | if (this.board[r][c + len] !== 0) { 139 | found = false; 140 | break; 141 | } 142 | } 143 | 144 | // Put snake on board 145 | if (found) { 146 | player.head = [r, c + 4]; 147 | player.tail = [r, c]; 148 | for (len = 0; len < 5; len++) { 149 | this.board[r][c + len] = player.id; 150 | this.directions[r][c + len] = 2; 151 | } 152 | return true; 153 | } 154 | } 155 | return false; 156 | } 157 | 158 | /** 159 | * Spawns a food at random position on board. 160 | */ 161 | _spawnFood() { 162 | var r = -1, c; 163 | while (r == -1 || this.board[r][c] !== 0) { 164 | r = Math.floor((Math.random() * this.boardSize)); 165 | c = Math.floor((Math.random() * this.boardSize)); 166 | } 167 | this.board[r][c] = -1; 168 | return true; 169 | } 170 | 171 | /** 172 | * Starts game state timer. 173 | */ 174 | _startGameTimer() { 175 | this._stopGameTimer(); 176 | this.gameTimer = setInterval(this._gameTimerEvent.bind(this), 100); 177 | return true; 178 | } 179 | 180 | /** 181 | * Stops game state timer. 182 | */ 183 | _stopGameTimer() { 184 | if (typeof this.gameTimer !== 'undefined') 185 | clearInterval(this.gameTimer); 186 | return true; 187 | } 188 | 189 | /** 190 | * Updates and sends game state. 191 | */ 192 | _gameTimerEvent() { 193 | this._nextFrame(); 194 | this._sendGameState(); 195 | } 196 | 197 | /** 198 | * Sends game state to listener. 199 | */ 200 | _sendGameState() { 201 | if (typeof this._gameEventListener !== 'undefined') { 202 | var payload = {players: this.players, board: this.board}; 203 | this._gameEventListener(this, 'state', payload); 204 | return true; 205 | } 206 | return false; 207 | } 208 | 209 | /** 210 | * Generates the next frame base on the snake game logic. 211 | */ 212 | _nextFrame() { 213 | // Process each player 214 | for (var playerID in this.players) { 215 | var player = this.players[playerID]; 216 | this._progressPlayer(player); 217 | } 218 | return true; 219 | } 220 | 221 | /** 222 | * Progresses player by 1 frame 223 | * @param {Object} player - player 224 | * @param {Boolean} moveTail - moves tail by default 225 | */ 226 | _progressPlayer(player, moveTail) { 227 | if (typeof moveTail === 'undefined') moveTail = true; 228 | 229 | var head = player.head; 230 | var tail = player.tail; 231 | 232 | // Release direction lock 233 | player.directionLock = false; 234 | 235 | // Generate new head and tail 236 | var newHead = this._nextPosition(player.head); 237 | var newTail = this._nextPosition(player.tail); 238 | 239 | // Check object in front 240 | var front_object = this.board[newHead[0]][newHead[1]]; 241 | 242 | // Handle collision, etc 243 | if (front_object == player.id) { 244 | // Hit self, dies. 245 | this._deletePlayer(player.id); 246 | return false; 247 | } else if (front_object > 0) { 248 | // Hit another player 249 | var theOtherPlayer = this.players[front_object]; 250 | if (theOtherPlayer.head[0] == newHead[0] && theOtherPlayer.head[1] == newHead[1]) { 251 | // If hits head, both dies. 252 | this._deletePlayer(player.id); 253 | this._deletePlayer(front_object); 254 | return false; 255 | } else { 256 | // Hit on body, grow, and the other player dies. 257 | this._deletePlayer(front_object); 258 | moveTail = false; 259 | } 260 | } else if (front_object == -1) { 261 | // Hit food, increase length by 1 and spawn new food. 262 | moveTail = false; 263 | this._spawnFood(); 264 | } 265 | 266 | // Checks passed, continue moving. 267 | // Update head 268 | this.board[newHead[0]][newHead[1]] = player.id; 269 | this.directions[newHead[0]][newHead[1]] = this.directions[head[0]][head[1]]; 270 | player.head = newHead; 271 | 272 | // Update tail 273 | if (moveTail) { 274 | this.board[tail[0]][tail[1]] = 0; 275 | this.directions[tail[0]][tail[1]] = 0; 276 | player.tail = newTail; 277 | } 278 | return true; 279 | } 280 | 281 | /** 282 | * Deletes a player. 283 | * @param {Number} playerID - player ID 284 | */ 285 | _deletePlayer(playerID) { 286 | var player = this.players[playerID]; 287 | if (typeof player === 'undefined') return false; 288 | 289 | var tail = player.tail; 290 | 291 | // Delete all blocks from tail 292 | while (this.board[tail[0]][tail[1]] == playerID) { 293 | this.board[tail[0]][tail[1]] = 0; 294 | tail = this._nextPosition(tail); 295 | } 296 | 297 | // Delete player object 298 | delete this.players[playerID]; 299 | 300 | // Broadcast event 301 | if (typeof this._gameEventListener !== 'undefined') 302 | this._gameEventListener(this, 'player_delete', playerID); 303 | 304 | return true; 305 | } 306 | 307 | /** 308 | * Finds next position with a given position and direction (same as key code). 309 | * @param {Array} position - current position, [r, c] 310 | * @param {Boolean} reverse - move in reverse direction 311 | */ 312 | _nextPosition(position, reverse) { 313 | if (typeof reverse === 'undefined') reverse = false; 314 | 315 | var r = position[0], c = position[1]; 316 | var d = this.directions[r][c]; 317 | var dr = 0, dc = -1; 318 | if (d == 1) { dr = -1; dc = 0; } 319 | if (d == 2) { dr = 0; dc = 1; } 320 | if (d == 3) { dr = 1; dc = 0; } 321 | if (reverse) { dr = -dr; dc = -dc; } 322 | return [(r + dr + this.boardSize) % this.boardSize, 323 | (c + dc + this.boardSize) % this.boardSize]; 324 | } 325 | } 326 | 327 | module.exports = Snake; 328 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snake", 3 | "version": "0.0.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 11 | "requires": { 12 | "mime-types": "~2.1.24", 13 | "negotiator": "0.6.2" 14 | } 15 | }, 16 | "after": { 17 | "version": "0.8.2", 18 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", 19 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" 20 | }, 21 | "arraybuffer.slice": { 22 | "version": "0.0.7", 23 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", 24 | "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" 25 | }, 26 | "backo2": { 27 | "version": "1.0.2", 28 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 29 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" 30 | }, 31 | "base64-arraybuffer": { 32 | "version": "0.1.4", 33 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", 34 | "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" 35 | }, 36 | "base64id": { 37 | "version": "2.0.0", 38 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 39 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" 40 | }, 41 | "blob": { 42 | "version": "0.0.5", 43 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", 44 | "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" 45 | }, 46 | "component-bind": { 47 | "version": "1.0.0", 48 | "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", 49 | "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" 50 | }, 51 | "component-emitter": { 52 | "version": "1.3.0", 53 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", 54 | "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" 55 | }, 56 | "component-inherit": { 57 | "version": "0.0.3", 58 | "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", 59 | "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" 60 | }, 61 | "cookie": { 62 | "version": "0.4.1", 63 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", 64 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" 65 | }, 66 | "debug": { 67 | "version": "4.1.1", 68 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 69 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 70 | "requires": { 71 | "ms": "^2.1.1" 72 | } 73 | }, 74 | "engine.io": { 75 | "version": "3.5.0", 76 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", 77 | "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", 78 | "requires": { 79 | "accepts": "~1.3.4", 80 | "base64id": "2.0.0", 81 | "cookie": "~0.4.1", 82 | "debug": "~4.1.0", 83 | "engine.io-parser": "~2.2.0", 84 | "ws": "~7.4.2" 85 | } 86 | }, 87 | "engine.io-client": { 88 | "version": "3.5.0", 89 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.0.tgz", 90 | "integrity": "sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==", 91 | "requires": { 92 | "component-emitter": "~1.3.0", 93 | "component-inherit": "0.0.3", 94 | "debug": "~3.1.0", 95 | "engine.io-parser": "~2.2.0", 96 | "has-cors": "1.1.0", 97 | "indexof": "0.0.1", 98 | "parseqs": "0.0.6", 99 | "parseuri": "0.0.6", 100 | "ws": "~7.4.2", 101 | "xmlhttprequest-ssl": "~1.5.4", 102 | "yeast": "0.1.2" 103 | }, 104 | "dependencies": { 105 | "debug": { 106 | "version": "3.1.0", 107 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 108 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 109 | "requires": { 110 | "ms": "2.0.0" 111 | } 112 | }, 113 | "ms": { 114 | "version": "2.0.0", 115 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 116 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 117 | } 118 | } 119 | }, 120 | "engine.io-parser": { 121 | "version": "2.2.1", 122 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", 123 | "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", 124 | "requires": { 125 | "after": "0.8.2", 126 | "arraybuffer.slice": "~0.0.7", 127 | "base64-arraybuffer": "0.1.4", 128 | "blob": "0.0.5", 129 | "has-binary2": "~1.0.2" 130 | } 131 | }, 132 | "has-binary2": { 133 | "version": "1.0.3", 134 | "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", 135 | "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", 136 | "requires": { 137 | "isarray": "2.0.1" 138 | } 139 | }, 140 | "has-cors": { 141 | "version": "1.1.0", 142 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", 143 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" 144 | }, 145 | "indexof": { 146 | "version": "0.0.1", 147 | "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", 148 | "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" 149 | }, 150 | "isarray": { 151 | "version": "2.0.1", 152 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 153 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 154 | }, 155 | "mime-db": { 156 | "version": "1.45.0", 157 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", 158 | "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" 159 | }, 160 | "mime-types": { 161 | "version": "2.1.28", 162 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", 163 | "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", 164 | "requires": { 165 | "mime-db": "1.45.0" 166 | } 167 | }, 168 | "ms": { 169 | "version": "2.1.3", 170 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 171 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 172 | }, 173 | "negotiator": { 174 | "version": "0.6.2", 175 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 176 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 177 | }, 178 | "parseqs": { 179 | "version": "0.0.6", 180 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", 181 | "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" 182 | }, 183 | "parseuri": { 184 | "version": "0.0.6", 185 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", 186 | "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" 187 | }, 188 | "socket.io": { 189 | "version": "2.4.0", 190 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.0.tgz", 191 | "integrity": "sha512-9UPJ1UTvKayuQfVv2IQ3k7tCQC/fboDyIK62i99dAQIyHKaBsNdTpwHLgKJ6guRWxRtC9H+138UwpaGuQO9uWQ==", 192 | "requires": { 193 | "debug": "~4.1.0", 194 | "engine.io": "~3.5.0", 195 | "has-binary2": "~1.0.2", 196 | "socket.io-adapter": "~1.1.0", 197 | "socket.io-client": "2.4.0", 198 | "socket.io-parser": "~3.4.0" 199 | } 200 | }, 201 | "socket.io-adapter": { 202 | "version": "1.1.2", 203 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", 204 | "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" 205 | }, 206 | "socket.io-client": { 207 | "version": "2.4.0", 208 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", 209 | "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", 210 | "requires": { 211 | "backo2": "1.0.2", 212 | "component-bind": "1.0.0", 213 | "component-emitter": "~1.3.0", 214 | "debug": "~3.1.0", 215 | "engine.io-client": "~3.5.0", 216 | "has-binary2": "~1.0.2", 217 | "indexof": "0.0.1", 218 | "parseqs": "0.0.6", 219 | "parseuri": "0.0.6", 220 | "socket.io-parser": "~3.3.0", 221 | "to-array": "0.1.4" 222 | }, 223 | "dependencies": { 224 | "debug": { 225 | "version": "3.1.0", 226 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 227 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 228 | "requires": { 229 | "ms": "2.0.0" 230 | } 231 | }, 232 | "ms": { 233 | "version": "2.0.0", 234 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 235 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 236 | }, 237 | "socket.io-parser": { 238 | "version": "3.3.2", 239 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", 240 | "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", 241 | "requires": { 242 | "component-emitter": "~1.3.0", 243 | "debug": "~3.1.0", 244 | "isarray": "2.0.1" 245 | } 246 | } 247 | } 248 | }, 249 | "socket.io-parser": { 250 | "version": "3.4.1", 251 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", 252 | "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", 253 | "requires": { 254 | "component-emitter": "1.2.1", 255 | "debug": "~4.1.0", 256 | "isarray": "2.0.1" 257 | }, 258 | "dependencies": { 259 | "component-emitter": { 260 | "version": "1.2.1", 261 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 262 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" 263 | } 264 | } 265 | }, 266 | "to-array": { 267 | "version": "0.1.4", 268 | "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", 269 | "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" 270 | }, 271 | "ws": { 272 | "version": "7.4.6", 273 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", 274 | "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" 275 | }, 276 | "xmlhttprequest-ssl": { 277 | "version": "1.5.5", 278 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", 279 | "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" 280 | }, 281 | "yeast": { 282 | "version": "0.1.2", 283 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", 284 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" 285 | } 286 | } 287 | } 288 | --------------------------------------------------------------------------------