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 @@