I just solved a fun little problem with my random puzzle generation. A little background first: the high-level algorithm for generating a puzzle is:
- Create an empty playfield grid.
- Place wall tiles at random until we reach our desired percentage of walls (say, 20%).
- Fill the entire rest of the playfield with lights.
- Remove lights until no light is visible to any other light, but all floor tiles are still lit.
- Place monoliths on some walls, indicating how many lights must be adjacent to that wall.
- Remove all the lights, then try to solve the puzzle. If we can do it without too much "guessing", then call it good.
- If we had to guess too much, throw away the puzzle and try again.
It can take 20-30 attempts to successfully get past that "guessing" check, but that's OK because that still takes 50ms or less. But here's the wrinkle: the classic version of this puzzle doesn't have a player character, you simply click on a tile to add or remove a light (space carrot). When I added a player character to my game, I decided that they wouldn't be able to cross over walls or monoliths, and in fact they can only walk on lit-up tiles, never dark tiles. Because of this new wrinkle, my puzzle generation has to do a reachability check at the end to ensure that all floor tiles are connected, otherwise you end up with sad unbeatable levels:
A straightforward way to do a reachability check is to use a flood fill algorithm, which is basically the paint bucket tool in MS Paint. Start at one floor tile, then paint outwards. After we're done painting, verify that every floor tile was painted. If not, we've got a reachability problem, we have to trash the puzzle and try again. The problem I was running into is that once my puzzles reach 10x10 tiles or so, the puzzle generator was failing the reachability check sometimes 2000 times in a row without successfully generating a puzzle, and that was taking much too long.
You might have already come to the same conclusion that I did: instead of one reachability check at the end, how about we do a reachability check after we place each wall tile, and move the wall if the check fails? This is a great approach, I love the levels it generates:
But, the downside: this is super slow, since we're doing a flood fill every time we try to place a wall (and as the board gets crowded, it could take many attempts to find a suitable spot for each new wall). At least, it was super slow with my naive initial flood fill algorithm, resulting in this sad debug logging:
Succeeded in 32 attempts with wallPct = 25. Spent 35.23 seconds total, avg of 1.1 seconds per attempt.35 seconds to generate a level! Ouuuuuch. But we can fix it! According to Jimbo's Crowd-Sourced Encyclopedia, a great optimization is the
scanline fill. Basically, rather than cycling through one pixel at a time throwing all neighboring pixels on the queue, attack each horizontal run of pixels all at once in an inner loop. A few minutes banging on the keyboard later, and we get this new timing:
Succeeded in 32 attempts with wallPct = 25. Spent 0.034 seconds total, avg of 0.001 seconds per attempt.Great success! We can definitely live with 32 attempts at "0.001 seconds per attempt". I threw
the flood fill code into a gist, in case anybody is interested.