Building a {r.oguelike} in R

An at symbol with arms and legs. It holds a shield in one hand and is raising a sword with the other.

Rogue… like?

There’s loads of video game genres: beat ’em up, platformer, rhythm, MMORPG, sports, puzzle. Have you heard of roguelikes?

The name is literal: they’re games that play like Rogue, a legendary dungeon-explorer from 1980 that set the bar for role-playing games.

Perhaps most recognisably, it used ASCII text as ‘graphics’: the player controls a character denoted by the at symbol (@), while floor tiles are made of periods (.), for example.

Screenshot of the game Rogue, which has graphics made entirely from ASCII characters. Several rooms are outlined with hyphens and pipes, with hashmark corridors between them. The player character is an at symbol. Some rooms have items in them, marked with various symbols. There's some commentary text printed above the map, which says '4 pieces of gold', and an inventory below, which says things like 'Hp 11.

Screenshot of Rogue via Thedarkb on Wikipedia

There are many interpretations of what exactly constitutes a ‘roguelike’, one of which is the strict ‘Berlin Interpretation’1. Must-haves include:

  • randomly-generated dungeons (a different map every time)
  • permadeath (it’s game over when you die)
  • turn-based battles (limitless thinking time, then one action)
  • grid-based (everything takes up one tile of space)
  • non-modal (all actions are possible at any time)
  • complexity (rich problem solving with items, characters and interactions)
  • resource management (items are limited and must be managed)
  • hack ‘n’ slash (kill lots of monsters)
  • exploration and discovery (find all corners of the map to solve problems)

These aren’t necessarily hard and fast rules—many games have added their own twist—but they provide the essence of the genre.

Like Rogue!

So, what would it take to make a roguelike using R?

I once made a tiny game-in-a-package called {ActionSquirrel}. You control an emoji squirrel on the R console, moving around a forest grid to collect randomly-placed nuts. Collect enough nuts to survive winter, which arrives within a certain number of turns, while avoiding a randomly-moving owl.

x <- ActionSquirrel::ActionSquirrel$new()
## 🌳 🌳 🌳 🌳 🌳 
## 🌳 🌳 🌰 🌳 🌳 
## 🌳 🌳 🌳 🐿 🌳 
## 🌳 🦉 🌳 🌳 🌳 
## 🌳 🌳 🌳 🌳 🌳 
## Moves: 0 
## Nuts: 0

That’s not far off from some of the roguelike requirements: it has randomness and permadeath, is turn-based and grid-based and has non-modality. But it’s missing complexity, resource management, hack ‘n’ slash gameplay and exploration.

And the aesthetic isn’t particularly… dungeony?

{r.oguelike}

So I started to build an R package containing an ‘engine’ for a game in the roguelike style, called {r.oguelike}.

You can visit the package website or look at the source code in the GitHub repo.

Hex sticker design for the 'r.oguelike' R package. Black background with bright green font, reminiscent of old computer terminal output. In the centre, a three-by-ten arrangement of hashmarks and periods, along with a single at symbol and dollar sign, which looks like a classic ACII tile-based roguelike game. The text 'r.oguelike' is underneath.

For now it’s just a toy to demo some possible approaches for some of the main game elements: ‘graphics’, movement, an inventory, item use and battling.

As ever, everything in the package is subject to change and improvement (though I also may never finish it). Consider this a quick devlog about progress so far.2

Install (or don’t)

You can install from GitHub to make the package available on your machine.

# install.packages("remotes")  # if not already installed
remotes::install_github("matt-dray/r.oguelike")

If you prefer, you can also play in your browser without having to install anything. I’ve set up a Binder instance of RStudio with {r.oguelike} preinstalled so you can just click the button below to launch it (it may take a moment to load):

Launch Rstudio Binder

This also means it’s possible to play this game from your phone, lol.

Demo

Note

The {r.oguelike} package is a work in progress and is developing at pace. Many things explained below may have been superseded or changed by the time you read this.

Top-tip: improve immersion by changing your console colour palette to dark mode, so it’s like you’re really inside a cave, wow.

To begin:

r.oguelike::start_game()

When you start, the console clears and the user interface is printed.

# # # # # # # # # 
# . . $ . . E . # 
# . . . . . . . # 
# . . . . a . . # 
# . . . . . @ . # 
# # # # # # # # # 
T: 25 | HP: 10 | G: 0 | A: 0
Press key to start
Input: 

