From b6910d8692e7b761d761ca4a36b48b565258d7cd Mon Sep 17 00:00:00 2001 From: Ben Burlingham Date: Sun, 21 Jun 2020 11:27:01 -0700 Subject: [PATCH] Stack implementation well underway. --- README.txt | 26 ++- client/connection.js | 31 ++- client/content.js | 317 ------------------------------ client/controls.js | 453 +++++++++++++++++++++---------------------- client/grid.js | 362 ++++++++++++++++++++++++++++++++++ client/squares.js | 52 ----- client/stack.js | 31 +++ content.css | 8 + controls.css | 15 -- index.html | 22 +-- server.js | 26 +-- server/game.js | 4 +- 12 files changed, 679 insertions(+), 668 deletions(-) delete mode 100644 client/content.js create mode 100644 client/grid.js delete mode 100644 client/squares.js create mode 100644 client/stack.js diff --git a/README.txt b/README.txt index 140820b..b4ca448 100644 --- a/README.txt +++ b/README.txt @@ -1,20 +1,32 @@ -## Architecture: +## Event Architecture - Local events are broadcast as custom events prefixed `L-` - Incoming global messages are broadcast as custom events prefixed with `G` +## Robot Movement +The state of the robots are fully contained in a central stack, allowing undo, reset, shadowing, replays, and move counting. + +Any movement, including initial locations, is represented by pushing or popping locations from the stack. + +A victory state can be stored by taking a snapshot of the current stack. + ## TODO -- be able to "back up" in a move sequence -- move guess and move logic out of server (clean up server file) +- be able to undo in a move sequence +- robot shadow starting spot +- robot icons with personality add credit to readmee +- countdown skip +- reset - restore state on join -- robot icons with personality -- robot GUIDs -- countdown skip +- win declare/add/remove +- goal +- no more guesses, send stack and replay +- move guess and move logic out of server (clean up server file) + - no cancel from name prompt - window resize update board - walls algorigthm -- win declare/add/remove +- restore state on join - limit concurrent players, make sure connections are closed, clean up empty rooms - cookie room link, add to all messages, namespace them diff --git a/client/connection.js b/client/connection.js index ae47695..22ad911 100644 --- a/client/connection.js +++ b/client/connection.js @@ -2,39 +2,33 @@ const Connection = function() { // Local event listeners - document.addEventListener('L-start', () => { - this.ws.send(JSON.stringify({ type: 'start' })); + document.addEventListener('L-guess', (evt) => { + this.ws.send(JSON.stringify({ type: 'guess', rawBody: evt.detail })); }); - document.addEventListener('L-stop', () => { - this.ws.send(JSON.stringify({ type: 'stop' })); + document.addEventListener('L-join', (evt) => { + this.connect(); }); - 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-guess', (evt) => { - this.ws.send(JSON.stringify({ type: 'guess', rawBody: evt.detail })); + document.addEventListener('L-skip', () => { + this.ws.send(JSON.stringify({ type: 'skip' })); }); - document.addEventListener('L-move', (evt) => { - if (evt.detail.emitIf) { - this.ws.send(JSON.stringify({ type: 'move', rawBody: evt.detail })); - } + document.addEventListener('L-start', () => { + this.ws.send(JSON.stringify({ type: 'start' })); }); - document.addEventListener('L-robots', () => { - this.ws.send(JSON.stringify({ type: 'robots' })); + 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-join', (evt) => { - this.connect(); - }); }; Connection.prototype.connect = function(){ @@ -78,7 +72,6 @@ Connection.prototype.onReceiveMessage = function({ data }) { switch (msg.type) { case 'connected': eventName = 'G-connected'; break; case 'guess': eventName = 'G-guess'; break; - case 'move': eventName = 'G-move'; break; case 'players': eventName = 'G-players'; break; case 'robots': eventName = 'G-robots'; break; case 'skip': eventName = 'G-skip'; break; diff --git a/client/content.js b/client/content.js deleted file mode 100644 index 802b9cd..0000000 --- a/client/content.js +++ /dev/null @@ -1,317 +0,0 @@ -//===== Constructor - -const Content = function({ parent, squares }) { - this.parent = parent; - this.squares = squares; - - this.walls = {}; - this.robots = {}; - this.listeners = {}; - - this.playerId = null; - this.isSolving = false; - this.isSolver = false; - - this.drawSquares(); - - // Message handlers: "Local message" and "Global message" - document.addEventListener('L-reset', this.msgReset.bind(this)); - - document.addEventListener('G-connected', this.msgConnected.bind(this)); - document.addEventListener('G-walls', this.msgWalls.bind(this)); - document.addEventListener('G-robots', this.msgRobots.bind(this)); - document.addEventListener('G-solve', this.msgSolve.bind(this)); - document.addEventListener('G-move', this.msgMove.bind(this)); -}; - -//===== UI - -Content.prototype.drawRobots = function(robotPositions) { - this.robots = {}; - - this.parent.querySelectorAll('.content-robot').forEach(el => el.parentNode.removeChild(el)); - this.parent.querySelectorAll('.content-arrows').forEach(el => el.parentNode.removeChild(el)); - - robotPositions.forEach(({ color, i, j }) => { - const { x, y } = this.squares.ijToXy({ i, j }); - const s = this.squares.sideLength; - const id = color.replace('#', '').toUpperCase(); - const ij = `${i}-${j}`; - this.robots[ij] = id; - - const robot = document.createElement('div'); - robot.id = `robot-${id}`; - robot.className = 'content-robot'; - robot.style.background = `radial-gradient(circle at ${s/3}px ${s/3}px, ${color} 10%, #000)`; - robot.style.borderRadius = (s / 2) + 'px'; - robot.style.height = s + 'px'; - robot.style.width = s + 'px'; - - const arrows = document.createElement('div'); - arrows.className = 'content-arrows'; - arrows.id = `arrows-${id}`; - arrows.style.position = 'absolute'; - - const up = this.drawArrow({ - direction: 'up', label: '▲', i, j, left: (s * 1.25), top: (s * 0.5), parentId: id - }); - - const down = this.drawArrow({ - direction: 'down', label: '▼', i, j, left: (s * 1.25), top: (s * 2), parentId: id - }); - - const left = this.drawArrow({ - direction: 'left', label: '◀', i, j, left: (s * 0.5), top: (s * 1.25), parentId: id - }); - - const right = this.drawArrow({ - direction: 'right', label: '▶', i, j, left: (s * 2), top: (s * 1.25), parentId: id - }); - - arrows.appendChild(up); - arrows.appendChild(down); - arrows.appendChild(left); - arrows.appendChild(right); - - this.parent.appendChild(robot); - this.parent.appendChild(arrows); - }); - - this.updateArrowVisibilities(); -}; - -Content.prototype.drawArrow = function({ direction, label, i, j, left, top, parentId }) { - const s = this.squares.sideLength / 2; - - const arrow = document.createElement('div'); - arrow.className = 'content-arrow'; - arrow.innerHTML = label; - arrow.style.left = left + 'px'; - arrow.style.top = top + 'px'; - arrow.style.lineHeight = s + 'px'; - arrow.style.height = s + 'px'; - arrow.style.width = s + 'px'; - arrow.dataset.direction = direction; - arrow.dataset.parent = parentId; - - arrow.addEventListener('click', this.onArrowClick.bind(this)); - - return arrow; -}; - -Content.prototype.drawSquares = function() { - for (let i = 0; i < this.squares.perSide; i++) { - for (let j = 0; j < this.squares.perSide; j++) { - // All squares are absolutely positioned relative to the viewport. - const { x, y } = this.squares.ijToXy({ i, j }); - - const square = document.createElement('div'); - square.className = 'content-square'; - square.style.height = this.squares.sideLength + 'px'; - square.style.width = this.squares.sideLength + 'px'; - square.style.left = x + 'px'; - square.style.top = y + 'px'; - - this.parent.appendChild(square); - } - } -}; - -Content.prototype.drawWalls = function(edges) { - this.walls = {}; - - this.parent.querySelectorAll('.content-wall-x').forEach(el => el.parentNode.removeChild(el)); - this.parent.querySelectorAll('.content-wall-y').forEach(el => el.parentNode.removeChild(el)); - - edges.forEach(edge => { - this.walls[edge] = true; - - const id = `wall-${edge}`; - - if (document.getElementById(id)) { - return; - } - - const [i1, j1, i2, j2] = edge.split('-'); - - const wall = document.createElement('div'); - wall.id = id; - wall.title = edge; - - const { x, y } = this.squares.ijToXy({ i: i1, j: j1 }); - wall.style.left = x + 'px'; - wall.style.top = y + 'px'; - - // Get wall from edge: document.querySelector('[data-wall=i1-j1-i2-j2]') - wall.dataset.wall = edge; - - if (i1 === i2) { - wall.className = 'content-wall-y'; - wall.style.height = this.squares.sideLength + 'px'; - } else { - wall.className = 'content-wall-x'; - wall.style.width = this.squares.sideLength + 'px'; - } - - this.parent.appendChild(wall) - }); - - this.updateArrowVisibilities(); -}; - -Content.prototype.moveRobot = function({ id, i, j }) { - const robot = document.getElementById(`robot-${id}`); - const arrows = document.getElementById(`arrows-${id}`); - - const { x, y } = this.squares.ijToXy({ i, j }); - const s = this.squares.sideLength; - - robot.style.left = x + 'px'; - robot.style.top = y + 'px'; - robot.dataset.i = i; - robot.dataset.j = j; - - this.robots[`${i}-${j}`] = id; - - arrows.style.left = (x - s) + 'px'; - arrows.style.top = (y - s) + 'px'; - - this.updateArrowVisibilities(); -}; - -Content.prototype.updateArrowVisibilities = function() { - const keys = Object.keys(this.robots); - - keys.forEach(key => { - const id = this.robots[key]; - const i = key.split('-')[0] * 1; - const j = key.split('-')[1] * 1; - - const arrows = document.getElementById(`arrows-${id}`); - - const ijR = `${i + 1}-${j}`; - const ijL = `${i - 1}-${j}`; - const ijU = `${i}-${j - 1}`; - const ijD = `${i}-${j + 1}`; - - const edgeR = `${i + 1}-${j}-${i + 1}-${j + 1}`; - const edgeL = `${i}-${j}-${i}-${j + 1}`; - const edgeU = `${i}-${j}-${i + 1}-${j}`; - const edgeD = `${i}-${j + 1}-${i + 1}-${j + 1}`; - - arrows.querySelector("[data-direction='right']").style.display = (this.robots[ijR] || this.walls[edgeR] || i === (this.squares.perSide - 1)) ? 'none' : 'block'; - arrows.querySelector("[data-direction='left']").style.display = (this.robots[ijL] || this.walls[edgeL] || i === 0) ? 'none' : 'block'; - arrows.querySelector("[data-direction='up']").style.display = (this.robots[ijU] || this.walls[edgeU] || j === 0) ? 'none' : 'block'; - arrows.querySelector("[data-direction='down']").style.display = (this.robots[ijD] || this.walls[edgeD] || j === (this.squares.perSide - 1)) ? 'none' : 'block'; - }); -}; - -Content.prototype.findNextObstacle = function({ direction, i, j }) { - switch (direction) { - case 'right': - for (let ii = i + 1; ii < this.squares.perSide; ii++) { - const edge = `${ii + 1}-${j}-${ii + 1}-${j + 1}`; - const ij = `${ii + 1}-${j}`; - - if (this.robots[ij] || this.walls[edge] || ii === (this.squares.perSide - 1)) { - return { i: ii, j }; - } - } - break; - case 'left': - for (let ii = i - 1; ii >= 0; ii--) { - const edge = `${ii}-${j}-${ii}-${j + 1}`; - const ij = `${ii - 1}-${j}`; - - if (this.robots[ij] || this.walls[edge] || ii === 0) { - return { i: ii, j }; - } - } - break; - - case 'up': - for (let jj = j - 1; jj >= 0; jj--) { - const edge = `${i}-${jj}-${i + 1}-${jj}`; - const ij = `${i}-${jj - 1}`; - - if (this.robots[ij] || this.walls[edge] || jj === 0) { - return { i, j: jj }; - } - } - break; - - case 'down': - for (let jj = j + 1; jj < this.squares.perSide; jj++) { - const edge = `${i}-${jj + 1}-${i + 1}-${jj + 1}`; - const ij = `${i}-${jj + 1}`; - - if (this.robots[ij] || this.walls[edge] || jj === (this.squares.perSide - 1)) { - return { i, j: jj }; - } - } - break; - } - - throw Error("Could not find next obstacle, no direction found. ", direction, i, j); -}; - -//===== Click handlers - -Content.prototype.onArrowClick = function(evt) { - const direction = evt.currentTarget.dataset.direction; - const id = evt.currentTarget.dataset.parent; - - const robot = document.getElementById(`robot-${id}`); - const i = robot.dataset.i * 1; - const j = robot.dataset.j * 1; - - delete this.robots[`${i}-${j}`]; - - const { i: i2, j: j2 } = this.findNextObstacle({ direction, i, j }); - - this.moveRobot({ id, i: i2, j: j2 }); - - const detail = { id, i: i2, j: j2, emitIf: this.isSolving }; - const evtMove = new CustomEvent('L-move', { detail }); - document.dispatchEvent(evtMove); -}; - -//===== Message handlers - -Content.prototype.msgRobots = function(evt) { - const robotPositions = evt.detail.body; - this.drawRobots(robotPositions); -}; - -Content.prototype.msgWalls = function(evt) { - this.drawWalls(evt.detail.body); -}; - -Content.prototype.msgMove = function(evt) { - const { id, i, j } = evt.detail.body; - this.moveRobot({ id, i, j }); -}; - -Content.prototype.msgSolve = function(evt) { - this.isSolving = true; - this.isSolver = (this.playerId === evt.detail.id); - - if (this.isSolver) { - console.error("The current player is attempting a solve."); - } else { - console.error("A different player is attempting a solve."); - //...modal. - } -}; - -Content.prototype.msgJoin = function(evt) { - -}; - -Content.prototype.msgConnected = function(evt) { - this.playerId = evt.detail.body; -} - -Content.prototype.msgReset = function() { - this.drawRobots(); -}; diff --git a/client/controls.js b/client/controls.js index f1a023e..1b098e6 100644 --- a/client/controls.js +++ b/client/controls.js @@ -1,257 +1,250 @@ -//===== Constructor +// //===== Constructor const Controls = function() { - this.names = {}; - this.positions = []; - this.timers = {}; - this.currentWinningGuess = Infinity; - - // this.drawGuesses(); - - // Message handlers: "Local message" and "Global message" - document.addEventListener('L-connected', this.msgConnected.bind(this)); - document.addEventListener('L-move', this.msgMove.bind(this)); - document.addEventListener('L-reset', this.msgReset.bind(this)); - document.addEventListener('L-undo', this.msgUndo.bind(this)); - - // document.addEventListener('G-move', this.msgMove.bind(this)); - // document.addEventListener('G-win', this.msgWin.bind(this)); - document.addEventListener('G-attempt', this.msgAttempt.bind(this)); - document.addEventListener('G-guess', this.msgGuess.bind(this)); - document.addEventListener('G-players', this.msgPlayers.bind(this)); - document.addEventListener('G-robots', this.msgPlayers.bind(this)); - document.addEventListener('G-skip', this.msgSkip.bind(this)); - document.addEventListener('G-start', this.msgStart.bind(this)); - document.addEventListener('G-stop', this.msgStop.bind(this)); +// this.moves = []; +// this.names = {}; +// this.starts = []; +// this.timers = {}; + +// // "Local" and "Global" messages +// document.addEventListener('L-connected', this.msgConnected.bind(this)); +// document.addEventListener('L-move', this.msgMove.bind(this)); +// document.addEventListener('L-reset', this.msgReset.bind(this)); +// document.addEventListener('L-undo', this.msgUndo.bind(this)); + +// document.addEventListener('G-attempt', this.msgAttempt.bind(this)); +// document.addEventListener('G-guess', this.msgGuess.bind(this)); +// document.addEventListener('G-players', this.msgPlayers.bind(this)); +// document.addEventListener('G-robots', this.msgRobots.bind(this)); +// document.addEventListener('G-skip', this.msgSkip.bind(this)); +// document.addEventListener('G-start', this.msgStart.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-undo').addEventListener('click', this.onClickUndo.bind(this)); - document.getElementById('controls-walls').addEventListener('click', this.onClickWalls.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-undo').addEventListener('click', this.onClickUndo.bind(this)); +// document.getElementById('controls-walls').addEventListener('click', this.onClickWalls.bind(this)); } -//===== UI - -Controls.prototype.countdownStart = function(seconds) { - clearTimeout(this.timers.countdown); - this.timers.countdown = this.countdownTick.bind(this); - - const countdown = document.getElementById('controls-countdown'); - countdown.dataset.tick = seconds; - - this.countdownTick(); -}; - -Controls.prototype.countdownTick = function() { - const countdown = document.getElementById('controls-countdown'); - const tick = countdown.dataset.tick * 1; - countdown.dataset.tick = tick - 1; - - const s = (tick !== 1) ? 's' : ''; - countdown.innerHTML = `${tick} second${s}!`; - - if (tick === 0) { - this.countdownComplete(); - return; - } - - this.timers.countdown = setTimeout(this.countdownTick.bind(this), 1000); -}; - -Controls.prototype.countdownComplete = function() { - document.getElementById('controls-countdown').dataset.tick = 0; -} ; - -Controls.prototype.drawGuesses = function() { - const container = document.getElementById('controls-guesses'); - container.querySelectorAll('.controls-guess').forEach(el => el.parentNode.removeChild(el)); - - for (let i = 1; i <= 30; i++) { - const guess = document.createElement('div'); - guess.className = 'controls-guess'; - guess.innerHTML = i; - guess.setAttribute('data-value', i); - guess.addEventListener('click', this.onClickGuess.bind(this)) - container.appendChild(guess); - } -}; - -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.showGuessing = function() { - document.getElementById('controls-start').parentNode.style.display = 'none'; - document.getElementById('controls-walls').parentNode.style.display = 'none'; - document.getElementById('controls-robots').parentNode.style.display = 'none'; - document.getElementById('controls-panic').style.display = 'none'; - - document.getElementById('controls-stop').parentNode.style.display = ''; - // document.getElementById('controls-moves-reset').parentNode.style.display = ''; - document.getElementById('controls-guesses').style.display = ''; -} +// //===== UI -Controls.prototype.showPanic = function() { - this.showGuessing(); - document.getElementById('controls-panic').style.display = ''; -}; +// Controls.prototype.countdownStart = function(seconds) { +// clearTimeout(this.timers.countdown); +// this.timers.countdown = this.countdownTick.bind(this); -Controls.prototype.updateMoves = function() { - // Initial robot placement is first move, to allow undo, but doesn't count as a move. - const moves = this.positions.length - 1; +// const countdown = document.getElementById('controls-countdown'); +// countdown.dataset.tick = seconds; - document.getElementById('controls-moves').innerHTML = moves; - document.getElementById('controls-undo').style.display = moves > 0 ? 'auto' : 'none'; -}; +// this.countdownTick(); +// }; -//===== Message handlers +// Controls.prototype.countdownTick = function() { +// const countdown = document.getElementById('controls-countdown'); +// const tick = countdown.dataset.tick * 1; +// countdown.dataset.tick = tick - 1; -Controls.prototype.msgAttempt = function() { - alert("Ready for winning attempt!"); -}; +// const s = (tick !== 1) ? 's' : ''; +// countdown.innerHTML = `${tick} second${s}!`; -Controls.prototype.msgGuess = function(evt) { - const blurbs = [ " has a solution: ", " can do it in ", " says, maybe ", " wagers ", - " reckons ", " is pretty sure it's ", ", confidently: ", " wants it to be ", - " says ", " hazards ", " guesses ", " thinks it might be "]; - const blurb = blurbs[Math.floor(Math.random() * blurbs.length)]; +// if (tick === 0) { +// this.countdownComplete(); +// return; +// } - const msg = evt.detail; - const guess = msg.guess; +// this.timers.countdown = setTimeout(this.countdownTick.bind(this), 1000); +// }; - this.currentWinningGuess = guess; +// Controls.prototype.countdownComplete = function() { +// document.getElementById('controls-countdown').dataset.tick = 0; +// }; - document.getElementById('controls-panic').querySelector('.controls-alert-urgent').innerHTML = (`${this.names[msg.id]}${blurb}${guess} moves.`); - this.showPanic(); - this.countdownStart(5); -} +// 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 = ''; -Controls.prototype.msgConnected = function() { - this.showWaiting(); -}; - -Controls.prototype.msgUndo = function(evt) { - // Remove most recent move and return to the one before that. - this.moves.pop(); - const secondToLast = this.moves.pop(); - - const evtMove = new CustomEvent('L-move', { detail: secondToLast }); - document.dispatchEvent(evtMove); -}; - -Controls.prototype.msgMove = function(evt) { - this.moves.push(evt.detail); - this.updateMoves(); -}; - -Controls.prototype.msgPlayers = function(evt) { - const container = document.getElementById('controls-players'); - const names = evt.detail.body; - const keys = Object.keys(names); - - if (keys.length > 0) { - const nobody = document.getElementById('controls-players-nobody'); - nobody && nobody.parentNode.removeChild(nobody); - } - - keys.forEach(connectionId => { - const id = `player-${connectionId}`; - - if (document.getElementById(id)) { - return; - } - - const n = document.createElement('div'); - n.id = id; - n.innerHTML = names[connectionId]; - n.className = 'controls-player'; - container.appendChild(n) - }); - - this.names = names; - - container.querySelectorAll('.controls-player').forEach(el => { - const id = el.id.split('player-').pop(); - if (!this.names[id]) { - container.removeChild(el); - } - }); -}; - -Controls.prototype.msgReset = function() { - this.moves = []; - this.updateMoves(); -}; - -Controls.prototype.msgSkip = function() { - this.coundownComplete(); -}; - -Controls.prototype.msgStart = function() { - this.showGuessing(); -}; - -Controls.prototype.msgStop = function() { - this.showWaiting(); -} +// 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'; +// }; -//===== Click handlers +// Controls.prototype.showPanic = function() { +// this.showGuessing(); +// document.getElementById('controls-panic').style.display = ''; +// }; -Controls.prototype.dispatch = function(evt, data) { - const e = (data ? new CustomEvent(evt, { detail: data }) : new Event(evt)); - document.dispatchEvent(e); -} +// Controls.prototype.updateMoves = function() { +// // Initial robot placement is first move, to allow undo, but doesn't count as a move. +// const moves = this.moves.length - this.starts.length; -Controls.prototype.onClickGuess = function(evt) { - const guess = evt.currentTarget.dataset.value * 1; +// document.getElementById('controls-moves').innerHTML = moves; +// document.getElementById('controls-undo').style.display = moves > 0 ? 'block' : 'none'; +// }; - if (!guess || guess < 1) { - return; - } +// //===== Message handlers - if (guess < this.currentWinningGuess) { - this.dispatch('L-guess', { moves: evt.currentTarget.dataset.value }); - } else { - alert(`That doesn't beat ${this.currentWinningGuess} - try again!`) - } -}; +// Controls.prototype.msgAttempt = function() { +// alert("Ready for winning attempt!"); +// }; -Controls.prototype.onClickRobots = function() { - this.dispatch('L-robots'); -}; +// Controls.prototype.msgGuess = function(evt) { +// const blurbs = [ " has a solution: ", " can do it in ", " says, maybe ", " wagers ", +// " reckons ", " is pretty sure it's ", ", confidently: ", " wants it to be ", +// " says ", " hazards ", " guesses ", " thinks it might be "]; +// const blurb = blurbs[Math.floor(Math.random() * blurbs.length)]; -Controls.prototype.onClickSkip = function() { - this.dispatch('L-skip'); -}; +// const msg = evt.detail; +// const guess = msg.guess; -Controls.prototype.onClickStart = function() { - this.dispatch('L-start'); -}; +// this.currentWinningGuess = guess; -Controls.prototype.onClickStop = function() { - this.dispatch('L-stop'); -}; +// document.getElementById('controls-panic').querySelector('.controls-alert-urgent').innerHTML = (`${this.names[msg.id]}${blurb}${guess} moves.`); +// this.showPanic(); +// this.countdownStart(5); +// } -Controls.prototype.onClickUndo = function() { - this.dispatch('L-undo'); -}; +// Controls.prototype.msgConnected = function() { +// this.showWaiting(); +// }; -Controls.prototype.onClickWalls = function() { - this.dispatch('L-walls'); -}; +// Controls.prototype.msgUndo = function(evt) { +// if (this.moves.length <= this.starts.length) { +// return; +// } -Controls.prototype.onClickReset = function() { - this.dispatch('L-reset'); -}; +// const { id } = this.moves.pop(); + +// const indexOfPreviousMove = this.moves.reduce((acc, v, i) => (v.id === id ? i : acc), -1); +// const previousMove = this.moves.splice(indexOfPreviousMove, 1); + +// const evtMove = new CustomEvent('L-move', { detail: previousMove[0] }); +// document.dispatchEvent(evtMove); +// }; + +// Controls.prototype.msgMove = function(evt) { +// this.moves.push(evt.detail); +// this.updateMoves(); +// }; + +// Controls.prototype.msgPlayers = function(evt) { +// const container = document.getElementById('controls-players'); +// const names = evt.detail.body; +// const keys = Object.keys(names); + +// if (keys.length > 0) { +// const nobody = document.getElementById('controls-players-nobody'); +// nobody && nobody.parentNode.removeChild(nobody); +// } + +// keys.forEach(connectionId => { +// const id = `player-${connectionId}`; + +// if (document.getElementById(id)) { +// return; +// } + +// const n = document.createElement('div'); +// n.id = id; +// n.innerHTML = names[connectionId]; +// n.className = 'controls-player'; +// container.appendChild(n) +// }); + +// this.names = names; + +// container.querySelectorAll('.controls-player').forEach(el => { +// const id = el.id.split('player-').pop(); +// if (!this.names[id]) { +// container.removeChild(el); +// } +// }); +// }; + +// Controls.prototype.msgReset = function() { +// // Broadcast starting locations. +// this.moves = []; +// this.starts.forEach(move => { +// const evtMove = new CustomEvent('L-move', { detail: move }); +// document.dispatchEvent(evtMove); +// }); +// }; + +// Controls.prototype.msgRobots = function(evt) { +// this.starts = []; +// this.moves = []; + +// evt.detail.body.forEach(({ id, i, j}) => { +// this.starts.push({ id, i, j }); +// this.moves.push({ id, i, j }); +// }); +// }; + +// Controls.prototype.msgSkip = function() { +// this.coundownComplete(); +// }; + +// Controls.prototype.msgStart = function() { +// // Trim moves array to last position of each robot. +// // Set robots array to these new initial positions. +// const mostRecentPositions = {}; + +// this.moves.forEach(({ id, i, j }) => { +// mostRecentPositions[id] = { id, i, j }; +// }); + +// this.starts = []; +// Object.values(mostRecentPositions).forEach(robot => { +// this.starts.push(robot); +// }); + +// // Broadcast starting locations. +// this.moves = []; +// this.starts.forEach(move => { +// const evtMove = new CustomEvent('L-move', { detail: move }); +// document.dispatchEvent(evtMove); +// }); +// }; + +// Controls.prototype.msgStop = function() { +// // this.showWaiting(); +// } + +// //===== Click handlers + +// Controls.prototype.dispatch = function(evt, data) { +// const e = (data ? new CustomEvent(evt, { detail: data }) : new Event(evt)); +// document.dispatchEvent(e); +// }; + +// Controls.prototype.onClickReset = function() { +// this.dispatch('L-reset'); +// }; + +// Controls.prototype.onClickRobots = function() { +// this.dispatch('L-robots'); +// }; + +// Controls.prototype.onClickSkip = function() { +// this.dispatch('L-skip'); +// }; + +// Controls.prototype.onClickStart = function() { +// this.dispatch('L-start'); +// }; + +// Controls.prototype.onClickStop = function() { +// this.dispatch('L-stop'); +// }; + +// Controls.prototype.onClickUndo = function() { +// this.dispatch('L-undo'); +// }; + +// Controls.prototype.onClickWalls = function() { +// this.dispatch('L-walls'); +// }; diff --git a/client/grid.js b/client/grid.js new file mode 100644 index 0000000..0233ccb --- /dev/null +++ b/client/grid.js @@ -0,0 +1,362 @@ +//===== Constructor + +const Grid = function() { + this.colors = {}; + this.obstacles = {}; + this.robots = []; + this.walls = []; + + this.squaresPerSide = 20; + this.squareSideLength = 0; + + document.addEventListener('L-stack', this.msgStack.bind(this)); + + document.addEventListener('G-robots', this.msgRobots.bind(this)); + document.addEventListener('G-walls', this.msgWalls.bind(this)); + + window.addEventListener('resize', this.debounce(this.onResize.bind(this), 500)); + + this.onResize(); +}; + +//===== UI drawing + +Grid.prototype.drawSquares = function() { + const grid = document.getElementById('content-grid'); + grid.querySelectorAll('.content-square').forEach(el => el.parentNode.removeChild(el)); + + const s = this.squareSideLength; + + for (let i = 0; i < this.squaresPerSide; i++) { + for (let j = 0; j < this.squaresPerSide; j++) { + + const square = document.createElement('div'); + square.className = 'content-square'; + + square.style.height = `${s}px`; + square.style.width = `${s}px`; + + square.style.left = `${i * s}px`; + square.style.top = `${j * s}px`; + + grid.appendChild(square); + } + } +}; + +Grid.prototype.drawWalls = function() { + const grid = document.getElementById('content-grid'); + grid.querySelectorAll('.content-wall-x').forEach(el => el.parentNode.removeChild(el)); + grid.querySelectorAll('.content-wall-y').forEach(el => el.parentNode.removeChild(el)); + + const s = this.squareSideLength; + + this.walls.forEach(edge => { + const [i1, j1, i2, j2] = edge.split('-'); + + const wall = document.createElement('div'); + wall.title = edge; + + wall.style.left = `${i1 * s}px`; + wall.style.top = `${j1 * s}px`; + + if (i1 === i2) { + wall.className = 'content-wall-y'; + wall.style.height = this.squareSideLength + 'px'; + } else { + wall.className = 'content-wall-x'; + wall.style.width = this.squareSideLength + 'px'; + } + + grid.appendChild(wall) + }); +}; + +Grid.prototype.drawRobots = function() { + const grid = document.getElementById('content-grid'); + grid.querySelectorAll('.content-robot').forEach(el => el.parentNode.removeChild(el)); + + const s = this.squareSideLength; + + this.robots.forEach(({ id, i, j }) => { + const color = this.colors[id]; + + const robot = document.createElement('div'); + robot.className = 'content-robot'; + + robot.style.background = `radial-gradient(circle at ${s/3}px ${s/3}px, ${color} 10%, #000)`; + robot.style.borderRadius = (s / 2) + 'px'; + + robot.style.height = s + 'px'; + robot.style.width = s + 'px'; + + robot.style.left = `${i * s}px`; + robot.style.top = `${j * s}px`; + + grid.appendChild(robot); + }); +}; + +Grid.prototype.drawArrows = function() { + const grid = document.getElementById('content-grid'); + grid.querySelectorAll('.content-arrows').forEach(el => el.parentNode.removeChild(el)); + + const s = this.squareSideLength; + + this.robots.forEach(({ id, i, j }) => { + const arrows = document.createElement('div'); + arrows.id = `arrows-${id}`; + arrows.className = 'content-arrows'; + + arrows.dataset.i = i; + arrows.dataset.j = j; + arrows.dataset.id = id; + + arrows.style.left = `${i * s - s}px`; + arrows.style.top = `${j * s - s}px`; + + const up = this.drawArrow({ direction: 'up', label: '▲', left: (s * 1.25), top: (s * 0.5) }); + const down = this.drawArrow({ direction: 'down', label: '▼', left: (s * 1.25), top: (s * 2) }); + const left = this.drawArrow({ direction: 'left', label: '◀', left: (s * 0.5), top: (s * 1.25) }); + const right = this.drawArrow({ direction: 'right', label: '▶', left: (s * 2), top: (s * 1.25) }); + + arrows.appendChild(up); + arrows.appendChild(down); + arrows.appendChild(left); + arrows.appendChild(right); + + grid.appendChild(arrows); + }); +}; + +Grid.prototype.drawArrow = function({ direction, label, left, top, parentId }) { + const s = this.squareSideLength / 2; + + const arrow = document.createElement('div'); + arrow.className = 'content-arrow'; + arrow.innerHTML = label; + + arrow.style.left = left + 'px'; + arrow.style.top = top + 'px'; + arrow.style.lineHeight = s + 'px'; + arrow.style.height = s + 'px'; + arrow.style.width = s + 'px'; + + arrow.dataset.direction = direction; + arrow.dataset.parent = parentId; + + arrow.addEventListener('click', this.onArrowClick.bind(this)); + + return arrow; +}; + +//===== Obstacle logic +// i and j are notations for the grid of squares. +// x and y are notations for absolute pixels. +// +// "Obstacles" is a lookup table. Values irrelevant. Keys are obstacles. +// Edge obstacle key: i1-j1-i2-j2 +// Robot obstacle key: i-j + +Grid.prototype.updateObstacles = function() { + this.obstacles = {}; + + this.walls.forEach(edge => { this.obstacles[edge] = true; }); + + this.robots.forEach(({ i, j }) => { this.obstacles[`${i}-${j}`] = true; }); +}; + +Grid.prototype.updateArrowVisibilities = function() { + this.robots.forEach(({ id, i, j }) => { + const ijR = `${i + 1}-${j}`; + const ijL = `${i - 1}-${j}`; + const ijU = `${i}-${j - 1}`; + const ijD = `${i}-${j + 1}`; + + const edgeR = `${i + 1}-${j}-${i + 1}-${j + 1}`; + const edgeL = `${i}-${j}-${i}-${j + 1}`; + const edgeU = `${i}-${j}-${i + 1}-${j}`; + const edgeD = `${i}-${j + 1}-${i + 1}-${j + 1}`; + + const arrows = document.getElementById(`arrows-${id}`); + arrows.querySelector("[data-direction='right']").style.display = (this.obstacles[ijR] || this.obstacles[edgeR] || i === (this.squaresPerSide - 1)) ? 'none' : 'block'; + arrows.querySelector("[data-direction='left']").style.display = (this.obstacles[ijL] || this.obstacles[edgeL] || i === 0) ? 'none' : 'block'; + arrows.querySelector("[data-direction='up']").style.display = (this.obstacles[ijU] || this.obstacles[edgeU] || j === 0) ? 'none' : 'block'; + arrows.querySelector("[data-direction='down']").style.display = (this.obstacles[ijD] || this.obstacles[edgeD] || j === (this.squaresPerSide - 1)) ? 'none' : 'block'; + }); +}; + +Grid.prototype.findNextObstacle = function({ direction, i, j }) { + const sps = this.squaresPerSide; + const obstacles = this.obstacles; + + switch (direction) { + case 'right': + for (let ii = i + 1; ii < sps; ii++) { + const edge = `${ii + 1}-${j}-${ii + 1}-${j + 1}`; + const ij = `${ii + 1}-${j}`; + + if (obstacles[ij] || obstacles[edge] || ii === (sps - 1)) { + return { i: ii, j }; + } + } + break; + case 'left': + for (let ii = i - 1; ii >= 0; ii--) { + const edge = `${ii}-${j}-${ii}-${j + 1}`; + const ij = `${ii - 1}-${j}`; + + if (obstacles[ij] || obstacles[edge] || ii === 0) { + return { i: ii, j }; + } + } + break; + + case 'up': + for (let jj = j - 1; jj >= 0; jj--) { + const edge = `${i}-${jj}-${i + 1}-${jj}`; + const ij = `${i}-${jj - 1}`; + + if (obstacles[ij] || obstacles[edge] || jj === 0) { + return { i, j: jj }; + } + } + break; + + case 'down': + for (let jj = j + 1; jj < sps; jj++) { + const edge = `${i}-${jj + 1}-${i + 1}-${jj + 1}`; + const ij = `${i}-${jj + 1}`; + + if (obstacles[ij] || obstacles[edge] || jj === (sps - 1)) { + return { i, j: jj }; + } + } + break; + } + + throw Error("Could not find next obstacle, no direction found. ", direction, i, j); +}; + +//===== DOM event handlers + +Grid.prototype.onArrowClick = function(evt) { + const parent = evt.currentTarget.parentNode; + const direction = evt.currentTarget.dataset.direction; + + const id = parent.dataset.id; + const i1 = parent.dataset.i; + const j1 = parent.dataset.j; + + const { i, j } = this.findNextObstacle({ direction, i: i1 * 1, j: j1 * 1 }); + + const evtMove = new CustomEvent('L-arrow', { detail: { id, i, j } }); + document.dispatchEvent(evtMove); +}; + +Grid.prototype.onResize = function() { + const controlBounds = document.getElementById('controls-container').getBoundingClientRect(); + const contentBounds = document.getElementById('content-container').getBoundingClientRect(); + const grid = document.getElementById('content-grid'); + + const h = contentBounds.height - 40; + const w = contentBounds.width - controlBounds.right - 40; + const min = Math.min(h, w); + this.squaresPerSide = 20; + + // Centering + const offsetX = (min === h) ? ((w - min) / 2) : 0; + const offsetY = (min === h) ? 0 : ((h - min) / 2); + + this.squareSideLength = Math.floor(min / this.squaresPerSide); + + // Origin (top left) + const x0 = controlBounds.left + controlBounds.width + 40 + offsetX; + const y0 = contentBounds.top + 40 + offsetY; + + grid.style.left = `${x0}px`; + grid.style.top = `${y0}px`; + + this.drawSquares(); + this.drawWalls(); + this.drawRobots(); + this.drawArrows(); + + this.updateArrowVisibilities(); +}; + +Grid.prototype.debounce = function(fn, ms) { + let timer = null; + + return () => { + clearTimeout(timer); + timer = setTimeout(fn, ms); + } +}; + +//===== Message handlers + +Grid.prototype.msgStack = function(evt) { + const latestPositions = evt.detail.reduce((acc, { id, i, j }) => { + acc[id] = { id, i, j }; + return acc; + }, {}); + + this.robots = Object.values(latestPositions); + + this.drawRobots(); + this.drawArrows(); + + this.updateObstacles(); + this.updateArrowVisibilities(); +}; + +Grid.prototype.msgWalls = function(evt) { + this.walls = evt.detail.body; + + this.drawWalls(); + + this.updateObstacles(); + this.updateArrowVisibilities(); +}; + +Grid.prototype.msgRobots = function(evt) { + // Do not assign position or redraw here: movements are fully managed using the stack. + this.colors = evt.detail.body.reduce((acc, { color, id }) => { + acc[id] = color; + return acc; + }, {}); +}; + + +//============ THE TRASH BIN OF HISTORY +// Content.prototype.drawRobot = function({ id, i, j }) { +// const robot = document.getElementById(`robot-${id}`); +// const arrows = document.getElementById(`arrows-${id}`); + +// const { x, y } = this.ijToXy({ i, j }); +// const s = this.squares.sideLength; + +// robot.style.display = 'block'; +// robot.style.left = x + 'px'; +// robot.style.top = y + 'px'; +// robot.dataset.i = i; +// robot.dataset.j = j; + +// this.robots[`${i}-${j}`] = id; + +// arrows.style.display = 'block'; +// arrows.style.left = (x - s) + 'px'; +// arrows.style.top = (y - s) + 'px'; +// }; + +// Content.prototype.ijToXy = function({ i, j }) { +// if ((i !== 0 && !i) || (j !== 0 && !j)) { +// return +// } + +// return { +// x: this.squares.x0 + i * this.squares.sideLength, +// y: this.squares.y0 + j * this.squares.sideLength +// } +// }; \ No newline at end of file diff --git a/client/squares.js b/client/squares.js deleted file mode 100644 index 11b9377..0000000 --- a/client/squares.js +++ /dev/null @@ -1,52 +0,0 @@ -// This layer acts to convert squares and their edges to pixel coords and back. -// i and j are notations for the grid of squares. -// x and y are notations for absolute pixels. -// -// Grid: XY pair. 0-i on x and 0-j on y. -// Absolute: XY pair. 0-width and 0-height, relative to viewport -// Edge: A string. i1-j1-i1-j2. - -const Squares = function() { - const controlBounds = document.getElementById('controls-container').getBoundingClientRect(); - const contentBounds = document.getElementById('content-container').getBoundingClientRect(); - - const h = contentBounds.height - 40; - const w = contentBounds.width - controlBounds.right - 40; - const min = Math.min(h, w); - - // Centering - const offsetX = (min === h) ? ((w - min) / 2) : 0; - const offsetY = (min === h) ? 0 : ((h - min) / 2); - - this.perSide = 20; - this.sideLength = Math.floor(min / this.perSide); - - // Origin (top left) - this.x0 = controlBounds.left + controlBounds.width + 40 + offsetX; - this.y0 = contentBounds.top + 40 + offsetY; -} - -Squares.prototype.ijToXy = function({ i, j }) { - if ((i !== 0 && !i) || (j !== 0 && !j)) { - return - } - - return { - x: this.x0 + i * this.sideLength, - y: this.y0 + j * this.sideLength - } -} - -Squares.prototype.xyToIj = function({ x, y }) { - const i = Math.floor((x - this.x0) / this.sideLength); - const j = Math.floor((y - this.y0) / this.sideLength); - - const min = 0; - const max = this.perSide - 1; - - // Coalesce outlying results to grid edges. - return { - i: Math.min(Math.max(min, i), max), - j: Math.min(Math.max(min, j), max), - } -} \ No newline at end of file diff --git a/client/stack.js b/client/stack.js new file mode 100644 index 0000000..247596f --- /dev/null +++ b/client/stack.js @@ -0,0 +1,31 @@ +//===== Constructor +const Stack = function() { + // This is the heart of the robot movement architecture. + // Its elements are of the form { robotId, i, j } + this.moves = []; + + document.addEventListener('L-arrow', this.msgArrow.bind(this)); + document.addEventListener('G-robots', this.msgRobots.bind(this)); +}; + +Stack.prototype.msgRobots = function(evt) { + this.moves = evt.detail.body.map(({ id, i, j }) => ({ id, i, j })); + + const evtStack = new CustomEvent('L-stack', { detail: this.moves }); + document.dispatchEvent(evtStack); +}; + +Stack.prototype.msgArrow = function(evt) { + this.moves.push(evt.detail); + + const evtStack = new CustomEvent('L-stack', { detail: this.moves }); + document.dispatchEvent(evtStack); +}; + + +// reset destroys stack +// undo decrements from stack +// store chops stack +// moves is stack length +// replay shares stack +// starting spot is present in stack \ No newline at end of file diff --git a/content.css b/content.css index f7e3896..8adfe95 100644 --- a/content.css +++ b/content.css @@ -9,12 +9,17 @@ z-index: 0; } +#content-grid { + position: absolute; +} + .content-square { background: #ddd; border-style: solid; border-color: #aaa; border-width: 0 1px 1px 0; position: absolute; + z-index: 0; } .content-wall-x { @@ -34,10 +39,13 @@ .content-robot { position: absolute; transition: left 0.4s cubic-bezier(0,1,.5,1), top 0.4s cubic-bezier(0,1,.5,1); + z-index: 1; } .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; } .content-arrow { diff --git a/controls.css b/controls.css index 8278442..3d48329 100644 --- a/controls.css +++ b/controls.css @@ -59,21 +59,6 @@ padding: 8px; } -.controls-guess { - background: none; - cursor: pointer; - display: inline-block; - font-size: 12px; - height: 30px; - line-height: 30px; - text-align: center; - width: 30px; -} - -.controls-guess:hover { - background: #21dfae; -} - #controls-footer { bottom: 0; left: 0; diff --git a/index.html b/index.html index ba17b8c..af3c2d9 100644 --- a/index.html +++ b/index.html @@ -9,12 +9,12 @@ - + - +
@@ -75,15 +75,11 @@
Local
Moves:
-
0
+
Undo
Reset
-
-
How many moves to win?
-
-
@@ -100,18 +96,18 @@
-
+
+
+
diff --git a/server.js b/server.js index 2d0a734..d29398f 100644 --- a/server.js +++ b/server.js @@ -90,19 +90,19 @@ const Server = { 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 '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; diff --git a/server/game.js b/server/game.js index 43820f7..52c346a 100644 --- a/server/game.js +++ b/server/game.js @@ -37,7 +37,7 @@ Game.prototype.getRobots = function() { // {i: 9, j: 1, color: '#F000F0' }, // ]; - return robots.map(color => ({ i: gen(), j: gen(), color })); + return robots.map(color => ({ i: gen(), j: gen(), color, id: uuid.v4() })); } Game.prototype.getWalls = function() { @@ -58,7 +58,7 @@ Game.prototype.getWalls = function() { const numberOfCorners = Math.ceil(Math.pow((squaresPerSide / 10), 2)); const numberOfWalls = Math.ceil(Math.pow((squaresPerSide / 5), 2)); - const gen = () => Math.floor(Math.random() * squaresPerSide + 1); + const gen = () => Math.floor(Math.random() * squaresPerSide); const edges = []; // DO NUMBER OF CORNERS FIRST AFTER TESTING