Hi everyone, I just released a little puzzle game in which the solutions become the tiles of the next level.
It was actually quite challenging finding a good type to represent a level, as it depends on the prior levels.
The type I came up with was
type alias Game =
{ stage : Stage
, levels : Dict String (Dict Int SavedStage)
, ...
}
type alias Stage =
{ grid : Dict ( Int, Int ) Cell
, gridSize : Int
, ...
}
type alias SavedStage =
{ connections :
Dict
RelativePos
{ from : RelativePos
, originId : Int
, path : List RelativePos
}
, paths : Dict RelativePos { origins : Set Int }
, grid : Dict RelativePos Cell
, gridSize : Int
, level : Level
}
A single puzzle of a level is a Stage
– multiple stages form one level. Once a player solves a stage, I compute everything I need to use it as a tile. In particular, the connections
that tells me where the power is flowing and paths
that tells me which pixels need to be lit up if power is coming in from a specific direction. Additionally, I also store the original grid, so that I can turn a SavedStage
back into a Stage
, if the user wants to change their solutions.
Another trick that helped me do fewer mistakes was to introduce a wrapper type for the relative positions:
{-| Comparable wrapper type for (Int,Int) -}
type alias RelativePos =
( ( Int, Int ), String )
fromTuple : ( Int, Int ) -> RelativePos
fromTuple pos =
( pos, "RelativePos" )
A RelativePos
is comparable, so I can use it as keys in a dict. A relative position can be turned into a direction, and a direction can be added to a position. This way, I made sure that I never mix the positions of the puzzle-tiles with the positions of the pixels in a tiles.
Developing a recursive game was quite challenging. I got into a lot of off-by-one errors and state explosion problems. But I also learned a lot. You can check out the source code if you are interested.
Thanks for reading this little post, and I hope you liked the game.