17

I've encountered an interesting problem while programming a random level generator for a tile-based game. I've implemented a brute-force solver for it but it is exponentially slow and definitely unfit for my use case. I'm not necessarily looking for a perfect solution, I'll be satisfied with a “good enough” solution that performs well.

Problem Statement:

Say you have all or a subset of the following tiles available (this is the combination of all possible 4-bit patterns mapped to the right, up, left and down directions):

alt text http://img189.imageshack.us/img189/3713/basetileset.png

You are provided a grid where some cells are marked (true) and others not (false). This could be generated by a perlin noise algorithm, for example. The goal is to fill this space with tiles so that there are as many complex tiles as possible. Ideally, all tiles should be connected. There might be no solution for some input values (available tiles + pattern). There is always at least one solution if the top-left, unconnected tile is available (that is, all pattern cells can be filled with that tile).

Example:

Images left to right: tile availability (green tiles can be used, red cannot), pattern to fill and a solution

alt text http://img806.imageshack.us/img806/2391/sampletileset.png + alt text http://img841.imageshack.us/img841/7/samplepattern.png = alt text http://img690.imageshack.us/img690/2585/samplesolution.png

What I tried:

My brute-force implementation attempts every possible tile everywhere and keeps track of the solutions that were found. Finally, it chooses the solution that maximizes the total number of connections outgoing from each of the tiles. The time it takes is exponential with regard to the number of tiles in the pattern. A pattern of 12 tiles takes a few seconds to solve.

Notes:

