After about 20 hours (distributed over 4 days) I finally got this one! I didn't give up, it felt like a real-life project, requiring research and intensive debugging, and after so much effort, it was SOOO satisfying to see it running.!

For me, Part 1 executed in 3 minutes, and Part 2 took 54 minutes!!!

I would be more than happy to answer questions and give tips about that, but basically I really thank @neilgall
for his fantastic explanation and the usage of Dijkstra's algorithm.

I'm gonna omit reader.js which is the same as the other solutions and jump to the point:

15-common.js

constMAP={WALL:'#',CAVERN:'.',GOBLIN:'G',ELF:'E'};constENEMIES={[MAP.GOBLIN]:MAP.ELF,[MAP.ELF]:MAP.GOBLIN}constMAX_HP=200;constINITIAL_AP=3;letgenerator=0;classSquare{constructor({x,y,type}){this.x=x;this.y=y;this.type=type;if([MAP.GOBLIN,MAP.ELF].includes(type)){this.unit={id:generator++,type,square:this,enemyOf:ENEMIES[type],hp:MAX_HP,ap:INITIAL_AP,isAlive:true}}}}constreadDungeon=lines=>{constn=lines.length;constdungeon=Array.from({length:n},row=>[]);constunits=[];for(leti=0;i<n;i++){letm=lines[i].indexOf(' ');m=m===-1?lines[i].length:m;for(letj=0;j<m;j++){constsquare=dungeon[i][j]=newSquare({x:i,y:j,type:lines[i][j]});if(square.unit){units.push(square.unit);}}}return{dungeon,units};};constgetAdjacents=(dungeon,square,type)=>{constn=dungeon.length;constm=dungeon[0].length;const{x,y}=square;constadjacents=[];if(x>0)adjacents.push(dungeon[x-1][y]);if(y>0)adjacents.push(dungeon[x][y-1]);if(y<m-1)adjacents.push(dungeon[x][y+1]);if(x<n-1)adjacents.push(dungeon[x+1][y]);returntype?adjacents.filter(square=>square.type===type):adjacents;}constgetKey=({x,y})=>`${x},${y}`;constgetMinimumDistance=(dungeon,start,end)=>{constunvisitedSquares=newSet();constdistances=newMap();constgetDistance=square=>distances.get(getKey(square));// Setting initial infinite distancesconstn=dungeon.length;constm=dungeon[0].length;for(leti=0;i<n;i++){for(letj=0;j<m;j++){constsquare=dungeon[i][j];if(square.type===MAP.CAVERN){distances.set(getKey(square),Number.POSITIVE_INFINITY);unvisitedSquares.add(square);}}}distances.set(getKey(start),0);letcurrent=start;while(current){constnextDistance=getDistance(current)+1;getAdjacents(dungeon,current,MAP.CAVERN).filter(square=>unvisitedSquares.has(square)).forEach(square=>distances.set(getKey(square),Math.min(getDistance(square),nextDistance)));unvisitedSquares.delete(current);current=unvisitedSquares.size>0?[...unvisitedSquares].reduce((minimum,square)=>getDistance(minimum)<=getDistance(square)?minimum:square):undefined;}constendDistance=Math.min(...getAdjacents(dungeon,end,MAP.CAVERN).map(getDistance));return{endDistance,getDistance};};constgetNext=(dungeon,unit,nearest)=>{const{endDistance,getDistance}=getMinimumDistance(dungeon,nearest,unit);returngetAdjacents(dungeon,unit,MAP.CAVERN).find(square=>getDistance(square)===endDistance);};conststep=(unit,nearest)=>{constoldSquare=unit.square;deleteoldSquare.unit;oldSquare.type=MAP.CAVERN;nearest.unit=unit;nearest.type=unit.type;unit.square=nearest;};constmove=(unit,units,enemies,openCaverns,dungeon)=>{constallReachables=[];for(letenemyofenemies){constadjacents=getAdjacents(dungeon,enemy.square,MAP.CAVERN);constinRange=adjacents.map(square=>{return{square,distance:getMinimumDistance(dungeon,square,unit.square).endDistance};});constreachables=inRange.filter(adjacent=>adjacent.distance<Number.POSITIVE_INFINITY);allReachables.push(...reachables);}if(allReachables.length>0){constnearest=allReachables.reduce((nearest,square)=>nearest.distance<=square.distance?nearest:square);constnext=getNext(dungeon,unit.square,nearest.square);step(unit,next);}}constattack=(unit,enemiesInRange)=>{constminHp=enemiesInRange.reduce((min,enemy)=>Math.min(min,enemy.hp),MAX_HP);constweakestEnemy=enemiesInRange.filter(({hp})=>hp===minHp)[0];weakestEnemy.hp-=unit.ap;if(weakestEnemy.hp<=0){weakestEnemy.isAlive=false;weakestEnemy.square.type=MAP.CAVERN;deleteweakestEnemy.square.unit;deleteweakestEnemy.square;}}constgetEnemiesInRange=(adjacents,{enemyOf})=>{returnadjacents.filter(square=>square.type===enemyOf&&square.unit).map(square=>square.unit);};constsort=units=>{units.sort((a,b)=>{constsA=a.square;constsB=b.square;return(sA.x===sB.x)?sA.y-sB.y:sA.x-sB.x;});};constmakeRound=(dungeon,units)=>{constn=dungeon.length;constm=dungeon[0].length;lethasCombatEndedEarly=false;for(letunitofunits){if(unit.isAlive){// If no enemies, combat ends earlyconsthasEnemies=units.some(enemy=>enemy.type===unit.enemyOf&&enemy.isAlive);if(hasEnemies){// Determine actionletadjacents=getAdjacents(dungeon,unit.square);letenemiesInRange=getEnemiesInRange(adjacents,unit);if(enemiesInRange.length===0){// Moves and attacksconstopenCaverns=adjacents.filter(square=>square.type===MAP.CAVERN);constenemies=units.filter(nextUnit=>unit.enemyOf===nextUnit.type&&nextUnit.isAlive);if(openCaverns.length>0&&enemies.length>0){// Movesmove(unit,units,enemies,openCaverns,dungeon);// Attacksadjacents=getAdjacents(dungeon,unit.square);enemiesInRange=getEnemiesInRange(adjacents,unit);if(enemiesInRange.length>0){attack(unit,enemiesInRange);}}}else{// Attacksattack(unit,enemiesInRange);}}else{hasCombatEndedEarly=true;}}}// Removes deadwhile(units.some(unit=>!unit.isAlive)){constnextDead=units.find(unit=>!unit.isAlive);units.splice(units.indexOf(nextDead),1);}sort(units);returnhasCombatEndedEarly;};constgetOutcome=(rounds,units)=>{constremainingHp=units.reduce((total,unit)=>total+=unit.hp,0);returnrounds*remainingHp;};constgetGoblins=units=>units.filter(unit=>unit.type===MAP.GOBLIN);constgetElves=units=>units.filter(unit=>unit.type===MAP.ELF);constprintStats=(rounds,dungeon,units)=>{console.log(`round ${rounds}:`);console.log(dungeon.map(row=>row.map(col=>col.type).join('')).join('\n'));console.log(units.map(u=>`${u.type}(${u.id}): ${u.hp}`));}module.exports={readDungeon,makeRound,getGoblins,getElves,printStats,getOutcome};

15a.js

const{readFile}=require('./reader');const{readDungeon,makeRound,getGoblins,getElves,printStats,getOutcome}=require('./15-common');(async()=>{constlines=awaitreadFile('15-input.txt');const{dungeon,units}=readDungeon(lines);letgoblins,elves,rounds=0;do{consthasCombatEndedEarly=makeRound(dungeon,units);if(!hasCombatEndedEarly)rounds++;goblins=getGoblins(units).length;elves=getElves(units).length;printStats(rounds,dungeon,units);}while(goblins>0&&elves>0);console.log(`The ${goblins>0?'goblins':'elves'} won!`);console.log(`The outcome of the combat is ${getOutcome(rounds,units)}`);})();

15b.js

const{readFile}=require('./reader');const{readDungeon,makeRound,getElves,getGoblins,printStats,getOutcome}=require('./15-common');(async()=>{constlines=awaitreadFile('15-input.txt');letap=3;letareAllElvesAlive;do{ap++;console.log(`\nWith AP as ${ap}:`);const{dungeon,units}=readDungeon(lines);constelves=getElves(units);elves.forEach(elf=>elf.ap=ap);letinitialElvesCount=elves.length;letelvesCount,goblinsCount;letrounds=0;do{console.log(`AP ${ap}, round ${rounds+1}`);consthasCombatEndedEarly=makeRound(dungeon,units);if(!hasCombatEndedEarly)rounds++;goblinsCount=getGoblins(units).length;elvesCount=getElves(units).length;areAllElvesAlive=elvesCount===initialElvesCount;}while(areAllElvesAlive&&goblinsCount>0);printStats(rounds,dungeon,units);if(areAllElvesAlive){console.log(`\nAll elves survived when AP is ${ap}`);console.log(`The outcome of the last combat is ${getOutcome(rounds,units)}`);}else{console.log(`There was an elf casualty!`);}}while(!areAllElvesAlive);})();

Ryan is an engineer in the Sacramento Area with a focus in Python, Ruby, and Rust. Bash/Python Exercism mentor. Coding, physics, calculus, music, woodworking.
Message me on DEV!

Location

Elk Grove, CA

Education

M.S.C.S. Lewis University (Spring of 2021), B.S.M.E. Cal Poly (SLO)

Is your path finding algorithm the dijkstra one? Thanks, it was very clear how it works, maybe because it in javascript :), I used it for my solution for part one.

