Namespacing socket server architecture.

master
Ben Burlingham 5 years ago
parent 33fb7a897e
commit 1b8244c8d7
  1. 15
      README.txt
  2. 5
      client/connection.js
  3. 14
      client/controls.js
  4. 8
      client/stack.js
  5. 3
      grid.css
  6. 2
      index.html
  7. 2
      package.json
  8. 130
      server.js
  9. 124
      server/game.js
  10. 227
      server/ricochet.js
  11. 35
      socket/messenger.js
  12. 33
      socket/server.js

@ -13,21 +13,18 @@ A victory state can be stored by taking a snapshot of the current stack.
Icons from [https://game-icons.net](https://game-icons.net)
## TODO
- win declare/add/remove
- no more guesses, send stack and replay
- win declare
- replay stack
- countdown skip
- move guess and move logic out of server (clean up server file)
- slide arrows
- chat box
- no cancel from name prompt
- restore state on join
- walls algorigthm
- limit concurrent players, make sure connections are closed, clean up empty rooms
- cookie room link, add to all messages, namespace them
- limit concurrent players, make sure connections are closed
- move websocket server to /core
- dynamic socket server resolution
- namespace server to /ricochet
- walls and winstate algorithm
- tutorial
- donate link

@ -18,6 +18,10 @@ const Connection = function() {
this.ws.send(JSON.stringify({ type: 'skip' }));
});
document.addEventListener('L-solve', (evt) => {
this.ws.send(JSON.stringify({ type: 'solve', rawBody: evt.detail }));
});
document.addEventListener('L-start', () => {
this.ws.send(JSON.stringify({ type: 'start' }));
});
@ -71,6 +75,7 @@ Connection.prototype.onReceiveMessage = function({ data }) {
switch (msg.type) {
case 'connected': eventName = 'G-connected'; break;
case 'countdown': eventName = 'G-countdown'; break;
case 'guess': eventName = 'G-guess'; break;
case 'players': eventName = 'G-players'; break;
case 'robots': eventName = 'G-robots'; break;

14
client/controls.js vendored

@ -11,6 +11,7 @@ const Controls = function() {
// document.addEventListener('L-undo', this.msgUndo.bind(this));
// document.addEventListener('G-guess', this.msgGuess.bind(this));
document.addEventListener('G-countdown', this.msgCountdown.bind(this));
document.addEventListener('G-players', this.msgPlayers.bind(this));
document.addEventListener('G-skip', this.msgSkip.bind(this));
document.addEventListener('G-start', this.msgStart.bind(this));
@ -22,6 +23,7 @@ const Controls = function() {
document.getElementById('controls-skip').addEventListener('click', this.onClickSkip.bind(this));
document.getElementById('controls-start').addEventListener('click', this.onClickStart.bind(this));
document.getElementById('controls-stop').addEventListener('click', this.onClickStop.bind(this));
document.getElementById('controls-submit').addEventListener('click', this.onClickSubmit.bind(this));
document.getElementById('controls-undo').addEventListener('click', this.onClickUndo.bind(this));
document.getElementById('controls-walls').addEventListener('click', this.onClickWalls.bind(this));
}
@ -77,9 +79,11 @@ const Controls = function() {
//===== Message handlers
// Controls.prototype.msgConnected = function() {
// this.showWaiting();
// };
Controls.prototype.msgCountdown = function(evt) {
// this.showWaiting();
console.error(evt);
alert("COUNTDOWN RECEIVED");
};
Controls.prototype.msgStack = function(evt) {
const robots = evt.detail.reduce((acc, { id }) => acc.has(id) ? acc : acc.add(id), new Set());
@ -162,6 +166,10 @@ Controls.prototype.onClickStop = function() {
this.dispatch('L-stop');
};
Controls.prototype.onClickSubmit = function() {
this.dispatch('L-submit');
};
Controls.prototype.onClickUndo = function() {
this.dispatch('L-undo');
};

@ -5,6 +5,7 @@ const Stack = function() {
this.moves = [];
document.addEventListener('L-arrow', this.msgArrow.bind(this));
document.addEventListener('L-submit', this.msgSubmit.bind(this));
document.addEventListener('L-undo', this.msgUndo.bind(this));
document.addEventListener('L-reset', this.msgReset.bind(this));
@ -47,6 +48,13 @@ Stack.prototype.msgReset = function() {
document.dispatchEvent(evtStack);
};
Stack.prototype.msgSubmit = function() {
this.moves = this.getInitialPositions();
const evtSolve = new CustomEvent('L-solve', { detail: this.moves });
document.dispatchEvent(evtSolve);
};
Stack.prototype.msgUndo = function() {
this.moves.pop();

@ -59,7 +59,7 @@
.content-arrows {
position: absolute;
transition: left 0.4s cubic-bezier(0,1,.5,1), top 0.4s cubic-bezier(0,1,.5,1);
z-index: 1;
z-index: 5;
}
.content-arrow {
@ -68,6 +68,7 @@
position: absolute;
text-align: center;
user-select: none;
z-index: 5;
}
.content-arrow:hover {

@ -81,6 +81,8 @@
<div class='controls-button' id='controls-reset'>Reset</div>
</div>
<button id='controls-submit'>Submit</button>
<div id="controls-panic">
<div class='controls-alert-urgent'></div>

@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"start": "node server.js"
"start": "node socket/server.js"
},
"author": "",
"license": "ISC",

@ -1,130 +0,0 @@
const WebSocket = require('ws');
const url = require('url');
const uuid = require('node-uuid');
const jsDir = `${__dirname}/server`;
const Game = require(`${jsDir}/game.js`);
const wss = new WebSocket.Server({ port: 8080 });
const DEBUG = true;
const G = new Game();
// Global, for now. Is there a need for an instance? Ben 052220
const Server = {
games: {},
messageOne: (ws, message) => {
DEBUG && console.log(`Sending to only ${ws.id}:`);
DEBUG && console.log(message);
ws.send(JSON.stringify(message));
},
messageOthers: (ws, message) => {
DEBUG && console.log(`Sending to other client(s):`);
DEBUG && console.log(message);
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
},
messageAll: (message) => {
DEBUG && console.log(`Sending to all ${wss.clients.size} client(s):`);
DEBUG && console.log(message);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
},
onDisconnect: (ws) => {
DEBUG && console.log("Disconnected:");
DEBUG && console.log(ws.id);
G.removePlayer(ws.id);
Server.messageOthers(ws, { type: 'players', body: G.getPlayers() });
},
onConnect: (ws, req) => {
ws.id = uuid.v4();
ws.on('message', Server.onMessage.bind(null, ws));
ws.on('close', Server.onDisconnect.bind(null, ws));
const query = url.parse(req.url, true).query;
const santizedName = (query.name || 'Unknown').replace(/[^\w ]/g, '');
DEBUG && console.log("Connected:");
DEBUG && console.log (`${santizedName} ${ws.id} via ${req.url}`);
G.addPlayer(ws.id, santizedName);
Server.messageAll({ type: 'players', body: G.getPlayers() });
Server.messageOne(ws, { type: 'walls', body: G.getWalls()});
Server.messageOne(ws, { type: 'robots', body: G.getRobots()});
Server.messageOne(ws, { type: 'winstate', body: G.getWinState()});
Server.messageOne(ws, { type: 'connected', body: ws.id});
},
onMessage: (ws, json) => {
const message = JSON.parse(json);
DEBUG && console.log('Received message: ');
DEBUG && console.log(message);
if (!message.type) {
DEBUG && console.warn("Unprocessable message: ")
DEBUG && console.warn(message);
return;
}
switch (message.type) {
case 'guess':
const santizedGuess = message.rawBody.moves * 1;
santizedGuess && Server.messageAll({ type: 'guess', guess: santizedGuess, id: ws.id });
G.onGuess(santizedGuess).then(Server.messageAll.bind(null, { type: 'solve', id: ws.id }));
break;
// case 'move':
// const sanitizedRobotId = message.rawBody.id.replace(/[^0-9a-zA-Z]/g, '');
// const sanitizedI = message.rawBody.i * 1;
// const sanitizedJ = message.rawBody.j * 1;
// const body = {
// i: sanitizedI,
// j: sanitizedJ,
// id: sanitizedRobotId
// };
// Server.messageOthers(ws, { type: 'move', body });
// break;
case 'robots':
Server.messageAll({ type: 'robots', body: G.getRobots()});
break;
case 'skip':
Server.messageAll({ type: 'start' });
break;
case 'start':
Server.messageAll({ type: 'start' });
break;
case 'stop':
Server.messageAll({ type: 'stop' });
break;
case 'walls':
Server.messageAll({ type: 'walls', body: G.getWalls()});
break;
default:
console.warn("Unknown message type: ", message.head.type)
}
},
};
wss.on('connection', Server.onConnect);
console.log("Websocket server listening on :8080");

@ -1,124 +0,0 @@
const uuid = require('node-uuid');
const players = {};
const Game = function() {
this.id = uuid.v4();
this.countdownTimer = null;
this.guess = Infinity;
this.squaresPerSide = 20;
const robots = ['#E00000', '#00C000', '#0000FF', '#00C0C0', '#F000F0'];
const icons = ['assets/comet.svg', 'assets/moon.svg', 'assets/planet.svg', 'assets/rocket.svg', 'assets/spacesuit.svg'];
// spider.svg, ufo.svg
const gen = () => Math.floor(Math.random() * this.squaresPerSide);
this.TEMP_ROBOTS = robots.map((color, idx) => ({ i: gen(), j: gen(), color, id: uuid.v4(), icon: icons[idx] }));
}
Game.prototype.addPlayer = function(id, name) {
if (!players[id]) {
players[id] = name;
}
}
Game.prototype.removePlayer = function(id) {
players[id] = undefined;
delete players[id];
}
Game.prototype.getPlayers = function() {
return players;
}
Game.prototype.getRobots = function() {
return this.TEMP_ROBOTS;
}
Game.prototype.getWalls = function() {
// Edge IDs are of the form [i1-j1-i2-j2]. Top left is 0, 0.
// Leave here for testing.
// return [
// "1-9-1-10",
// "9-1-10-1",
// "9-19-10-19",
// "19-9-19-10"
// ];
// console.log("Generating walls.");
// Squares per side has quadratic relationship with wall/corner requirements.
const numberOfCorners = Math.ceil(Math.pow((this.squaresPerSide / 10), 2));
const numberOfWalls = Math.ceil(Math.pow((this.squaresPerSide / 5), 2));
const gen = () => Math.floor(Math.random() * this.squaresPerSide);
const edges = [];
// DO NUMBER OF CORNERS FIRST AFTER TESTING
for (let n = 0; n < numberOfWalls; n++) {
const ri = gen();
const rj = gen();
const isHorizontal = Math.random() < 0.5;
const isBackward = Math.random() < 0.5;
let i1, j1, i2, j2;
if (isHorizontal) {
i1 = isBackward ? ri - 1 : ri;
i2 = isBackward ? ri : ri + 1;
j1 = rj;
j2 = rj;
} else {
i1 = ri;
i2 = ri;
j1 = isBackward ? rj - 1 : rj;
j2 = isBackward ? rj : rj + 1;
}
const edge = `${i1}-${j1}-${i2}-${j2}`;
if (edges.includes(edge)) {
n--;
} else {
edges.push(edge);
}
}
return edges;
};
Game.prototype.getWinState = function() {
// const gen = () => Math.floor(Math.random() * this.squaresPerSide);
return { i: 0, j: 0, id: this.TEMP_ROBOTS[0].id };
};
Game.prototype.onGuess = function(guess) {
return new Promise((resolve, reject) => {
const timeIsUp = () => {
this.guess = Infinity;
resolve();
};
if (guess < 1) {
reject(`${guess} is less than 1.`);
return;
}
if (guess >= this.guess) {
reject(`${guess} is greater than ${this.guess}.`);
return;
}
this.guess = guess;
clearTimeout(this.countdownTimer);
this.countdownTimer = setTimeout(timeIsUp, 5 * 1000);
});
};
module.exports = Game;

@ -0,0 +1,227 @@
const uuid = require('node-uuid');
const UrlParser = require('url');
///////////////
// REMEMBER THIS WILL BE IN A COMPLETELY SEPARATE DIRECTORY
///////////////
const Ricochet = function({ messenger }) {
this.messenger = messenger;
console.log("HELLO RICOCHET")
// const players = {};
// this.id = uuid.v4();
// this.countdownTimer = null;
// this.guess = Infinity;
// this.squaresPerSide = 20;
// this.winningStack;
// const robots = ['#E00000', '#00C000', '#0000FF', '#00C0C0', '#F000F0'];
// const icons = ['assets/comet.svg', 'assets/moon.svg', 'assets/planet.svg', 'assets/rocket.svg', 'assets/spacesuit.svg'];
// // spider.svg, ufo.svg
// const gen = () => Math.floor(Math.random() * this.squaresPerSide);
// this.TEMP_ROBOTS = robots.map((color, idx) => ({ i: gen(), j: gen(), color, id: uuid.v4(), icon: icons[idx] }));
};
Ricochet.prototype.onConnect = function(ws, req) {
console.log("CONNECTED TO RICOCHET INSTANCE")
// const query = url.query;
// const santizedName = (query.name || 'Unknown').replace(/[^\w ]/g, '');
// DEBUG && console.log("Connected:");
// DEBUG && console.log (`${santizedName} ${ws.id} via ${req.url}`);
// G.addPlayer(ws.id, santizedName);
// Server.messageAll({ type: 'players', body: G.getPlayers() });
// Server.messageOne(ws, { type: 'walls', body: G.getWalls()});
// Server.messageOne(ws, { type: 'robots', body: G.getRobots()});
// Server.messageOne(ws, { type: 'winstate', body: G.getWinState()});
// Server.messageOne(ws, { type: 'connected', body: ws.id});
}
Ricochet.prototype.onMessage = function(ws, json) {
};
Ricochet.prototype.onDisconnect = function(ws) {
// onDisconnect: (ws) => {
// DEBUG && console.log("Disconnected:");
// DEBUG && console.log(ws.id);
// G.removePlayer(ws.id);
// Server.messageOthers(ws, { type: 'players', body: G.getPlayers() });
// },
};
// Game.prototype.addPlayer = function(id, name) {
// if (!players[id]) {
// players[id] = name;
// }
// }
// Game.prototype.removePlayer = function(id) {
// players[id] = undefined;
// delete players[id];
// }
// Game.prototype.getPlayers = function() {
// return players;
// }
// Game.prototype.getRobots = function() {
// return this.TEMP_ROBOTS;
// }
// Game.prototype.getWalls = function() {
// // Edge IDs are of the form [i1-j1-i2-j2]. Top left is 0, 0.
// // Leave here for testing.
// // return [
// // "1-9-1-10",
// // "9-1-10-1",
// // "9-19-10-19",
// // "19-9-19-10"
// // ];
// // console.log("Generating walls.");
// // Squares per side has quadratic relationship with wall/corner requirements.
// const numberOfCorners = Math.ceil(Math.pow((this.squaresPerSide / 10), 2));
// const numberOfWalls = Math.ceil(Math.pow((this.squaresPerSide / 5), 2));
// const gen = () => Math.floor(Math.random() * this.squaresPerSide);
// const edges = [];
// // DO NUMBER OF CORNERS FIRST AFTER TESTING
// for (let n = 0; n < numberOfWalls; n++) {
// const ri = gen();
// const rj = gen();
// const isHorizontal = Math.random() < 0.5;
// const isBackward = Math.random() < 0.5;
// let i1, j1, i2, j2;
// if (isHorizontal) {
// i1 = isBackward ? ri - 1 : ri;
// i2 = isBackward ? ri : ri + 1;
// j1 = rj;
// j2 = rj;
// } else {
// i1 = ri;
// i2 = ri;
// j1 = isBackward ? rj - 1 : rj;
// j2 = isBackward ? rj : rj + 1;
// }
// const edge = `${i1}-${j1}-${i2}-${j2}`;
// if (edges.includes(edge)) {
// n--;
// } else {
// edges.push(edge);
// }
// }
// return edges;
// };
// Game.prototype.getWinState = function() {
// // const gen = () => Math.floor(Math.random() * this.squaresPerSide);
// return { i: 0, j: 0, id: this.TEMP_ROBOTS[0].id };
// };
// // Game.prototype.onGuess = function(guess) {
// // return new Promise((resolve, reject) => {
// // const timeIsUp = () => {
// // this.guess = Infinity;
// // resolve();
// // };
// // if (guess < 1) {
// // reject(`${guess} is less than 1.`);
// // return;
// // }
// // if (guess >= this.guess) {
// // reject(`${guess} is greater than ${this.guess}.`);
// // return;
// // }
// // this.guess = guess;
// // clearTimeout(this.countdownTimer);
// // this.countdownTimer = setTimeout(timeIsUp, 5 * 1000);
// // });
// // };
// Game.prototype.onSolve = function(rawMoveStack) {
// const sanitizedStack = rawMoveStack.map(move => {
// const sanitizedRobotId = move.id.replace(/[^0-9a-zA-Z]/g, '');
// const sanitizedI = move.i * 1;
// const sanitizedJ = move.j * 1;
// return {
// i: sanitizedI,
// j: sanitizedJ,
// id: sanitizedRobotId
// };
// });
// this.winningStack = sanitizedStack;
// return sanitizedStack;
// }
// onMessage: (ws, json) => {
// const message = JSON.parse(json);
// DEBUG && console.log('Received message: ');
// DEBUG && console.log(message);
// if (!message.type) {
// DEBUG && console.warn("Unprocessable message: ")
// DEBUG && console.warn(message);
// return;
// }
// switch (message.type) {
// case 'robots':
// Server.messageAll({ type: 'robots', body: G.getRobots()});
// break;
// case 'skip':
// Server.messageAll({ type: 'start' });
// break;
// case 'solve':
// const sanitizedStack = G.onSolve(message.rawBody);
// Server.messageAll(ws, { type: 'countdown', body: { id: ws.id, stack: sanitizedStack } });
// break;
// case 'start':
// Server.messageAll({ type: 'start' });
// break;
// case 'stop':
// Server.messageAll({ type: 'stop' });
// break;
// case 'walls':
// Server.messageAll({ type: 'walls', body: G.getWalls()});
// break;
// default:
// console.warn("Unknown message type: ", message.head.type)
// }
// },
module.exports = {
new: Ricochet
};

@ -0,0 +1,35 @@
const WebSocket = require('ws');
const DEBUG = (process.env.NODE_ENV !== "production");
const Messenger = {
messageOne: (ws, message) => {
DEBUG && console.log(`Sending to only ${ws.id}:`);
DEBUG && console.log(message);
ws.send(JSON.stringify(message));
},
messageOthers: (ws, message) => {
DEBUG && console.log(`Sending to other client(s):`);
DEBUG && console.log(message);
wss.clients.forEach((client) => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
},
messageAll: (message) => {
DEBUG && console.log(`Sending to all ${wss.clients.size} client(s):`);
DEBUG && console.log(message);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
},
};
module.exports = Messenger;

@ -0,0 +1,33 @@
const WebSocket = require('ws');
const UrlParser = require('url');
const uuid = require('node-uuid');
const messenger = require('./messenger');
const jsDir = `${__dirname}/server`;
const RicochetApp = require(`${jsDir}/ricochet.js`);
const clientApps = {
'/ricochet': RicochetApp.new({ messenger })
};
const onConnect = (ws, req) => {
// Store an ID on the socket connection.
ws.id = uuid.v4();
const url = UrlParser.parse(req.url, true);
if (clientApps[url.pathname]) {
app.onConnect(ws, req);
ws.on('message', app.onMessage);
ws.on('close', app.onDisconnect);
}
};
//===== Generic socket server for any application instantiated above.
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', onConnect);
console.log("Websocket server listening on :8080");
Loading…
Cancel
Save