14

I have been working on a JavaScript implementation of the early 90's adventure games and specifically plotting a path from the place the hero is standing to the location the player has clicked on. My approach is to first determine if a strait line (without obstructions) can be drawn, if not then to search for a path of clear way-points using Brian Grinstead's excellent javascript-astar. The problem I'm facing however is the path (while optimal will veer into spaces that would seem to the user an unintended. Here is a classic example of what I'm talking about (the green path is the generated path, the red dots are each turns where the direction of the path changes):

The green path is the hero, the red dots are the turns

Now I know that A* is only guaranteed to return a path that cannot be simpler (in terms of steps), but I'm struggling to implement a heuristic that weights turns. Here is a picture that show two other paths that would also qualify as just as simple (with an equal number of steps)

Alternate paths

The Blue path would present the same number of steps and turns while the red path has the same number of steps and fewer turns. In my code I have a simplifyPath() function that removes steps where the direction changes, so if I could get all possible paths from astar then I could select the one with the least turns, but that's not how A* fundamentally works, so I'm looking for a way to incorporate simplicity into the heuristic.

Here is my current code:

var img,
    field = document.getElementById('field'),
    EngineBuilder = function(field, size) {
        var context = field.getContext("2d"),
            graphSettings = { size: size, mid: Math.ceil(size/2)},
            engine = {
                getPosition: function(event) {
                    var bounds = field.getBoundingClientRect(),
                        x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
                        y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
                        node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];

                    return {
                        x: x,
                        y: y,
                        node: node
                    }
                },
                drawObstructions: function() {
                    context.clearRect (0, 0, 320, 200);
                    if(img) {
                        context.drawImage(img, 0, 0);
                    } else {
                        context.fillStyle = 'rgb(0, 0, 0)';
                        context.fillRect(200, 100, 50, 50);
                        context.fillRect(0, 100, 50, 50);
                        context.fillRect(100, 100, 50, 50);
                        context.fillRect(0, 50, 150, 50);
                    }
                },
                simplifyPath: function(start, complexPath, end) {
                    var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
                    for(i = 1; i < (complexPath.length - 1); i++) {
                        classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
                        
                        if(classification !== previousClassification) {
                            simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
                        } else {
                            simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
                        }
                        previous = complexPath[i];
                        previousClassification = classification;
                    }
                    simplePath.push(end);
                    return simplePath;
                },
                drawPath: function(start, end) {
                    var path, step, next;
                    if(this.isPathClear(start, end)) {
                       this.drawLine(start, end);
                    } else {
                        path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
                        if(path.length > 1) {
                            step = path[0];
                            for(next = 1; next < path.length; next++) {
                                this.drawLine(step, path[next]);
                                step = path[next];
                            }
                        }
                    }
                },
                drawLine: function(start, end) {
                    var x = start.x,
                        y = start.y,
                        dx = Math.abs(end.x - start.x),
                        sx = start.x<end.x ? 1 : -1,
                        dy = -1 * Math.abs(end.y - start.y),
                        sy = start.y<end.y ? 1 : -1,
                        err = dx+dy,
                        e2, pixel;

                    for(;;) {
                        pixel = context.getImageData(x, y, 1, 1).data[3];
                        if(pixel === 255) {
                            context.fillStyle = 'rgb(255, 0, 0)';
                        } else {
                            context.fillStyle = 'rgb(0, 255, 0)';
                        }
                        context.fillRect(x, y, 1, 1);
                        
                        if(x === end.x && y === end.y) {
                            break;
                        } else {
                            e2 = 2 * err;
                            if(e2 >= dy) {
                                err += dy;
                                x += sx;
                            }
                            if(e2 <= dx) {
                                err += dx;
                                y += sy;
                            }
                        }
                    }
                },
                isPathClear: function(start, end) {
                    var x = start.x,
                        y = start.y,
                        dx = Math.abs(end.x - start.x),
                        sx = start.x<end.x ? 1 : -1,
                        dy = -1 * Math.abs(end.y - start.y),
                        sy = start.y<end.y ? 1 : -1,
                        err = dx+dy,
                        e2, pixel;
                    
                    for(;;) {
                        pixel = context.getImageData(x, y, 1, 1).data[3];
                        if(pixel === 255) {
                            return false;
                        }
                        
                        if(x === end.x && y === end.y) {
                            return true;
                        } else {
                            e2 = 2 * err;
                            if(e2 >= dy) {
                                err += dy;
                                x += sx;
                            }
                            if(e2 <= dx) {
                                err += dx;
                                y += sy;
                            }
                        }
                    }
                }
            }, graph;
            engine.drawObstructions();
            graph = (function() {
                var x, y, rows = [], cols, js = '[';
                for(y = 0; y < 200; y += graphSettings.size) {
                    cols = [];
                    
                    for(x = 0; x < 320; x += graphSettings.size) {
                        cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
                    }
                    js += '['+cols+'],\n';
                    rows.push(cols);
                }
                js = js.substring(0, js.length - 2);
                js += ']';
                document.getElementById('Graph').value=js;
                return new Graph(rows, { diagonal: true });
            })();
            return engine;
        }, start, end, engine = EngineBuilder(field, 10);

