From 1cf7e0b75670d61f16506154311c1f76d76f78a2 Mon Sep 17 00:00:00 2001 From: Ben Burlingham Date: Sun, 28 Jun 2020 16:09:50 -0700 Subject: [PATCH] Countdown management complete. --- README.txt | 11 +- client/connection.js | 35 ++---- client/controls.js | 215 ++++++++++++++++++++--------------- client/grid.js | 36 +++--- client/stack.js | 10 +- index.html | 26 +++-- server/ricochet.js | 264 ++++++++++++++++++++++++++----------------- socket/server.js | 4 +- style/controls.css | 37 +++--- style/grid.css | 2 +- style/index.css | 1 + 11 files changed, 363 insertions(+), 278 deletions(-) diff --git a/README.txt b/README.txt index a2a70e0..90973fa 100644 --- a/README.txt +++ b/README.txt @@ -13,16 +13,23 @@ 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 +- (n solves) in players list, if > 0 +- join mid-countdown +- robot color update +- countdown solution moves value +- countdown solution name value +- better solution - replay stack - countdown skip - slide arrows - chat box - no cancel from name prompt -- clear out the trash bins of history (client) +- clear out the trash bins of history - handle conn interrupt - donate link heart fill in not underline - Robots not resizing (shadows do though) +- Robots can gen on top of eachother +- star is black sometimes - limit concurrent players, make sure connections are closed - move websocket server to /core diff --git a/client/connection.js b/client/connection.js index 7bf511d..19d21b9 100644 --- a/client/connection.js +++ b/client/connection.js @@ -2,37 +2,29 @@ const Connection = function() { // Local event listeners - document.addEventListener('L-guess', (evt) => { - this.ws.send(JSON.stringify({ type: 'guess', rawBody: evt.detail })); - }); - document.addEventListener('L-join', (evt) => { this.connect(); }); - document.addEventListener('L-robots', () => { - this.ws.send(JSON.stringify({ type: 'robots' })); + document.addEventListener('L-objective', () => { + this.ws.send(JSON.stringify({ type: 'objective' })); }); - document.addEventListener('L-skip', () => { - this.ws.send(JSON.stringify({ type: 'skip' })); + document.addEventListener('L-robots', () => { + this.ws.send(JSON.stringify({ type: 'robots' })); }); 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' })); - }); - - document.addEventListener('L-stop', () => { - this.ws.send(JSON.stringify({ type: 'stop' })); - }); - document.addEventListener('L-walls', () => { this.ws.send(JSON.stringify({ type: 'walls' })); }); + + document.addEventListener('L-skip', () => { + this.ws.send(JSON.stringify({ type: 'skip' })); + }); }; Connection.prototype.connect = function(){ @@ -75,22 +67,17 @@ Connection.prototype.onReceiveMessage = function({ data }) { let eventName; switch (msg.type) { - case 'countdown': eventName = 'G-countdown'; break; case 'connected': eventName = 'G-connected'; break; - case 'guess': eventName = 'G-guess'; break; + case 'countdown': eventName = 'G-countdown'; break; + case 'objective': eventName = 'G-objective'; break; case 'players': eventName = 'G-players'; break; case 'robots': eventName = 'G-robots'; break; - case 'skip': eventName = 'G-skip'; break; - case 'solve': eventName = 'G-solve'; break; - case 'start': eventName = 'G-start'; break; case 'state': eventName = 'G-state'; break; - case 'stop': eventName = 'G-stop'; break; case 'walls': eventName = 'G-walls'; break; - case 'winstate': eventName = 'G-winstate'; break; } if (eventName) { const evt = new CustomEvent(eventName, { detail: msg }); document.dispatchEvent(evt); } -}; \ No newline at end of file +}; diff --git a/client/controls.js b/client/controls.js index d7ffb14..fed47ad 100644 --- a/client/controls.js +++ b/client/controls.js @@ -1,89 +1,68 @@ //===== Constructor const Controls = function() { this.names = {}; + this.playerId = null; + this.countdownTimer = null; - - -// this.starts = []; -// this.timers = {}; - -// // "Local" and "Global" messages + // "Local" and "Global" messages document.addEventListener('L-conn-error', this.msgConnectionError.bind(this)); + document.addEventListener('L-complete', this.msgComplete.bind(this)); + document.addEventListener('L-join', this.msgJoin.bind(this)); + document.addEventListener('L-skip', this.msgSkip.bind(this)); document.addEventListener('L-stack', this.msgStack.bind(this)); -// document.addEventListener('L-reset', this.msgReset.bind(this)); -// document.addEventListener('L-undo', this.msgUndo.bind(this)); -// document.addEventListener('G-guess', this.msgGuess.bind(this)); - document.addEventListener('G-connected', this.msgCountdown.bind(this)); - document.addEventListener('G-countdown', this.msgConnected.bind(this)); + document.addEventListener('G-connected', this.msgConnected.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)); document.addEventListener('G-state', this.msgState.bind(this)); - // document.addEventListener('G-stop', this.msgStop.bind(this)); // Click handlers - // document.getElementById('controls-reset').addEventListener('click', this.onClickReset.bind(this)); - // document.getElementById('controls-robots').addEventListener('click', this.onClickRobots.bind(this)); - // 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)); + document.getElementById('controls-moves-reset').addEventListener('click', this.onClickMovesReset.bind(this)); + document.getElementById('controls-moves-undo').addEventListener('click', this.onClickMovesUndo.bind(this)); + document.getElementById('controls-moves-solve').addEventListener('click', this.onClickMovesSolve.bind(this)); + + document.getElementById('controls-options-objective').addEventListener('click', this.onClickOptionsObjective.bind(this)); + document.getElementById('controls-options-robots').addEventListener('click', this.onClickOptionsRobots.bind(this)); + document.getElementById('controls-options-walls').addEventListener('click', this.onClickOptionsWalls.bind(this)); + + document.getElementById('controls-countdown-skip').addEventListener('click', this.onClickSkip.bind(this)); this.setState('CONNECTING'); } //===== Countdown -// Controls.prototype.countdownStart = function(seconds) { -// clearTimeout(this.timers.countdown); -// this.timers.countdown = this.countdownTick.bind(this); +Controls.prototype.countdownStart = function(seconds) { + clearTimeout(this.countdownTimer); + this.countdownTimer = this.countdownTick.bind(this); -// const countdown = document.getElementById('controls-countdown'); -// countdown.dataset.tick = seconds; + const countdown = document.getElementById('controls-countdown-value'); + countdown.dataset.tick = seconds * 1; -// this.countdownTick(); -// }; + this.countdownTick(); +}; -// Controls.prototype.countdownTick = function() { -// const countdown = document.getElementById('controls-countdown'); -// const tick = countdown.dataset.tick * 1; -// countdown.dataset.tick = tick - 1; +Controls.prototype.countdownTick = function() { + const countdown = document.getElementById('controls-countdown-value'); + const tick = countdown.dataset.tick * 1; + countdown.dataset.tick = tick - 1; -// const s = (tick !== 1) ? 's' : ''; -// countdown.innerHTML = `${tick} second${s}!`; + countdown.innerHTML = tick; -// if (tick === 0) { -// this.countdownComplete(); -// return; -// } + if (tick === 0) { + this.countdownComplete(); + return; + } -// this.timers.countdown = setTimeout(this.countdownTick.bind(this), 1000); -// }; + this.countdownTimer = setTimeout(this.countdownTick.bind(this), 1000); +}; -// Controls.prototype.countdownComplete = function() { -// document.getElementById('controls-countdown').dataset.tick = 0; -// }; +Controls.prototype.countdownComplete = function() { + clearTimeout(this.countdownTimer); + document.getElementById('controls-countdown-value').innerHTML = 0; +}; //===== UI -// Controls.prototype.showWaiting = function() { -// document.getElementById('controls-start').parentNode.style.display = ''; -// document.getElementById('controls-walls').parentNode.style.display = ''; -// document.getElementById('controls-robots').parentNode.style.display = ''; - -// document.getElementById('controls-stop').parentNode.style.display = 'none'; -// // document.getElementById('controls-moves-reset').parentNode.style.display = 'none'; -// document.getElementById('controls-guesses').style.display = 'none'; -// document.getElementById('controls-panic').style.display = 'none'; -// }; - -// Controls.prototype.showPanic = function() { -// this.showGuessing(); -// document.getElementById('controls-panic').style.display = ''; -// }; - Controls.prototype.setState = function(state) { const blocks = [ 'controls-connection', @@ -109,24 +88,49 @@ Controls.prototype.setState = function(state) { //===== Message handlers -Controls.prototype.msgConnected = function() { - +Controls.prototype.msgJoin = function() { + this.setState('CONNECTING'); + + document.querySelector('#controls-connection .controls-message').innerHTML = 'Connecting...'; +}; + +Controls.prototype.msgComplete = function(evt) { + document.getElementById('controls-moves-solve').style.display = (evt.detail.complete ? 'block' : 'none'); +} + +Controls.prototype.msgConnected = function(evt) { + this.playerId = evt.detail.body; + console.log("Setting player id to " + this.playerId) }; Controls.prototype.msgConnectionError = function() { this.setState('CONNECTING'); + document.querySelector('#controls-connection .controls-message').innerHTML = "Can't reach the server."; }; Controls.prototype.msgCountdown = function(evt) { - + this.setState('COUNTDOWN'); + + document.getElementById('controls-countdown-moves').innerHTML = evt.detail.body.moveCount; + document.getElementById('controls-countdown-player').innerHTML = this.names[evt.detail.body.id]; + + this.countdownStart(evt.detail.body.duration); +}; + +Controls.prototype.msgSkip = function() { + this.countdownComplete(); }; Controls.prototype.msgStack = function(evt) { const robots = evt.detail.reduce((acc, { id }) => acc.has(id) ? acc : acc.add(id), new Set()); const moveCount = evt.detail.length - robots.size; - // document.getElementById('controls-moves').innerHTML = moveCount; - // document.getElementById('controls-undo').style.display = moveCount > 0 ? 'block' : 'none'; + document.getElementById('controls-moves-solve').style.display = 'none'; + + document.getElementById('controls-moves-count').innerHTML = moveCount; + + document.getElementById('controls-moves-reset').style.display = moveCount > 0 ? 'block' : 'none'; + document.getElementById('controls-moves-undo').style.display = moveCount > 0 ? 'block' : 'none'; }; Controls.prototype.msgPlayers = function(evt) { @@ -141,7 +145,7 @@ Controls.prototype.msgPlayers = function(evt) { if (keys.length > 1) { keys.forEach(connectionId => { const n = document.createElement('div'); - n.innerHTML = this.names[connectionId]; + n.innerHTML = this.playerId === connectionId ? `${this.names[connectionId]} (you)` : this.names[connectionId]; n.className = 'controls-player'; container.appendChild(n) }); @@ -152,19 +156,10 @@ Controls.prototype.msgPlayers = function(evt) { msg.style.display = (keys.length > 1 ? 'none' : 'block'); }; -Controls.prototype.msgSkip = function() { - // this.coundownComplete(); -}; - Controls.prototype.msgState = function(evt) { - // this.setState(evt.detail.body); - this.setState('SOLUTION') + this.setState(evt.detail.body); }; -Controls.prototype.msgStop = function() { - // -} - //===== Click handlers Controls.prototype.dispatch = function(evt, data) { @@ -172,36 +167,38 @@ Controls.prototype.dispatch = function(evt, data) { document.dispatchEvent(e); }; -Controls.prototype.onClickReset = function() { - this.dispatch('L-reset'); +// Options block + +Controls.prototype.onClickOptionsObjective = function() { + this.dispatch('L-objective'); }; -Controls.prototype.onClickRobots = function() { +Controls.prototype.onClickOptionsRobots = function() { this.dispatch('L-robots'); }; -Controls.prototype.onClickSkip = function() { - this.dispatch('L-skip'); +Controls.prototype.onClickOptionsWalls = function() { + this.dispatch('L-walls'); }; -Controls.prototype.onClickStart = function() { - this.dispatch('L-start'); -}; +// Moves block -Controls.prototype.onClickStop = function() { - this.dispatch('L-stop'); +Controls.prototype.onClickMovesReset = function() { + this.dispatch('L-reset'); }; -Controls.prototype.onClickSubmit = function() { +Controls.prototype.onClickMovesSolve = function() { this.dispatch('L-submit'); }; -Controls.prototype.onClickUndo = function() { +Controls.prototype.onClickMovesUndo = function() { this.dispatch('L-undo'); }; -Controls.prototype.onClickWalls = function() { - this.dispatch('L-walls'); +// Countdown block + +Controls.prototype.onClickSkip = function() { + this.dispatch('L-skip'); }; //===== THE TRASH BIN OF HISTORY @@ -256,4 +253,40 @@ Controls.prototype.onClickWalls = function() { // Controls.prototype.msgAttempt = function() { // alert("Ready for winning attempt!"); -// }; \ No newline at end of file +// }; + +// Controls.prototype.showWaiting = function() { +// document.getElementById('controls-start').parentNode.style.display = ''; +// document.getElementById('controls-walls').parentNode.style.display = ''; +// document.getElementById('controls-robots').parentNode.style.display = ''; + +// document.getElementById('controls-stop').parentNode.style.display = 'none'; +// // document.getElementById('controls-moves-reset').parentNode.style.display = 'none'; +// document.getElementById('controls-guesses').style.display = 'none'; +// document.getElementById('controls-panic').style.display = 'none'; +// }; + +// Controls.prototype.showPanic = function() { +// this.showGuessing(); +// document.getElementById('controls-panic').style.display = ''; +// }; + +// Controls.prototype.onClickStart = function() { +// this.dispatch('L-start'); +// }; + +// Controls.prototype.onClickStop = function() { +// this.dispatch('L-stop'); +// }; + +// Controls.prototype.msgStop = function() { +// +// } + +// document.addEventListener('L-reset', this.msgReset.bind(this)); +// document.addEventListener('L-undo', this.msgUndo.bind(this)); +// document.addEventListener('G-stop', this.msgStop.bind(this)); +// document.addEventListener('G-start', this.msgStart.bind(this)); +// document.addEventListener('G-guess', this.msgGuess.bind(this)); +// document.getElementById('controls-start').addEventListener('click', this.onClickStart.bind(this)); +// document.getElementById('controls-stop').addEventListener('click', this.onClickStop.bind(this)); \ No newline at end of file diff --git a/client/grid.js b/client/grid.js index bec1af2..68309de 100644 --- a/client/grid.js +++ b/client/grid.js @@ -7,7 +7,7 @@ const Grid = function() { this.robots = []; this.shadows = []; this.walls = []; - this.winstate = {}; + this.objective = {}; this.squaresPerSide = 20; this.squareSideLength = 0; @@ -17,7 +17,7 @@ const Grid = function() { document.addEventListener('G-robots', this.msgRobots.bind(this)); document.addEventListener('G-walls', this.msgWalls.bind(this)); - document.addEventListener('G-winstate', this.msgWinState.bind(this)); + document.addEventListener('G-objective', this.msgObjective.bind(this)); window.addEventListener('resize', this.debounce(this.onResize.bind(this), 500)); @@ -197,18 +197,19 @@ Grid.prototype.drawArrow = function({ direction, label, left, top, parentId }) { return arrow; }; -Grid.prototype.drawWinState = function() { - if (this.winstate.i === undefined || this.winstate.j === undefined) { +Grid.prototype.drawObjective = function() { + if (this.objective.i === undefined || this.objective.j === undefined) { return; } const s = this.squareSideLength; const grid = document.getElementById('content-grid'); - grid.querySelectorAll('.content-winstate').forEach(el => el.parentNode.removeChild(el)); + document.getElementById('content-objective') && grid.removeChild(document.getElementById('content-objective')); const star = document.createElementNS("http://www.w3.org/2000/svg", "svg"); star.setAttribute("viewBox", '0 0 100 100'); + star.id = 'content-objective'; const path = document.createElementNS("http://www.w3.org/2000/svg", 'path'); path.setAttribute('d', "M 0,38 L 38,38 L 49,0 L 60,38 L 100,38 L 68,66 L 77,100 L 47,79 L 17,100 L 28,64 Z"); @@ -216,13 +217,12 @@ Grid.prototype.drawWinState = function() { path.setAttribute('stroke-linejoin', 'null'); path.setAttribute('stroke-dasharray', 'null'); path.setAttribute('stroke-width', '0'); - path.setAttribute('fill', this.colors[this.winstate.id]); + path.setAttribute('fill', this.colors[this.objective.id]); star.appendChild(path); star.style.position = 'absolute'; - star.style.zIndex = 100; - star.style.left = '50%' - star.style.top = '50%' + star.style.left = `${s * this.objective.i}px` + star.style.top = `${s * this.objective.j}px` star.style.height = `${s}px`; star.style.width = `${s}px`; @@ -317,12 +317,10 @@ Grid.prototype.findNextObstacle = function({ direction, i, j }) { throw Error("Could not find next obstacle, no direction found. ", direction, i, j); }; -Grid.prototype.checkWin = function({ id, i, j }) { - if (i !== this.winstate.i || j !== this.winstate.j || id !== this.winstate.id) { - return; - } +Grid.prototype.checkObjective = function({ id, i, j }) { + const complete = (i === this.objective.i && j === this.objective.j && id === this.objective.id); - const evtSolve = new Event('L-solve'); + const evtSolve = new CustomEvent('L-complete', { detail: { complete }}); document.dispatchEvent(evtSolve); }; @@ -341,7 +339,7 @@ Grid.prototype.onArrowClick = function(evt) { const evtMove = new CustomEvent('L-arrow', { detail: { id, i, j } }); document.dispatchEvent(evtMove); - this.checkWin({ id, i, j }); + this.checkObjective({ id, i, j }); }; Grid.prototype.onResize = function() { @@ -372,7 +370,7 @@ Grid.prototype.onResize = function() { this.drawRobots(); this.drawArrows(); this.drawShadows(); - this.drawWinState(); + this.drawObjective(); this.updateArrowVisibilities(); }; @@ -428,7 +426,7 @@ Grid.prototype.msgWalls = function(evt) { this.updateArrowVisibilities(); }; -Grid.prototype.msgWinState = function(evt) { - this.winstate = evt.detail.body; - this.drawWinState(); +Grid.prototype.msgObjective = function(evt) { + this.objective = evt.detail.body; + this.drawObjective(); }; diff --git a/client/stack.js b/client/stack.js index 0ac75c6..59ebf48 100644 --- a/client/stack.js +++ b/client/stack.js @@ -3,12 +3,14 @@ const Stack = function() { // This is the heart of the robot movement architecture. // Its elements are of the form { robotId, i, j } this.moves = []; + this.playerId = null; 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)); + document.addEventListener('G-connected', this.msgConnected.bind(this)); document.addEventListener('G-robots', this.msgRobots.bind(this)); }; @@ -24,6 +26,10 @@ Stack.prototype.getInitialPositions = function() { return Object.values(moves); }; +Stack.prototype.msgConnected = function(evt) { + this.playerId = evt.detail.body; +}; + Stack.prototype.msgRobots = function(evt) { this.moves = evt.detail.body.map(({ id, i, j }) => ({ id, i, j })); @@ -49,9 +55,7 @@ Stack.prototype.msgReset = function() { }; Stack.prototype.msgSubmit = function() { - this.moves = this.getInitialPositions(); - - const evtSolve = new CustomEvent('L-solve', { detail: this.moves }); + const evtSolve = new CustomEvent('L-solve', { detail: { stack: this.moves, id: this.playerId } }); document.dispatchEvent(evtSolve); }; diff --git a/index.html b/index.html index df72504..316e7f3 100644 --- a/index.html +++ b/index.html @@ -23,36 +23,38 @@
Moves
-
24
+
-
Undo
-
Reset
+
Reset
+
Undo
-
Solve the puzzle
+
Solve the puzzle
Options
-
Move Robots
-
Move Walls
+
Move Robots
+
Move Walls
+
Move Objective
+
Careful - these affect all players!
-
Connecting...
-
Can't reach the server.
+
Connection
+
-
Aardvark Matthews Dunkirk has a solution!
+
has a solution!
-
-
11
+
+
11
19
-
Skip
+
Skip
diff --git a/server/ricochet.js b/server/ricochet.js index df60f41..a8fa394 100644 --- a/server/ricochet.js +++ b/server/ricochet.js @@ -5,7 +5,7 @@ const DEBUG = (process.env.NODE_ENV !== "production"); const STATE = { COUNTDOWN: 'COUNTDOWN', PLAY: 'PLAY', - SOLUTION: 'SOLUTION' + REPLAY: 'REPLAY' }; const Ricochet = function({ messenger }) { @@ -17,13 +17,17 @@ const Ricochet = function({ messenger }) { this.robots = this.freshRobots(); this.state = STATE.PLAY; this.walls = this.freshWalls(); + this.objective = this.freshObjective(); - // this.id = uuid.v4(); - // this.countdownTimer = null; - // this.guess = Infinity; - // this.winningStack; + this.countdownTimer = null; + this.countdownTimestamp = null; + + this.winningPlayerId = null; + this.winningStack = null; }; +//===== Connection management + Ricochet.prototype.onConnect = function(ws, req) { const url = UrlParser.parse(req.url, true); const query = url.query; @@ -33,16 +37,12 @@ Ricochet.prototype.onConnect = function(ws, req) { this.addPlayer(ws.id, santizedName); + this.messenger.messageOne(ws, { type: 'connected', body: ws.id}); this.messenger.messageAll({ type: 'players', body: this.players }); this.messenger.messageOne(ws, { type: 'robots', body: this.robots}); - this.messenger.messageOne(ws, { type: 'connected', body: ws.id}); + this.messenger.messageOne(ws, { type: 'objective', body: this.objective}); this.messenger.messageOne(ws, { type: 'state', body: this.state}); this.messenger.messageOne(ws, { type: 'walls', body: this.walls}); - // this.messenger.messageOne(ws, { type: 'winstate', body: G.getWinState()}); -} - -Ricochet.prototype.onMessage = function(ws, json) { - }; Ricochet.prototype.onDisconnect = function(ws) { @@ -64,13 +64,21 @@ Ricochet.prototype.removePlayer = function(id) { delete this.players[id]; }; + +//===== State generators + Ricochet.prototype.freshRobots = function() { 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 + // spider.svg, ufo.svg - const gen = () => Math.floor(Math.random() * this.squaresPerSide); - return robots.map((color, idx) => ({ i: gen(), j: gen(), color, id: uuid.v4(), icon: icons[idx] })); + return robots.map((color, idx) => ({ + i: this.randomSquare(), + j: this.randomSquare(), + color, + id: uuid.v4(), + icon: icons[idx] + })); }; Ricochet.prototype.freshWalls = function() { @@ -90,13 +98,13 @@ Ricochet.prototype.freshWalls = function() { 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 ri = this.randomSquare(); + const rj = this.randomSquare(); + const isHorizontal = Math.random() < 0.5; const isBackward = Math.random() < 0.5; @@ -129,89 +137,139 @@ Ricochet.prototype.freshWalls = function() { 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 = Ricochet; \ No newline at end of file +Ricochet.prototype.freshObjective = function() { + const isValid = ({ i, j }) => { + // Square has a robot on it. + for (let k = 0; k < this.robots.length; k++) { + if (this.robots[k].i === i && this.robots[k].j === j) { + return false; + } + } + + return true; + }; + + const objective = { i: this.robots[0].i, j: this.robots[0].j }; + let counter = 0; + + while (isValid(objective) === false && counter < 20) { + counter++; + objective.i = this.randomSquare(); + objective.j = this.randomSquare(); + } + + const rand = Math.floor(Math.random() * this.robots.length); + objective.id = this.robots[0].id; + // objective.id = this.robots[rand].id; + return objective; +}; + +Ricochet.prototype.onMessage = function(ws, rawBody) { + try { + const message = JSON.parse(rawBody); + + 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 'objective': this.msgObjective(); break; + case 'robots': this.msgRobots(); break; + case 'skip': this.msgSkip(); break; + case 'solve': this.msgSolve(message.rawBody); break; + case 'walls': this.msgWalls(); break; + default: console.warn("Unknown message type: ", message.type) + } + } catch (error) { + console.error(error); + } +}; + +//===== Message handlers +Ricochet.prototype.msgObjective = function() { + this.objective = this.freshObjective(); + this.messenger.messageAll({ type: 'objective', body: this.objective }); +}; + +Ricochet.prototype.msgRobots = function() { + this.robots = this.freshRobots(); + this.messenger.messageAll({ type: 'robots', body: this.robots}); +}; + +Ricochet.prototype.msgWalls = function() { + this.walls = this.freshWalls(); + this.messenger.messageAll({ type: 'walls', body: this.walls }); +}; + +Ricochet.prototype.msgSkip = function() { + this.onCountdownComplete(); +}; + +Ricochet.prototype.msgSolve = function(message) { + clearTimeout(this.countdownTimer); + + const duration = 15; + + this.countdownTimer = setTimeout(this.onCountdownComplete.bind(this), duration * 1000); + this.countdownTimestamp = new Date().getTime(); + + this.state = STATE.COUNTDOWN; + + this.winningStack = this.sanitizeStack(message.stack); + + this.winningPlayerId = this.sanitizeId(message.id); + + console.log("=========", this.winningStack.length, this.robots.length); + + this.messenger.messageAll({ + type: 'countdown', + body: { + duration, + id: this.winningPlayerId, + moveCount: this.winningStack.length - this.robots.length, + timestamp: this.countdownTimestamp + } + }); +}; + +//===== Helper functions + +Ricochet.prototype.randomSquare = function() { + return Math.floor(Math.random() * this.squaresPerSide); +}; + +Ricochet.prototype.sanitizeStack = function(rawMoveStack) { + const sanitizedStack = rawMoveStack.map(move => { + const sanitizedRobotId = this.sanitizeId(move.id); + const sanitizedI = move.i * 1; + const sanitizedJ = move.j * 1; + + return { + i: sanitizedI, + j: sanitizedJ, + id: sanitizedRobotId + }; + }); + + return sanitizedStack; +}; + +Ricochet.prototype.sanitizeId = function(id) { + return id.replace(/[^0-9a-zA-Z\-]/g, ''); +}; + +Ricochet.prototype.onCountdownComplete = function() { + clearTimeout(this.countdownTimer); + + this.countdownTimestamp = null; + this.state = STATE.REPLAY; + + this.messenger.messageAll({ type: 'win', body: { id: this.winningPlayerId, stack: this.winningStack } }); +}; + +module.exports = Ricochet; diff --git a/socket/server.js b/socket/server.js index 421d6a7..092e095 100644 --- a/socket/server.js +++ b/socket/server.js @@ -33,7 +33,9 @@ const onConnect = (ws, req) => { app.onConnect(ws, req); - ws.on('message', app.onMessage); + ws.on('message', (rawBody) => { + app.onMessage(ws, rawBody); + }); ws.on('close', () => { app.onDisconnect(ws); diff --git a/style/controls.css b/style/controls.css index 68f8690..15987d0 100644 --- a/style/controls.css +++ b/style/controls.css @@ -18,6 +18,7 @@ overflow: hidden; padding-bottom: 24px; position: relative; + text-align: center; } .controls-title { @@ -26,6 +27,7 @@ font-size: 14px; margin-bottom: 16px; padding: 8px 24px; + text-align: left; } .controls-message { @@ -37,7 +39,6 @@ .controls-button { cursor: pointer; padding: 8px 0; - text-align: center; } .controls-button:hover { @@ -66,7 +67,6 @@ #controls-moves-count { cursor: default; font-size: 84px; - text-align: center; } .controls-moves-buttons-layout { @@ -74,14 +74,14 @@ flex-direction: row; } -#controls-reset, -#controls-undo { +#controls-moves-reset, +#controls-moves-undo { flex: 1; font-size: 12px; padding: 12px; } -#controls-submit { +#controls-moves-solve { font-size: 24px; padding: 8px 0; } @@ -95,6 +95,12 @@ padding: 8px 24px; } +#controls-options .controls-message { + color: #ffa283; + font-size: 12px; + padding: 16px 0 0 0; +} + /*===== COUNTDOWN BLOCK =====*/ #controls-countdown { background: #F96600; @@ -108,7 +114,7 @@ color: #ffec83; } -.controls-countdown-values-layout { +.controls-countdown-layout { display: flex; flex-direction: row; } @@ -117,7 +123,6 @@ cursor: default; flex: 1; font-size: 64px; - text-align: center; } #controls-countdown-value::after { @@ -127,21 +132,20 @@ margin-top: -12px; } -#controls-solution-value { +#controls-countdown-moves { cursor: default; flex: 1; font-size: 64px; - text-align: center; } -#controls-solution-value::after { +#controls-countdown-moves::after { content: 'moves'; display: block; font-size: 12px; margin-top: -12px; } -#controls-skip { +#controls-countdown-skip { margin-top: 16px; } @@ -150,7 +154,6 @@ background: #483c6c; border-width: 0; color: #F96600; - text-align: center; } #controls-solution-message { @@ -158,16 +161,6 @@ padding-bottom: 0; } -/* -0cffe1 CYAN 00dfd4 -ff217c PINK -483c6c PURPLE 23074d -fe5e78 SALMON -ffa283 PEACH -F96600 ORANGE -ffec83 YELLOW FFFF00 -*/ - #controls-solution-count { color: #ffec83; cursor: default; diff --git a/style/grid.css b/style/grid.css index 1e4c9fe..275d46c 100644 --- a/style/grid.css +++ b/style/grid.css @@ -49,7 +49,7 @@ z-index: 1; } -.content-winstate { +#content-objective { position: absolute; z-index: 1; } diff --git a/style/index.css b/style/index.css index 90484a1..7fa6b4e 100644 --- a/style/index.css +++ b/style/index.css @@ -1,6 +1,7 @@ * { box-sizing: border-box; font-family: Days One; + user-select: none; } body {