I'm trying to get good at JavaScript and I wrote this code in order to make mistakes and learn better ways to do it. It's a silly UTF-8 horse trying to find its way towards the flag. You can check it here.
- It can only see the surrounding squares
- It remembers where it has been
- It knows how far away each square is from the flag
script.js
var makeApp = function () {
// settings
var MAX_WIDTH = 64;
var MAX_HEIGHT = 32;
var BLOCK_PROBABILITY = 0.45;
var REVISIT_PENALTY_MULTIPLIER = Math.floor(MAX_WIDTH / 20);
var LEEWAY_MULTIPLIER = 3;
var BASE_LEEWAY = 1.5;
var SUMMED_LEEWAY_BASE = 1;
var TIMEOUT = 200;
// types
var Point = {
x : 0,
y : 0,
price : 0,
summedPrice : 0,
priced : false,
blocked : false,
visits : 0,
node : null // DOM node
};
// data
var w = Math.floor(Math.random() * (MAX_WIDTH - 2)) + 2;
var h = Math.floor(Math.random() * (MAX_HEIGHT - 2)) + 2;
var goal = makeRandPoint();
var start = makeRandPoint();
var grid = [];
// internal methods
function init(node) {
while (goal.x === start.x && goal.y === start.y) {
goal = makeRandPoint();
}
createGrid(node);
}
function getPoint(x,y) {
return grid[y * w + x];
}
function makeRandPoint() {
var p = Object.create(Point);
p.x = Math.floor(Math.random() * w);
p.y = Math.floor(Math.random() * h);
return p;
}
function approxDistance(p0, p1) {
var xd = Math.abs(p1.x - p0.x);
var yd = Math.abs(p1.y - p0.y);
// the greater distance can absorb the smaller by making a diagonal
var d = Math.max(xd, yd);
// allow the horse to consider more paths,
// the farther away, the greater the leeway
var div = BASE_LEEWAY + LEEWAY_MULTIPLIER * (d / Math.max(w, h));
return Math.floor(d / div);
};
function realDistance(p0, p1) {
var xd = Math.abs(p1.x - p0.x);
var yd = Math.abs(p1.y - p0.y);
return Math.max(xd, yd);
}
// without this, the horse will tend to move diagonally
function summedDistance(p0, p1) {
var d = Math.abs(p1.x - p0.x) + Math.abs(p1.y - p0.y);
var div = SUMMED_LEEWAY_BASE + d / Math.max(w, h);
return Math.floor(d / div);
}
function createGrid(node) {
var table = document.createElement('table');
var tr;
var td;
var y;
var x;
var p;
for (y = 0; y < h; ++y) {
tr = document.createElement('tr');
for (x = 0; x < w; ++x) {
td = document.createElement('td');
p = Object.create(Point);
p.x = x;
p.y = y;
p.blocked = Math.random() <= BLOCK_PROBABILITY;
p.node = td;
grid.push(p);
if (y === goal.y && x === goal.x) {
td.className = 'goal';
td.innerText = '⚑';
p.blocked = false;
} else if(y === start.y && x === start.x) {
td.className = 'start';
td.innerText = '♞'
p.blocked = false;
p.price = approxDistance(start, goal);
p.priced = true;
} else if (p.blocked) {
td.className = 'blocked';
}
tr.appendChild(td);
}
table.appendChild(tr);
}
node.appendChild(table);
}
function alivenHorse () {
var p = getPoint(start.x, start.y); // current position
function forEveryNeighbor(pos, f) {
var x0 = pos.x - 1;
var x1 = pos.x + 1;
var y0 = pos.y - 1;
var y1 = pos.y + 1;
var x;
var y;
if (x0 === -1) {
x0 = 0;
}
if (x1 === w) {
x1 = w - 1;
}
if (y0 === -1) {
y0 = 0;
}
if (y1 === h) {
y1 = h - 1;
}
for (y = y0; y <= y1; ++y) {
for (x = x0; x <= x1; ++x) {
// skip current node
if (y === pos.y && x === pos.x) {
continue;
}
f(getPoint(x, y), x, y);
}
}
}
function step() {
var bestNeighbor = Object.create(Point);
bestNeighbor.price = Number.MAX_VALUE;
// price neighbors
forEveryNeighbor(p, function (np, x, y) {
var price;
var realPrice;
if (np.blocked) {
return;
}
if (!np.priced) {
np.price = approxDistance(np, goal);
np.summedPrice = summedDistance(np, goal);
np.realPrice = realDistance(np, goal);
np.priced = true;
np.node.innerText = np.price;
}
if (np.price < bestNeighbor.price) {
bestNeighbor = np;
} else if (np.price === bestNeighbor.price) {
if (np.summedPrice < bestNeighbor.summedPrice) {
bestNeighbor = np;
} else if (np.summedPrice === bestNeighbor.summedPrice) {
if (np.realPrice < bestNeighbor.realPrice) {
bestNeighbor = np;
}
}
}
});
// revisited nodes should be assigned a higher-price to avoid dead-ends
bestNeighbor.price += REVISIT_PENALTY_MULTIPLIER * bestNeighbor.visits;
bestNeighbor.visits++;
p.node.innerText = p.price;
p = bestNeighbor;
p.node.innerText = '♞';
if (p.y !== goal.y || p.x !== goal.x) {
setTimeout(step, TIMEOUT);
}
}
setTimeout(step, TIMEOUT);
};
function run(node) {
init(node);
alivenHorse();
}
// expose public interface
return {
run : run,
};
};
// spawn and let it run
makeApp().run(document.body);
tr.appendChild(td);
into the logical grid creation. Also, I only skimmed the code, but is there guaranteed to be a solution? \$\endgroup\$