//===== Constructor const Controls = function() { this.names = {}; this.playerId = null; this.countdownTimer = null; this.currentMoveCount = 0; this.winningMoveCount = Infinity; // "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-replay-complete', this.msgReplayComplete.bind(this)); document.addEventListener('L-skip', this.msgSkip.bind(this)); document.addEventListener('L-stack', this.msgStack.bind(this)); document.addEventListener('G-connected', this.msgConnected.bind(this)); document.addEventListener('G-countdown', this.msgCountdown.bind(this)); document.addEventListener('G-full', this.msgFull.bind(this)); document.addEventListener('G-newround', this.msgNewRound.bind(this)); document.addEventListener('G-players', this.msgPlayers.bind(this)); document.addEventListener('G-state', this.msgState.bind(this)); document.addEventListener('G-win', this.msgWin.bind(this)); // Click handlers 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.onClickCountdownSkip.bind(this)); document.getElementById('controls-win-next').addEventListener('click', this.onClickWinNext.bind(this)); this.setState('CONNECTING'); } //===== Countdown Controls.prototype.countdownStart = function(seconds) { clearTimeout(this.countdownTimer); this.countdownTimer = this.countdownTick.bind(this); const countdown = document.getElementById('controls-countdown-value'); countdown.dataset.tick = seconds * 1; this.countdownTick(); }; Controls.prototype.countdownTick = function() { const countdown = document.getElementById('controls-countdown-value'); const tick = countdown.dataset.tick * 1; countdown.dataset.tick = tick - 1; countdown.innerHTML = tick; if (tick === 0) { this.countdownComplete(); return; } this.countdownTimer = setTimeout(this.countdownTick.bind(this), 1000); }; Controls.prototype.countdownComplete = function() { clearTimeout(this.countdownTimer); document.getElementById('controls-countdown-value').innerHTML = 0; }; //===== UI Controls.prototype.setState = function(state) { const blocks = [ 'controls-connection', 'controls-countdown', 'controls-moves', 'controls-options', 'controls-players', 'controls-win', ]; blocks.forEach(id => document.getElementById(id).style.display = 'none'); // IDs to show for each state. const STATE = { CONNECTING: ['controls-connection'], PLAY: ['controls-players', 'controls-moves', 'controls-options'], COUNTDOWN: ['controls-players', 'controls-moves', 'controls-countdown'], WIN: ['controls-players', 'controls-win'] }; (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); 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 > 0 ? '' : 'Nobody is in the game yet.'); msg.style.display = (keys.length > 0 ? 'none' : 'block'); }; //===== Message handlers Controls.prototype.msgJoin = function() { this.setState('CONNECTING'); document.querySelector('#controls-connection .controls-message').innerHTML = 'Connecting...'; }; Controls.prototype.msgComplete = function(evt) { if (this.currentMoveCount < this.winningMoveCount) { document.getElementById('controls-moves-solve').style.display = (evt.detail.complete ? 'block' : 'none'); } }; Controls.prototype.msgConnected = function(evt) { this.playerId = evt.detail.body.player_id; Cookie.setCookie({ name: 'player_name', value: evt.detail.body.player_name, days: 1 }); Cookie.setCookie({ name: 'player_id', value: this.playerId, days: 1 }); this.drawPlayers(); }; Controls.prototype.msgConnectionError = function(evt) { this.setState('CONNECTING'); const msg = (evt.detail === 'noname' ? "Didn't get your name - refresh to try again." : "Can't reach the server."); document.querySelector('#controls-connection .controls-message').innerHTML = msg; }; Controls.prototype.msgFull = function() { this.setState('CONNECTING'); document.querySelector('#controls-connection .controls-message').innerHTML = "Sorry, the game is full right now. Please try again later."; }; Controls.prototype.msgCountdown = function(evt) { this.winningMoveCount = evt.detail.body.moveCount; document.getElementById('controls-countdown-moves').innerHTML = evt.detail.body.moveCount; document.getElementById('controls-countdown-player').innerHTML = `${this.names[evt.detail.body.id]} has a solution!`; const { duration, timestamp } = evt.detail.body; const now = new Date().getTime(); const diff = Math.ceil((now - timestamp) / 1000); const remaining = duration - diff; this.countdownStart(remaining); }; 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()); this.currentMoveCount = evt.detail.length - robots.size; document.getElementById('controls-moves-solve').style.display = 'none'; document.getElementById('controls-moves-count').innerHTML = this.currentMoveCount; document.getElementById('controls-moves-reset').style.display = this.currentMoveCount > 0 ? 'block' : 'none'; document.getElementById('controls-moves-undo').style.display = this.currentMoveCount > 0 ? 'block' : 'none'; }; Controls.prototype.msgPlayers = function(evt) { this.names = evt.detail.body; this.drawPlayers(); }; Controls.prototype.msgState = function(evt) { this.setState(evt.detail.body); }; Controls.prototype.msgWin = function(evt) { document.getElementById('controls-win-message').innerHTML = `Congratulations ${this.names[evt.detail.body.player_id]} !`; document.getElementById('controls-win-count').innerHTML = evt.detail.body.moveCount; }; Controls.prototype.msgNewRound = function() { this.winningMoveCount = Infinity; document.getElementById('controls-win-next').style.display = 'none'; }; Controls.prototype.msgReplayComplete = function() { document.getElementById('controls-win-next').style.display = 'block'; }; //===== Click handlers Controls.prototype.dispatch = function(evt, data) { const e = (data ? new CustomEvent(evt, { detail: data }) : new Event(evt)); document.dispatchEvent(e); }; // Options block Controls.prototype.onClickOptionsObjective = function() { this.dispatch('L-objective'); }; Controls.prototype.onClickOptionsRobots = function() { this.dispatch('L-robots'); }; Controls.prototype.onClickOptionsWalls = function() { this.dispatch('L-walls'); }; // Moves block Controls.prototype.onClickMovesReset = function() { this.dispatch('L-reset'); }; Controls.prototype.onClickMovesSolve = function() { this.dispatch('L-submit'); }; Controls.prototype.onClickMovesUndo = function() { this.dispatch('L-undo'); }; // Countdown/win block Controls.prototype.onClickCountdownSkip = function() { this.dispatch('L-skip'); }; Controls.prototype.onClickWinNext = function() { this.dispatch('L-newround'); };