field.addEventListener('click', function(event) {
    var position = engine.getPosition(event);
    if(!start) {
        start = position;
    } else {
        end = position;
    }
    if(start && end) {
        engine.drawObstructions();
        engine.drawPath(start, end);
        start = end;
    }
}, false);
#field {
border: thin black solid;
    width: 98%;
    background: #FFFFC7;
}
#Graph {
    width: 98%;
    height: 300px;
    overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
    
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>

After digging into Michail Michailidis' excellent answer I've added the following code to my simplifyPath() function) (demo):

simplifyPath: function(start, complexPath, end) {
    var previous = complexPath[1],
        simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}],
        i,
        finalPath = [simplePath[0]],
        classification,
        previousClassification;

    for(i = 1; i < (complexPath.length - 1); i++) {
        classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();

        if(classification !== previousClassification) {
            simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
        } else {
            simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
        }
        previous = complexPath[i];
        previousClassification = classification;
    }

    simplePath.push(end);
    previous = simplePath[0];
    for(i = 2; i < simplePath.length; i++) {
        if(!this.isPathClear(previous, simplePath[i])) {
            finalPath.push(simplePath[i-1]);
            previous = simplePath[i-1];
        }
    }
    finalPath.push(end);

    return finalPath;
}

Basically after it reduces redundant steps in the same direction, it tries to smooth out the path by looking ahead to see if it can eliminate any steps.

Community
  • 1
  • 1
Jason Sperske
  • 29,816
  • 8
  • 73
  • 124
  • 1
    From what I understood your simplifyPath checks just two edges and not more than those .. so this algorithm is not correct you have to check for two edges, three edges, ... till the length of the whole path for it to work. Probably there is a better algorithm than this one up there O(2^N) but certainly not with O(N) like yours which is using the basic heuristic of just checking contiguous edges. – Michail Michailidis Oct 31 '14 at 16:32

6 Answers6

15

Very very intresting problem! Thanks for this question! So...some observations first:

Not allowing diagonal movement fixes this problem but since you are interested in diagonal movement I had to search more.

I had a look at path simplifying algorithms like: Ramer Douglas Peuker (http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm) and an implementation: https://gist.github.com/rhyolight/2846020. I added an implementation to your code without success. This algorithm doesn't take into account obstacles so it was difficult to adapt it.

I wonder what would the behavior be (for diagonal movements) if you had used Dijkstra instead of A* or if you used an 'all shortest paths between a pair of nodes' algorithm and then you sorted them by increasing changes in direction.

After reading a bit about A* here http://buildnewgames.com/astar/ I thought that the implementation of A-star you are using is the problem or the heuristics. I tried all the heuristics on the a-star of your code including euclidean that I coded myself and tried also all the heuristics in the http://buildnewgames.com/astar code Unfortunately all of the diagonal allowing heuristics were having the same issue you are describing.

