Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What to use for flow free-like game random level creation?

I need some advice. I'm developing a game similar to Flow Free wherein the gameboard is composed of a grid and colored dots, and the user has to connect the same colored dots together without overlapping other lines, and using up ALL the free spaces in the board.

My question is about level-creation. I wish to make the levels generated randomly (and should at least be able to solve itself so that it can give players hints) and I am in a stump as to what algorithm to use. Any suggestions?

Game objective

Note: image shows the objective of Flow Free, and it is the same objective of what I am developing.

Thanks for your help. :)

like image 882
user1239714 Avatar asked Oct 17 '12 02:10

user1239714


People also ask

Is there more than one solution to flow free?

Yes, there are many possibilities when playing Flow. The game has an intended way of how you play. If you start to go left, right, left and so on, it means you haven't found the proper way. Source: I used to play Flow a lot.

How many levels are in flow game?

Flow Free is a simple yet addictive puzzle game. It includes 300 free levels with puzzles appropriate for phones and for tablets.


2 Answers

Consider solving your problem with a pair of simpler, more manageable algorithms: one algorithm that reliably creates simple, pre-solved boards and another that rearranges flows to make simple boards more complex.

The first part, building a simple pre-solved board, is trivial (if you want it to be) if you're using n flows on an nxn grid:

  • For each flow...
    • Place the head dot at the top of the first open column.
    • Place the tail dot at the bottom of that column.

Alternatively, you could provide your own hand-made starter boards to pass to the second part. The only goal of this stage is to get a valid board built, even if it's just trivial or predetermined, so it's worth keeping it simple.

The second part, rearranging the flows, involves looping over each flow, seeing which one can work with its neighboring flow to grow and shrink:

  • For some number of iterations...
    • Choose a random flow f.
    • If f is at the minimum length (say 3 squares long), skip to the next iteration because we can't shrink f right now.
    • If the head dot of f is next to a dot from another flow g (if more than one g to choose from, pick one at random)...
      • Move f's head dot one square along its flow (i.e., walk it one square towards the tail). f is now one square shorter and there's an empty square. (The puzzle is now unsolved.)
      • Move the neighboring dot from g into the empty square vacated by f. Now there's an empty square where g's dot moved from.
      • Fill in that empty spot with flow from g. Now g is one square longer than it was at the beginning of this iteration. (The puzzle is back to being solved as well.)
    • Repeat the previous step for f's tail dot.

The approach as it stands is limited (dots will always be neighbors) but it's easy to expand upon:

  • Add a step to loop through the body of flow f, looking for trickier ways to swap space with other flows...
  • Add a step that prevents a dot from moving to an old location...
  • Add any other ideas that you come up with.

The overall solution here is probably less than the ideal one that you're aiming for, but now you have two simple algorithms that you can flesh out further to serve the role of one large, all-encompassing algorithm. In the end, I think this approach is manageable, not cryptic, and easy to tweek, and, if nothing else, a good place to start.


Update: I coded a proof-of-concept based on the steps above. Starting with the first 5x5 grid below, the process produced the subsequent 5 different boards. Some are interesting, some are not, but they're always valid with one known solution.

Starting Point
Image 1 - starting frame

5 Random Results (sorry for the misaligned screenshots)
Image 2Image 3Image 4Image 5Image 6

And a random 8x8 for good measure. The starting point was the same simple columns approach as above.

Image 7

like image 175
user1201210 Avatar answered Sep 22 '22 09:09

user1201210


The most straightforward way to create such a level is to find a way to solve it. This way, you can basically generate any random starting configuration and determine if it is a valid level by trying to have it solved. This will generate the most diverse levels.

And even if you find a way to generate the levels some other way, you'll still want to apply this solving algorithm to prove that the generated level is any good ;)

Brute-force enumerating

If the board has a size of NxN cells, and there are also N colours available, brute-force enumerating all possible configurations (regardless of wether they form actual paths between start and end nodes) would take:

  • N^2 cells total
  • 2N cells already occupied with start and end nodes
  • N^2 - 2N cells for which the color has yet to be determined
  • N colours available.
  • N^(N^2 - 2N) possible combinations.

So,

  • For N=5, this means 5^15 = 30517578125 combinations.
  • For N=6, this means 6^24 = 4738381338321616896 combinations.

In other words, the number of possible combinations is pretty high to start with, but also grows ridiculously fast once you start making the board larger.

Constraining the number of cells per color

Obviously, we should try to reduce the number of configurations as much as possible. One way of doing that is to consider the minimum distance ("dMin") between each color's start and end cell - we know that there should at least be this much cells with that color. Calculating the minimum distance can be done with a simple flood fill or Dijkstra's algorithm. (N.B. Note that this entire next section only discusses the number of cells, but does not say anything about their locations)

In your example, this means (not counting the start and end cells)

