Stack implementation well underway.

master
Ben Burlingham 5 years ago
parent 303dbd939b
commit b6910d8692
  1. 26
      README.txt
  2. 31
      client/connection.js
  3. 317
      client/content.js
  4. 453
      client/controls.js
  5. 362
      client/grid.js
  6. 52
      client/squares.js
  7. 31
      client/stack.js
  8. 8
      content.css
  9. 15
      controls.css
  10. 22
      index.html
  11. 26
      server.js
  12. 4
      server/game.js

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

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

@ -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();
};

453
client/controls.js vendored

@ -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');
// };

@ -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: '&#9650;', left: (s * 1.25), top: (s * 0.5) });
const down = this.drawArrow({ direction: 'down', label: '&#9660;', left: (s * 1.25), top: (s * 2) });
const left = this.drawArrow({ direction: 'left', label: '&#9664;', left: (s * 0.5), top: (s * 1.25) });
const right = this.drawArrow({ direction: 'right', label: '&#9654;', 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
// }
// };

@ -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),
}
}

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

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

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

@ -9,12 +9,12 @@
<link rel="stylesheet" href="content.css">
<link rel="stylesheet" href="join.css">
<link href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap" rel="stylesheet">
<script type='text/javascript' src='client/content.js'></script>
<script type='text/javascript' src='client/connection.js'></script>
<script type='text/javascript' src='client/controls.js'></script>
<script type='text/javascript' src='client/cookie.js'></script>
<script type='text/javascript' src='client/grid.js'></script>
<script type='text/javascript' src='client/join.js'></script>
<script type='text/javascript' src='client/squares.js'></script>
<script type='text/javascript' src='client/stack.js'></script>
</head>
<body>
<div id="join">
@ -75,15 +75,11 @@
<div class='controls-subtitle'>Local</div>
<div class="controls-row">
<div>Moves:</div>
<div id="controls-moves">0</div>
<div id="controls-moves"></div>
<div class='controls-button' id='controls-undo'>Undo</div>
<div class='controls-button' id='controls-reset'>Reset</div>
</div>
<div id="controls-guesses">
<div class="controls-alert-info">How many moves to win?</div>
</div>
<div id="controls-panic">
<div class='controls-alert-urgent'></div>
@ -100,18 +96,18 @@
</div>
</div>
<div id="content-container"></div>
<div id="content-container">
<div id="content-grid"></div>
</div>
<script>
window.addEventListener('load', () => {
const squares = new Squares();
// Order-independent. Communication via local events.
new Join();
new Connection();
new Controls();
new Content({ parent: document.getElementById('content-container'), squares });
new Grid();
new Stack();
})
</script>
</body>

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

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

Loading…
Cancel
Save