I started working with their code because it is a one-to-one grid and yours was giving me issues drawing. Your simplifyPath that I tried to remove was also causing additional problems. You have to keep in mind that since you are doing a remapping this could be an issue based on that

  • On a square grid that allows 4 directions of movement, use Manhattan distance (L1).
  • On a square grid that allows 8 directions of movement, use Diagonal distance (L∞).
  • On a square grid that allows any direction of movement, you might or might not want Euclidean distance (L2). If A* is finding paths on the grid but you are allowing movement not on the grid, you may want to consider other representations of the map. (from http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html)

What is my pseudocode algorithm:

var path = A-star();
for each node in path {
    check all following nodes till some lookahead limit
    if you find two nodes in the same row but not column or in the same column but not row { 
       var nodesToBeStraightened = push all nodes to be "straightened" 
       break the loop;
    }
    skip loop index to the next node after zig-zag
}

if nodesToBeStraightened length is at least 3 AND
   nodesToBeStraightened nodes don't form a line AND
   the resulting Straight line after simplification doesn't hit an obstruction

       var straightenedPath =  straighten by getting the first and last elements of nodesToBeStraightened  and using their coordinates accordingly

return straightenedPath;

Here is the visual explanation of what is being compared in the algorithm:

Visual Explanation: Visual Representation of the algorithm

How this code will be used with yours (I did most of the changes - I tried my best but there are so many problems like with how you do drawing and because of the rounding of the grid etc - you have to use a grid and keep the scale of the paths accurate - please see also assumptions below):

    var img,
    field = document.getElementById('field'),
    EngineBuilder = function(field, size) {
        var context = field.getContext("2d"),
        graphSettings = { size: size, mid: Math.ceil(size/2)},
        engine = {
            //[...] missing code
            removeZigZag: function(currentPath,lookahead){
                //for each of the squares on the path - see lookahead more squares and check if it is in the path
                for (var i=0; i<currentPath.length; i++){

                    var toBeStraightened = [];
                    for (var j=i; j<lookahead+i+1 && j<currentPath.length; j++){         
                        var startIndexToStraighten = i;
                        var endIndexToStraighten = i+1;

                        //check if the one from lookahead has the same x xor the same y with one later node in the path
                        //and they are not on the same line
                        if( 
                           (currentPath[i].x == currentPath[j].x && currentPath[i].y != currentPath[j].y) ||
                           (currentPath[i].x == currentPath[j].y && currentPath[i].x != currentPath[j].x)   
                        ) { 
                            endIndexToStraighten = j;

                            //now that we found something between i and j push it to be straightened
                            for (var k = startIndexToStraighten; k<=endIndexToStraighten; k++){
                                toBeStraightened.push(currentPath[k]);
                            }
                            //skip the loop forward
                            i = endIndexToStraighten-1;
                            break;
                        }
                    }

                    if (toBeStraightened.length>=3                                   
                        && !this.formsALine(toBeStraightened) 
                        && !this.lineWillGoThroughObstructions(currentPath[startIndexToStraighten], currentPath[endIndexToStraighten],this.graph?????)
                        ){
                        //straightening:
                        this.straightenLine(currentPath, startIndexToStraighten, endIndexToStraighten);
                    }
                }
                return currentPath;
            },

            straightenLine: function(currentPath,fromIndex,toIndex){
                for (var l=fromIndex; l<=toIndex; l++){

                    if (currentPath[fromIndex].x == currentPath[toIndex].x){
                         currentPath[l].x = currentPath[fromIndex].x;
                    }
                    else if (currentPath[fromIndex].y == currentPath[toIndex].y){
                         currentPath[l].y = currentPath[fromIndex].y;       
                    }
                }
            },

            lineWillGoThroughObstructions: function(point1, point2, graph){
                var minX = Math.min(point1.x,point2.x);
                var maxX = Math.max(point1.x,point2.x);
                var minY = Math.min(point1.y,point2.y);
                var maxY = Math.max(point1.y,point2.y);

                //same row
                if (point1.y == point2.y){
                    for (var i=minX; i<=maxX && i<graph.length; i++){
                        if (graph[i][point1.y] == 1){ //obstacle
                            return true;
                        } 
                    }
                }
                //same column
                if (point1.x == point2.x){
                    for (var i=minY; i<=maxY && i<graph[0].length; i++){
                        if (graph[point1.x][i] == 1){ //obstacle
                            return true;
                        } 
                    }
                }
                return false;
            },

            formsALine: function(pointsArray){
                //only horizontal or vertical
                if (!pointsArray || (pointsArray && pointsArray.length<1)){
                    return false;
                }

                var firstY = pointsArray[0].y;
                var lastY = pointsArray[pointsArray.length-1].y;

                var firstX = pointsArray[0].x;
                var lastX = pointsArray[pointsArray.length-1].x;

                //vertical line
                if (firstY == lastY){
                    for (var i=0; i<pointsArray.length; i++){
                        if (pointsArray[i].y!=firstY){
                            return false;
                        }
                    }
                    return true;
                }

                //horizontal line
                else if (firstX == lastX){
                    for (var i=0; i<pointsArray.length; i++){
                        if (pointsArray[i].x!=firstX){
                            return false;
                        }
                    }
                    return true;
                }       
                return false;
            }
            //[...] missing code
        }
        //[...] missing code
    }

