You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
444 lines
14 KiB
444 lines
14 KiB
//===== Constructor
|
|
|
|
const Grid = function() {
|
|
this.colors = {};
|
|
this.icons = {};
|
|
this.obstacles = {};
|
|
this.robots = [];
|
|
this.shadows = [];
|
|
this.walls = [];
|
|
this.objective = {};
|
|
|
|
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-robots', this.msgRobots.bind(this));
|
|
document.addEventListener('G-walls', this.msgWalls.bind(this));
|
|
document.addEventListener('G-objective', this.msgObjective.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 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}px`
|
|
star.style.top = `${s * this.objective.j}px`
|
|
star.style.height = `${s}px`;
|
|
star.style.width = `${s}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 && j === this.objective.j && id === this.objective.id);
|
|
|
|
const evtSolve = new CustomEvent('L-complete', { detail: { complete }});
|
|
document.dispatchEvent(evtSolve);
|
|
};
|
|
|
|
//===== 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.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();
|
|
};
|
|
|