From 96b0d73f83aea8e4cfcc29a8f1c25bf5f4f30bc9 Mon Sep 17 00:00:00 2001 From: Ben Burlingham Date: Thu, 2 Jul 2020 19:21:15 -0700 Subject: [PATCH] Fixes to enforce synchronicity. Fixes to avoid position overlaps. --- README.txt | 16 ++--- client/connection.js | 2 + client/controls.js | 50 ++++++------- client/grid.js | 52 ++++++++------ server/ricochet.js | 168 +++++++++++++++++++++++++++++++++---------- socket/server.js | 2 +- style/controls.css | 7 +- style/index.css | 1 + 8 files changed, 202 insertions(+), 96 deletions(-) diff --git a/README.txt b/README.txt index 5163c8e..1f6132f 100644 --- a/README.txt +++ b/README.txt @@ -13,22 +13,18 @@ A victory state can be stored by taking a snapshot of the current stack. Icons from [https://game-icons.net](https://game-icons.net) ## TODO -- (n solves) in players list, if > 0 -- robot color update -- test multi solve +- multi solve doesn't check if one solution is better - replay stack -- slide arrows - chat box - no cancel from name prompt -- Robots not resizing (shadows do though) -- Robots can gen on top of eachother -- star is black sometimes because of async -- "you" not picked up sometimes because of async - - limit concurrent players, make sure connections are closed +- walls and winstate algorithm + +# Final install - move websocket server to /core - dynamic socket server resolution -- walls and winstate algorithm - restore name prompt +# Nice to haves +- shuffle colors - tutorial diff --git a/client/connection.js b/client/connection.js index 578b7c3..308c2da 100644 --- a/client/connection.js +++ b/client/connection.js @@ -14,6 +14,8 @@ const Connection = function() { this.connect(); }); + document.addEventListener('L-newround', () => this.send({ type: 'newround' }) ); + document.addEventListener('L-objective', () => this.send({ type: 'objective' }) ); document.addEventListener('L-robots', () => this.send({ type: 'robots' }) ); diff --git a/client/controls.js b/client/controls.js index b04c99f..061a44c 100644 --- a/client/controls.js +++ b/client/controls.js @@ -90,6 +90,27 @@ Controls.prototype.setState = function(state) { (STATE[state] || []).forEach(id => document.getElementById(id).style.display = 'block'); }; +Controls.prototype.drawPlayers = function() { + const container = document.getElementById('controls-players'); + container.querySelectorAll('.controls-player').forEach(el => { + container.removeChild(el); + }); + + const keys = Object.keys(this.names); + if (keys.length > 1) { + keys.forEach(connectionId => { + const n = document.createElement('div'); + n.innerHTML = this.playerId === connectionId ? `${this.names[connectionId]} (you)` : this.names[connectionId]; + n.className = 'controls-player'; + container.appendChild(n) + }); + } + + const msg = container.querySelector('.controls-message'); + msg.innerHTML = (keys.length > 1 ? '' : 'Nobody is in the game yet.'); + msg.style.display = (keys.length > 1 ? 'none' : 'block'); +}; + //===== Message handlers Controls.prototype.msgJoin = function() { @@ -100,11 +121,11 @@ Controls.prototype.msgJoin = function() { 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) + this.drawPlayers(); }; Controls.prototype.msgConnectionError = function() { @@ -143,25 +164,7 @@ Controls.prototype.msgStack = function(evt) { Controls.prototype.msgPlayers = function(evt) { this.names = evt.detail.body; - - const container = document.getElementById('controls-players'); - container.querySelectorAll('.controls-player').forEach(el => { - container.removeChild(el); - }); - - const keys = Object.keys(this.names); - if (keys.length > 1) { - keys.forEach(connectionId => { - const n = document.createElement('div'); - n.innerHTML = this.playerId === connectionId ? `${this.names[connectionId]} (you)` : this.names[connectionId]; - n.className = 'controls-player'; - container.appendChild(n) - }); - } - - const msg = container.querySelector('.controls-message'); - msg.innerHTML = (keys.length > 1 ? '' : 'Nobody is in the game yet.'); - msg.style.display = (keys.length > 1 ? 'none' : 'block'); + this.drawPlayers(); }; Controls.prototype.msgState = function(evt) { @@ -221,6 +224,5 @@ Controls.prototype.onClickWinReplay = function() { } Controls.prototype.onClickWinNext = function() { - // this.dispatch('L-skip'); - alert('win next') -} \ No newline at end of file + this.dispatch('L-newround'); +}; \ No newline at end of file diff --git a/client/grid.js b/client/grid.js index 68309de..42c0c4e 100644 --- a/client/grid.js +++ b/client/grid.js @@ -87,6 +87,9 @@ Grid.prototype.drawRobots = function() { robot.style.left = `${i * s}px`; robot.style.top = `${j * s}px`; + robot.style.height = `${s}px`; + robot.style.width = `${s}px`; + ids.add(robot.id); }); @@ -111,9 +114,6 @@ Grid.prototype.drawRobot = function({ id, i, j }) { robot.style.background = color; - robot.style.height = `${s}px`; - robot.style.width = `${s}px`; - grid.appendChild(robot); return robot; @@ -146,14 +146,30 @@ Grid.prototype.drawShadows = function() { Grid.prototype.drawArrows = function() { const grid = document.getElementById('content-grid'); - grid.querySelectorAll('.content-arrows').forEach(el => el.parentNode.removeChild(el)); - const s = this.squareSideLength; + const ids = new Set(); this.robots.forEach(({ id, i, j }) => { - const arrows = document.createElement('div'); - arrows.id = `arrows-${id}`; - arrows.className = 'content-arrows'; + let arrowId = `arrows-${id}`; + let arrows = document.getElementById(arrowId); + + if (!arrows) { + arrows = document.createElement('div'); + arrows.id = arrowId; + arrows.className = 'content-arrows'; + + 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); + } arrows.dataset.i = i; arrows.dataset.j = j; @@ -161,18 +177,14 @@ Grid.prototype.drawArrows = function() { 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); + ids.add(arrowId); + }); + + grid.querySelectorAll('.content-arrows').forEach(el => { + if (ids.has(el.id) === false) { + el.parentNode.removeChild(el); + } }); }; @@ -217,7 +229,7 @@ Grid.prototype.drawObjective = function() { path.setAttribute('stroke-linejoin', 'null'); path.setAttribute('stroke-dasharray', 'null'); path.setAttribute('stroke-width', '0'); - path.setAttribute('fill', this.colors[this.objective.id]); + path.setAttribute('fill', this.objective.color); star.appendChild(path); star.style.position = 'absolute'; diff --git a/server/ricochet.js b/server/ricochet.js index fa6755c..ee90284 100644 --- a/server/ricochet.js +++ b/server/ricochet.js @@ -8,10 +8,41 @@ const STATE = { WIN: 'WIN' }; +const COLORS = [ + '#FF6200', // Orange + '#06E746', // Green + '#E370FF', // Purple + '#06D6C4', // Turq + '#ff217c' // Pink +]; + +const ICONS = [ + 'assets/comet.svg', + 'assets/moon.svg', + 'assets/planet.svg', + 'assets/rocket.svg', + 'assets/spacesuit.svg', + 'assets/spider.svg', + 'assets/ufo.svg' +]; + const Ricochet = function({ messenger }) { this.messenger = messenger; this.squaresPerSide = 20; + // Reference lookups + this.robotIds = Array.from(Array(5).keys()).map(_ => uuid.v4()); + + this.colors = this.robotIds.reduce((acc, id, i) => { + acc[id] = COLORS[i]; + return acc; + }, {}); + + this.icons = this.robotIds.reduce((acc, id, i) => { + acc[id] = ICONS[i]; + return acc; + }, {}); + // Properties that will be emitted this.players = {}; this.robots = this.freshRobots(); @@ -78,17 +109,22 @@ Ricochet.prototype.removePlayer = function(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 - - return robots.map((color, idx) => ({ - i: this.randomSquare(), - j: this.randomSquare(), - color, - id: uuid.v4(), - icon: icons[idx] - })); + const result = []; + + for (let k = 0; k < this.robotIds.length; k++) { + const id = this.robotIds[k]; + const { i, j } = this.randomUnoccupiedSquare(); + + result.push({ + i, + j, + color: this.colors[id], + id, + icon: this.icons[id] + }); + } + + return result; }; Ricochet.prototype.freshWalls = function() { @@ -112,8 +148,8 @@ Ricochet.prototype.freshWalls = function() { // DO NUMBER OF CORNERS FIRST AFTER TESTING for (let n = 0; n < numberOfWalls; n++) { - const ri = this.randomSquare(); - const rj = this.randomSquare(); + break; + // const { i: ri, j: rj } = this.randomSquare(); const isHorizontal = Math.random() < 0.5; @@ -148,30 +184,16 @@ Ricochet.prototype.freshWalls = function() { }; 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 rand = Math.floor(Math.random() * this.robotIds.length); + const id = this.robotIds[rand]; + const { i, j } = this.randomUnoccupiedSquare(); + + return { + i, + j, + id: this.robotIds[0], //id; + color: this.colors[id], }; - - 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) { @@ -188,6 +210,7 @@ Ricochet.prototype.onMessage = function(ws, rawBody) { } switch (message.type) { + case 'newround': this.msgNewRound(); break; case 'objective': this.msgObjective(); break; case 'robots': this.msgRobots(); break; case 'skip': this.msgSkip(); break; @@ -201,6 +224,34 @@ Ricochet.prototype.onMessage = function(ws, rawBody) { }; //===== Message handlers +Ricochet.prototype.msgNewRound = function() { + this.objective = this.freshObjective(); + this.state = STATE.PLAY; + + const lastPositions = this.winningStack.reduce((acc, v) => { + acc[v.id] = v; + return acc; + }, {}) + + this.robots = Object.values(lastPositions).map(({ i, j, id }) => ({ + i, + j, + id, + color: this.colors[id], + icon: this.icons[id] + })); + + this.countdownTimer = null; + this.countdownTimestamp = null; + + this.winningPlayerId = null; + this.winningStack = null; + + this.messenger.messageAll({ type: 'objective', body: this.objective}); + this.messenger.messageAll({ type: 'state', body: this.state}); + this.messenger.messageAll({ type: 'robots', body: this.robots}); +}; + Ricochet.prototype.msgObjective = function() { this.objective = this.freshObjective(); this.messenger.messageAll({ type: 'objective', body: this.objective }); @@ -240,7 +291,50 @@ Ricochet.prototype.msgSolve = function(message) { //===== Helper functions Ricochet.prototype.randomSquare = function() { - return Math.floor(Math.random() * this.squaresPerSide); + return { + i: Math.floor(Math.random() * this.squaresPerSide), + j: Math.floor(Math.random() * this.squaresPerSide) + }; +}; + +Ricochet.prototype.randomUnoccupiedSquare = function() { + let result = this.randomSquare(); + let CONTROL_COUNTER = 0; + + while (this.isSquareOccupied(result) && CONTROL_COUNTER < 20) { + CONTROL_COUNTER++; + result = this.randomSquare(); + } + + if (CONTROL_COUNTER > 19) { + console.log(`==================\n\nCRITICAL ERROR!\n\nrandomUnoccupiedSquare() while() short-circuited!\n\n==================`); + } + + return result; +}; + +Ricochet.prototype.isSquareOccupied = function({ i, j }) { + if (i < 0 || i > this.squaresPerSide) { + return true; + } + + if (j < 0 || j > this.squaresPerSide) { + return true; + } + + if (this.robots) { + for (let k = 0; k < this.robots.length; k++) { + if (this.robots[k].i === i && this.robots[k].j === j) { + return true; + } + } + } + + if (this.objective && i === this.objective.i && j === this.objective.j) { + return true; + } + + return false; }; Ricochet.prototype.sanitizeStack = function(rawMoveStack) { diff --git a/socket/server.js b/socket/server.js index 092e095..726fdbf 100644 --- a/socket/server.js +++ b/socket/server.js @@ -17,7 +17,7 @@ Object.keys(apps).forEach((key) => { if (!valid) { delete apps[key]; - console.log(`Application at ${key} is missing one or more: messenger, onConnect, onMessage, onDisconnect.`); + console.log(`==================== \n\nCRITICAL ERROR! \n\nApplication at ${key} is missing one or more: messenger, onConnect, onMessage, onDisconnect.\n\n====================\n`); } }); diff --git a/style/controls.css b/style/controls.css index adeb068..824e749 100644 --- a/style/controls.css +++ b/style/controls.css @@ -197,8 +197,7 @@ } #controls-footer a:hover { - background: #ffec83; - border-color: #ff217c; - color: #ff217c; - font-size: 30px; + background: #FFFF00; + border-color: #23074d; + color: #23074d; } diff --git a/style/index.css b/style/index.css index 7fa6b4e..85a30f2 100644 --- a/style/index.css +++ b/style/index.css @@ -24,4 +24,5 @@ fe5e78 SALMON ffa283 PEACH F96600 ORANGE ffec83 YELLOW FFFF00 +06CC83 GREEN */ \ No newline at end of file