Polymorph

archived personal rust game procgen

About

The roguelike genre encompasses some of my favorite video games, and I’ve always wanted to make my own. I started work on Polymorph sometime in 2018. At the time it was based on the Roguebasin Python tutorial. After some work, I abandoned it and later revived it as a learning exercise for the Rust programming language in 2019. I’ve always had the belief that the best way to learn a new programming language or tool is to try to build something with it, rather than following tutorials or reading documentation.

The Rust version of Polymorph quickly eclipsed the scope of the original Python version. My vision for this new version was to make a traditional roguelike that was “accessible” in terms of gameplay and looked visually compelling despite having ASCII graphics; roguelikes are viewed as a niche genre for their complexity and their (lack of) visuals. My direct inspiration for this was the game Brogue.

A screenshot of Brogue
Brogue is a pretty game - screenshot from Rock Paper Shotgun

I worked on Polymorph for about a year before moving on due to lack of time from school. I was able to implement a lot of the visuals, and I started a little bit of the UI. Despite not finishing it, I still think I ended up with something interesting.

Features

Level Generation

One of the first things I worked on was player movement and level generation. A lot of traditional roguelikes use methods such as binary space partitioning to generate levels. These methods tend to produce more structured/square rooms like a dungeon or castle. I wanted something that was more organic - like the idea of delving into a deep cave. My level generator makes use of various layers of cellular automata to generate levels. The generator creates all features of a cave: cavern generation, lakes and lava pools, and foliage. Each of these world features are generated by a different ruleset for the generator. After all world features are generated, a staircase is placed somewhere in the level. Walking down this staircase leads to another level. An overview of the process is as follows:

  1. Populate the initial field of tiles and run several generations of the cave ruleset, then run a few generations of smoothing.
  2. Determine if there are any separated pockets of caves using a flood-fill algorithm. If there are, use pathfinding to carve our pathways between the different caverns.
  3. Place lakes using a separate lake ruleset. Run smoothing on the lakes and then run smoothing again on the caves (lakes make create some unwanted features or shapes in the cavern walls).
  4. Perform foliage growth using the foliage ruleset. Foliage (grass) grows based on the moisture level of the ground tile it is placed on (moisture level is just the distance between the tile and the nearest water tile). Foliage has several “stages” of growth, so thicker, taller foliage will grow closer to sources of water and thin out.
  5. Determine where to place the player and the staircase.
An example of Polymorph’s level generation
An example of level generation from an early version prior to shadow-casting and map memory. The player is in the center (@). Glimpses of a small lake and thick foliage can be seen towards the Northeast, and dead foliage can be seen to the Northwest along with the downward staircase (the red >).
Another example of Polymorph’s level generation
An example from a later version with shadows and map memory. Glowing mushrooms can be seen to the North, and a large lava lake to the East.

Lighting and Shadows

Lighting is usually an afterthought for most ASCII roguelikes. Typically, roguelikes only implement field-of-view shadow casting to restrict what the player can see. However, I implemented a per-tile dynamic lighting system that allows for multi-color lighting. This can be seen in the image above with the red glow of the lava and the blue lights from the mushrooms.

The lighting is recalculated every frame as all lights are considered dynamic - even if they don’t move or change strength. This is very expensive and not something you generally want to do. I was actually working on a system to differentiate between static and dynamic lights prior to dropping the project. However, in practice the performance hit isn’t noticeable because it’s still a relatively simple image.

The lighting system uses a “reverse” Dijkstra’s algorithm for lighting calculations. Essentially, the light wants to “run away” from its source. Each color channel is computed simultaneously when a node is visited in the algorithm. Dijkstra allows for multiple light sources to be computed at once which helps with the whole “every light is a dynamic light” thing - so for cases such as a lava pool where each tile is changing color and glowing, it’s perfect.

Tile Animations

Another graphical enhancement though small, adds a lot to the visuals is tile animations. This includes things such as flowing lava or shimmering water. At one point I was also working on a particle system for things such as smoke from fire (or fire itself), prior the abandoning the project.

Acknowledgements and Further Reading