2

I'm trying to implement A* algorithm in react.js but I'm quite stuck when it comes to implement the fScore function. I know that f=g+h where g is the gScore from the start node till the current node and h is the heuristic distance from the currentNode till the end node. I calculated the heuristic using the euclidean distance where I'm sending the coordinates of the current and End nodes but I don't know how to calculate the gScore. Each node in my graph has: id, name, x, y, connectedToIds:[] //list of neihbours or connectedNodes. Update: I added the variables parentId, fscore, gscore, hscore to each node. So now each node has the variables : id, name, x, y, connectedToIds:[], fscore: 0, gscore: 0, hscore: 0, parentId: null. Update2: originLocationId is the id of the start node. destinationLocationId is the id of the end node. locations is a list of all nodes. my code:

export default class TurnByTurnComponent extends React.PureComponent {
    constructor(props) {
        super(props);
    }

    render() {
        const {
            destinationLocationId,
            locations,
            originLocationId
        } = this.props;
        console.log(locations)
        console.log(originLocationId)
        console.log(destinationLocationId)


        var openedList = [];
        var closedList = [];

        if (destinationLocationId != null && originLocationId != null) {
            openedList.push(originLocationId);
            while (openedList.length != 0) {
                var currentLoc = openedList[0]; //minFvalue
                const currIndex = openedList.indexOf(currentLoc);
                openedList.splice(currIndex, 1); //deleting currentNode from openedList
                closedList.push(currentLoc) //adding currentNode to closedList

                if (currentLoc == destinationLocationId) {
                    //return path
                }

                

            }

        }

        function heuristic(currentNode, endNode) { //euclidean distance
            var x = Math.pow(endNode.x - currentNode.x, 2);
            var y = Math.pow(endNode.y - currentNode.y, 2);
            var dist = Math.sqrt(x + y);
            return dist;
        }

        function gScore(startNode, currentNode) {

        }




        return (
            <div className="turn-by-turn-component">
                {locations.map(loc => (
                    <li key={loc.id}>
                        {loc.name}
                    </li>
                ))}

                <TodoList
                    title="Mandatory work"
                    list={[
                      
                    ]}
                />
                <TodoList
                    title="Optional work"
                    list={[
                      
                    ]}
                />
            </div>
        );
    }
}

TurnByTurnComponent.propTypes = {
    destinationLocationId: PropTypes.number,
    locations: PropTypes.arrayOf(PropTypes.shape({
        id: PropTypes.number.isRequired,
        name: PropTypes.string.isRequired,
        x: PropTypes.number.isRequired,
        y: PropTypes.number.isRequired,
        connectedToIds: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired
    })),
    originLocationId: PropTypes.number
};

Update3: New version of my code

export default class TurnByTurnComponent extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = { shortestPath: [] }
    }


    render() {
        const {
            destinationLocationId,
            locations,
            originLocationId
        } = this.props;


        if (destinationLocationId != null && originLocationId != null) {

            if (originLocationId == destinationLocationId) { //check if the startNode node is the end node
                return originLocationId;
            }

            var openList = [];
            let startNode = getNodeById(originLocationId);
            let endNode = getNodeById(destinationLocationId)

            startNode.gcost = 0
            startNode.heuristic = manhattanDistance(startNode, endNode)
            startNode.fcost = startNode.gcost + startNode.heuristic;


            //start A*
            openList.push(startNode); //starting with the startNode 
            while (openList.length) {
                console.log("inside while")

                var currentNode = getNodeOfMinFscore(openList);

                if (currentIsEqualDistanation(currentNode)) {
                    var path = getPath(currentNode)
                    this.setState({
                        shortestPath: path,
                    });
                    return path //todo
                }
                deleteCurrentFromOpenList(currentNode, openList);

                for (let neighbourId of currentNode.connectedToIds) {

                    var neighbourNode = getNodeById(neighbourId);
                    var currentNodeGcost = currentNode.gcost + manhattanDistance(currentNode,         neighbourNode);
                    console.log(currentNodeGcost)
                    console.log(neighbourNode.gcost)
                    if (currentNodeGcost < neighbourNode.gcost) {
                        console.log("Helloooo")
                        neighbourNode.parentId = currentNode.id;
                        // keep track of the path
                        // total cost saved in neighbour.g
                        neighbourNode.gcost = currentNodeGcost;
                        neighbourNode.heuristic = manhattanDistance(neighbourNode, endNode);
                        neighbourNode.fcost = neighbourNode.gcost + neighbourNode.heuristic;
                        if (!openList.includes(neighbourId)) {
                            openList.push(neighbourNode);
                        }
                    }
                }
            }
            return null;
        }


        function deleteCurrentFromOpenList(currentNode, openList) {
            const currIndex = openList.indexOf(currentNode);
            openList.splice(currIndex, 1); //deleting currentNode from openList
        }

        function currentIsEqualDistanation(currentNode) {
            //check if we reached out the distanation node
            return (currentNode.id == destinationLocationId)
        }

        function getNodeById(id) {
            var node;
            for (let i = 0; i < locations.length; i++) {
                if (locations[i].id == id) {
                    node = locations[i]
                }
            }
            return node
        }

        function getPath(endNode) {
            var path = []
            while (endNode.parentId) {
                path.push(endNode.name)
                endNode = endNode.parentId;
            }
            return path;
        }

        function getNodeOfMinFscore(openList) {
            var minFscore = openList[0].fcost; //initValue
            var nodeOfminFscore;
            for (let i = 0; i < openList.length; i++) {

                if (openList[i].fcost <= minFscore) {

                    minFscore = openList[i].fcost //minFvalue
                    nodeOfminFscore = openList[i]
                }
            }

            return nodeOfminFscore
        }

        //manhattan distance is for heuristic and gScore. Here I use Manhattan instead of Euclidean 
        //because in this example we dont have diagnosal path.
        function manhattanDistance(startNode, endNode) {
            var x = Math.abs(endNode.x - startNode.x);
            var y = Math.abs(endNode.y - startNode.y);
            var dist = x + y;
            return dist;
        }


        return (
            <div className="turn-by-turn-component">
                {locations.map(loc => (
                    <li key={loc.id}>
                        {JSON.stringify(loc.name)},
                    </li>
                ))}
                <TodoList
                    title="Mandatory work"
                    list={
                        this.state.shortestPath
                    }
                />
                <TodoList
                    title="Optional work"
                    list={[

                    ]}
                />
            </div>
        );
    }
}

