From c1764a7d89b9a480da8559d154eb0423e8c63764 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 29 Oct 2020 15:32:54 +0100 Subject: [PATCH] Ported server code to nodeJS --- client/src/App.vue | 2 +- client/src/components/Game.vue | 2 +- client/src/store/socketPlugin.js | 10 +-- server/package-lock.json | 5 ++ server/package.json | 9 +-- server/src/game.js | 107 ++++++++++++++++++++++++++++++ server/src/index.js | 11 --- server/src/models/gameSettings.js | 8 +++ server/src/models/player.js | 74 +++++++++++++++++++++ server/src/models/wall.js | 8 +++ server/src/server.js | 35 ++++++++++ 11 files changed, 249 insertions(+), 22 deletions(-) create mode 100644 server/src/game.js delete mode 100644 server/src/index.js create mode 100644 server/src/models/gameSettings.js create mode 100644 server/src/models/player.js create mode 100644 server/src/models/wall.js create mode 100644 server/src/server.js diff --git a/client/src/App.vue b/client/src/App.vue index 11c3fb6..9a59dc6 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -72,7 +72,7 @@ export default { login (name) { send({ type: 'login', - message: name + name: name }) } } diff --git a/client/src/components/Game.vue b/client/src/components/Game.vue index 32ac548..caee1ea 100644 --- a/client/src/components/Game.vue +++ b/client/src/components/Game.vue @@ -141,7 +141,7 @@ export default { this.player.targetAngle = Math.atan2(dy, dx) this.player.angle = this.player.targetAngle - send({ message: this.player, type: 'update' }) + send({ player: this.player, type: 'update' }) }, updatePlayer (player) { player.x += this.settings.playerSpeed * Math.cos(player.angle) diff --git a/client/src/store/socketPlugin.js b/client/src/store/socketPlugin.js index 3f72e3f..d6665f0 100644 --- a/client/src/store/socketPlugin.js +++ b/client/src/store/socketPlugin.js @@ -31,19 +31,19 @@ export default function createSocketPlugin () { switch (data.type) { case 'login': - store.dispatch('game/login', data.message) + store.dispatch('game/login', data.player) break case 'gameSettings': - store.dispatch('game/settings', data.message) + store.dispatch('game/settings', data.gameSettings) break case 'gameUpdate': - store.dispatch('game/update', data.message) + store.dispatch('game/update', data) break case 'gamePlayerDead': - store.dispatch('game/dead', data.message) + store.dispatch('game/dead', data.player) break case 'gamePlayerSpawn': - store.dispatch('game/spawn', data.message) + store.dispatch('game/spawn', data.player) break default: break diff --git a/server/package-lock.json b/server/package-lock.json index 8b90e1d..9bff68d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1910,6 +1910,11 @@ "requires": { "mkdirp": "^0.5.1" } + }, + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" } } } diff --git a/server/package.json b/server/package.json index 7e7265c..cef5fcb 100644 --- a/server/package.json +++ b/server/package.json @@ -1,10 +1,10 @@ { "name": "tronio-server", "version": "2.0.0", - "description": "Tron.io server", - "main": "index.js", + "description": "Tron.io game server", + "main": "server.js", "scripts": { - "start": "node src/index.js", + "start": "node src/server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -14,7 +14,8 @@ "author": "gltron", "license": "ISC", "dependencies": { - "express": "^4.17.1" + "express": "^4.17.1", + "ws": "^7.3.1" }, "devDependencies": { "eslint": "^7.12.1", diff --git a/server/src/game.js b/server/src/game.js new file mode 100644 index 0000000..3950382 --- /dev/null +++ b/server/src/game.js @@ -0,0 +1,107 @@ +const Player = require('./models/player') +const gameSettings = require('./models/gameSettings') + +const players = new Map() +const sockets = new Map() + +let lastUpdateTime = Date.now() +let doUpdate = true + +function login (connection, name) { + sockets[connection.id] = connection + players[connection.id] = new Player(connection.id, name) + + connection.send({ + type: 'login', + player: players[connection.id] + }) + + connection.send({ + type: 'gameSettings', + gameSettings: gameSettings + }) +} + +function logout (connection) { + sockets.delete(connection.id) + players.delete(connection.id) +} + +function respawn (connection) { + players[connection.id].reset() + + connection.send({ + type: 'gamePlayerSpawn', + player: players[connection.id] + }) +} + +function update (connection, player) { + players[connection.id].angle = player.angle +} + +function kill (player) { + player.kill() + sockets[player.id].send({ + type: 'gamePlayerDead', + player: player + }) +} + +function step () { + const currentTime = Date.now() + const durationSinceLastUpdate = (currentTime - lastUpdateTime) / 1000 + const tickToSimulate = (durationSinceLastUpdate * 120) / 1000 + lastUpdateTime = currentTime + + players.forEach((player, id) => { + if (player.isOutOfBorders()) kill(player) + + players.forEach((player2, id2) => { + if (player.state === 'DEAD') return + + for (let i = 0; i < player2.walls.length - 2; i++) { + // Prevent self destroy on last wall + if (id === id2 && i >= player2.walls.length - 1) break + + const wallA = player2.walls[i] + const wallB = player2.walls[i + 1] + + if (player.isCloseToWall(wallA, wallB)) { + if (player.isCrossingLine(wallA, wallB)) { + if (id !== id2) player2.score += 300 + kill(player) + return + } + } + } + }) + }) + + players.forEach((player, id) => { + player.step(tickToSimulate) + }) + + if (doUpdate) broadcastUpdate() + doUpdate = !doUpdate +} + +function broadcastUpdate () { + const update = { + type: 'update', + players: players, + time: lastUpdateTime + } + sockets.forEach((connection, id) => { + connection.send(update) + }) +} + +setInterval(() => step(), 1000 / 60) + +module.exports = { + login, + logout, + respawn, + update +} diff --git a/server/src/index.js b/server/src/index.js deleted file mode 100644 index 14356c1..0000000 --- a/server/src/index.js +++ /dev/null @@ -1,11 +0,0 @@ -const express = require('express') -const app = express() -const port = 3000 - -app.get('/', (req, res) => { - res.send('Hello World!') -}) - -app.listen(port, () => { - console.log(`Example app listening at http://localhost:${port}`) -}) diff --git a/server/src/models/gameSettings.js b/server/src/models/gameSettings.js new file mode 100644 index 0000000..df1d75a --- /dev/null +++ b/server/src/models/gameSettings.js @@ -0,0 +1,8 @@ +module.exports = Object.freeze({ + playerSize: 10, + playerSpeed: 2.3, + playerTurnSpeed: 10, + wallSize: 8, + wallUpdate: 20, + arenaSize: 1000 +}) diff --git a/server/src/models/player.js b/server/src/models/player.js new file mode 100644 index 0000000..338903a --- /dev/null +++ b/server/src/models/player.js @@ -0,0 +1,74 @@ +const Wall = require('./wall') +const gameSettings = require('./gameSettings') + +class Player { + constructor (id, name) { + this.id = id + this.name = name + this.bestScore = 0 + this.angle = 0 + this.reset() + } + + reset () { + this.score = 0 + this.color = '#' + (0x1000000 + (Math.random()) * 0xffffff).toString(16).substr(1, 6) + this.x = gameSettings.arenaSize * (0.25 + Math.random() * 0.5) + this.y = gameSettings.arenaSize * (0.25 + Math.random() * 0.5) + this.walls = [] + this.lastWall = 0 + this.state = 'ALIVE' + } + + kill () { + this.state = 'DEAD' + if (this.bestScore < this.score) this.bestScore = this.score + } + + isOutOfBorders () { + return this.x - gameSettings.playerSize < 0 || + this.x + gameSettings.playerSize > gameSettings.arenaSize || + this.y - gameSettings.playerSize < 0 || + this.y + gameSettings.playerSize > gameSettings.arenaSize + } + + isCloseToWall (wallA, wallB) { + const xar = Math.min(wallA.x, wallB.x) - gameSettings.playerSize + const yar = Math.min(wallA.y, wallB.y) - gameSettings.playerSize + const xbr = Math.min(wallA.x, wallB.x) + gameSettings.playerSize + const ybr = Math.min(wallA.y, wallB.y) + gameSettings.playerSize + + return ((this.x >= xar && this.x <= xbr) && (this.y >= yar && this.y <= ybr)) + } + + isCrossingLine (wallA, wallB) { + const xa = wallA.x + const ya = wallA.y + const xb = wallB.x + const yb = wallB.y + const xc = this.x + const yc = this.y + const radius = gameSettings.playerSize + + return Math.abs((yb - ya) * xc - (xb - xa) * yc + xb * ya - yb * xa) / Math.sqrt(Math.pow(xb - xa, 2) + Math.pow(yb - ya, 2)) < radius + } + + step (tickToSimulate) { + if (this.state === 'DEAD') return + + for (let i = 0; i < tickToSimulate; i++) { + this.lastWall++ + + if (this.lastWall > gameSettings.wallUpdate) { + this.walls.push(new Wall(this.x, this.y)) + this.score++ + this.lastWall = 0 + } + + this.x = this.x + gameSettings.playerSpeed * Math.cos(this.angle) + this.y = this.y + gameSettings.playerSpeed * Math.sin(this.angle) + } + } +} + +module.exports = { Player } diff --git a/server/src/models/wall.js b/server/src/models/wall.js new file mode 100644 index 0000000..a0b0446 --- /dev/null +++ b/server/src/models/wall.js @@ -0,0 +1,8 @@ +class Wall { + constructor (x, y) { + this.x = x + this.y = y + } +} + +module.exports = { Wall } diff --git a/server/src/server.js b/server/src/server.js new file mode 100644 index 0000000..9845fcd --- /dev/null +++ b/server/src/server.js @@ -0,0 +1,35 @@ +const express = require('express') +const app = express() +const game = require('./game') +const WebSocketServer = require('ws').Server +const port = process.env.PORT || 3000 + +app.get('/', (req, res) => { + res.send('Hello World!') +}) + +const server = app.listen(port, () => { + console.log(`Tron.io running on port ${port}`) +}) + +const wss = new WebSocketServer({ server }) + +wss.on('connection', function (connection) { + connection.on('close', game.logout(connection)) + + connection.on('message', (message) => { + let data + + try { + data = JSON.parse(message) + } catch (e) { + return + } + + switch (data.type) { + case 'login': game.login(connection, data.name); break + case 'respawn': game.respawn(connection); break + case 'update': game.update(connection, data.player); break + } + }) +})