{"id":19034143,"url":"https://github.com/aurasphere/reply-challenge-2018","last_synced_at":"2026-06-13T20:03:41.760Z","repository":{"id":134256031,"uuid":"125250454","full_name":"aurasphere/reply-challenge-2018","owner":"aurasphere","description":"My solution to the Reply challenge training problem.","archived":false,"fork":false,"pushed_at":"2020-12-23T14:33:10.000Z","size":4042,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-30T06:02:13.140Z","etag":null,"topics":["astar-algorithm","challenge","dijkstra-algorithm","java","pathfinding","shortest"],"latest_commit_sha":null,"homepage":"https://challenges.reply.com/tamtamy/challenge/3/detail","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aurasphere.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-14T17:36:34.000Z","updated_at":"2022-03-07T17:41:33.000Z","dependencies_parsed_at":null,"dependency_job_id":"84673d31-b7f8-4a0b-9026-e5c22fe49f9f","html_url":"https://github.com/aurasphere/reply-challenge-2018","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/aurasphere/reply-challenge-2018","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurasphere%2Freply-challenge-2018","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurasphere%2Freply-challenge-2018/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurasphere%2Freply-challenge-2018/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurasphere%2Freply-challenge-2018/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aurasphere","download_url":"https://codeload.github.com/aurasphere/reply-challenge-2018/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurasphere%2Freply-challenge-2018/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34298266,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["astar-algorithm","challenge","dijkstra-algorithm","java","pathfinding","shortest"],"created_at":"2024-11-08T21:43:38.240Z","updated_at":"2026-06-13T20:03:41.732Z","avatar_url":"https://github.com/aurasphere.png","language":"Java","funding_links":["https://www.paypal.com/donate/?cmd=_donations\u0026business=8UK2BZP2K8NSS"],"categories":[],"sub_categories":[],"readme":"[![Donate](https://img.shields.io/badge/Donate-PayPal-orange.svg)](https://www.paypal.com/donate/?cmd=_donations\u0026business=8UK2BZP2K8NSS)\n\n# Reply Code Challenge 2018 Training\n\nThis repository contains both the problem statement and my solution to the Reply Code Challenge training problem. The solution is written in Java.\n\n\u003cs\u003e **Note:** since the method which prints the final solution uses recursion to print the visited nodes by traversing back the last one, you may get a StackOverflowError. To avoid that, you should increase your stack size using the \u003ccode\u003e-Xss\u003c/code\u003e argument when starting the JVM. I've run this code with \u003ccode\u003e-Xss515m\u003c/code\u003e and it worked fine.\u003c/s\u003e\n\n**Note:** this program is memory intensive, so start it up with a large amount of heap using the \u003ccode\u003e-Xmx\u003c/code\u003e argument. I've run this code with \u003ccode\u003e-Xmx10g\u003c/code\u003e and it worked fine.\n\n## Log\n\nThe problem statement required to find the shortest path from point A to point B, so I've started looking into pathfinding algorithms, in particular [Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) since it was the only one I knew at the time.\n\n### Dijkstra\n\nIt usually requires a graph to run Dijkstra but I couldn't afford to build the whole grid because of its huge size, hence, I've used a discovery method that, at each iteration, would lazily compute and add the valid neighbour nodes of the current one to the \"to visit\" list:\n\n    public List\u003cNode\u003e getAdjacentNodes() {\n        if (adjacentNodes == null) {\n            adjacentNodes = new ArrayList\u003cNode\u003e();\n    \n            // Lazy init of new nodes to avoid out of memory errors. For\n            // reference, here's a visual representation of the neighbour nodes\n            // of a node X:\n            // 1 2 3\n            // 4 X 6\n            // 7 8 9\n    \n            // 1\n            if (isValid(x - 1, y + 1)) {\n                new Node(x - 1, y + 1, this);\n            }\n            // 2\n            if (isValid(x, y + 1)) {\n                new Node(x, y + 1, this);\n            }\n            // 3\n            if (isValid(x + 1, y + 1)) {\n                new Node(x + 1, y + 1, this);\n            }\n            // 4\n            if (isValid(x - 1, y)) {\n                new Node(x - 1, y, this);\n            }\n            // 6\n            if (isValid(x + 1, y)) {\n                new Node(x + 1, y, this);\n            }\n            // 7\n            if (isValid(x - 1, y - 1)) {\n                new Node(x - 1, y - 1, this);\n            }\n            // 8\n            if (isValid(x, y - 1)) {\n                new Node(x, y - 1, this);\n            }\n            // 9\n            if (isValid(x + 1, y - 1)) {\n                new Node(x + 1, y - 1, this);\n            }\n        }\n        return adjacentNodes;\n    }\n\nHere's a visual representation of Dijkstra's method:\n\n\u003cp align=\"center\"\u003e\u003cimg alt=\"Dijkstra 1\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/dijkstra_1.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;-\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;\u003cimg alt=\"Dijkstra 2\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/dijkstra_2.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u003c/p\u003e\n\n### Nodes Validations\n\nTo be valid, a node needed not to be within an obstacle, a 3-tuple of points which defines a triangle, and inside the grid boundary of 10\u003csup\u003e-6\u003c/sup\u003e and 10\u003csup\u003e6\u003c/sup\u003e on both axis. Certainly, the latter validation was trivial to implement but the former was not that difficult either when considering that I was working with triangles and taking advantage of the [baricentric coordinates](https://stackoverflow.com/a/2049712/4921205):\n\n    public boolean isPointInside(long x, long y) {\n        long s = a.y * c.x - a.x * c.y + (c.y - a.y) * x + (a.x - c.x) * y;\n        long t = a.x * b.y - a.y * b.x + (a.y - b.y) * x + (b.x - a.x) * y;\n    \n        if ((s \u003c 0) != (t \u003c 0)) {\n            return false;\n        }\n    \n        long A = -b.y * c.x + a.y * (c.x - b.x) + a.x * (b.y - c.y) + b.x * c.y;\n        if (A \u003c 0.0) {\n            s = -s;\n            t = -t;\n            A = -A;\n        }\n        return s \u003e 0 \u0026\u0026 t \u003e 0 \u0026\u0026 (s + t) \u003c= A;\n    }\n\nLater on, I've also added a new validation to check whether a path between nodes would cross an obstacle. This could have happened in case they were neighbours even if none of them was inside the obstacles themselves like in the following example. It was easy to fix this using the Java 2D API and intersections. Here are the code and two examples showing after and before the check:\n\n    public boolean isPathObstructed(Point origin, Point destination) {\n        Line2D pathToCheck = new Line2D.Float(origin, destination);\n\n        return firstSegment.intersectsLine(pathToCheck) || secondSegment.intersectsLine(pathToCheck)\n                || thirdSegment.intersectsLine(pathToCheck);\n    }\n\n\u003cp align=\"center\"\u003e\u003cimg alt=\"Before\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/illegal_crossing_1.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;-\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;\u003cimg alt=\"After\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/illegal_crossing_2.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u003c/p\u003e\n\nAll of this checks were performed by iterating all the obstacles each time we needed to explore a node. Although I didn't think that would work both due to performance and memory limitations, I resisted the urge to perform any premature optimization and I gave it a shot instead. It actually worked, to my surprise, and it was fast enough so I never really looked into a way to improve this:\n\n    public boolean isValid(int x, int y) {\n        // Checks that the point is within the boundary.\n        if (x \u003c -ProblemStatement.BOUND_CONSTRAINT || x \u003e ProblemStatement.BOUND_CONSTRAINT\n                || y \u003c -ProblemStatement.BOUND_CONSTRAINT || y \u003e ProblemStatement.BOUND_CONSTRAINT) {\n            return false;\n        }\n\n        for (Obstacle t : ProblemStatement.obstacles) {\n            // Checks that the point is not within an obstacle.\n            if (t.isPointInside(x, y)) {\n                return false;\n            }\n            \n            // Checks that there's no obstacle obstructing the path.\n            if (t.isPathObstructed(this, new Point(x, y))){\n                return false;\n            }\n        }\n        \n        // This point is valid.\n        return true;\n    }\n\n### A*\n\nUnluckily, Dijkstra method quickly proved to be too slow even for the smallest input dataset, therefore I switched over to a plain implementation of the [A* algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm). \n\nThe new solution was basically an improved version of the first one: Dijkstra's fitness function only took into account the node cost *g()* but A* also used a heuristic function *h()* to select only the nodes that moved towards the goal. The heuristic function used was the [Chebyshev distance](https://en.wikipedia.org/wiki/Chebyshev_distance) formula because it allows movement in 8 directions:\n\n    private int h(Node n) {\n        return Math.max(Math.abs(n.x - target.x), Math.abs(n.y - target.y));\n    }\n\nThe whole A* algorithm was the following:\n\n\tpublic Node calculateShortestPath(Node start, Node goal) {\n\t\tthis.target = goal;\n\n\t\t// Unexplored nodes.\n\t\tQueue\u003cNode\u003e openList = new PriorityQueue\u003cNode\u003e((n1, n2) -\u003e Double.compare(f(n1), f(n2)));\n\t\tMap\u003cNode, Node\u003e openMap = new HashMap\u003cNode, Node\u003e();\n\t\t// Explored nodes.\n\t\tSet\u003cNode\u003e closedList = new HashSet\u003cNode\u003e();\n\n\t\t// Initializes the first node by forcing g to 0.\n\t\tstart.setG(0);\n\t\topenList.offer(start);\n\n\t\twhile (!openList.isEmpty()) {\n\t\t\tNode q = openList.poll();\n\n\t\t\t// Uncomment to enable logging.\n\t\t\t// System.out.println(q);\n\n\t\t\t// Main loop of the algorithm.\n\t\t\tfor (Node successor : q.getAdjacentNodes()) {\n\n\t\t\t\t// If we already explored that node or we already added to the\n\t\t\t\t// \"to explore\" list, we just skip it.\n\t\t\t\tif (closedList.contains(successor)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Stop if we reached the goal.\n\t\t\t\tif (successor.equals(goal)) {\n\t\t\t\t\treturn successor;\n\t\t\t\t}\n\n\t\t\t\t// Add the node to the list to explore if not already there.\n\t\t\t\tNode oldSuccessor = openMap.get(successor);\n\t\t\t\tif (oldSuccessor == null) {\n\t\t\t\t\topenList.offer(successor);\n\t\t\t\t\topenMap.put(successor, successor);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// The distance from start to a neighbor.\n\t\t\t\tdouble tentativeGScore = g(q) + distanceBetween(q, successor);\n\n\t\t\t\t// This is not a better path.\n\t\t\t\tif (tentativeGScore \u003e= g(oldSuccessor)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// This path is the best until now.\n\t\t\t\toldSuccessor.setParent(q);\n\t\t\t\toldSuccessor.setG(tentativeGScore);\n\t\t\t\topenList.remove(oldSuccessor);\n\t\t\t\topenList.add(oldSuccessor);\n\t\t\t}\n\n\t\t\t// This node has been fully explored.\n\t\t\tclosedList.add(q);\n\t\t}\n\n\t\t// No path has been found.\n\t\treturn null;\n\t}\n\nHere's a visual representation of how the A* algorithm moved towards the goal:\n\n\u003cp align=\"center\"\u003e\u003cimg alt=\"A* 1\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/astar_1.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;-\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;\u003cimg alt=\"A* 2\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/astar_2.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u003c/p\u003e\n\u003cp align=\"center\"\u003e\u003cimg alt=\"A* 3\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/astar_3.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;-\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;\u003cimg alt=\"A* 4\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/astar_4.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u003c/p\u003e\n\u003cp align=\"center\"\u003e\u003cimg alt=\"A* 5\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/astar_5.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u003c/p\u003e\n\n### Bounded Relaxation\n\nAlthough the A* proved to be much quicker and efficient than Dijkstra, it still took a very long time to find the best path between the two nodes and I was still testing with the smallest dataset. At this point, I just decided that finding a solution would have been better than trying to find the best solution and this ultimately led me to accept a tradeoff between optimality and performances.\n\nWith this in mind, I've added the concept of [bounded relaxation](https://en.wikipedia.org/wiki/Relaxation_(approximation)) inside the algorithm by adding a static weight *w()* to the heuristic *h()* when computing the fitness function *f()*:\n\n    private double f(Node n) {\n        return g(n) + w(n) * h(n);\n    }\n\nHere's a depiction of the improved algorithm with a weight value of 10.0. The last picture shows how a path would be traced when a solution would have been found to highlight the performed tradeoff (in this dataset the final node is actually obstructed so that is not a correct solution):\n\n\u003cp align=\"center\"\u003e\u003cimg alt=\"A* with bounded relaxation 1\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/bounded_relaxation_10_1.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;-\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;\u003cimg alt=\"A* with bounded relaxation 2\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/bounded_relaxation_10_2.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u003c/p\u003e\n\u003cp align=\"center\"\u003e\u003cimg alt=\"A* with bounded relaxation 3\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/bounded_relaxation_10_final.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u003c/p\u003e\n\n### Limitations\n\nWhen working with the first dataset, I've stumbled upon the situation in which the goal node was obstructed. The way I coped with that was by limiting the maximum number of nodes that could be visited. Although this prevented the algorithm to go on forever if the goal was unreachable, it also meant that the algorithm would yield a negative result for a feasible path that required to explore more nodes than this threshold.\n\nOf course, there are plenty of better and cleaner solutions like running the algorithm in a different thread with a timeout or checking this limit relative to a node, but this was the cheapest to implement so I just went for it. \n\nTo give a little more insight, here is a visual representation of both the problem I wanted to avoid and the algorithm dealing with it by going on exploring until the threshold is reached after 170.000 nodes explored:\n\n\u003cp align=\"center\"\u003e\u003cimg alt=\"Obstructed node\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/obstructed_node.jpg\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e \u003cimg alt=\"Obstructed node stalemate\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/obstructed_node_stalemate.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e\u003c/p\u003e\n\n### (Almost) Total Rework\n\nAfter some time, I gave this problem another shoot and improved the solution a whole lot with some optimizations.\n\nFirst of all, I made the A* algorithm implementation much lighter by caching all the obstacles perimetral points instead of checking each time with a mathematical formula. This sacrified some memory but I could afford that and so I started the program with a heap size of 10 GB (\u003ccode\u003e-Xmx10g\u003c/code\u003e). This was the new validity check (after a bit of refactoring):\n\n    public boolean addIfValid(Node n) {\n\t\t// Checks that there's no obstacle obstructing the path.\n\t\tif (ProblemStatement.obstaclePoints.contains(n)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Checks that the point is within the boundary.\n\t\tif (n.x \u003c -ProblemStatement.BOUND_CONSTRAINT || n.x \u003e ProblemStatement.BOUND_CONSTRAINT\n\t\t\t\t|| n.y \u003c -ProblemStatement.BOUND_CONSTRAINT || n.y \u003e ProblemStatement.BOUND_CONSTRAINT) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// This point is valid.\n\t\tadjacentNodes.add(n);\n\t\treturn true;\n\t}\n\nTo add the nodes inside the set, I used a custom version of the [Bresenham's lines Algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm) which extracted each perimetral point of the obstacles [in a 4-connected representation instead of the classic 8-connected](https://en.wikipedia.org/wiki/Pixel_connectivity), in order to prevent the nodes cutting through obstacle's problem:\n\n\tpublic static List\u003cNode\u003e getLinePoints(int x0, int y0, int x1, int y1) {\n\t\tList\u003cNode\u003e line = new ArrayList\u003cNode\u003e();\n\n\t\tfinal int dx = Math.abs(x1 - x0);\n\t\tfinal int dy = Math.abs(y1 - y0);\n\t\tfinal int totalD = dx + dy;\n\n\t\tfinal int ix = x0 \u003c x1 ? 1 : x0 \u003e x1 ? -1 : 0;\n\t\tfinal int iy = y0 \u003c y1 ? 1 : y0 \u003e y1 ? -1 : 0;\n\n\t\tint e = 0;\n\t\tint xD = x0;\n\t\tint yD = y0;\n\t\tint e1, e2;\n\n\t\tfor (int i = 0; i \u003c totalD; i++) {\n\t\t\tline.add(new Node(xD, yD));\n\t\t\te1 = e + dy;\n\t\t\te2 = e - dx;\n\t\t\tif (Math.abs(e1) \u003c Math.abs(e2)) {\n\t\t\t\txD += ix;\n\t\t\t\te = e1;\n\t\t\t} else {\n\t\t\t\tyD += iy;\n\t\t\t\te = e2;\n\t\t\t}\n\t\t}\n\t\treturn line;\n\t}\n\nAfter some testing, I've noticed that altough not stated anywhere, the solution score was not based on the number of nodes but on the real distance. Initially, the distance between any nodes and its neihbors was always 1 but since we were now considering real distances, I've added a check to set the distance equal to \u003ccode\u003esqrt(2)\u003c/code\u003e if the nodes were diagonally aligned, to improve the approximation:\n\n\tpublic static double distanceBetween(Node start, Node end) {\n\t\t// Nodes are diagonal.\n\t\tif (start.x != end.x \u0026\u0026 start.y != end.y) {\n\t\t\treturn DIAGONAL_COST;\n\t\t}\n\t\t// Nodes are not diagonal.\n\t\treturn 1;\n\t}\n\nAnother thing I've noticed (also not stated) was that the nodes didn't need to be continuos but could \"jump\". To improve readability, I've started pruning unnecessary nodes (this was also needed to keep the number of nodes below the maximum threshold):\n\n    public static List\u003cNode\u003e compressPath(List\u003cNode\u003e points) {\n\t\tList\u003cNode\u003e compressedPath = new ArrayList\u003cNode\u003e();\n\n\t\tIterator\u003cNode\u003e pointIterator = points.iterator();\n\t\tNode previous = pointIterator.next();\n\t\tint currentXDirection = 0;\n\t\tint currentYDirection = 0;\n\t\twhile (pointIterator.hasNext()) {\n\t\t\tNode current = pointIterator.next();\n\t\t\t// Checks for any direction changes by looking at the coordinates of\n\t\t\t// the current and previous node.\n\t\t\tint tmpXDirection = Double.compare(previous.x, current.x);\n\t\t\tint tmpYDirection = Double.compare(previous.y, current.y);\n\n\t\t\t// If the direction changes, the previous is a new point of the\n\t\t\t// compression.\n\t\t\tif (tmpXDirection != currentXDirection || tmpYDirection != currentYDirection) {\n\t\t\t\tcompressedPath.add(previous);\n\t\t\t\tcurrentXDirection = tmpXDirection;\n\t\t\t\tcurrentYDirection = tmpYDirection;\n\t\t\t}\n\n\t\t\t// Moves to the next point.\n\t\t\tprevious = current;\n\t\t}\n\t\tcompressedPath.add(previous);\n\t\treturn compressedPath;\n\t}\n\nAfter this compression, I saw more clearly that some nodes were extending the path for no real reasons, so I pruned them as well with this code:\n\n\tpublic static List\u003cNode\u003e reduce(List\u003cNode\u003e points) {\n\t\tList\u003cNode\u003e reducedPath = new ArrayList\u003cNode\u003e();\n\n\t\t// Iterates all the points from the first.\n\t\tfor (int i = 0; i \u003c points.size(); i++) {\n\t\t\tNode fromStart = points.get(i);\n\n\t\t\t// Iterates all the points from the last.\n\t\t\tmiddle: for (int j = points.size() - 1; j \u003e i; j--) {\n\t\t\t\tNode fromEnd = points.get(j);\n\n\t\t\t\t// If there's an obstacle between the two points, there's no\n\t\t\t\t// path between the points. Let's consider the next one from the\n\t\t\t\t// end.\n\t\t\t\tfor (Obstacle o : ProblemStatement.obstacles) {\n\t\t\t\t\tif (o.isPathObstructed(fromStart, fromEnd)) {\n\t\t\t\t\t\tcontinue middle;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// No obstacles if here.\n\t\t\t\treducedPath.add(fromStart);\n\t\t\t\treducedPath.add(fromEnd);\n\t\t\t\t// Let's jump to the next point. At the next iteration i will\n\t\t\t\t// increase again so we go to the previous point.\n\t\t\t\ti = j - 1;\n\n\t\t\t}\n\t\t}\n\t\treturn reducedPath;\n\t}\n\nLast but not least, I've noticed (after definely too much time) that the first input set is not impossible as I originally thought but it actually has a solution. This was a really difficult problem to solve because the 4C representation of the obstacles cut that solution away, adding the goal node and its neighbours to the obstacle points set. \n\nTo solve this, I've added a new check before actually running the algorithm on the start and end point. If any of the point is in the obstacle points set, I try to make a path to that specific node by converting the surrounding obstacles back to their 8C representation. If after this there's no such path to that node, I declare that problem impossible without even running the algorithm:\n\n    public static boolean clearPath(Node terminalNode) {\n\t\t// First of all we check that the point is not within any obstacle. If\n\t\t// it is, the problem doesn't have any solution.\n\t\tfor (Obstacle obstacle : ProblemStatement.obstacles) {\n\t\t\tif (obstacle.isPointInside(terminalNode.x, terminalNode.y)) {\n\t\t\t\t// This problem doesn't have any solution.\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t// We add the first node to the next nodes to visit.\n\t\tSet\u003cNode\u003e visitedNodes = new HashSet\u003cNode\u003e();\n\t\tLinkedList\u003cNode\u003e nextNodes = new LinkedList\u003cNode\u003e();\n\t\tnextNodes.add(terminalNode);\n\n\t\t// If there's more than just one node, the path to the node is open.\n\t\twhile (nextNodes.size() == 1) {\n\t\t\tNode currentNode = nextNodes.poll();\n\n\t\t\t// This node is not within any obstacles.\n\t\t\tProblemStatement.obstaclePoints.remove(currentNode);\n\n\t\t\t// Current point neighbors.\n\t\t\tList\u003cNode\u003e currentNeighbors = Arrays.asList(new Node(currentNode.x - 1, currentNode.y - 1),\n\t\t\t\t\tnew Node(currentNode.x, currentNode.y - 1), new Node(currentNode.x + 1, currentNode.y - 1),\n\t\t\t\t\tnew Node(currentNode.x - 1, currentNode.y), new Node(currentNode.x + 1, currentNode.y),\n\t\t\t\t\tnew Node(currentNode.x - 1, currentNode.y + 1), new Node(currentNode.x, currentNode.y + 1),\n\t\t\t\t\tnew Node(currentNode.x + 1, currentNode.y + 1));\n\n\t\t\t// Adds all the neighbors nodes not already visited to the \"to\n\t\t\t// visit\" list.\n\t\t\tcurrentNeighbors.forEach(node -\u003e {\n\t\t\t\tif (!visitedNodes.contains(node)) {\n\t\t\t\t\tnextNodes.add(node);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// If any of the current node's neighbors is obstructed by\n\t\t\t// an obstacle is removed from the \"to visit\" list.\n\t\t\tfor (Obstacle obstacle : ProblemStatement.obstacles) {\n\t\t\t\tcurrentNeighbors.forEach(node -\u003e {\n\t\t\t\t\tif (obstacle.isPathObstructed(node, currentNode)) {\n\t\t\t\t\t\tProblemStatement.obstaclePoints.add(node);\n\t\t\t\t\t\tnextNodes.remove(node);\n\t\t\t\t\t}\n\t\t\t\t});\n\n\t\t\t}\n\t\t\t// This node has been fully explored.\n\t\t\tvisitedNodes.add(currentNode);\n\t\t}\n\n\t\t// An optimization may have been happened.\n\t\treturn true;\n\t}\n\n\n## Scoring\n\n### Formula\n\nThe scoring method is the following:\n\n - if the solution is IMPOSSIBLE, the score is 0.\n - if the solution is not correct or incomplete, the score is -100\n - otherwise the score is 1 / (total_path_length) * 1.000.000\n \n### Partial Scores\n\nOrdered by dataset input:\n\n - *220 points*: the path found is 18 nodes long\n    \u003cp align=\"center\"\u003e\u003cimg alt=\"Solution 1\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/input_1_solution.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e \n \n - *0 points*: both nodes are within an obstacle, therefore the result is again IMPOSSIBLE\n    \u003cp align=\"center\"\u003e\u003cimg alt=\"Solution 2\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/input_2_solution.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e \n \n - *161 points*: the path found is 64 nodes long\n    \u003cp align=\"center\"\u003e\u003cimg alt=\"Solution 3\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/input_3_solution.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e \n \n - *66 points*: here, the path found is 142 nodes long\n    \u003cp align=\"center\"\u003e\u003cimg alt=\"Solution 4\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/input_4_solution.png\" width=\"300px\" height=\"300px\" align=\"middle\"\u003e \n \n### Total Score\n\n*447 points*\n\n \u003cp align=\"center\"\u003e\u003cimg alt=\"Total Score\" src=\"https://github.com/aurasphere/reply-challenge/raw/master/screenshots/final_submission.png\" align=\"middle\"\u003e \n \n## Final Considerations\n\nThis project was really fun and interesting. I've surely learned a lot on pathfinding by working on it.\n\nMy solution is not the best for sure and can be further improved. In particular, a more efficient approach would probably be the [Jump Point algorithm](https://users.cecs.anu.edu.au/~dharabor/data/papers/harabor-grastien-aaai11.pdf), which I may study in the future if I get any spare time.\n\nI've also worked on a practical application of this [in a maze solving app that can be found here](https://github.com/aurasphere/maze-solver).\n\n## Acknowledgements\n\nSpecial thanks to @enricoaleandri for building the [Processing](https://processing.org/) visualizer used to show the paths (which I'm temporarily hosting under the directory visualizer).\n\n## Discussions\n\nIf you want to say something, feel free to open an issue on this project. I don't guarantee I'll make any fixes on this project but I'll read and reply for sure.\n\nIf instead you want to get in touch directly, just send me an email.\n\n\u003csub\u003eCopyright (c) 2018 Donato Rimenti\u003c/sub\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurasphere%2Freply-challenge-2018","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faurasphere%2Freply-challenge-2018","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurasphere%2Freply-challenge-2018/lists"}