a-star/a-star.js

472 lines
9.4 KiB
JavaScript
Raw Normal View History

2022-05-29 19:20:47 +02:00
class Position
{
constructor(x, y)
{
this.x = x;
this.y = y;
}
getDistance(position, test)
{
return Math.sqrt(Math.pow(position.x - this.x, 2) + Math.pow(position.y - this.y, 2));
}
}
class Node
{
constructor(x, y)
{
this.position = new Position(x, y);
this.g_cost = null;
this.h_cost = null;
this.f_cost = null;
this.previous = null;
this.isTraversable = true;
}
}
class Map
{
constructor(width, height)
{
this.width = width;
this.height = height;
this.nodes = [];
this.startNode = null;
this.targetNode = null;
this._initialize();
}
_initialize()
{
for (let y = 0; y < this.height; y++) {
let nodes = [];
for (let x = 0; x < this.width; x++) {
nodes.push(new Node(x, y));
}
this.nodes.push(nodes);
}
}
getNodeAt(x, y)
{
return this.nodes[y][x];
}
getNeighbours(node, allowDiagonals = false)
{
let positions = [
new Position(node.position.x - 1, node.position.y),
new Position(node.position.x + 1, node.position.y),
new Position(node.position.x, node.position.y - 1),
new Position(node.position.x, node.position.y + 1),
];
if (allowDiagonals) {
positions = positions.concat(
[
new Position(node.position.x - 1, node.position.y - 1),
new Position(node.position.x + 1, node.position.y - 1),
new Position(node.position.x - 1, node.position.y + 1),
new Position(node.position.x + 1, node.position.y + 1),
]
);
}
const neighbours = [];
for (const position of positions) {
if (position.x < 0 || position.x >= this.width || position.y < 0 || position.y >= this.height) {
continue;
}
neighbours.push(this.getNodeAt(position.x, position.y));
}
return neighbours;
}
draw(canvas, unitWidth, unitHeight) {
canvas.width = this.width * unitWidth;
canvas.height = this.height * unitHeight;
const context = canvas.getContext('2d');
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
this.drawNode(x, y, this.getNodeAt(x, y).isTraversable ? COLOR_GROUND : COLOR_WALL, canvas);
}
}
}
drawNode(x, y, color, canvas)
{
const context = canvas.getContext('2d');
const unitWidth = canvas.width / this.width;
const unitHeight = canvas.height / this.height;
context.fillStyle = color;
context.fillRect(x * unitWidth, y * unitHeight, unitWidth, unitHeight);
}
static loadFromLayout(layout)
{
const width = layout[0].length;
const height = layout.length;
const map = new Map(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
switch (layout[y].substr(x, 1)) {
case '#':
map.getNodeAt(x, y).isTraversable = false;
break;
case ' ':
map.getNodeAt(x, y).isTraversable = true;
break;
case 'S':
map.startNode = map.getNodeAt(x, y);
map.startNode.isTraversable = true;
break;
case 'T':
map.targetNode = map.getNodeAt(x, y);
map.targetNode.isTraversable = true;
break;
}
}
}
return map;
}
}
class Pathfinder
{
constructor(map)
{
this.map = map;
this.open = [];
this.closed = [];
this.paths = {};
}
calculatePath(positionStart, positionTarget)
{
this.open.push(this.map.getNodeAt(positionStart.x, positionStart.y));
let count = 0;
while (this.open.length > 0) {
count++;
if (count > 1000) {
alert('Path could not be calculated!');
return;
}
let current = this._getCheapestNode(this.open);
this._removeNodeFromOpen(current);
this.closed.push(current);
if (current.position.x === positionTarget.x && current.position.y === positionTarget.y) {
break;
}
for (const neighbour of this.map.getNeighbours(current)) {
if (!neighbour.isTraversable || this.closed.indexOf(neighbour) > -1) {
continue;
}
const is_inside_open = this.open.indexOf(neighbour) > -1;
const g_cost = neighbour.position.getDistance(positionStart) * 10;
const h_cost = neighbour.position.getDistance(positionTarget) * 10;
const f_cost = g_cost + h_cost;
if (!is_inside_open) {
neighbour.g_cost = g_cost;
neighbour.h_cost = h_cost;
neighbour.f_cost = f_cost;
neighbour.previous = current;
}
if (!is_inside_open || current.position.getDistance(neighbour.position) < neighbour.position.getDistance(neighbour.previous.position)) {
neighbour.f_cost = f_cost;
neighbour.previous = current;
if (!is_inside_open) {
this.open.push(neighbour);
}
}
}
}
}
getPath(positionStart, positionTarget)
{
this.map.startNode = this.map.getNodeAt(positionStart.x, positionStart.y);
this.map.targetNode = this.map.getNodeAt(positionTarget.x, positionTarget.y);
let path = [];
if (!this.map.getNodeAt(positionTarget.x, positionTarget.y).isTraversable) {
return path;
}
this.calculatePath(positionStart, positionTarget);
let targetNode = null;
for (const node of this.closed) {
if (node.position.x == positionTarget.x && node.position.y == positionTarget.y) {
targetNode = node;
break;
}
}
let node = targetNode;
while (node !== null) {
path = [node].concat(path);
node = node.previous;
}
return path;
}
reset()
{
this.open = [];
this.closed = [];
for (const node of this.map.nodes) {
node.g_cost = null;
node.h_cost = null;
node.f_cost = null;
node.previous = null;
}
this.map.startNode = null;
this.map.targetNode = null;
}
_getCheapestNode(nodes)
{
let cheapest = nodes[0];
for (const node of nodes) {
if (node.f_cost < cheapest.f_cost || (node.f_cost == cheapest.f_cost && node.h_cost < cheapest.h_cost)) {
cheapest = node;
}
}
return cheapest;
}
_removeNodeFromOpen(node)
{
const index = this.open.indexOf(node);
if (index === -1) {
return;
}
this.open = this.open.slice(0, index).concat(this.open.slice(index + 1));
}
}
class Player
{
constructor(x, y)
{
this.x = x;
this.y = y;
this.speed = 0.5;
this.color = '#ff0000';
}
draw(canvas, color = this.color)
{
const context = canvas.getContext('2d');
const width = canvas.width / GRID_WIDTH;
const height = canvas.height / GRID_HEIGHT;
context.fillStyle = color;
context.fillRect(this.x * GRID_WIDTH, this.y * GRID_HEIGHT, GRID_WIDTH, GRID_HEIGHT);
}
}
function calculateRoute(start, target)
{
const map = Map.loadFromLayout(layout);
const canvas = document.getElementById('canvas');
if (!map.getNodeAt(target.x, target.y).isTraversable) {
return [];
}
map.draw(canvas, GRID_WIDTH, GRID_HEIGHT);
const pathfinder = new Pathfinder(map);
const path = pathfinder.getPath(start, target);
/*
for (const node of path) {
map.drawNode(node.position.x, node.position.y, '#ffff00', canvas);
}
*/
return path;
}
function movePlayerTo(player, x, y, canvas)
{
const deltaX = x - player.x;
const deltaY = y - player.y;
player.draw(canvas, COLOR_GROUND);
if (x !== player.x) {
player.x += deltaX > 0 ? player.speed : -player.speed;
if (deltaX < 0.1 && deltaX > -0.1) {
player.x = x;
}
}
if (y !== player.y) {
player.y += deltaY > 0 ? player.speed : -player.speed;
if (deltaY < 0.1 && deltaY > -0.1) {
player.y = y;
}
}
player.draw(canvas);
}
function movePlayerRoute(player, route, canvas)
{
let index = 1;
const interval = setInterval(
() => {
movePlayerTo(player, route[index].position.x, route[index].position.y, canvas);
if (route[index].position.x === player.x && route[index].position.y === player.y) {
index++;
}
if (index === route.length) {
clearInterval(interval);
oldTarget = route[route.length - 1];
isTargeting = false;
}
},
20
);
}
function update()
{
map.draw(canvas, GRID_WIDTH, GRID_HEIGHT);
player.draw(canvas);
}
const layout = [
' ##########################',
' S # # # # #',
' # ###### # # ####### # #',
' # # # ##### #',
' # #### # # ####### # #',
' # # # # # ##### # #',
' # # #### # # ### # # # #',
' # # # # # # # # # #',
' # ###### # ### # # # # # #',
' # # # # # # #',
'###################### # ##### #',
'# # # # # # # ',
'# # # # # ## # # # ##### # ',
'# # # # # # # # # ',
'# ##### ## ##### ##### # # ',
'# # # # # # ',
'##### ###### # ######### # ',
'# # # ',
'########################## '
];
const GRID_WIDTH = 40;
const GRID_HEIGHT = GRID_WIDTH;
const COLOR_WALL = '#aaaaaa';
const COLOR_GROUND = '#005500';
const map = Map.loadFromLayout(layout);
const canvas = document.getElementById('canvas');
let oldTarget = map.startNode.position;
let isTargeting = false;
const player = new Player(oldTarget.x, oldTarget.y);
canvas.addEventListener(
'click',
(event) => {
if (isTargeting) {
return;
}
isTargeting = true;
const rect = canvas.getBoundingClientRect();
const x = parseInt((event.clientX - rect.x) / GRID_WIDTH);
const y = parseInt((event.clientY - rect.y) / GRID_HEIGHT);
if (player.x === x && player.y === y) {
isTargeting = false;
return;
}
const target = new Position(x, y);
const route = calculateRoute(new Position(player.x, player.y), new Position(x, y));
if (route.length > 1) {
movePlayerRoute(player, route, canvas);
} else {
isTargeting = false;
}
}
);
const picker = document.getElementById('color-picker')
picker.value = player.color;
picker.addEventListener(
'input',
(event) => {
player.color = event.target.value;
update();
}
);
update();