It has always been a dream of mine to create a text-based game (a.k.a interactive fiction or text adventure). Text-based games were common in the 1980s when I was a kid, and personal computers were lacking in high-end graphics. The idea is using (almost) only ASCII text to present the player with the game and to input her moves and progress. Famous text-based games are Zork and the Hitch Hiker’s Guide to the Galaxy, based on the book by Douglas Adams.

I’ve never thought of text-based games in the context of R though. Until recently, when Bram Ramaekers approached me after reading my blog and suggested I’d read this post about R pranks by Gabriel Vasconcelos. In this post, it is shown how to override one of R’s functions on someone’s laptop causing it to behave unexpectedly. It reminded me of how much “open” and friendly R is, and I thought R’s console could be a great platform for a text-based game. I also saw the opporunity to do this the Object-Oriented way, using the new (to me) R classes system - R6. And while I was at it, I was going to make that game about R. It would have riddles to test the player’s skills in base R (though I made the game configurable enough to be “charged” with riddles about any R topic or even general knowledge trivia - see later). I’d like to see such games contributing to the very important trend of teaching R and other programming languages in a fun and interactive way (see Swirl and learnr).

Without further ado, I present to you: “The Castle of R”. If you wish to play the game - the rest of this post would be extremely boring for you. Head over to my Github page, download the CastleOfR package, load it and startGame().

The following are some of the major challenges I faced while building the game.

Learn to Listen

The first thing I needed for a text-based game is a way to “listen” to what the user is “saying”, while in the console. I asked for help in Stack Overflow, and someone suggested I’d look at the Swirl source code. This was exactly what I needed:

startGame <- function(...){
  game <- new.env(globalenv())
  cb <- function(expr, val, ok, vis, data = game){
    game$expr <- expr
    game$val <- val
    game$ok <- ok
    game$vis <- vis
    return(react(game, ...))
  }
  addTaskCallback(cb, name = "CastleOfR")
  message("How much is 2+2?")
  invisible()
}

This mini-startGame() function is the key to making the Castle of R game. It creates a new environment called “game”, then registers cb - a “task callback” on R’s “task callback list” with the addTaskCallback function. This “task callback list” can be thought of as a list (or rather vector) of functions R performs with every “top-level task” the user is making in the console. Usually this list an empty character vector, but now:

startGame()
## How much is 2+2?
getTaskCallbackNames()
## [1] "CastleOfR"

It has our “CastleOfR” task.

At the end of the mini-startGame() the player is asked a question, “How much is 2+2?”. And we want to say “Correct!” only if she inputs “4”, “2+2” etc. We do this with the react function, which is what cb returns:

react <- function(game, ...){
  if(!is.null(.Last.value)) {
    if (.Last.value == 4) {
      message("Correct!")
    } else {
      message("You trippin'.")
    }
  }
  return(TRUE)
}

The react function will check whether the last value input to console was indeed 4. If so, it will output “Correct!”. Let’s try:

3
## [1] 3
## You trippin'.
4
## [1] 4
## Correct!

And remember to remove the task callback when you’re done:

removeTaskCallback("CastleOfR")
## [1] TRUE

The full startGame and react functions of the Castle of R game are of course somewhat more complex. But the mechanism is the same. Also if you look closely at the R Markdown I used to produce this webpage you would see I cheated a bit because this task-manager-thingy seems to not work/persist in R Markdown. But it should work in your console.

Build a Castle

The next step in building the Castle of R is, well, building the Castle of R. The Castle is composed of many rooms, in each room there are objects which you might want to grab, and doors which lead to other rooms. Yes, this is a classic use-case for classes. Or Object-Oriented Programming (OOP). My code still could use major refactoring, but I cannot imagine how ugly it would have been had I not used classes!

Here is a high-level UML-ish diagram of my Room class, minus some secrets and minus some stuff for brevity:

So a room has a title, it has doors and objects, a time limit and a floor number. You can set the doors to a room, the objects, the time limit. You can get the count of locked doors for a room and it also comes with a greet function, summarizing for the player the room “situation”.

Here is how it looks in R (again, abbreviated somewhat - you can check out the full code, though it contains spoilers):

Room <- R6::R6Class("Room",
                public = list(
                  name = NULL,
                  title = NULL,
                  door = NULL,
                  object = NULL,
                  timeLimit = Inf,
                  floor = NULL,
                  
                  initialize = function(name = NA, title = NA, floor = NA) {
                    self$name <- name
                    self$title <- title
                    self$floor <- floor
                  },
                  set_doors = function(doors) {
                    self$door <- doors
                  },
                  set_objects = function(objects) {
                    self$object <- objects
                    self$object_numbers <- 1:length(objects)
                  },
                  set_timeLimit = function(timeLimit) {
                    self$timeLimit <- as.numeric(timeLimit)
                  },
                  countLockedDoors = function() {
                    sum(sapply(self$door, function(door) !door$open))
                  },
                  greet = function(directionChosen = NULL) {
                    floorNum <- switch(self$floor, "1" = "1st", "2" = "2nd",
                                       "3" = "3rd", "4" = "4th")
                    message(paste0("You are in ", self$title, ", ", floorNum,
                                   " floor. Around you you see..."))
                  }
                )
)