But I didn't understand how you choose the next step exactly. It works, but I can't see where you break the ties in reading order. Anyhow, nicely done, learned how to find shortest route with your code.

Hi @askeroff
, thanks!! Yes, I'm using Dijkstra's algorithm (even though I didn't explicitly mention it in the code), but it's implemented on getMinimumDistance function, where I basically do the following steps to get the minimum step between "start" and "end" squares:

create a map for the distances of all squares and set each one of them as Number.POSITIVE_INFINITY

create a set to mark unvisited squares and add every square to it

set "current" as the unvisited square with the lowest distance (or the "start" square in the beginning)

for each unvisited unblocked neighbors of "current", update the distance to the minimum between distance(current)+1 and the neighbor's current distance.

mark "current" as visited (by removing it from the unvisited squares set)

go back to step 3 and repeat until there are no more unvisited squares

The final distance between "start" and "end" is the smallest distance of all "end"'s unblocked neighbors.

Also, to break the ties in the reading order, it's all in getAdjacents function, where I look for the following neighbor squares and return in their reading order.

Considering we're getting adjacents for position X,Y, N=max(X) and M=max(Y)

## JavaScript solution

After about 20 hours (distributed over 4 days) I finally got this one! I didn't give up, it felt like a real-life project, requiring research and intensive debugging, and after so much effort, it was SOOO satisfying to see it running.!

For me, Part 1 executed in 3 minutes, and Part 2 took 54 minutes!!!

I would be more than happy to answer questions and give tips about that, but basically I really thank @neilgall for his fantastic explanation and the usage of Dijkstra's algorithm.

I'm gonna omit reader.js which is the same as the other solutions and jump to the point:

## 15-common.js

## 15a.js

## 15b.js

Awesome! Really nice work!

Is your path finding algorithm the dijkstra one? Thanks, it was very clear how it works, maybe because it in javascript :), I used it for my solution for part one.

But I didn't understand how you choose the next step exactly. It works, but I can't see where you break the ties in reading order. Anyhow, nicely done, learned how to find shortest route with your code.

Hi @askeroff , thanks!! Yes, I'm using Dijkstra's algorithm (even though I didn't explicitly mention it in the code), but it's implemented on

`getMinimumDistance`

function, where I basically do the following steps to get the minimum step between "start" and "end" squares:`Number.POSITIVE_INFINITY`

The final distance between "start" and "end" is the smallest distance of all "end"'s unblocked neighbors.

Also, to break the ties in the reading order, it's all in

`getAdjacents`

function, where I look for the following neighbor squares and return in their reading order.Considering we're getting adjacents for position X,Y, N=max(X) and M=max(Y)

In other words,

Oh, awesome. I got it. I want to come back after I've done others and revisit this problem with breadth-first-search. Maybe it'll be faster.