At the top is a map of a dungeon room, made of floor (.) and wall (#) tiles. The room has randomly-selected dimensions from within a certain range. Within the room are randomly-placed characters and objects: the player (@), an enemy (E), and a collectible apple (a) and gold ($).

Below the dungeon room there’s:

  • a status/inventory bar, which gives a numeric value for Turns remaining, Hit Points, Gold and Apples3
  • a status message to provide information, usually to let the user know what has just happened
  • a prompt for the user to input a key press

The game is turn-based and begins when the user chooses a direction to move the player character. There are two methods for registering a key press:

In this demo, let’s aim first for the apple. The apple will return us 1 HP when consumed, so it’s a good idea to get it in our inventory as soon as possible. So, let’s input W and Enter (or press the up arrow if {keypress} is enabled).

# # # # # # # # # 
# . . $ . . E . # 
# . . . . . . . # 
# . . . . a @ . # 
# . . . . . . . # 
# # # # # # # # # 
T: 24 | HP: 10 | G: 0 | A: 0
Moved up
Input: 

The console will wipe the user interface will be re-printed. You’ll notice that your character has moved up one tile, the turn counter has decreased by 1 and the status message has changed to say ‘Moved up’.

Now we can move left to collect the apple.

# # # # # # # # # 
# . . $ . . E . # 
# . . . . . . . # 
# . . . . @ . . # 
# . . . . . . . # 
# # # # # # # # # 
T: 23 | HP: 10 | G: 0 | A: 1
Collected apple (+1 A)
Input: 

Again, you can see the player has progressed one tile and the turn counter decreased. You’ll notice that the inventory spot for the apple increased by 1 and the status message has changed to Collected apple (+1 A) so we know what happened.

What next? Let’s aim for the loot, signified by $ on the map. I’ll fast-forward to show you what happens after moving left twice and up twice.

# # # # # # # # # 
# . . @ . . E . # 
# . . . . . . . # 
# . . . . . . . # 
# . . . . . . . # 
# # # # # # # # # 
T: 19 | HP: 10 | G: 1 | A: 1
Found gold (+1 G)
Input: 

The player now occupies the space where the gold was and the turn counter has decreased by 4. You’ll see that the status messages has updated to Found gold (+1 G) and the gold spot in the inventory has increased by 1, but note that the amount of gold is randomly selected from a range of possible values.

There’s one obvious target left: the enemy character (E). So if we move right twice, we’ll start an encounter.

When you occupy the space of the enemy, you begin a turn-based battle. At the moment, this is actually an ‘auto-battler’: a routine is run under-the-hood where the player and enemy trade blows until one is vanquished.

Each character has attack and HP values. Of course, you can see that the player has 10 HP to start, but they also have attack strength of 2. The enemy character starts with a randomly selected HP value from within a range, and their attack strength is 1. The player attacks first, so will receive three points of damage from the enemy that has 4 HP, for example.

# # # # # # # # # 
# . . . . . @ . # 
# . . . . . . . # 
# . . . . . . . # 
# . . . . . . . # 
# # # # # # # # # 
T: 12 | HP: 7 | G: 1 | A: 1
You win! (-3 HP)
Input: 

So we know we won because the status message changed to You win! and a note of how many hit points we lost: (-3 HP). Concurrently the HP in the inventory bar has reduced by that amount.

Having lost some HP, we can add some back by consuming the apple, an action mapped to the number 1 key on your keyboard (regardless of whether you’re using {keypress} or not).

# # # # # # # # # 
# . . . . . @ . # 
# . . . . . . . # 
# . . . . . . . # 
# . . . . . . . # 
# # # # # # # # # 
T: 11 | HP: 8 | G: 1 | A: 0
Ate apple (+1 HP)
Input: 

So we get a message Ate apple and that our hit points have increased as a result: (+1 HP). Of course, this means that the apple spot in the inventory has decreased to zero. Note that the HP maxes out at 10, so eating the apple won’t raise the HP value above that.

This is the end of the demo: you’ve collected all the items and defeated the enemy. But I also added a lose condition, which occurs when you run out of turns.

# # # # # # # # # 
# . . . . . . . # 
# . . . . . . . # 
# . . . . . . . # 
# @ . . . . . . # 
# # # # # # # # # 
T: 1 | HP: 8 | G: 1 | A: 0
Moved left
Input: a
You died (max turns)! Try again!
> 

The game ends and the command prompt (>) returns.

Engine?

The technicals aren’t much to marvel at, really, but you can take a look at the code in the GitHub repo or on the website.

I called it an ‘engine’ earlier, but that was deceitful, lol.

It’s just a while loop that keeps running so long as the is_alive state is set to TRUE. So running out of turns sets the is_alive value to FALSE and the loop is broken.

The content of the loop is run after the player inputs a key press, which results in various counters being adjusted for the HP, etc. The loop concludes by printing the room with updated player locations, inventory bar and status messsage, ready for the next input.

The room itself is just a matrix. When you move the player, a small calculation is done to determine where the player character should be in the next iteration. Imagine the player is in the centre of a 3 by 3 room, i.e. they’re in position [2,2] of a matrix with x and y dimensions of 3. If they move down, that’s equivalent to adding 1 to their current position, so 5 + 1 = 6. Similarly, moving right would be equivalent to adding 3, so 5 + 3 = 8.

matrix(1:9, 3)
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    2    5    8
## [3,]    3    6    9

The code is pretty rough and you can see that the logic can start to become complicated quickly, but remember it’s just a demo for now.

Obvious improvements

There’s some obvious user-facing improvements to the features that are already in place:

  • interactive turn-based battles, where the user can choose what move to make (perhaps defensive moves, HP replenishment or magic)
  • enemy movement, so they aren’t just stationary
  • different enemy types, with differing ‘AI’ (random movement, ‘chase’ player, etc) and attack/HP stats
  • traps (e.g. certain tiles are hidden traps, some collectible items are bogus)
  • fog of war/vision cones (you can’t see what’s ahead until you get there, or you can only see a certain distance around you at all times)

There’s also a big-ticket item I haven’t touched: randomised or procedural dungeon generation. This is quite a big task and might end up as a blog post of its own. I encourage you to watch Herbert Wolverson’s talk at Roguelike Celebration 2020 for some ideas on this. At least at first, it could simply involve letting the player walk through doors to a few other rooms that contain randomised items.

On the back-end, I’ve so far written everything in base R; the only dependency is {keypress} to make inputs easier for consoles that support it. But there’s only so many if-else statements you can write before your brain explodes. To this end, I’m working in a branch to make use of the object-oriented approach of the {R6} package–as used in {ActionSquirrel}–to create general objects like enemies, rooms, etc, that should make it easier to handle and work with the elements of the game.

The future

The package will change and grow as I add stuff, so do check out the repo on GitHub for any updates that may happened since you read this post.

Update

You can now read about how I’ve generated and integrated (very simple) procedural dungeons into the package, replacing the rectangular rooms demonstrated above.

Obviously I’ll need some seed funding to set up my indie game company so I can begin making a cool 3D version of this. Oh, wait, mifekc is already on the case!

Alright, nevermind. How about, erm, a roguelike-themed Wordle? Oh wait, it’s already been done!

Might just take a nap instead, to be honest.


Session info
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value                       
##  version  R version 4.1.1 (2021-08-10)
##  os       macOS Mojave 10.14.6        
##  system   x86_64, darwin17.0          
##  ui       X11                         
##  language (EN)                        
##  collate  en_GB.UTF-8                 
##  ctype    en_GB.UTF-8                 
##  tz       Europe/London               
##  date     2022-05-09                  
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package        * version date       lib
##  ActionSquirrel   0.1.0   2021-10-07 [1]
##  blogdown         1.5     2021-09-02 [1]
##  bookdown         0.25    2022-03-16 [1]
##  bslib            0.3.1   2021-10-06 [1]
##  cli              3.2.0   2022-02-14 [1]
##  digest           0.6.29  2021-12-01 [1]
##  evaluate         0.15    2022-02-18 [1]
##  fastmap          1.1.0   2021-01-25 [1]
##  fontawesome      0.2.2   2021-07-02 [1]
##  htmltools        0.5.2   2021-08-25 [1]
##  jquerylib        0.1.4   2021-04-26 [1]
##  jsonlite         1.8.0   2022-02-22 [1]
##  knitr            1.38    2022-03-25 [1]
##  magrittr         2.0.3   2022-03-30 [1]
##  R6               2.5.1   2021-08-19 [1]
##  rlang            1.0.2   2022-03-04 [1]
##  rmarkdown        2.13    2022-03-10 [1]
##  rstudioapi       0.13    2020-11-12 [1]
##  sass             0.4.1   2022-03-23 [1]
##  sessioninfo      1.1.1   2018-11-05 [1]
##  stringi          1.7.6   2021-11-29 [1]
##  stringr          1.4.0   2019-02-10 [1]
##  withr            2.5.0   2022-03-03 [1]
##  xfun             0.30    2022-03-02 [1]
##  yaml             2.3.5   2022-02-21 [1]
##  source                                   
##  Github (matt-dray/ActionSquirrel@19cd339)
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.0)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.2)                           
##  CRAN (R 4.1.2)                           
## 
## [1] /Users/matt.dray/Library/R/x86_64/4.1/library
## [2] /Library/Frameworks/R.framework/Versions/4.1/Resources/library

  1. The Design Doc YouTube channel has a nice explainer.↩︎

  2. I’ve been watching indie game devlogs on YouTube by people like Sebastian Lague, Karl (ThinMatrix) and Dani. I’d love to see more of this sort of thing for R package development, like what Bruno Rodrigues has been doing on YouTube for the development of the {chronicler} package, for example.↩︎

  3. Why are they $ and a on the map, but G and A in the inventory bar? Good question. I’ll probably change that in future.↩︎