Ironwood, a Roguelike Game in 7 Days
« Extracting Immutable Objects
» Distributed ID Generation and Bit Packing
Games: 7drl, game design, game development, projects, roguelikes
Ironwood is a roguelike game developed for the 2014 7 Day Roguelike Challenge. “Roguelike” is a pretty poorly defined genre, but generally speaking it’s a turn-based game with generated environments, emergent gameplay from simulationism, and a steep learning curve. (For more, see an informal definition, a formal attempt, or a current take on its broad use.)
The 7DRLC is an annual worldwide attempt to create a roguelike game within 7 days. It’s a personal challenge rather than a strict contest with winners and losers, so developers may bring in lots of previous code or start from scratch, and their goal is more often to experiment with a new idea than make a polished game.
Ironwood is my first roguelike game. I went in with a strong idea of what I wanted to develop — as I should, it’s been gathering dust since I tried and failed to code it years ago, when I got hung up on a bug getting 256 colors from terminals <ominous music here>. So I was starting with well-marinated ideas for gameplay and some old code that could display a world and track limited fields-of-view.
I really wanted Ironwood to be a game about sneaking around guards (“Renfields”) to slay vampires for their powers. As you gathered their abilities you’d hibernate to “lock them in” and the city you were doing this in would grow and change, sort of a progressive world generation. Combat wouldn’t be a hit point-based bumpfest but about moving in patterns to activate your powers or attacks.
But that’s way, way too many features to finish in 7 days. I wanted to start with the core gameplay, sneaking around guards, and incorporate whatever else I had time to do. Success is defined by making a playable game, but the scale of that game was totally open.
If you want to see what I came up with you can drop down to “The Results” heading for a description and downloads, or read on for the process.
Remember the ominous music? I’m using a different terminal program than I was back in 2009 (was XTerm, now urxvt) and the version I defaulted to didn’t support 256 colors. This was kind of a ridiculous hassle to start off with, but I found a Ruby library (Dispel) to insulate me from the pain of Curses, a ~35 year old interface for controlling text terminals. They stopped me before, but I was off to a brisk start. <more ominous music>
My old code was working fine. I could move my character around the world, and quickly created guards that stood in one place looking off into the distance. Then I gave them some AI. If you blundered into their field of view, they’d chase after you. If they heard you running, they’d come investigate the sound. Then, if they saw you, they would call for help and any guard who hadn’t seen you already would run to that noise. This all works on a pretty simple state machine that transitions between standing guard, chasing a seen player, investigating a noise, or returning to post.
I figured running is about twice the speed of walking, so as you move around you have to move, rest, move, rest to keep from making noise. This was a little tedious, so I bumped it up to allow you to move twice before you make a noise. This also had an element of risk-taking: if you rest-move-rest you have an extra movement available if you come around a corner and see a guard coming, but if you rest-move-move-rest you don’t have that wiggle room. So the game sort of had creeping, walking, and running without extra movement keys or a ‘run’ mode. It felt very nicely integrated into typical roguelike commands.
Movement combined really nicely with the AI systems, actually. You could make a noise, fall back behind a corner, wait for the guard to find nothing, then hustle to catch them as they walked back to post. Move too fast, though, and you’d be scrabbling away as they turned back in curiosity.
Noise integrates really well with the guards. They have a little nested state machine to their walking so that they alternate moving and resting like the player. (They even have another one that has them glancing left and right as they take those rests, but it’s not really clear what’s happening unless you see one walk in a straight line. It also makes them harder to sneak up on… maybe it’d be improved if they peeked much less frequently.)
When a guard chases a player or a noise, though, their move-move-move to their destination makes running noises the player can hear. And that other guards can hear — it’s quite common to make a noise, hide from the guard who investigates, then hide from the guard who investigates that guard’s noise. Two guards can even oscillate a while as they run up and down a hallway chasing the noise each other made running to the opposite end. I didn’t want to spend time coding them to recognize/remember that the noise down the hall was another guard, but that could be a nice improvement. Overall I’m really satisfied with how complex effects comes out of my simple systems interacting.
Around this time I was surprised to realize I was enjoying the game when I fired it up to test something. I was genuinely startled and creeped out the first time a guard came over to investigate, then worried when I realized I’d left myself nowhere to retreat to. And I was only trying to see if the audio worked at all!
The guards were too good at investigating, actually. Without the perfect outcropping or door to tuck behind, any noise meant game over. Guards and the player share most of their code, so the player was outnumbered, couldn’t see the guards coming, and couldn’t fight back. I like having an even playing field and making the player use their brans, but this was unfair. I’ve played a lot of stealth-based games like Thief, Art of Theft, and Monaco, but I never noticed how much extra information or abilities they give the player until I started making this game. So I put my thumb on the scales: reduced the guard’s sight radius, increased the player’s, and decreased the distance noises travel. This made the game playable but difficult, a decent place for a designer to aim for because it’s unlikely they’ll be as good as an experienced player.
Game balancing aside, I got moving on generating the world. I did some research and saw a picture of the kind of layout I’d like to have (the screenshot labeled chambers) and spent a couple hours coding something to produce similar output. This wasn’t at all my vision of a living, evolving city, but my strategy for this project was to do the simplest thing that could possibly work and revisit improvements as necessity demanded and time permitted. Lacking this compromise is what scuttled my last timed game attempt (though I’ll be getting back to that before too much longer). This willingness to blitz code I knew wasn’t right and that I’d likely redo is why I was able to finish Ironwood: every time I lost time to bugs or outside obligations, I could drop an improvement without dropping a part of the game.
This did bite me a bit, though. I dropped in a 3rd-party library for pathfinding and suffered from its bugs. If I had guessed it wasn’t up-to-snuff earlier I could’ve replaced it or rewritten it in the time I spent kludging around it, trying to hide the problems.
One thing that went very right, though, was from some advice I read on writing a 7DRL. I kept the my last day for playtesting on a friend’s couch. The feedback I got was excellent; I knew the game was hard but I didn’t realize that it was basically impossible if you didn’t know the details of how the AI works. This led to a lot of tweaks in level generation to give the player better odds and two big changes. First, I gave the player more ability to move quietly – you can move four times without resting without making a noise. The game was just way, way too hard without this. Second, the addition of smokebombs to stun guards. I never would have thought up such an offensive ability, but it gives the player a limited chance to recover from their mistakes.
After that quick addition and some last-minute bugfixes, I released the game right at the 168-hour mark. Success!
<ominous music comes to a horrifying crescendo> Another friend was trying to play Ironwood from his computer. He could get it installed, but it only displayed a black screen. Yes, it was another major bug related to colors in the terminal. After giving up on 256 colors, I had shipped a game in the curious condition that it could only use 16 colors but required a 256 color terminal. Few people have that set up properly, so the game was black-on-black.
After some attempts to fix this in the last few days, I cut the game down to eight colors and I think it will work anywhere… but I don’t want to deal with debugging terminal code for at least a year. It’s endlessly frustrating to wrestle wity incompletely-supported, under-documented ancient standards in the hopes of low-quality output. Gary Bernhardt has an excellent 20m talk on this kind of infrastructure.
No vampires, no city. The player sneaks around an elaborate catacomb filching treasure from beneath the noses of guards, creeping along with limited vision and open ears. Perhaps the player even knocks out the guards… but if they catch sight or sound of the player, it’s time to run and hide, or flee to the next deeper level of the dungeon.
I’m very pleased to have successfully finished a game as part of this event, and I’m proud of the result. It’s not incredible, but I enjoy playing it.
To play, you’ll need to download the source code and see the
README.md to install it. I thought I would use Releasy to make executables, but it doesn’t (yet?) support Ruby 2.1 and the Dispel library (sorry I ever added a dependency on that dang thing…) needs the curses gem from 2 (<ominous music returns for an inappropriate extra crescendo seriously just fuck everything about terminals>). I don’t have the patience try to work around that or replace Dispel (which would be the best course).
On the off chance you play the game, please email me or leave a comment; it seems pretty unlikely anyone will, so I’d love to hear of it. Even it you don’t have fun. :)