Each room has a list of doors and a list of objects. This is very convenient in OOP, as I only need to declare a Door and an Object (what they hold and what can be done with/to them):

So a Door: has two rooms, two riddles to get pass each room, too directions for each room (e.g. “north” and “south”) and an indicator for whether it is open or not. And an object: has a name (“pen”), a location (“in the dustbin”), a riddle to take it, type of reward and number of R Power points if the reward is R Power points.

But what is a Riddle?

So the relation between the above 4 classes is something like:

  • A Castle has a few rooms
  • Each room has a few doors and a few objects
  • A single door is always shared between two rooms
  • Each door has two Riddles for two directions
  • Each object has a single riddle

Or in a diagram:

Now try to implement that without OOP…

OOP and R6 also give us “inheritance”. For example, suppose we have a “special” room, like the Castle dungeon, which the player accidentally falls into. Obviously that room needs a special kind of greet message, not just “You’re in the Castle Dungeon, around you you can see…” This is a big event! If we didn’t have OOP and inheritance our functional code would become quite cumbersome, something like:

  1. Enter the next room.
  2. If it is a regular room greetRegular()
  3. If it is the dungeon greetDungeon()
  4. If it is cellar greetCellar()

etc.

In R6 we can just make a new class Dungeon, which would inherit Room, and override its greet function:

Dungeon <- R6::R6Class("Dungeon",
                  inherit = Room,
                  public = list(
                    greet = function() {
                      message("Oh my God! Oh no! You fell into the Castle Dungeon!")
                    }
                  )
)

Then we can simply do:

  1. Enter the next room.
  2. greet()

That’s it.

Now my code can still be more OOP “pure”, I know, I’m improving it. But the OOP framework is already there, saving countless unnecessary lines of code.

Ask a Question

This is an educational game. To win, you must answer questions. I wanted to take advantage of the R platform and have the answers to questions be actual code you need to run. As an inspiration I took the canonical An Introduction to R book, by Venables, Smith and the R Core Team. Each room in the Castle of R is named after one of the chapters in the book, and contains riddles mainly dealing with that chapter.

But I’m already thinking ahead. What if we wanted a different game, for the tidyverse. Do we really need to “touch the code”? We don’t. We need a way to “charge” the game with whatever questions we’d like. Currently this is possible by changing a few text files (I do it in MS Excel) and simply replacing the questions, hints, solutions etc. This is the CastleOfR_Objects.txt file, edited in MS Excel, with only some of the first (easy!) questions shown:

I’m thinking of moving this to JSON or yaml like in the swirl package.

The questions (as well as rooms data, doors data and other secret stuff) are quickly loaded into the game environment, and turned into Riddle instances like this:

objects_file <- system.file("extdata", "CastleOfR_Objects.txt",
                            package = "CastleOfR")
objects_df <- read.table(objects_file, stringsAsFactors = FALSE,
                         header = TRUE, sep = "\t", comment.char = "|")
  
objects_list <- apply(objects_df, 1, function(object) {
  riddle <- Riddle$new(object[["question"]], object[["solution"]],
                       object[["val"]], object[["hint"]], object[["tip"]],
                       object[["floorMapsIdx"]], object[["prepare"]],
                       object[["cleanup"]])
  Object$new(object[["objName"]], object[["location"]], object[["type"]],
             object[["points"]], riddle)
})
  
names(objects_list) <- objects_df$name
list2env(objects_list, envir = environment())

Again, done is better than perfect. For example, all of the texts and messages are still hard-coded and not loaded with a configurable file. And if you’d like to make this game about the tidyverse you would have to also change somewhere the line “The purpose of this game is to test your skills in base R.” If you’d like to translate this game, we1 would have to move the text out.

Test It Out

Forgive me Hadley, for I have sinned. This package needs a suite of tests, and fast. Once you make something this complex, depending on the user’s strategy and personal style, you can’t really tell what he or she can do, and you can’t really anticipate all the dark corners in which your code might break. The more I played the game the more I found intricate and subtle bugs. This may be my next challenge, again you could help me, if not by sending pull requests, then by sending a courtesy email detailing what exactly went wrong: where were you in the game, what was the question, what did you do, and attach a screenshot. Thank you!

Play the Game

I’d like to thank Keren Ratner my friend the gamer and product expert for some great ideas. And Udi, my partner, who played the game and found beautiful bugs even though he really really preferred going to sleep. Please play, enjoy, and send me any comment, sad (bug) or happy (a new idea for a feature).


  1. I’m saying we because I’m assuming you’d like to help me…