Saturday, March 11, 2023

7DRL 2023: Lurk, Leap, Loot

Once again, I've participated in the Seven-Day Roguelike Challenge. My entry's called Lurk, Leap, Loot. It iterates on the stealth gameplay I've developed over previous 7DRL entries.


In the lead-up to the game jam I had several different game ideas from my notebook that I was developing. I came very close to doing a version of Paradroid, an old C64 game. But I changed my mind at the last minute and decided to return to stealth mechanics. When I mentioned this on the roguelikedev subreddit, Damien Moore contacted me asking if I'd like to collaborate. We made this game together and it turned out much better than I would have been able to do on my own. Damien created a new tile set and did the entire audio end of things. The game has music, sound effects, and voiced dialogue! Evan Moore (Damien's son) recorded the guard voices; it adds a lot of character to the game.

More importantly, he proved an invaluable design partner. One of the things he pushed for was for guards to move on predictable patrols rather than wandering semi-randomly. It took me about a day and a half to come up with a patrol route generation system, but I think it worked out pretty well.

A game ideally gets the player into a state where they are absorbed in anticipating and predicting what will happen, and then correcting based on their observations of how things actually play out. Ideally not only on a moment-to-moment level but also on longer time-frames. Patrol paths serve this purpose; players can watch, plan, and then try to execute their plan. It doesn't always go as expected, which is what keeps it interesting.

The patrol path generator shuffles a list of all the room adjacency pairs, then adds each pair if each room has zero or one edges already connected to it. This produces long chains and sometimes loops, with the occasional room that gets left out. To improve on this, I first chop up long chains into pieces to get down to a target length, and then insert side trips to the left-out rooms. The end result is that every room is on a single patrol path. Here's an example; I've hand-drawn over the patrol routes:


The lower left route is a loop; the rest are lines that the guards go back and forth along. Longer routes mean a lower overall guard density; this is from an earlier level in the game so the routes are allowed to be long. At the endpoints, the algorithm looks for a point of interest in the room for the guard to stand by for a few turns. Highest priority is windows: if there are any windows, the guard will stand looking out of one. Next highest priority is sitting in a chair, then standing near loot. As a final resort the guard will attempt to stand two spaces in from the door, which gives the player the opportunity to sneak into the room from behind them. This is important for some of the tiny dead-end rooms.

The other big design challenge I set myself was to try to move from the eight-way movement of ThiefRL2 to four-way movement. Skyrogue was an inspiration. Guards are still allowed to move diagonally if the space is clear on either side of the move. The player, instead, has a leap move. In my case it moves two spaces instead of one, and can go over some obstacles. The eight-way movement had given the player a speed advantage by allowing diagonal movement even if the spaces on either side were solid. This is similar to how in Pac-Man the player character can get around corners slightly more quickly than the ghosts. The leap move gives the player the advantage when moving in a straight line, which is a contrast to the corner-cutting.

Lastly, I ported in torches that can be lit or doused, from the very first ThiefRL game. I'd left it out of ThiefRL2 in the interests of simplicity, but it gives players a mechanism with a few tradeoffs. First, guards really want all the torches to be lit, so they'll interrupt their patrols to do that. This can be used to slow them down a bit if you need to do something elsewhere on their route. Standing in a lit room allows you to be seen from much farther away, so having torches unlit is good from that perspective. On the other hand, torches allow you to see which floor boards are creaky, so there's a slight reason for having them lit.

This is my first game made in TypeScript. Last year's 7DRL entry was written in JavaScript. In the leadup to this year's jam I ported it to TypeScript and then ported a bunch of my ThiefRL2 code from the C++ and Rust versions. It mostly went smoothly. The one gigantic gotcha in TypeScript/JavaScript is that any data type beyond a boolean or number will be passed around by reference instead of by value. I used 2-dimensional coordinates extensively throughout, and more than one time accidentally ended up pointing at a coordinate stored in something when I intended to copy it. It's ironic that scripting languages like Python or JavaScript try to insulate programmers from pointers, but then hit you with this. Languages that are clearer (like Rust) or simpler (like Haskell) avoid the problem.

I also used Parcel.js to run a server that automatically rebuilds whenever you save a source file. It worked great about 95% of the time, but whenever we did something like merge code in Git, it would break down. The fix appears to be clearing its .parcel-cache directory, and I got into the habit of doing that regularly. Feels kind of junky; you don't ever quite trust that you're seeing the version of the program that you just edited. I would make trivial changes in a place where it'd be immediately obvious if the program had updated.

We used Howler.js to play audio, although Damien did all the work of setting that up and using it. One thing we discovered while testing on various platforms is that Safari does not have built-in support for playing Ogg files. They aren't new so it's kind of annoying.

Overall I did not get anywhere close to the amount done on the fundamental game design as I would have wished. It still feels very much like the previous games in its series. On the final day I did a little bit of work to try to give the game an ending and a score, but it's very basic.

However, I'm happy with how it turned out. It's fairly polished and fun. The sound effects for player actions add a ton of great feedback; it's very satisfying to collect loot, based on the sound alone. (It's a bunch of Australian 50-cent pieces in a sock.)

2 comments:

Jonathan Sharman said...

This was fun! In comparison to the diagonal movement in previous incarnations, leaping feels really strong, especially since you can leap over a single guard indefinitely (poor little guys).

> It's ironic that scripting languages like Python or JavaScript try to insulate programmers from pointers, but then hit you with this. Languages that are clearer (like Rust) or simpler (like Haskell) avoid the problem.

I feel that. I can't believe how implicit mutable aliasing is in those languages and how awkward it is to deep-copy even simple types.

James McNeill said...

Hi; thanks!

Yes, a lot of people say the leaping is over-powered. I‘ve tried a stamina model for restricting it but didn’t feel like that was necessarily the right approach. I think it might be live over at mcneja.github.io at the moment. I’ve got another idea to try, if I ever get a spare hour, which is to disallow leaping through doors or onto doors, chairs, tables, bushes, etc. Basically make leaping something that has to land on open space. I may also fix the bit that keeps guards from hitting you if you jump over them.