{"id":94,"date":"2010-04-16T11:46:32","date_gmt":"2010-04-16T18:46:32","guid":{"rendered":"http:\/\/zalbee.intricus.net\/immerge\/blog\/?p=94"},"modified":"2025-05-23T22:49:05","modified_gmt":"2025-05-24T02:49:05","slug":"solving-continuity-levels-with-dijkstras-shortest-path","status":"publish","type":"post","link":"https:\/\/zalbee.intricus.net\/immerge\/blog\/94","title":{"rendered":"Solving Continuity Levels with Dijkstra&#8217;s Shortest Path"},"content":{"rendered":"<p><em>This post is probably off-topic for this blog, but it is the only blog I use, so enjoy. (By the way, WordPress editor still greatly annoys me and so does this theme&#8217;s style. I&#8217;m going to have to fix the CSS eventually).<\/em><\/p>\n<p><a href=\"http:\/\/www.kongregate.com\/games\/glimajr\/continuity?sfa=permalink&amp;referrer=zAlbee\">Continuity<\/a> is a new Flash game that I was playing recently. It combines platforming and puzzle in a novel way &#8211; your character must jump from platform to platform collecting keys to open a door, but must do so inside mini-levels (squares). The macro-game is essentially an <a href=\"http:\/\/en.wikipedia.org\/wiki\/8_puzzle\">8-puzzle<\/a> (8 pieces in a 3&#215;3 square grid), where you shift the puzzle pieces using the one empty space, such that your character can succeed in the mini-levels.<\/p>\n<p>This game is perfectly solvable through logic and working backwards, though for some of the harder levels you need to keep a long memory of steps. In short, I became bored and impatient of solving the puzzles in my head, and decided after ~30 levels, it&#8217;d be more interesting to automate it by writing a solver.<\/p>\n<p>I also took this opportunity to get familiar with <a href=\"http:\/\/www.python.org\/\">Python<\/a>, a language well known for its fast prototyping and easy learning curve. I have only worked in Python once, during a 4-month internship 4 years ago, but otherwise, my main skills are in C and Java. However the syntax required to write good algorithms and data structures in these &#8220;classic&#8221; languages can get quite long and annoying. So I coded up a Python script to solve the final two levels.<\/p>\n<p>The rest of this post details how to develop the solution. The solution is relatively simple because we can just treat each exit in a square as a point in a directed graph, where each edge in the graph A-&gt;B specifies that point B is reachable from point A, and then apply a search algorithm through the graph. I chose <a href=\"http:\/\/en.wikipedia.org\/wiki\/Dijkstra's_algorithm\">Dijkstra&#8217;s shortest path<\/a> algorithm, which runs fast and finds the best (shortest) solution. However, my program did require a lot of manual data entering to specify the paths\/exits in each puzzle piece, so it wasn&#8217;t fully automatic. Perhaps Python has some <a href=\"http:\/\/xkcd.com\/353\/\" target=\"_blank\" rel=\"noopener\">magic<\/a> that could automate this :). Lastly, I skipped solving the 8-puzzle, since it&#8217;s not necessary.<\/p>\n<p><!--more--><\/p>\n<h2 style=\"font-size: 1.5em;\">The Basics<\/h2>\n<p>First some notation. I will number the squares (from 1-n). In this game, your character can only move between two squares if the sides match\u00a0<em>exactly<\/em>. So, for each square, I will classify its sides with a letter. Two sides that can connect to each other will have the same letter, but different case (e.g. side X connects to side x). \u00a0Top and left sides will use capital letters; bottom and right sides use lower case. We will label each opening in an side A as A1, A2, A3, etc&#8230; in order from top to bottom or left to right.<\/p>\n<p>I will use Level 31 in Continuity as an example. I have labelled this level in the below image. The labels just make it easier for a human to follow (and to input).<\/p>\n<div style=\"width: 650px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" title=\"Continuity Level 31\" src=\"http:\/\/zalbee.intricus.net\/pics\/continuity31_labels.png\" alt=\"\" width=\"640\" height=\"480\" \/><p class=\"wp-caption-text\">Matching sides are labelled with letters.<\/p><\/div>\n<p>In this level, there are 7 types of sides with these openings:<\/p>\n<div>Number of openings\u00a0\u00a0(points)\u00a0per side:<\/div>\n<div>Left\/right sides:<\/div>\n<div>\n<ul>\n<li><span style=\"white-space: pre;\"> <\/span>side A\/a: 2<\/li>\n<li><span style=\"white-space: pre;\"> <\/span>side B\/b: 2<\/li>\n<li><span style=\"white-space: pre;\"> <\/span>side C\/c: 2<\/li>\n<\/ul>\n<\/div>\n<div>Top\/bottom sides:<\/div>\n<div>\n<ul>\n<li><span style=\"white-space: pre;\"> <\/span>side Z\/z: 2<\/li>\n<li><span style=\"white-space: pre;\"> <\/span>side Y\/y: 2<\/li>\n<li><span style=\"white-space: pre;\"> <\/span>side X\/x: 0<\/li>\n<li><span style=\"white-space: pre;\"> <\/span>side W\/w: 1<\/li>\n<\/ul>\n<\/div>\n<p>In Python:<\/p>\n<pre style=\"padding-left: 30px;\">numOpenings = {'a':2, 'b':2, 'c':2, 'z':2, 'y':2, 'x':0, 'w':1}<\/pre>\n<p>The following are the sides that each square possesses. Also noted the special points and their related openings:<\/p>\n<table>\n<tbody>\n<tr>\n<th>Square<\/th>\n<th>Sides<\/th>\n<th>Notes<\/th>\n<\/tr>\n<tr>\n<td>1:<\/td>\n<td>A, b, Z, z<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>2:<\/td>\n<td>B, b, Y, y<\/td>\n<td>&lt;Keys: b2&gt;<\/td>\n<\/tr>\n<tr>\n<td>3:<\/td>\n<td>B, a, W, w<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>4:<\/td>\n<td>C, a, Z, y<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>5:<\/td>\n<td>A, a, W, z<\/td>\n<td>&lt;Start: A1, a1, W1&gt;<\/td>\n<\/tr>\n<tr>\n<td>6:<\/td>\n<td>A, b, Z, z<\/td>\n<td>&lt;Finish: Z2&gt;<\/td>\n<\/tr>\n<tr>\n<td>7:<\/td>\n<td>B, c, Y, x<\/td>\n<td><\/td>\n<\/tr>\n<tr>\n<td>8:<\/td>\n<td>C, b, Z, w<\/td>\n<td>Keys: b1, (Z1 or Z2)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>In my code, I simply use strings to store the sides per square, since they are easy to deal with:<\/p>\n<pre style=\"padding-left: 30px;\">sqEdges = [\"AbZz\",\u00a0\"BbYy\",\u00a0\"BaWw\",\u00a0\"CaZy\",\u00a0\"AaWz\",\u00a0\"AbZz\",\u00a0\"BcYx\",\u00a0\"CbZw\"]<\/pre>\n<h2>Setting up the Graph Data<\/h2>\n<p>Construct a directed graph of connected points. There is a connection (edge) from p1 -&gt; p2 if you can get from p1 to p2. That does not imply you can get from p2 -&gt; p1 because of game physics (jumping and gravity), so this is a directed graph.<\/p>\n<h3>First, inter-square connections (between different squares).<\/h3>\n<p>It is simple to see that any 2 squares can be placed adjacent to each other in whichever way desirable. We just assume that any such move is equally easy to do, with weight 1. (Because it really is easy!)<\/p>\n<p>All matching sides can be connected to each other, with the exception of top and bottom sides, where the connection is one-way (top to bottom). So we can infer all inter-square connections from the square-&gt;sides table above, using a loop:<\/p>\n<div>for each square S:<\/div>\n<div style=\"padding-left: 30px;\"><span style=\"white-space: pre;\"> <\/span>for each side E in S:<\/div>\n<div style=\"padding-left: 60px;\"><span style=\"white-space: pre;\"> <\/span>for each other square T with matching side e:<\/div>\n<div style=\"padding-left: 90px;\"><span style=\"white-space: pre;\"> <\/span>for each point i per side E\/e:<\/div>\n<div style=\"padding-left: 120px;\"><span style=\"white-space: pre;\"> <\/span>if (S.E is not a top side):<\/div>\n<div style=\"padding-left: 150px;\"><span style=\"white-space: pre;\"> <\/span>S.Ei -&gt; T.ei<\/div>\n<h3>Secondly, find the intra-square connections (within same square).<\/h3>\n<p>Unfortunately, the only way to do this is inspect each square to see if it is possible to reach the opening via jumping inside the square alone. This was easily the longest (and most error-prone) part of the process. I won&#8217;t even list them all here because the list is so long.<\/p>\n<div>Connections:<\/div>\n<div style=\"padding-left: 30px;\">1: Z1 -&gt; A1, Z2 -&gt; z2, Z2 -&gt; b2, A2 -&gt; z1<\/div>\n<div style=\"padding-left: 30px;\">2: Y1 -&gt; B1, Y1 -&gt; B2, Y1 -&gt; y2, Y2 -&gt; B2, Y2 -&gt; y2, B1 -&gt; B2, B1 -&gt; y2<\/div>\n<div style=\"padding-left: 30px;\">(or, minimally: Y1 -&gt; B1, B1 -&gt; B2, Y1 -&gt; B2, Y2 -&gt; B2, B2 -&gt; y2)<\/div>\n<div style=\"padding-left: 30px;\">3:<\/div>\n<div style=\"padding-left: 30px;\">&#8230;<\/div>\n<div style=\"padding-left: 30px;\">etc.<\/div>\n<h3>Finally, connections from special points to openings.<\/h3>\n<p>Ah, these are the keys and doors in the game. Yes, there is still a game here ;-).<\/p>\n<p>Most of the time, extra points are not necessary. Most keys or doors can only be reached by one opening, so just use that point. For starting point, just pick the closest opening. If a key or door can be reached by more than one opening, we will create an extra point with connections. For example, in this level, &#8216;key2&#8217; is the only\u00a0extra point:<\/p>\n<div style=\"padding-left: 30px;\">8.Z1 -&gt; key2<\/div>\n<div style=\"padding-left: 30px;\">8.Z2 -&gt; key2<\/div>\n<div style=\"padding-left: 30px;\">key2 -&gt; w1<\/div>\n<div style=\"padding-left: 30px;\">key2 -&gt; b2<\/div>\n<p>and the others we can just alias to existing points:<\/p>\n<div style=\"padding-left: 30px;\">Start = A1<\/div>\n<div style=\"padding-left: 30px;\">Finish = Z2<\/div>\n<div style=\"padding-left: 30px;\">key3 = 2.b2<\/div>\n<div style=\"padding-left: 30px;\">key1 = 8.b1<\/div>\n<h2>Solving the Path!<\/h2>\n<p>Now we need to find a path that collects all keys and reaches the goal (door). The order of which keys to collect does not matter.\u00a0Let&#8217;s just pick an arbitrary order of key collecting. Then find a path from &lt;Start&gt; to &lt;key1&gt; &#8230; &lt;keyN&gt; to &lt;Finish&gt;. Call these the checkpoints.<\/p>\n<p>One solution is to run some search algorithm from each checkpoint to the next, then concatenate the paths together. We could use common search algorithms (DFS, BFS) to traverse though the nodes until we find the goal. DFS backtracks when it reaches a dead end, while BFS searches all paths in parallel, one step at a time.<\/p>\n<p>A better way is to run a global shortest path algorithm, that finds shortest path from one point to every other point, such as Dijkstra&#8217;s algorithm. This algorithm is nice, because it only needs to check each node once, not doing any actual traversal. A bonus: We can select whichever order of collecting keys that gives the shortest total path. We can also give weights to the edges (e.g. I used cost = 1 to travel inside a square, but cost=10 to go between squares).<\/p>\n<p>This does not take into account how many moves are required in the macro puzzle to shuffle the squares around to make the desired connection. Assigning a weight to each inter-square connection equal to number of &#8220;shuffles&#8221; might work, except this number depends on the previous arrangement of squares. Thus we would have to track the state of the macro puzzle with each step in a DFS or BFS search, and the weight would only be valid for that state; we&#8217;d have to recompute at each new state. Not only that, but DFS\/BFS are not well-suited for weighted search. It wouldn&#8217;t work at all using the Dijkstra&#8217;s shortest path method, since that relies on global costs of each path that are known beforehand to be unchanging.<\/p>\n<p>In any case, that is enough, because shifting two squares next to each other is extremely easy.<\/p>\n<h2>Coding<\/h2>\n<p>Here is the best part about Python. It is extremely easy to go from pseudo-code of a Dijkstra&#8217;s algorithm to Python code, thanks to its built in list and dictionary types and short syntax.<\/p>\n<p>I used some object-oriented features (to encapsulate squares, each which hold a list of its points, each point holds an adjacency list of connected points), or you can go C-style and use global list variables like you would arrays. In fact, I ended up with a mix of both styles.<\/p>\n<h2>The Final Solution<\/h2>\n<h3>Level 31<\/h3>\n<p>Here are the results for Level 31. I grabbed a <a href=\"http:\/\/snippets.dzone.com\/posts\/show\/753\">permutation function<\/a> off the Internet that was only 7 lines (wow!), and so found all the solutions for each different order of collecting the 3 keys. This was the shortest one:<\/p>\n<pre>###############\r\nSolving for Permutation 6 of key collecting order:\r\n-------------------\r\nFinding shortest path from Sq 5-A1 (left top) to Sq 8-b1 (right top).\r\nSq 5-A1 (left top)\r\nSq 4-a1 (right top)\r\nSq 4-y1 (bottom left)\r\nSq 2-Y1 (top left)\r\nSq 2-B1 (left top)\r\nSq 8-b1 (right top)\r\nDistance is 32\r\nFinding shortest path from Sq 8-b1 (right top) to Sq 2-b2 (right bottom).\r\nSq 8-b1 (right top)\r\nSq 3-B1 (left top)\r\nSq 3-B2 (left bottom)\r\nSq 2-b2 (right bottom)\r\nDistance is 21\r\nFinding shortest path from Sq 2-b2 (right bottom) to key2.\r\nSq 2-b2 (right bottom)\r\nSq 7-B2 (left bottom)\r\nSq 8-b2 (right bottom)\r\nSq 8-w1 (bottom )\r\nSq 3-W1 (top )\r\nSq 3-a2 (right bottom)\r\nSq 1-A2 (left bottom)\r\nSq 1-z1 (bottom left)\r\nSq 8-Z1 (top left)\r\nkey2\r\nDistance is 54\r\nFinding shortest path from key2 to Sq 6-Z2 (top right).\r\nkey2\r\nSq 8-w1 (bottom )\r\nSq 3-W1 (top )\r\nSq 3-a2 (right bottom)\r\nSq 1-A2 (left bottom)\r\nSq 5-a2 (right bottom)\r\nSq 5-z2 (bottom right)\r\nSq 6-Z2 (top right)\r\nDistance is 43\r\nTotal Distance is 150<\/pre>\n<h3>\n<p><div style=\"width: 650px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" title=\"Continuity Level 31\" src=\"http:\/\/zalbee.intricus.net\/pics\/continuity31_labels.png\" alt=\"\" width=\"640\" height=\"480\" \/><p class=\"wp-caption-text\">Level 31 again, for cross-reference.<\/p><\/div><\/h3>\n<h3>Level 32<\/h3>\n<p>As a bonus, here is the best solution for Level 32 using the same technique. Being the last level, the difficulty is higher, and you can see because of the very few common sides between squares. Four of the sides do not even have a corresponding match (A, F, h, i). But it could be worse &#8212; at least all the bottom sides have some square to fall into!  For this level, the shortest path for the best permutation of key collecting had distance 143 and the worst was 283. So that&#8217;s about half. Bet your Youtube video walkthrough didn&#8217;t have that information ;-).<\/p>\n<div style=\"width: 650px\" class=\"wp-caption alignnone\"><img loading=\"lazy\" decoding=\"async\" title=\"Continuity Level 32\" src=\"http:\/\/zalbee.intricus.net\/pics\/continuity32_labels.png\" alt=\"\" width=\"640\" height=\"480\" \/><p class=\"wp-caption-text\">This one is harder.<\/p><\/div>\n<pre>###############<\/pre>\n<pre>Solving for Permutation 3 of key collecting order:\r\n-------------------\r\nFinding shortest path from Sq5 Central Area to Sq 2-V3 (top right).\r\nSq5 Central Area\r\nSq 5-d2 (right bottom)\r\nSq 4-D2 (left bottom)\r\nSq 1-d2 (right bottom)\r\nSq 1-v3 (bottom right)\r\nSq 2-V3 (top right)\r\nDistance is 32\r\nFinding shortest path from Sq 2-V3 (top right) to Sq 7-w1 (bottom ).\r\nSq 2-V3 (top right)\r\nSq 2-x3 (bottom right)\r\nSq 6-X3 (top right)\r\nSq 6-c1 (right top)\r\nSq 3-C1 (left top)\r\nSq 3-W1 (top )\r\nSq 7-w1 (bottom )\r\nDistance is 33\r\nFinding shortest path from Sq 7-w1 (bottom ) to Sq 2-B1 (left top).\r\nSq 7-w1 (bottom )\r\nSq 3-W1 (top )\r\nSq 3-g1 (right top)\r\nSq 7-G1 (left top)\r\nSq 7-U1 (top left)\r\nSq 6-u1 (bottom left)\r\nSq 1-U1 (top left)\r\nSq 1-A1 (left top)\r\nSq 1-A2 (left bottom)\r\nSq 1-v1 (bottom left)\r\nSq 2-V1 (top left)\r\nSq 2-B1 (left top)\r\nDistance is 56\r\nFinding shortest path from Sq 2-B1 (left top) to Sq 2-b1 (right top).\r\nSq 2-B1 (left top)\r\nSq 2-x1 (bottom left)\r\nSq 8-X1 (top left)\r\nSq 8-B1 (left top)\r\nSq 2-b1 (right top)\r\nDistance is 22\r\nTotal Distance is 143<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>This post is probably off-topic for this blog, but it is the only blog I use, so enjoy. (By the way, WordPress editor still greatly annoys me and so does this theme&#8217;s style. I&#8217;m going to have to fix the CSS eventually). Continuity is a new Flash game that I was playing recently. It combines [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7,9,10],"tags":[],"class_list":["post-94","post","type-post","status-publish","format-standard","hentry","category-algorithms","category-games","category-puzzles"],"_links":{"self":[{"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/posts\/94","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/comments?post=94"}],"version-history":[{"count":19,"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/posts\/94\/revisions"}],"predecessor-version":[{"id":219,"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/posts\/94\/revisions\/219"}],"wp:attachment":[{"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/media?parent=94"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/categories?post=94"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/zalbee.intricus.net\/immerge\/blog\/wp-json\/wp\/v2\/tags?post=94"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}