Init
This commit is contained in:
commit
de18c904c4
471
a-star.js
Normal file
471
a-star.js
Normal file
@ -0,0 +1,471 @@
|
||||
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();
|
33
index.html
Normal file
33
index.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="de">
|
||||
<head>
|
||||
<style>
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background-color: black;
|
||||
min-height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#color-picker {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script defer src="a-star.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="bar"><input id="color-picker" type="color"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user