dMin(orange) = 1 dMin(red) = 1 dMin(green) = 5 dMin(yellow) = 3 dMin(blue) = 5 

This means that, of the 15 cells for which the color has yet to be determined, there have to be at least 1 orange, 1 red, 5 green, 3 yellow and 5 blue cells, also making a total of 15 cells. For this particular example this means that connecting each color's start and end cell by (one of) the shortest paths fills the entire board - i.e. after filling the board with the shortest paths no uncoloured cells remain. (This should be considered "luck", not every starting configuration of the board will cause this to happen).

Usually, after this step, we have a number of cells that can be freely coloured, let's call this number U. For N=5,

U = 15 - (dMin(orange) + dMin(red) + dMin(green) + dMin(yellow) + dMin(blue)) 

Because these cells can take any colour, we can also determine the maximum number of cells that can have a particular colour:

dMax(orange) = dMin(orange) + U dMax(red)    = dMin(red) + U dMax(green)  = dMin(green) + U dMax(yellow) = dMin(yellow) + U dMax(blue)   = dMin(blue) + U 

(In this particular example, U=0, so the minimum number of cells per colour is also the maximum).

Path-finding using the distance constraints

If we were to brute force enumerate all possible combinations using these color constraints, we would have a lot less combinations to worry about. More specifically, in this particular example we would have:

  15! / (1! * 1! * 5! * 3! * 5!) = 1307674368000 / 86400 = 15135120 combinations left, about a factor 2000 less. 

However, this still doesn't give us the actual paths. so a better idea would be to a backtracking search, where we process each colour in turn and attempt to find all paths that:

  • doesn't cross an already coloured cell
  • Is not shorter than dMin(colour) and not longer than dMax(colour).

The second criteria will reduce the number of paths reported per colour, which causes the total number of paths to be tried to be greatly reduced (due to the combinatorial effect).

In pseudo-code:

function SolveLevel(initialBoard of size NxN) {     foreach(colour on initialBoard)     {         Find startCell(colour) and endCell(colour)         minDistance(colour) = Length(ShortestPath(initialBoard, startCell(colour), endCell(colour)))     }      //Determine the number of uncoloured cells remaining after all shortest paths have been applied.     U = N^(N^2 - 2N) - (Sum of all minDistances)      firstColour = GetFirstColour(initialBoard)     ExplorePathsForColour(         initialBoard,          firstColour,          startCell(firstColour),          endCell(firstColour),          minDistance(firstColour),          U)     } }  function ExplorePathsForColour(board, colour, startCell, endCell, minDistance, nrOfUncolouredCells) {     maxDistance = minDistance + nrOfUncolouredCells     paths = FindAllPaths(board, colour, startCell, endCell, minDistance, maxDistance)      foreach(path in paths)     {         //Render all cells in 'path' on a copy of the board         boardCopy = Copy(board)         boardCopy = ApplyPath(boardCopy, path)          uRemaining = nrOfUncolouredCells - (Length(path) - minDistance)          //Recursively explore all paths for the next colour.         nextColour = NextColour(board, colour)         if(nextColour exists)         {             ExplorePathsForColour(                 boardCopy,                  nextColour,                  startCell(nextColour),                  endCell(nextColour),                  minDistance(nextColour),                  uRemaining)         }         else         {             //No more colours remaining to draw             if(uRemaining == 0)             {                 //No more uncoloured cells remaining                 Report boardCopy as a result             }         }     } } 

FindAllPaths

This only leaves FindAllPaths(board, colour, startCell, endCell, minDistance, maxDistance) to be implemented. The tricky thing here is that we're not searching for the shortest paths, but for any paths that fall in the range determined by minDistance and maxDistance. Hence, we can't just use Dijkstra's or A*, because they will only record the shortest path to each cell, not any possible detours.

One way of finding these paths would be to use a multi-dimensional array for the board, where each cell is capable of storing multiple waypoints, and a waypoint is defined as the pair (previous waypoint, distance to origin). The previous waypoint is needed to be able to reconstruct the entire path once we've reached the destination, and the distance to origin prevents us from exceeding the maxDistance.

Finding all paths can then be done by using a flood-fill like exploration from the startCell outwards, where for a given cell, each uncoloured or same-as-the-current-color-coloured neigbour is recursively explored (except the ones that form our current path to the origin) until we reach either the endCell or exceed the maxDistance.

An improvement on this strategy is that we don't explore from the startCell outwards to the endCell, but that we explore from both the startCell and endCell outwards in parallel, using Floor(maxDistance / 2) and Ceil(maxDistance / 2) as the respective maximum distances. For large values of maxDistance, this should reduce the number of explored cells from 2 * maxDistance^2 to maxDistance^2.

like image 21
Leon Bouquiet Avatar answered Sep 20 '22 09:09

Leon Bouquiet