Home Reference Source Repository

app/entities/creatures/strategies/Pather.js

var AStar = (function() {
    var StringSet = function() {};
    StringSet.prototype.add = function(key) {this[key] = true;};
    StringSet.prototype.contains = function(key) {return this[key];};

    var dict = function() {};
    dict.prototype.set = function(key, value) {this[key] = value;};
    dict.prototype.get = function(key) {return this[key];};
    dict.prototype['delete'] = function(key) {delete this[key];};

    var Heap = function(comparator) {
        this._comparator = comparator;
        this._list = [];
    };
    Heap.prototype.push = function(value) {
        this._list.unshift(value);
        this._list.sort(this._comparator);
    };
    Heap.prototype.pop = function() {
        return this._list.shift();
    };
    Heap.prototype.size = function() {
        return this._list.length;
    };

    function aStar(params) {
        if(params.start === undefined) {
            throw new Error('No starting node');
        }
        if(typeof params.isEnd !== 'function') {
            throw new Error('No isEnd function provided');
        }
        if(typeof params.neighbor !== 'function') {
            throw new Error('No neighbors function provided');
        }
        if(typeof params.distance !== 'function') {
            throw new Error('No distance function provided');
        }
        if(typeof params.heuristic !== 'function') {
            throw new Error('No heuristic function provided');
        }

        if(params.timeout !== undefined && typeof params.timeout !== 'number') {
            throw new Error('Optional parameter timeout must be a number');
        }
        var hash = params.hash || defaultHash;

        var startNode = {
            data: params.start,
            g: 0,
            h: params.heuristic(params.start)
        };
        var bestNode = startNode;
        startNode.f = startNode.h;
        // leave .parent undefined
        var closedDataSet = new StringSet();
        var openHeap = new Heap(heapComparator);
        var openDataMap = new dict();
        openHeap.push(startNode);
        openDataMap.set(hash(startNode.data), startNode);
        var startTime = new Date();
        while (openHeap.size()) {
            if (new Date() - startTime > params.timeout) {
                return {
                    status: 'timeout',
                    cost: bestNode.g,
                    path: reconstructPath(bestNode)
                };
            }
            var node = openHeap.pop();
            openDataMap.delete(hash(node.data));
            if (params.isEnd(node.data)) {
                // done
                return {
                    status: 'success',
                    cost: node.g,
                    path: reconstructPath(node)
                };
            }
            // not done yet
            closedDataSet.add(hash(node.data));
            var neighbors = params.neighbor(node.data);
            for (var i = 0; i < neighbors.length; i++) {
                var neighborData = neighbors[i];
                if (closedDataSet.contains(hash(neighborData))) {
                    // skip closed neighbors
                    continue;
                }
                var gFromThisNode = node.g + params.distance(node.data, neighborData);
                var neighborNode = openDataMap.get(hash(neighborData));
                var update = false;
                if (neighborNode === undefined) {
                    // add neighbor to the open set
                    neighborNode = {
                        data: neighborData
                    };
                    // other properties will be set later
                    openDataMap.set(hash(neighborData), neighborNode);
                } else {
                    if (neighborNode.g < gFromThisNode) {
                        // skip this one because another route is faster
                        continue;
                    }
                    update = true;
                }
                // found a new or better route.
                // update this neighbor with this node as its new parent
                neighborNode.parent = node;
                neighborNode.g = gFromThisNode;
                neighborNode.h = params.heuristic(neighborData);
                neighborNode.f = gFromThisNode + neighborNode.h;
                if (neighborNode.h < bestNode.h) bestNode = neighborNode;
                if (update) {
                    //openHeap.heapify();
                } else {
                    openHeap.push(neighborNode);
                }
            }
        }
        // all the neighbors of every accessible node have been exhausted
        return {
            status: 'noPath',
            cost: bestNode.g,
            path: reconstructPath(bestNode)
        };
    }

    function reconstructPath(node) {
        if (node.parent !== undefined) {
            var pathSoFar = reconstructPath(node.parent);
            pathSoFar.push(node.data);
            return pathSoFar;
        } else {
            // this is the starting node
            return [node.data];
        }
    }

    function defaultHash(node) {
        return node.toString();
    }

    function heapComparator(a, b) {
        return a.f - b.f;
    }

    return aStar;
}());

import Moves from '../moves/Moves.js';

export default module = {
    getMoveSequenceToward: function(dungeon, creature, target) {
        const start = dungeon.getTile(creature);

        if(start === target) {
            console.warn('Creature trying to move to path to its own location', this);
        }

        var pathfinding = AStar({
            start: start,
            isEnd: (node)=>node===target,
            neighbor: (node)=>node.getNeighbors8(dungeon).filter(
                (neighbor)=>(creature.canOccupy(neighbor) && (neighbor===target || neighbor.getCreature() == null || !creature.canSee(dungeon, dungeon.getTile(neighbor.getCreature()))))),
            distance: (a,b)=>a.getDirectDistance(b),
            heuristic: (a)=>a.getEuclideanDistance(target)
        });

        if(pathfinding.status === 'success') {
            var prevX = start.getX();
            var prevY = start.getY();
            return pathfinding.path.slice(1).map(function(location) {
                // TODO: Consider making MovementMove take two tiles instead of deltas
                var nextX = location.getX();
                var nextY = location.getY();
                var dx = nextX - prevX;
                var dy = nextY - prevY;
                prevX = nextX;
                prevY = nextY;
                return new Moves.MovementMove(dungeon.getTile(prevX, prevY), dx, dy);
            });
        } else {
            return null;
        }
    },

    getMoveToward: function(dungeon, creature, target) {
        var path = module.getMoveSequenceToward(dungeon, creature, target);
        if(path) {
            return path[0];
        }
    }
};