diff --git a/README.txt b/README.txt index 5a2831c..bc3e548 100644 --- a/README.txt +++ b/README.txt @@ -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 diff --git a/client/connection.js b/client/connection.js index f7d5a2f..a877f8b 100644 --- a/client/connection.js +++ b/client/connection.js @@ -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; diff --git a/client/controls.js b/client/controls.js index 3c32983..82f7d43 100644 --- a/client/controls.js +++ b/client/controls.js @@ -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'); }; diff --git a/client/stack.js b/client/stack.js index 2df84e3..0ac75c6 100644 --- a/client/stack.js +++ b/client/stack.js @@ -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(); diff --git a/grid.css b/grid.css index b5297b0..c94dc72 100644 --- a/grid.css +++ b/grid.css @@ -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 { diff --git a/index.html b/index.html index b50aeb4..1ac1932 100644 --- a/index.html +++ b/index.html @@ -81,6 +81,8 @@
Reset
+ +
diff --git a/package.json b/package.json index 0ea4626..0678fd2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "node server.js" + "start": "node socket/server.js" }, "author": "", "license": "ISC", diff --git a/server.js b/server.js deleted file mode 100644 index 1e9eec0..0000000 --- a/server.js +++ /dev/null @@ -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"); \ No newline at end of file diff --git a/server/game.js b/server/game.js deleted file mode 100644 index f8762f3..0000000 --- a/server/game.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/server/ricochet.js b/server/ricochet.js new file mode 100644 index 0000000..eada038 --- /dev/null +++ b/server/ricochet.js @@ -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 +}; \ No newline at end of file diff --git a/socket/messenger.js b/socket/messenger.js new file mode 100644 index 0000000..78dbf1c --- /dev/null +++ b/socket/messenger.js @@ -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; \ No newline at end of file diff --git a/socket/server.js b/socket/server.js new file mode 100644 index 0000000..00edcb0 --- /dev/null +++ b/socket/server.js @@ -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"); +