TurnByTurnComponent.propTypes = {
    destinationLocationId: PropTypes.number,
    locations: PropTypes.arrayOf(PropTypes.shape({
        id: PropTypes.number.isRequired,
        name: PropTypes.string.isRequired,
        x: PropTypes.number.isRequired,
        y: PropTypes.number.isRequired,
        connectedToIds: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired
    })),
    originLocationId: PropTypes.number
};
AdamA
  • 25
  • 6

1 Answers1

2

While h is the heuristic, a plausible guess of the possible cost it will be needed to reach the end node, g is the actual cost spent to reach the current node. In your case it could be even the same euclidian distance you use for h.

In a real case scenario, using euclidian distance, a graph connecting different cities, h is the air-distance of two cities while g is their road-distance.

Furthermore, if you are using a monotonic heuristic (or an underestimating heuristic, such as the euclidian distance) you will not need the close list, since it is provent that the first found path will also be the shortest: longer or already visited paths will be dropped before being explored.

Probably what is confusing is the fact that you need to keep track of g during the exploration of the graph, while h just measure a straight line between current and end nodes, g measure all the lines between the nodes you explored to reach the current.

// h
function heuristic(n1, n2) {
    return Math.sqrt(
        Math.pow(n1.x - n2.x, 2) +
        Math.pow(n1.y - n2.y, 2)
    );
}
// g - actually is the same as h
const cost = heuristic;
function astar(start, end, graph, h, g) {
    if (start.id == end.id) { return [ start.id ] }

    // omitted CLEAN-UP of the graph

    // close is not needed with an optimistic heuristic 
    // so I've commented the "close" parts.
    // An optimistic heuristic works also if you decomment them

    // var close = [];
    var open  = [];

    start.g = 0;
    start.h = h(start, end);
    start.f = start.g + start.h;

    open.push(start);

    while (open.length) {
        // since open is sorted, by popping
        // the last element we take the cheapest node
        var curr = open.pop();

        if (curr == end) { return resolvePath(curr, graph); }
        // close.push(curr);
        for (let nid of curr.connectedToIds) {
            // if (close.some(n => n.id == nid)) { continue; }
            var neighbour = graph.find(n => n.id == nid);
            
            // HERE you compute and store
            // the current g of the node 
            var current_g = curr.g + g(curr, neighbour);

            var isBetter_g = false;
            var isInOpen = open.some(n => n.id == nid);

            // Some implementations skip this check 
            // because they assume that the cost function
            // is a non-negative distance.
            // if so you could check just the two nodes g
            // and insert the node if not already in the open set
            // because the default g of a node is 0
            if (!isInOpen) {
                // unexplored node, maybe we should explore it
                // later, so we add to the open set and
                // we update it with the current path cost
                open.push(neighbour)
                isBetter_g = true;
            }
            else if (current_g < neighbour.g) {
                // the current path is better than
                // those already explored, we need to 
                // update the neighbour with the current path cost
                isBetter_g = true;
            }
            if (isBetter_g) {
                // === path cost update ===

                // track current path
                neighbour.parent = curr.id;
           
                // HERE you keep track of the path
                // total cost saved in neighbour.g
                neighbour.g = current_g;
           
                // neighbour.h is redundant, can be stored directly in f
                // but keep it for debugging purpose
                neighbour.h = h(neighbour, end);

                neighbour.f = neighbour.g + neighbour.h;
                
                if (!isInOpen) {
                    // sorting makes the last element of
                    // open the cheapest node. We sort after
                    // the path cost update to ensure this property
                    open.sort((n1, n2) => n2.f - n1.f)
                }
            }
        }
    }

    // failure
    return []; // or return null
}
// utility to retrieve an array of ids
// from the tracked path
function resolvePath(end, graph) {
    let path = [ end.id ];
    while (end && end.parent >= 0) {
        path.unshift(end.parent);
        end = graph.find(n => n.id == end.parent);
    }
    return path;
}
// example of using the function
astar(startNode, endNode, locations, heuristic, cost);
DDomen
  • 1,808
  • 1
  • 7
  • 17
  • Thank you. Would you please explain in what way you're using the isBest_g variable? I mean why do we need it? Cant we just check the minimum Fcost? – AdamA Nov 05 '21 at 13:35
  • 1
    If your movement does not allow for diagonal travel, your heuristic should use the manhattan distance between the nodes, which can be calculated without sqrt or pow. H = Math.abs(ax - bx) + Math.abs(ay - by) – Phaelax z Nov 05 '21 at 14:03
  • @AdamA `isBest_g`, maybe not the best variable name (`isBetter_g` probably would be "better"), keeps track if the neighbour should be updated or ignored. Yes we should check `f` cost, I've mistaken while porting the code, because the open set must be sorted by the `f`, such that each time you do `open.pop` you are actually getting the cheaper node. Note that if you find an unexplored node you must add it to the open list (without checking `f`), that's the objective of `isBest_g`. I'll edit the answer to clarify – DDomen Nov 05 '21 at 14:35
  • @Phaelaxz true for Manhattan, but if you use euclidean as cost, Manhattan would lose the monotonic property. In case need extra care for mix up them. – DDomen Nov 05 '21 at 14:38
  • @AdamA I've also assumed that you want the iterative algorithm, probably would be more easy to start with a recursive algorithm to simplify the problem and understand how the graph is explored – DDomen Nov 05 '21 at 15:05
  • @Phaelaxz Yes true. – AdamA Nov 05 '21 at 16:56
  • @DDomen But how does isBetter_g referes to a specific node? In your example you're updating a global variable and not for a specific node? I guess you use this variable to traverse back when you reach the end node to check for each node if it has bestGscore=true. Or am I totally lost? – AdamA Nov 05 '21 at 17:00
  • And I am now in endless loop:D – AdamA Nov 05 '21 at 17:13
  • @AdamA `current_g < neighbour.g` this refers to a specific node – DDomen Nov 05 '21 at 17:30
  • 1
    @AdamA I would encourage reading through the following link, as I know many folks who have written A* implementations based on these articles. https://www.redblobgames.com/ – Phaelax z Nov 05 '21 at 20:26
  • @DDomen this condition is false all time current_g < neighbour.g because neighbour.g =0 in the very begining and currect_g is updated. So im stuck with neighbour.g = 0 always and never goes inside this if statement. – AdamA Nov 05 '21 at 20:27
  • @DDomen i've added a new version of my code, I hope you check it out. – AdamA Nov 05 '21 at 20:40
  • 1
    @AdamA i think you missed/misunderstood the `g(curr, neighbour)` part if the `current_g` never been different than 0. `current_g = curr.g + g(curr, neighbour) -=-> 0 + cost{current_node -> child_node}`. `g(curr, neighbour)` is the same as `manhattanDistance(currentNode, neighbourNode)` in your code. By the way your updated code seems fine to me, at least at a logical level, just missing the non-negative check (but it is unneded for Manhattan). – DDomen Nov 05 '21 at 22:38
  • @DDomen would you please help me with the new small issue? check the update (new issue) in this link if you dont mind https://stackoverflow.com/questions/69865950/aa-star-algorithm-gives-wrong-path-and-crashes – AdamA Nov 07 '21 at 15:45
  • @Phaelaxz would you please check the new issue(update) in this link too? https://stackoverflow.com/questions/69865950/aa-star-algorithm-gives-wrong-path-and-crashes – AdamA Nov 07 '21 at 15:48