As I said, performance is more important than perfection. However, the final solution must be properly connected (no tile pointing to a tile which doesn't point to the original tile). To give an idea of scope, I'd like to handle a pattern of 100 tiles under about 2 seconds.

Trillian
  • 6,207
  • 1
  • 26
  • 36
  • What are the tiles with red lines? – NullUserException Jul 26 '10 at 01:35
  • @NullUserException Those are the tiles that cannot be used in this particular example. The tileset doesn't always contain all possible tiles, it would be too easy! I'll make it clearer. – Trillian Jul 26 '10 at 01:37
  • @Trillian Oh bummer. What does "number tile complexities" mean? – NullUserException Jul 26 '10 at 01:44
  • @NullUserException Yeah that's not clear either. I meant the sum for each tile of the number of directions that tile is connected. I'll rectify. – Trillian Jul 26 '10 at 01:53
  • So depending on the available tiles and the desired pattern, you might not get an answer after all? – NullUserException Jul 26 '10 at 01:59
  • Yes, it's possible that there are no possible solutions. The algorithm should be able to deal with that. However, in my use case, I'll try to always have the unconnected tile available as a fallback. – Trillian Jul 26 '10 at 02:08
  • So the tiles don't *have* to be connected? – NullUserException Jul 26 '10 at 05:11
  • Someone should make a code-golf spinoff of this. It'd be very interesting, haha. – Warty Jul 26 '10 at 05:20
  • NullUserException, it would seem not. Given a non-degenerate set of tiles, though, I suspect that one can discard disconnected tilings without too much expense on average -- the level isn't fixed. – user382751 Jul 26 '10 at 05:47
  • The solution must not have tiles that point to another tile while that other tile doesn't point to the first tile. However, it doesn't have to be fully connected. For example, unconnected dots everywhere is a valid solution (if that tile is permitted), although it is also the worse-case one. – Trillian Jul 26 '10 at 13:33
  • @ItzWarty I thought about marking this code-golf, but I'm interested in readable answers/code, which code golf doesn't typically yield. – Trillian Jul 26 '10 at 13:35
  • Your question seems to be missing its images. Is there any chance you could replace them with something that still illustrates the problem? – Ilmari Karonen Mar 13 '16 at 16:59
  • Can you paste images here in your question? Seems that links are broken. – Przemyslaw Remin Feb 15 '19 at 09:46
  • 1
    @PrzemyslawRemin, sorry but I asked this 9 years ago, I don't have the source material anymore! – Trillian Feb 16 '19 at 16:56

3 Answers3

6

For 100-tile instances, I believe that a dynamic program based on a carving decomposition of the input graph could fit the bill.

Carving decomposition

In graph theory, a carving decomposition of a graph is a recursive binary partition of its vertices. For example, here's a graph

1--2--3
|  |
|  |
4--5

and one of its carving decompositions

     {1,2,3,4,5}
     /         \
  {1,4}        {2,3,5}
  /   \        /     \
{1}   {4}  {2,5}     {3}
           /   \
         {2}   {5}.

The width of a carving decomposition is the maximum number of edges leaving one of its partitions. In this case, {2,5} has outgoing edges 2--1, 2--3, and 5--4, so the width is 3. The width of a kd-tree-style partition of a 10 x 10 grid is 13.

The carving-width of a graph is the minimum width of a carving decomposition. It is known that planar graphs (in particular, subgraphs of grid graphs) with n vertices have carving-width O(√n), and the big-O constant is relatively small.

Dynamic program

Given an n-vertex input graph and a carving decomposition of width w, there is an O(2w n)-time algorithm to compute the optimal tile choice. This running time grows rapidly in w, so you should try decomposing some sample inputs by hand to get an idea of what kind of performance to expect.

The algorithm works on the decomposition tree from the bottom up. Let X be a partition, and let F be the set of edges that leave X. We make a table mapping each of 2|F| possibilities for the presence or absence of edges in F to the optimal sum on X under the specified constraints (-Infinity if there is no solution). For example, with the partition {1,4}, we have entries

{} -> ??
{1--2} -> ??
{4--5} -> ??
{1--2,4--5} -> ??

For the leaf partitions with only one vertex, the subset of F completely determines the tile, so it's easy to fill in the number of connections (if the tile is valid) or -Infinity otherwise. For the other partitions, when computing an entry of the table, try all different connectivity patterns for the edges that go between the two children.

For example, suppose we have pieces

                       |
.    .-    .-    -.    .
     |                 

The table for {1} is

{} -> 0
{1--2} -> 1
{1--4} -> -Infinity
{1--2,1--4} -> 2

The table for {4} is

{} -> 0
{1--4} -> 1
{4--5} -> 1
{1--4,4--5} -> -Infinity

Now let's compute the table for {1,4}. For {}, without the edge 1--4 we have score 0 for {1} (entry {}) plus score 0 for {4} (entry {}). With edge 1--4 we have score -Infinity + 1 = -Infinity (entries {1--4}).

{} -> 0

For {1--2}, the scores are 1 + 0 = 1 without 1--4 and 2 + 1 = 3 with.

{1--2} -> 3

Continuing.

{4--5} -> 0 + 1 = 1 (> -Infinity = -Infinity + (-Infinity))
{1--2,4--5} -> 1 + 1 = 2 (> -Infinity = 2 + (-Infinity))

At the end we can use the tables to determine an optimal solution.

Finding a carving decomposition

There are sophisticated algorithms for finding good carving decompositions, but you might not need them. Try a simple binary space partitioning scheme.

user382751
  • 1,213
  • 8
  • 10
  • Wow, that looks quite smart! I'm unfamiliar with most of the material here, so I'll have to read it a couple more times to get it down and see if I can come up with an implementation based on this. – Trillian Jul 26 '10 at 13:24
  • It's been a couple of day and, while this is a solution with interesting potential, I'm still not too sure how that stuff works. Googling "Graph Carving" isn't too helpful either. Could you point me to an article on the matter? – Trillian Jul 29 '10 at 18:33
  • "Carving decomposition" should be a better query. This looks like a reasonable introduction: http://www.cs.brown.edu/courses/cs250/lectures/19.pdf – user382751 Jul 29 '10 at 23:13
4

As a base, take a look at an earlier answer I gave on searching. Hill-climbing search programs are a tool every programmer should have in their arsenal as they work much better than plain brute-force solvers.

Here, even a relatively bad search algorithm has as an advantage the fact that it won't generate illegal boards, greatly reducing the expected run time.

Community
  • 1
  • 1
Borealid
  • 95,191
  • 9
  • 106
  • 122
  • I don't think A* can be applied here as I have no way of knowing if I'm approaching the solution. To use Dijkstra's algorithm, I'd need to be able to assign different movement costs between states of the graph. Could you be a bit more precise on how to apply a search algorithm here? – Trillian Jul 26 '10 at 02:30
  • @Trillian Dijkstra isn't a search algorithm, it's a path-cost one. If you can't come up with a heuristic you can't do A-star, but I'm not convinced none exists; for example, try "total complexity of connected placed tiles". As that is maximized, you approach a solution. DFS/BFS can be used here directly - they're just a way to avoid having to try *every* board and instead only try the *legal* boards. – Borealid Jul 26 '10 at 04:24
  • @Borealid Right, I hadn't thought about that heuristic. Your approach is interesting, I'll give it a try. Just a question: in the worse case, if there's no solution, won't the pathfinding approach be as slow as the brute-force approach? – Trillian Jul 26 '10 at 13:28
  • @Trillian: no, because brute-force will try *every* possibility, whereas search will only build boards which are incrementally correct. That is to say, brute force builds every possible combination of tiles, where search only builds boards which can be formed by repeatedly adding tiles and having the rules be correct at every step. – Borealid Jul 26 '10 at 13:33
  • @Borealid: Right, my algorithm didn't do that. It would not continue attempting to place tiles if previous tiles were in invalid positions. Maybe it wasn't so much brute-force, after all. – Trillian Jul 26 '10 at 13:39
  • @Trillian Then what you build was a BFS or DFS search :-). You can still improve it by better pruning, or heuristic search, but unless you have a problem-specific solution you won't get any better **worst-case** performance. – Borealid Jul 26 '10 at 14:05
2

I think I may have a better idea. I didn't test it, but I'm pretty sure it will be faster than a purely brute-force-ish solution for large zones.

First, create an empty set (a "set" being a collection that only contains unique objects) of nodes. This collection will be used to identify which tiles have broken connections that need to be fixed.

Fill the data structures to represent the board with the pieces that are available, using the ones you see the most fit based on your personal criteria with no regard to the correctness of the solution. This will almost certainly lead you to an invalid state, but it's okay for now. Iterate through the board, and find all tiles that have connections leading to nowhere. Add them to the set of broken tiles.

Now, iterate through the set. Change the tiles it refers to by reducing their number of connections (otherwise you could get into an infinite loop) so they have no broken connection, respecting the currently available pieces. Check their neighbors again, and if you broke connections to other tiles, add these to the set of broken ones too.

Once the set of broken connections will be empty, you should have a fine-looking pattern. Note however that it has an important caveat: it might to tend to oversimplify patterns, since the "fixing" phase will always attempt to reduce the number of connections. You may have to be lucky to get interesting patterns since this could be greatly affected by first piece you put on each tile.

zneak
  • 134,922
  • 42
  • 253
  • 328
  • Hmm, this should fit the performance criterion quite well. I'll test to see if it yields interesting results in practice. – Trillian Jul 29 '10 at 22:28