Assumptions and Incompatibilities of the above code:

  • obstacle is 1 and not 0
  • the orginal code I have in the demo is using array instead of {x: number, y:number}
  • in case you use the other a-star implementation, the point1 location is on the column 1 and row 2.
  • converted to your {x: number, y:number} but haven't checked all the parts:
  • I couldn't access the graph object to get the obstacles - see ?????
  • You have to call my removeZigZag with a lookahead e.g 7 (steps away) in the place where you were doing your path simplification
  • admittedly their code is not that good compared to the a-star implementation from Stanford but for our purposes it should be irelevant
  • possibly the code has bugs that I don't know of and could be improved. Also I did my checks only in this specific world configuration
  • I believe the code has complexity O(N x lookahead)~O(N) where N is the number of steps in the input A* shortest path.

Here is the code in my github repository (you can run the demo) https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag

Their clickHandling and world boundary code is broken as when you click on the right side of the map the path calculation is not working sometimes. I didn't have time to find their bug. As a result my code has the same issue Probably it is because the map I put from your question is not square - but anyway my algorithm should be unaffected. You will see this weird behavior is happening if non of my remove ZigZag code runs. (Edit: It was actually because the map was not square - I updated the map to be square for now)

Feel free to play around by uncommenting this line to see the before-after:

result = removeZigZag(result,7);

I have attached 3 before after image sets so the results can be visualized: (Keep in mind to match start and goal if you try them - direction DOES matter ;) )

Case 1: Before Case 1: Before Case 1: After Case 1: After Case 2: Before Case 2 Before Case 2: After Case 2: After Case 3: Before Case 3: Before Case 3: After Case 3: After Case 4: Before Case 4: Before Case 4: After Case 4: After Resources:

Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
  • Thanks :) What I suggest is use your one grid on top of that but your paths should be precise to the pixel level for this to work. I don't see why this is not a general approach for any grid that has these properties and doesn't distort the original grid ;). If you have to do it in a distorted grid we will be talking about problems found in Computational Geometry. Something that could involve academic paper reading etc. I believe your application is quite smaller to need something that is not a perfect grid :) – Michail Michailidis Nov 02 '14 at 20:21
  • 1
    Here is the code using a scene from KQ3 :) http://jsfiddle.net/sperske/q7er3sy7/ (and you can load in your own images (as Data URIs) – Jason Sperske Nov 03 '14 at 22:54
2

You can use a modified A* algorithm to account for changes in direction. While simplifying the result of the standard A* algorithm may yield good results, it may not be optimal. This modified A* algorithm will return a path of minimal length with the least number of turns.

In the modified A* algorithm, each position corresponds to eight different nodes, each with their own heading. For example, the position (1, 1) corresponds to the eight nodes

(1,1)-up, (1,1)-down, (1,1)-right, (1,1)-left,

(1,1)-up-left, (1,1)-up-right, (1,1)-down-left, and (1,1)-down-right

The heuristic distance from a node to the goal is the the heuristic distance from the corresponding point to the goal. In this case, you probably want to use the following function:

