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)