function init() { const map = document.querySelector('.map') const spriteContainer = document.querySelector('.sprite_container') const instruction = document.querySelector('.instruction') const sprite = document.querySelector('.sprite') const motionSpeed = 150 const height = 10 const width = 20 const delay = 20 let cellSize = 40 let spritePos = -cellSize let x = 0 let y = 0 let instructionTimer let motionTimer let displayTimer let boneTimer let start = 42 let goal = 0 let carryOn = true let triedAnotherWay = false let route = [] let happy let happyFrame = 10 const cellsWithWalls = [ 31,45,69,68,71,73,82,83,85,88,91,94,95,96,98,105,110,111,112,123,125,128,137,141,142,143,145,146,154,156,157,171 ] const bone = ` ` const searchMemory = new Array(height * width).fill('').map(()=>{ return { path: null, searched: false, prev: null } }) const mapMap = ()=>{ const mapArr = [] for (let i = 0; i < (width * height); i++){ mapArr.push(i) } return mapArr.map((ele,i)=>{ const dataX = i % width const dataY = Math.floor(i / width) return `
` }).join('') } map.innerHTML = mapMap() const mapTiles = document.querySelectorAll('.map_tile') const setX = num =>{ x = num spriteContainer.style.left = `${x}px` } const setY = num =>{ y = num spriteContainer.style.top = `${y}px` } const setSpritePos = num =>{ spritePos = num sprite.style.marginLeft = `${spritePos}px` } const positionSprite = pos =>{ const paraX = pos % width * cellSize const paraY = Math.floor(pos / width) * cellSize setX(paraX) setY(paraY) } const NoWall = pos =>{ return !mapTiles[pos].classList.contains('wall') } const setUpWalls = ()=>{ cellsWithWalls.forEach(cell=>{ mapTiles[cell].classList.add('wall') }) mapTiles.forEach(tile=>{ if (tile.dataset.y === '0' || tile.dataset.y === '9' || tile.dataset.x === '0' || tile.dataset.x === '19') tile.classList.add('wall') }) } const clearTiles = ()=>{ mapTiles[goal].innerHTML = '' mapTiles.forEach(tile=>{ tile.className = 'map_tile' }) setUpWalls() } const spriteWalk = e=>{ if (!e) return const direction = e.key ? e.key.toLowerCase().replace('arrow','') : e const current = (x / cellSize) + ((y / cellSize) * width) let m = -cellSize switch (direction) { case 'right': if (x < ((width - 1) * cellSize) && NoWall(current + 1)) setX(x + cellSize) m = spritePos === m * 8 ? m * 9 : m * 8 break case 'left': if (x > 0 && NoWall(current - 1)) setX(x - cellSize) m = spritePos === m * 6 ? m * 7 : m * 6 break case 'up': if (y > 0 && NoWall(current - width)) setY(y - cellSize) m = spritePos === m * 3 ? m * 5 : m * 3 break case 'down': if (y < ((height - 1) * cellSize) && NoWall(current + width)) setY(y + cellSize) m = spritePos === m * 0 ? m * 2 : m * 0 break default: console.log('invalid command') } setSpritePos(m) start = (x / cellSize) + ((y / cellSize) * width) if (goal === (x / cellSize) + ((y / cellSize) * width)) { boneTimer = setTimeout(()=>{ mapTiles[goal].innerHTML = '' animateHappyDog() },100) } } const chainMotion = (instruction,index) => { if (index >= instruction.length) return spriteWalk(instruction[index]) motionTimer = setTimeout(()=>{ chainMotion(instruction, index + 1) },motionSpeed) } const displayPath = current =>{ searchMemory[current].path = 'path' mapTiles[current].classList.add('path') let prev = searchMemory[current].prev //! when sprite is one square away from start and prev is undefined, prev is corrected. if (current - width === start || current + width === start || current - 1 === start || current + 1 === start) prev = start let direction if (current - width === prev) direction = 'down' if (current + width === prev) direction = 'up' if (current - 1 === prev) direction = 'right' if (current + 1 === prev) direction = 'left' if (prev) route.push(direction) if (!prev) { route.push('reset') return } if (prev === start || !prev) { const reversedRoute = route.reverse() chainMotion(reversedRoute,0) return } displayTimer = setTimeout(()=>{ displayPath(prev) },delay) } const distanceBetween = (a,b) =>{ return Math.abs(a % width - b % width) + Math.abs(Math.floor(a / width) - Math.floor(b / width)) } const decideNextMove = (current, count) =>{ if (!carryOn) return const possibleDestination = [ current + 1, current - 1, current - width, current + width ] const mapInfo = [] if (possibleDestination.filter(cell=> cell === goal).length === 1) { carryOn = false searchMemory[goal].prev = current displayPath(goal) return } possibleDestination.forEach(option=>{ if (mapTiles[option] && !mapTiles[option].classList.contains('wall') && !searchMemory[option].searched && option !== start) { mapInfo.push( { cell: option, prev: current, distanceFromStart: distanceBetween(start,option), distanceToGoal: distanceBetween(goal,option) } ) } }) const minValue = Math.min(...mapInfo.map(cell=> cell.distanceFromStart + cell.distanceToGoal)) const optionsWithMinValue = mapInfo.filter(cell=> (cell.distanceFromStart + cell.distanceToGoal) === minValue) mapInfo.filter(cell=> (cell.distanceFromStart + cell.distanceToGoal) !== minValue).forEach(option=>{ mapTiles[option.cell].classList.add('sub_node') }) if (optionsWithMinValue.length === 0 && !triedAnotherWay) { triedAnotherWay = true tryAnotherWay(count) } optionsWithMinValue.forEach(option=>{ searchMemory[option.cell].searched = true searchMemory[option.cell].prev = current mapTiles[option.cell].classList.add('node') setTimeout(()=>{ decideNextMove(option.cell, count + 1) },delay) }) } const tryAnotherWay = count =>{ if (!carryOn) return const possibleDestination = [ start + 1, start - 1, start - width, start + width ] possibleDestination.forEach(path=>{ if (mapTiles[path] && !searchMemory[path].searched && !mapTiles[path].classList.contains('wall')) { decideNextMove(path, count + 1) } }) } const resetMotion = () =>{ clearTimeout(motionTimer) clearTimeout(displayTimer) clearTimeout(boneTimer) clearInterval(happy) clearTiles() searchMemory.forEach(memory=>{ memory.path = null, memory.searched = false, memory.prev = null }) route = [] carryOn = true triedAnotherWay = false } const triggerMotion = e =>{ if (Number(e.target.dataset.index) === goal || !e.target.dataset.index) return instruction.classList.add('hide') clearTimeout(instructionTimer) instructionTimer = setTimeout(()=>{ instruction.classList.remove('hide') },5 * 1000) mapTiles[goal].innerHTML = '' goal = Number(e.target.dataset.index) if (mapTiles[goal].classList.contains('wall')) return resetMotion() mapTiles[start].classList.add('start') positionSprite(start) mapTiles[goal].classList.add('goal') mapTiles[goal].innerHTML = bone searchMemory[start].path = 'start' searchMemory[goal].path = 'goal' decideNextMove(start, 0) } const animateHappyDog = () =>{ happy = setInterval(()=>{ happyFrame = happyFrame === 10 ? 11 : 10 setSpritePos(happyFrame * -cellSize) },150) } //! comment this bit out to enable keyboard control // window.addEventListener('keydown', spriteWalk) const resize = () =>{ const wrapper = document.querySelector('.wrapper') let pWidth = 800 if (wrapper.offsetWidth < 800) { pWidth = wrapper.offsetWidth } cellSize = Math.floor(pWidth / width) const calcWidth = Math.floor(pWidth / width) * width const mapCover = document.querySelector('.map_cover') //*resize sprite positionSprite(start) setSpritePos(-cellSize) sprite.style.height = `${cellSize}px` sprite.style.width = `${cellSize * 12}px` spriteContainer.style.height = `${cellSize}px` spriteContainer.style.width = `${cellSize}px` //* resize map map.style.width = `${calcWidth}px` map.style.height = `${calcWidth / 2}px` mapCover.style.width = `${calcWidth}px` mapCover.style.height = `${calcWidth / 2}px` mapCover.style.marginTop = `-${calcWidth / 2}px` mapTiles.forEach(tile=>{ tile.style.width = `${cellSize}px` tile.style.height = `${cellSize}px` }) } //*setup setUpWalls() positionSprite(start) mapTiles.forEach(mapTile=>{ mapTile.addEventListener('click', triggerMotion) }) resize() window.addEventListener('resize', resize) } window.addEventListener('DOMContentLoaded', init)