core/pathfinder.js

import {Grid, Heap, UTILS, Vector2} from '../index.js';

/**
 * Creates pathfinders.
 *
 * @example
 *
 * // minimal
 * const pathfinder = new Pathfinder();
 *
 * const path = pathfinder.find({
 *
 *     $grid: grid,
 *     $start: new Vector2(-2, 0),
 *     $finish: new Vector2(2, 0),
 *     $access: accessor
 * });
 *
 * // full
 * const pathfinder = new Pathfinder(heuristic);
 *
 * const path = pathfinder.find({
 *
 *     $grid: grid,
 *     $start: new Vector2(-2, 0),
 *     $finish: new Vector2(2, 0),
 *     $access: accessor
 * });
 */
class Pathfinder {

    /**
     * @template {any} TypeGeneric The generic type of the items.
     * @callback TypeAccessor An accessor to the cost of a cell.
     * @param {TypeGeneric} $item The item.
     * @returns {number}
     * @protected
     *
     * @memberof Pathfinder
     */

    /**
     * @callback TypeHeuristic A heuristic to use during a pathfinding.
     * @param {Vector2} $a The position of the first cell to compare.
     * @param {Vector2} $b The position of the second cell to compare.
     * @returns {number}
     * @protected
     *
     * @memberof Pathfinder
     */

    /**
     * @typedef {object} TypeNode A pathfinder node.
     * @property {number} $cost The current cost.
     * @property {number} $heuristic The heuristic cost.
     * @property {TypeNode} [$parent] The parent node.
     * @property {Vector2} $position The position.
     * @protected
     *
     * @memberof Pathfinder
     */

    /**
     * Stores the heuristic function.
     * @type {TypeHeuristic}
     * @private
     */
    $heuristic;

    /**
     * Creates a new pathfinder.
     * @param {TypeHeuristic} $heuristic The heuristic function to use.
     */
    constructor($heuristic = Vector2.distanceManhattan) {

        this.$heuristic = $heuristic;
    }

    /**
     * Gets the shortest path between the positions of the two given cells of the given weighted grid.
     * @template {any} TypeGeneric The generic type of the items.
     * @param {object} $parameters The given parameters.
     * @param {Grid<TypeGeneric>} $parameters.$grid The weighted grid.
     * @param {Vector2} $parameters.$start The position of the 'start' cell.
     * @param {Vector2} $parameters.$finish The position of the 'finish' cell.
     * @param {TypeAccessor<TypeGeneric>} $parameters.$access The accessor to the cost of a cell.
     * @returns {Array<Vector2>}
     * @public
     */
    find({$grid, $start, $finish, $access}) {

        if ($grid.has($start) === false) {

            return [];
        }

        if ($grid.has($finish) === false) {

            return [];
        }

        if ($finish.equal($start) === true) {

            return [$finish.clone()];
        }

        /**
         * @type {Heap<TypeNode>}
         */
        const open = new Heap(($a, $b) => {

            const a = $a.$cost + $a.$heuristic;
            const b = $b.$cost + $b.$heuristic;

            if (a === b) {

                return $a.$heuristic - $b.$heuristic;
            }

            return a - b;
        });

        open.push({

            $cost: 0,
            $heuristic: this.$heuristic($start, $finish),
            $position: $start.clone()
        });

        const closed = new Set();

        while (open.size > 0) {

            const current = open.pop();
            const key = Vector2.serialize(current.$position);

            if (closed.has(key) === true) {

                continue;
            }

            if (current.$position.equal($finish) === true) {

                const path = [];

                let node = current;

                while (typeof node !== 'undefined') {

                    path.unshift(node.$position.clone());

                    node = node.$parent;
                }

                return path;
            }

            closed.add(key);

            const neighbors = UTILS.shuffle([

                new Vector2(-1, 0),
                new Vector2(1, 0),
                new Vector2(0, -1),
                new Vector2(0, 1)
            ]);

            neighbors.forEach(($position) => {

                const position = $position.add(current.$position);

                if ($grid.has(position) === false) {

                    return;
                }

                const key = Vector2.serialize(position);

                if (closed.has(key) === true) {

                    return;
                }

                open.push({

                    $cost: current.$cost + $access($grid.get(position)),
                    $heuristic: this.$heuristic(position, $finish),
                    $parent: current,
                    $position: position
                });
            });
        }

        return [];
    }
}

export {

    Pathfinder
};

export default Pathfinder;