commit de18c904c4772e1470d0ef658b8af23c91924e1a Author: Mal Date: Sun May 29 19:20:47 2022 +0200 Init diff --git a/a-star.js b/a-star.js new file mode 100644 index 0000000..6b2a9db --- /dev/null +++ b/a-star.js @@ -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(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..ffc0e3c --- /dev/null +++ b/index.html @@ -0,0 +1,33 @@ + + + + + + + + + +
+ +
+
+ +