H(point) = max(abs(goal.xcor-point.xcor), abs(goal.ycor-point.ycor))

The nodes that correspond to a particular position are connected to the nodes of the neighboring positions with the proper heading. For example, the nodes corresponding to the position (1,1) are all connected to the following eight nodes

(1,2)-up, (1,0)-down, (2,1)-right, (0,1)-left,

(0,2)-up-left, (2,2)-up-right, (0,0)-down-left, and (2,0)-down-right

The distance between any two connected nodes depends on their heading. If they have the same head, then the distance is 1, otherwise, we have made a turn, so the distance is 1+epsilon. epsilon represents an arbitrarily small value/number.

We know need to have a special case for the both the start and goal. The start and goal are both represented as a single node. At the start, we have no heading, so the distance between the start node and any connected node is 1.

We can now run the standard A* algorithm on the modified graph. We can map the returned path to a path in the original grid, by ignoring the headings. The total length of the returned path will be of the form n+m*epsilon. n is the total length of the corresponding path in the original grid, and m is the number of turns. Because A* returns a path of minimal length, the path in the original grid is a path of minimal length that makes the least turns.

Joshua
  • 2,431
  • 15
  • 23
  • Now having a more thorough look in your algorithm. How will you weight your distance and change direction heuristics for each of the steps of the algorithm?? Usually G (cost) is very short-sighted and it is constant based on the type of movement (e.g diagonal vs non-diagonal movement). But your algorithm needs to take into account the previous step to figure it out.. is it for sure possible to adapt A* this way - if that is the case why the original algorithm doesn't do this already? – Michail Michailidis Nov 03 '14 at 14:58
  • Also you have to prove that your heuristic is admissible (http://en.wikipedia.org/wiki/A*_search_algorithm#Admissibility_and_optimality) and is not overestimating the cost to the goal because that could lead you to suboptimal paths since the algorithm will start considering less candidate nodes from what I understand – Michail Michailidis Nov 03 '14 at 15:12
  • 1
    @MichailMichailidis The heading at any node is explicitly accounted for by splitting each position into eight position-heading pairs. My heuristic is admissible because it is the minimal distance between any two points in the grid, where the distance to any adjacent node is of length 1. Every path in the original grid corresponds to exactly one path in the modified graph, where the path in the modified graph is at least as long as the path in the original grid, and only equal if it has no turns. I will try to code a demo as soon as I have time, it might make things a bit clearer. – Joshua Nov 03 '14 at 19:22
  • @MichailMichailidis In regards to optimality, I am claiming that your algorithm does not always return a minimal path with the least number of turns. For example, in case 3 of your post, you can find a minimal path with only two turn. – Joshua Nov 03 '14 at 19:25
  • Well I am just getting basic A* that actually hugs the obstacles. Mine just simplifies/straightens the path. It doesn't make it have optional number of turns if A*'s one was not optimal turn-wise either. I am curious about the implementation :) when you have time. Also I wonder what is the complexity for the modified grid you are talking about. I assume that A* will run only once right? – Michail Michailidis Nov 03 '14 at 19:31
1

I have come up with somewhat of a fix that is a simple addition to your original code, but it doesn't work in all situations (see image below) because we are limited to what the A* returns us. You can see my jsfiddle here

I added the following code to your simplifyPath function right before the return. What it does is strips out extra steps by seeing if there is a clear path between non-adjacent steps (looking at larger gaps first). It could be optimized, but you should get the gist from what I've got.

do{
    shortened = false;
    loop:
    for(i = 0; i < simplePath.length; i++) {
        for(j = (simplePath.length - 1); j > (i + 1); j--) {
            if(this.isPathClear(simplePath[i],simplePath[j])) {
                simplePath.splice((i + 1),(j - i - 1));
                shortened = true;
                break loop;
            }
        }
    }
} while(shortened == true);

You can see below that this removes the path that goes in on the left (as in the question) but also that not all the odd turns are removed. This solution only uses the points provided from the A*, not points in between on the path - for example, because the 2nd point does not have a straight unobstructed line to the 4th or 5th points, it cannot optimize point 3 out. It happens a lot less than the original code, but it still does give weird results sometimes. incorrect path

