Down the River - Elm game jam - (March/April 2018 - Randomness)

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 :laughing:

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.

down-the-river-with-images

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.

consecutive-obstacles-red-and-green-arrows

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 maps.

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! :laughing:

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. :sweat_smile: 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.

down-the-river-no-graphics

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.

8 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.