I built a game! Play it on itch.io, view the source on GitHub
The theme for the #gamedev
channel’s March/April gamejam was randomness. I was excited to see that the Roman Mytholojam was also running during the same period, so I decided to dual-submit a game that was both Roman and random. Apparently it’s a combination worth revisiting
I took my inspiration from the myth of Romulus and Remus and added some procedural generation. The goal is to get as far possible downstream and reach a wolf without hitting an obstacle.
Below are some interesting decisions I had to make, lessons that were learned, and things I couldn’t figure out in the alloted time.
Modeling the river and obstacles
This was an interesting challenge. I needed to be able to model an “infinite” river. I also needed to be able to procedurally generate more of the river as the player moves down.
My solution was to model the river as a list of “sections”, each of which could be generated randomly. When the player is less than a preset distance from the end of the last section, then I generate a new one.
Every section starts with an optional wolf, followed by a variable number (minimum 4) of log groups. Logs are grouped vertically and can be at either at the top, middle or bottom of the river. The type ended up looking like:
type alias Section =
{ save : Savepoint
, b1 : ObstacleArrangement
, b2 : ObstacleArrangement
, b3 : ObstacleArrangement
, extras : List ObstacleArrangement
, last : ObstacleArrangement
}
type ObstacleArrangement
= TwoTop
| TwoBottom
| OneTop
| OneBottom
| OneMiddle
| TopAndBottom
| ClearWater
type Savepoint
= TopWolf
| BottomWolf
| NoWolf
Invalid transitions
I wanted to make sure the user always has a path forward and that the random generator wouldn’t generate sections that were impassable since that would kill the fun. I couldn’t quite define what “has a path forward” meant other than “I’ll know it when I see it”. Since there are only 7 configurations, that means there are only 7x7 = 49 possible pairs. That’s few enough that I decided to brute force it by hand.
I ended up drawing all the possible combinations, noting which ones were valid and invalid.
While I could have used insights from this diagram to create an algorithm that could calculate valid next configurations, I decided to go the naive route and just express the diagram as a big case statement.
Random generators with dependencies
Due to the requirements mentioned previously, randomly generating a new obstacle configuration required knowing the previous configuration. To the generate that list of extras
in a section, I needed to write a list generator using a combination of recursion and Random.andThen
.
For the section generator, normally I’d write an andMap
-based solution like:
constant Section
|> andMap savepoint
|> andMap obstacleArrangement
|> andMap obstacleArrangement
|> andMap obstacleArrangement
|> andMap obstacleList
|> andMap obstacleArrangement
but that doesn’t work since I need to pass the previous arrangement into each step. andMap
only works for independent rolls and these rolls are definitely dependent. Sounds like a job for andThen
! That works but accumulating the values along the way is painful.
This got me thinking: what if I could create a generator type that came with a “lookback” functionality and always carried the value of the previous computation?
This allowed for a fairly nice generator that looks a lot like the original andMap
implementation:
section : ObstacleArrangement -> Generator Section
section arr =
Random.Lookback.constant arr Section
|> Random.Lookback.andMap savepoint
|> Random.Lookback.andMapBoth obstacleArrangement
|> Random.Lookback.andMapBoth obstacleArrangement
|> Random.Lookback.andMapBoth obstacleArrangement
|> Random.Lookback.andMapList obstacleList
|> Random.Lookback.andMapBoth obstacleArrangement
|> Random.Lookback.toNormalGenerator
However things were a bit off. For one, the signature of Random.Lookback.andMap
was way off. It should match the same pattern as other andMap
functions.
Secondly, the implementation of Random.Lookback
was an awkward combination of nested andThen
's and map
s.
It felt like lookbacks were a valid concept but that the way I’d try to model things just weren’t playing nicely with random generators. So I took it a step further and experimented with a separate Lookback
type with an associated Lookback.andThen
function that could get called in combination with Random.andThen
.
This still didn’t quite work so I gave up. I think there was a valid solution in that area but I wasn’t able to find it.
Perhaps I would have been better off just chaining basic Random.andThen
and dealing with the accumulation instead of looking at these fancy solutions. I would certainly have saved multiple hours on the project!
Graphics
I used evancz/elm-graphics for the rendering. To enable me to focus on game mechanics, I had colored rectangles for most of the development process. This worked exactly as intended. I’d strongly consider taking this approach again on my next project. It’s so easy to spend all your time on the graphics side of the game.
I ended up with the opposite problem. I still didn’t have any “real” graphics by the last day of my (personal) deadline. Inspired by this article, I decided to see what I could do by overlapping a few basic colored shapes.
I was pleasantly surprised with the result. While super basic, I was surprised at what you could do with a couple colored circles and a rectangle. The logs in particular turned out well.
Monomorphic transformations
My game type is defined as:
type Game
= Intro
| Playing State
| Lost State LossReason
| Won State
When processing a game tick, I want to apply a lot of transformations, several of which can result in the game being won or lost. Should that happen, the remaining transformations should be ignored.
I ended up defining map
and andThen
for my Game
type which allowed for really nice pipelineing:
tick : Time -> Game -> Game
tick diff game =
game
|> map (moveTwinsDownstream diff)
|> andThen checkLoseCondition
|> andThen checkArrivalOnBank
The interesting thing here is that map
and andThen
were monomorphic. That is to say that the function they take was Game.State -> Game.State
, not a -> b
. I’ve done this for map
functions before but this is the first time that I’ve done so with an andThen
.