aboutsummaryrefslogtreecommitdiffstats
path: root/src/pathfinder/index.ts
blob: dc11e2e6a820c7f58c2981a4342d2a36aac71d41 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
  * @module Pathfinder
*/

import { Graph, Vertex } from "./Graph";

/**
  * Function that find path from start to goal in given map
  * @param map - map
*/
export default function findPath(map: Graph): Path | null {
  const start = map.find(v => v.start)
  const goal = map.find(v => v.goal)

  if (!start || !goal) {
    throw new Error("Pathfinder@findPath: There's no start and/or goal in given graph")
  }

  return A_Star(start, () => ((start.x! - goal.x!) ** 2 + (start.y! - goal.y!) ** 2) * 0.5);
}

/**
  * Function implementing A Star algorith
  @param start - start
  @param h - heuristic function
*/
function A_Star(start: Vertex, h: (arg0: Vertex) => number): Path | null {
  type verObj = { [index: string]: Vertex }
  type numObj = { [index: string]: number }
  const openSet = [start]
  const cameFrom: verObj = {}
  const gScore: numObj = {}
  gScore[start.uuid] = 0
  const fScore: numObj = {}
  fScore[start.uuid] = h(start)

  while (openSet.length) {
    const current = smallest(openSet, (pv, cv) => getOrInf(fScore, pv.uuid) < getOrInf(fScore, cv.uuid))
    if (current.goal) return reconstruct_path(cameFrom, current)
    removeItem(openSet, current)

    const neighbours = current.edges.filter(edge => edge.to.walkable).map(edge => { return { edge, neighbour: edge.to } });
    for (const { neighbour, edge } of neighbours) {
      const tentative_gScore = getOrInf(gScore, current.uuid) + edge.weight
      if (tentative_gScore < getOrInf(gScore, neighbour.uuid)) {
        cameFrom[neighbour.uuid] = current
        gScore[neighbour.uuid] = tentative_gScore
        fScore[neighbour.uuid] = tentative_gScore + h(neighbour)
        if (!openSet.includes(neighbour)) openSet.push(neighbour)
      }
    }
  }
  return null;
}

/**
  * Function that reconstructs path, from finish to start
  * @param cameFrom - finish
  * @param current - whereami vertex
*/
function reconstruct_path(cameFrom: { [index: string]: Vertex }, current: Vertex): Path {
  let c = current;
  const total_path = [c]
  while (Object.keys(cameFrom).includes(c.uuid)) {
    c = cameFrom[c.uuid];
    total_path.unshift(c)
  }
  return total_path;
}

/** 
  * Getter to get value or object or null
  * @param arr - object to look in
  * @param key - key at which to search
*/
function getOrInf(arr: { [index: string]: number }, key: keyof typeof arr): number {
  return arr[key] ?? Infinity
}

/**
  * Gets smallest element from array 
  * @typeParam T - typeof elements inside given array
  * @param arr - array to look in 
  * @param fn - function to determine which is smaller element
*/
function smallest<T>(arr: Array<T>, fn: (prev: T, curr: T) => boolean): T {
  let smallest = arr[0]
  arr.forEach((el, i) => {
    if (!i) return;
    if (fn(arr[i - 1], el)) smallest = arr[i - 1]
  });
  return smallest;
}

/**
  * Remove element from given array
  * @typeParam T - typeof elements inside given array
  * @param item - to be removed
*/
function removeItem<T>(arr: Array<T>, item: T): Array<T> {
  const index = arr.indexOf(item);
  if (index > -1) {
    arr.splice(index, 1);
  }
  return arr;
}

/** Shortcut */
export type Path = Array<Vertex>