Jo.
  • 778
  • 1
  • 12
  • 17
1

In edition to nodes having references to their parent nodes. Also store which direction that node came from inside a variable. In my case there was only two possibilities horizontally or vertically. So I created two public static constants for each possibility. And a helper function named "toDirection" which takes two nodes and return which direction should be taken in order to go from one to another:

public class Node {
    final static int HORIZONTALLY = 0;
    final static int VERTICALLY = 1;

    int col, row;
    boolean isTravelable;
    int fromDirection;
    double hCost;
    double gCost;
    double fCost;

    Node parent;

    public Node(int col, int row, boolean isTravelable) {
        this.col = col;
        this.row = row;
        this.isTravelable = isTravelable;
    }

    public static int toDirection(Node from, Node to) {
        return (from.col != to.col) ? Node.HORIZONTALLY : Node.VERTICALLY;
    }
}

Then you can change your weight calculation function to take turns into account. You can now give a small punishment for turns like:

public double calcGCost(Node current, Node neighbor) {
    if(current.fromDirection == Node.toDirection(current, neighbor)) {
        return 1;
    } else{
        return 1.2;
    }
}

Full code: https://github.com/tezsezen/AStarAlgorithm

user11629899
  • 19
  • 1
  • 5
0

At the risk of potential down voting, I will try my best to suggest an answer. If you weren't using a third party plugin I would suggest a simple pop/push stack object be built however since you are using someone else's plugin it might be best to try and work along side it rather than against it.

That being said I might just do something simple like track my output results and try to logically determine the correct answer. I would make a simple entity type object literal for storage within an array of all possible path's? So the entire object's life span is only to hold position information. Then you could later parse that array of objects looking for the smallest turn count.

Also, since this third party plugin will do most of the work behind the scenes and doesn't seem very accessible to extract, you might need to feed it criteria on your own. For example if its adding more turns then you want, i.e. inside the door looking square, then maybe sending it the coordinates of the start and end arent enouugh. Perhaps its better to stop at each turn and send in the new coordinates to see if a straight line is now possible. If you did this then each turn would have a change to look and see if there is an obstruction stopping a straight line movement.

I feel like this answer is too simple so it must be incorrect but I will try nonetheless...

//Entity Type Object Literal
var pathsFound = function() {

    //Path Stats
    straightLine: false,
    turnCount: 0,
    xPos: -1, //Probably should not be instantiated -1 but for now it's fine
    yPos: -1,

    //Getters
    isStraightLine: function() { return this.straightLine; },
    getTurnCount: function() { return this.turnCount; },
    getXPos: function() { return this.xPos; },
    getYPos: function() { return this.yPos; },

    //Setters
    setStraightLine: function() { this.straightLine = true; },
    setCrookedLine: function() { this.straightLine = false; },
    setXPos: function(val) { this.xPos = val; },
    setYPos: function(val) { this.yPos = val; },

    //Class Functionality
    incrementTurnCounter: function() { this.turnCount++; },
    updateFullPosition: function(xVal, yVal) { 
        this.xPos = xVal;
        this.yPos = yVal. 
    },
}

This way you could report all the data every step of the way and before you draw to the screen you could iterate through your array of these object literals and find the correct path by the lowest turnCount.

GoreDefex
  • 1,461
  • 2
  • 17
  • 41
  • Your answer is just a data model - it doesn't have an algorithm/heuristic. On top of that I don't think that A* can return all shortest paths... He will have to probably use an adaptation of Dijkstra's or Johnson's algorithm to return all shortest paths between source and destination and then run something like yours. – Michail Michailidis Oct 31 '14 at 16:42
-3

