//===== Constructor const Grid = function() { this.colors = {}; this.icons = {}; this.obstacles = {}; this.robots = []; this.shadows = []; this.walls = []; this.objective = {}; this.timers = {}; this.squaresPerSide = 20; this.squareSideLength = 0; document.addEventListener('L-stack', this.msgStack.bind(this)); document.addEventListener('L-shadows', this.msgShadows.bind(this)); document.addEventListener('G-newround', this.msgNewRound.bind(this)); document.addEventListener('G-robots', this.msgRobots.bind(this)); document.addEventListener('G-walls', this.msgWalls.bind(this)); document.addEventListener('G-objective', this.msgObjective.bind(this)); document.addEventListener('G-win', this.msgWin.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 + 2}px`; } else { wall.className = 'content-wall-x'; wall.style.width = `${this.squareSideLength + 2}px`; } grid.appendChild(wall) }); }; Grid.prototype.drawRobots = function() { const s = this.squareSideLength; const ids = new Set(); this.robots.forEach(({ id, i, j }) => { const robot = document.getElementById(`robot-${id}`) || this.drawRobot({ id, i, j }); 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); }); const grid = document.getElementById('content-grid'); grid.querySelectorAll('.content-robot').forEach(el => { if (ids.has(el.id) === false) { el.parentNode.removeChild(el); } }); }; Grid.prototype.drawRobot = function({ id, i, j }) { const grid = document.getElementById('content-grid'); const color = this.colors[id]; const s = this.squareSideLength; // const robot = document.createElement('div'); const robot = document.createElement('img'); robot.src = this.icons[id]; robot.className = 'content-robot'; robot.id = `robot-${id}`; robot.style.background = color; grid.appendChild(robot); return robot; }; Grid.prototype.drawShadows = function() { const grid = document.getElementById('content-grid'); grid.querySelectorAll('.content-shadow').forEach(el => el.parentNode.removeChild(el)); const s = this.squareSideLength; this.shadows.forEach(({ id, i, j }) => { const color = this.colors[id]; const shadow = document.createElement('img'); shadow.src = this.icons[id]; shadow.className = 'content-shadow'; shadow.style.background = color; shadow.style.height = `${s}px`; shadow.style.width = `${s}px`; shadow.style.left = `${i * s}px`; shadow.style.top = `${j * s}px`; grid.appendChild(shadow); }); }; Grid.prototype.drawArrows = function() { const grid = document.getElementById('content-grid'); const s = this.squareSideLength; const ids = new Set(); this.robots.forEach(({ id, i, j }) => { 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; arrows.dataset.id = id; arrows.style.left = `${i * s - s}px`; arrows.style.top = `${j * s - s}px`; ids.add(arrowId); }); grid.querySelectorAll('.content-arrows').forEach(el => { if (ids.has(el.id) === false) { el.parentNode.removeChild(el); } }); }; 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; }; Grid.prototype.drawObjective = function() { if (this.objective.i === undefined || this.objective.j === undefined) { return; } const s = this.squareSideLength; const grid = document.getElementById('content-grid'); 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"); path.setAttribute('stroke-linecap', 'null'); path.setAttribute('stroke-linejoin', 'null'); path.setAttribute('stroke-dasharray', 'null'); path.setAttribute('stroke-width', '0'); path.setAttribute('fill', this.objective.color); star.appendChild(path); star.style.position = 'absolute'; star.style.left = `${s * this.objective.i + 4}px` star.style.top = `${s * this.objective.j + 4}px` star.style.height = `${s - 8}px`; star.style.width = `${s - 8}px`; grid.appendChild(star); }; //===== 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); }; Grid.prototype.checkObjective = function({ id, i, j }) { const complete = (i === this.objective.i * 1 && j === this.objective.j * 1 && id === this.objective.id); const evtSolve = new CustomEvent('L-complete', { detail: { complete }}); document.dispatchEvent(evtSolve); }; Grid.prototype.replayStack = function(stack) { // All to initial positions for (let i = 0; i < this.robots.length; i++) { this.replayMove(stack[i]); document.getElementById(`arrows-${stack[i].id}`).style.display = 'none'; } function replayRemaining(remainingStack) { if (remainingStack.length > 0) { this.replayMove(remainingStack[0]); this.timers.replay = setTimeout(replayRemaining.bind(this, remainingStack.slice(1)), 750); } else { const evtSolve = new Event('L-replay-complete'); document.dispatchEvent(evtSolve); this.timers.replay = setTimeout(this.replayStack.bind(this, stack), 750); } } this.timers.replay = setTimeout(replayRemaining.bind(this, stack.slice(this.robots.length)), 750) }; Grid.prototype.replayMove = function({ id, i, j }) { const robot = document.getElementById(`robot-${id}`); const s = this.squareSideLength; robot.style.left = `${i * s}px`; robot.style.top = `${j * s}px`; }; //===== 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); this.checkObjective({ id, i, j }); }; 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.drawShadows(); this.drawObjective(); this.updateArrowVisibilities(); }; Grid.prototype.debounce = function(fn, ms) { let timer = null; return () => { clearTimeout(timer); timer = setTimeout(fn, ms); } }; //===== Message handlers Grid.prototype.msgNewRound = function() { this.robots.forEach(({ id }) => document.getElementById(`arrows-${id}`).style.display = 'block'); clearTimeout(this.timers.replay); }; Grid.prototype.msgRobots = function(evt) { // Do not assign position or redraw here: movements are fully managed using the stack. this.colors = {}; this.icons = {}; evt.detail.body.forEach(({ id, color, icon }) => { this.colors[id] = color; this.icons[id] = icon; }, {}); }; 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.msgShadows = function(evt) { this.shadows = evt.detail; this.drawShadows(); }; Grid.prototype.msgWalls = function(evt) { this.walls = evt.detail.body; this.drawWalls(); this.updateObstacles(); this.updateArrowVisibilities(); }; Grid.prototype.msgObjective = function(evt) { this.objective = evt.detail.body; this.drawObjective(); }; Grid.prototype.msgWin = function(evt) { this.replayStack(evt.detail.body.stack) };