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?
Note: image shows the objective of Flow Free, and it is the same objective of what I am developing.
Thanks for your help. :)
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.
Flow Free is a simple yet addictive puzzle game. It includes 300 free levels with puzzles appropriate for phones and for tablets.
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 n
xn
grid:
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:
f
.f
is at the minimum length (say 3 squares long), skip to the next iteration because we can't shrink f
right now.f
is next to a dot from another flow g
(if more than one g
to choose from, pick one at random)... 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.)g
into the empty square vacated by f
. Now there's an empty square where g
's dot moved 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.)f
's tail dot.The approach as it stands is limited (dots will always be neighbors) but it's easy to expand upon:
f
, looking for trickier ways to swap space with other flows...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
5 Random Results (sorry for the misaligned screenshots)
And a random 8x8 for good measure. The starting point was the same simple columns approach as above.
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 ;)
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:
So,
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.
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).
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:
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 } } } }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With