var img,
    field = document.getElementById('field'),
    EngineBuilder = function(field, size) {
        var context = field.getContext("2d"),
            graphSettings = { size: size, mid: Math.ceil(size/2)},
            engine = {
                getPosition: function(event) {
                    var bounds = field.getBoundingClientRect(),
                        x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
                        y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
                        node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];

                    return {
                        x: x,
                        y: y,
                        node: node
                    }
                },
                drawObstructions: function() {
                    context.clearRect (0, 0, 320, 200);
                    if(img) {
                        context.drawImage(img, 0, 0);
                    } else {
                        context.fillStyle = 'rgb(0, 0, 0)';
                        context.fillRect(200, 100, 50, 50);
                        context.fillRect(0, 100, 50, 50);
                        context.fillRect(100, 100, 50, 50);
                        context.fillRect(0, 50, 150, 50);
                    }
                },
                simplifyPath: function(start, complexPath, end) {
                    var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
                    for(i = 1; i < (complexPath.length - 1); i++) {
                        classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
                        
                        if(classification !== previousClassification) {
                            simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
                        } else {
                            simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
                        }
                        previous = complexPath[i];
                        previousClassification = classification;
                    }
                    simplePath.push(end);
                    return simplePath;
                },
                drawPath: function(start, end) {
                    var path, step, next;
                    if(this.isPathClear(start, end)) {
                       this.drawLine(start, end);
                    } else {
                        path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
                        if(path.length > 1) {
                            step = path[0];
                            for(next = 1; next < path.length; next++) {
                                this.drawLine(step, path[next]);
                                step = path[next];
                            }
                        }
                    }
                },
                drawLine: function(start, end) {
                    var x = start.x,
                        y = start.y,
                        dx = Math.abs(end.x - start.x),
                        sx = start.x<end.x ? 1 : -1,
                        dy = -1 * Math.abs(end.y - start.y),
                        sy = start.y<end.y ? 1 : -1,
                        err = dx+dy,
                        e2, pixel;

                    for(;;) {
                        pixel = context.getImageData(x, y, 1, 1).data[3];
                        if(pixel === 255) {
                            context.fillStyle = 'rgb(255, 0, 0)';
                        } else {
                            context.fillStyle = 'rgb(0, 255, 0)';
                        }
                        context.fillRect(x, y, 1, 1);
                        
                        if(x === end.x && y === end.y) {
                            break;
                        } else {
                            e2 = 2 * err;
                            if(e2 >= dy) {
                                err += dy;
                                x += sx;
                            }
                            if(e2 <= dx) {
                                err += dx;
                                y += sy;
                            }
                        }
                    }
                },
                isPathClear: function(start, end) {
                    var x = start.x,
                        y = start.y,
                        dx = Math.abs(end.x - start.x),
                        sx = start.x<end.x ? 1 : -1,
                        dy = -1 * Math.abs(end.y - start.y),
                        sy = start.y<end.y ? 1 : -1,
                        err = dx+dy,
                        e2, pixel;
                    
                    for(;;) {
                        pixel = context.getImageData(x, y, 1, 1).data[3];
                        if(pixel === 255) {
                            return false;
                        }
                        
                        if(x === end.x && y === end.y) {
                            return true;
                        } else {
                            e2 = 2 * err;
                            if(e2 >= dy) {
                                err += dy;
                                x += sx;
                            }
                            if(e2 <= dx) {
                                err += dx;
                                y += sy;
                            }
                        }
                    }
                }
            }, graph;
            engine.drawObstructions();
            graph = (function() {
                var x, y, rows = [], cols, js = '[';
                for(y = 0; y < 200; y += graphSettings.size) {
                    cols = [];
                    
                    for(x = 0; x < 320; x += graphSettings.size) {
                        cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
                    }
                    js += '['+cols+'],\n';
                    rows.push(cols);
                }
                js = js.substring(0, js.length - 2);
                js += ']';
                document.getElementById('Graph').value=js;
                return new Graph(rows, { diagonal: true });
            })();
            return engine;
        }, start, end, engine = EngineBuilder(field, 10);

field.addEventListener('click', function(event) {
    var position = engine.getPosition(event);
    if(!start) {
        start = position;
    } else {
        end = position;
    }
    if(start && end) {
        engine.drawObstructions();
        engine.drawPath(start, end);
        start = end;
    }
}, false);
#field {
border: thin black solid;
    width: 98%;
    background: #FFFFC7;
}
#Graph {
    width: 98%;
    height: 300px;
    overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
    
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>
lulunac27
  • 1
  • 1