escapeR was first
created for an Ecological Statistics course. The bundled rooms therefore
use ecological examples: field counts, survey data, detection,
modelling, and reproducible reporting. The package architecture is more
general than that, however. An escape room is just a sequence of small
tasks where students use R to unlock the next step.
This vignette explains how to create rooms in any theme, such as
medicine, agriculture, social sciences, economics, psychology, or
environmental policy. It also shows how to bundle those rooms into a
shareable room pack that can be submitted to the escapeR
GitHub repository.
New rooms and quests are welcome. To contribute them to
escapeR, submit a GitHub pull request containing the
room-pack definition, its registration entry, tests, and any data files
it needs. The complete process is described below.
For coursework submissions through Moodle, start with Make your own room: a simple
coursework guide. It explains how to edit and submit a single
.R file. The guide below describes the fuller route for
creating themed packs and contributing them to the package.
Every room is built from eleven components, although some are optional:
id: a short unique identifier, at most 8 characters,
such as "bpmean" or "yield".module: the broad teaching module, such as
"Medicine" or "Agriculture".title: the room title shown to students.learning_goal: what the student should practise.introduction: the story or setting.challenge: the task the student must solve.hints: one or more hints. Repeated calls to
hint() reveal them in order.correct_result: the expected answer for simple numeric
or text answers.checker: an optional custom function for more complex
answer checking.success: the message shown after a correct answer.failure: an optional message shown after a wrong
answer.Most beginner rooms only need a simple correct_result.
For example, if the correct answer is 130,
escapeR checks numeric submissions with a small tolerance.
If the correct answer is "high", escapeR
checks text after trimming spaces and ignoring case. Use
checker instead of correct_result when you
need more control.
Start with a familiar domain and a small learning goal. Good first rooms are short, concrete, and assess one idea at a time.
Examples:
For a first contribution, aim for two or three rooms. That is enough to create a small story arc while keeping the pull request easy to review.
The example below creates a tiny medicine-themed escape room. The first room asks students to create a vector and calculate a mean. The second asks them to use a logical comparison and submit a category. The third asks for a simple proportion.
library(escapeR)
bpmean <- new_room(
id = "bpmean",
module = "Medicine",
title = "The Clinic Intake",
learning_goal = "Create a numeric vector and calculate its mean.",
introduction = paste(
"A clinic intake sheet lists systolic blood pressure readings for",
"three patients seen before lunch."
),
challenge = paste(
"Create a vector with the values 120, 124, 111, 182, 130, and 145.",
"What is the average systolic blood pressure? Submit the mean, rounded to 1 decimal."
),
hints = c(
"Use c() to combine multiple readings into one vector.",
"Use mean() to calculate the average of a numeric vector."
),
correct_result = 133.3,
success = "The intake sheet is complete, and the first cabinet opens.",
failure = "Not quite, young doctor. Check that all readings are included before taking the mean."
)
riskcat <- new_room(
id = "riskcat",
module = "Medicine",
title = "The Risk Label",
learning_goal = "Use a logical comparison to classify a value.",
introduction = paste(
"A label printer waits beside the triage desk.",
"It needs the correct risk category for the next patient."
),
challenge = paste(
"A systolic blood pressure of 145 is considered high if it is above 140.",
"Submit high or normal."
),
hints = c(
"Ask R whether 145 > 140.",
"If the comparison is TRUE, the category requested by the room is high."
),
correct_result = "high",
success = "The label prints clearly, and the triage desk unlocks."
)
posprop <- new_room(
id = "posprop",
module = "Medicine",
title = "The Test Result Board",
learning_goal = "Calculate a proportion from counts.",
introduction = paste(
"A result board shows 8 positive tests out of 40 tests performed.",
"The ward door asks for the positive proportion."
),
challenge = "Submit the proportion of tests that were positive.",
hints = c(
"A proportion is part divided by whole.",
"In R, calculate 8 / 40."
),
correct_result = 0.2,
success = "The board accepts the proportion, and the ward door opens."
)Notice that each authoring object is self-contained. It contains
everything needed to review, edit, translate, and reuse the room. During
play, escapeR exposes the introduction, challenge, learning
goal, and hints, but keeps the correct result and checker out of the
playable room object.
A room pack is the unit that is easiest to share. It contains a pack ID, a short description, the rooms, and one or more named escape sequences. A sequence is just an ordered vector of room IDs.
medicine_pack <- new_room_pack(
id = "medpack",
title = "Introductory medicine rooms",
description = paste(
"A small set of medicine-themed rooms for practising vectors,",
"means, comparisons, and proportions."
),
rooms = list(bpmean, riskcat, posprop),
escapes = list(
medmini = c("bpmean", "riskcat"),
medfull = c("bpmean", "riskcat", "posprop")
)
)The pack can contain more than one escape sequence. In this example,
medmini is a two-room activity and medfull
uses all three rooms.
Before submitting a pull request, register the pack in your R session, inspect the rooms and escape sequences, and then start a test game.
register_room_pack(medicine_pack, replace = TRUE)
list_rooms()
list_escapes()
medicine_escape <- build_escape("medfull")
medicine_escape$room_idsThis block is intentionally not evaluated while the vignette is built because registration changes the catalogue for the rest of the R session. Run it interactively when testing the pack; a fresh package session continues to list only genuinely bundled and deliberately registered sequences.
To play it interactively:
escape(player = "demo_medicine", reset = TRUE, escape = medicine_escape)
# Room 1 answer
submit(133.3)
# Room 2 answer
submit("high")
# Room 3 answer
submit(0.2)When developing a pack, test wrong answers as well as correct ones. Where relevant, try numeric values, text, and the kinds of R objects your checker is designed to accept. Good failure messages help students recover and may offer a small nudge without giving away the solution.
Sometimes a room has several acceptable answers. For example, a
social-science room might accept "increase",
"increased", or "up". In that case, provide a
checker function instead of
correct_result.
trend <- new_room(
id = "trend",
module = "Social sciences",
title = "The Survey Trend",
learning_goal = "Interpret a direction of change.",
introduction = "A survey dashboard compares this year with last year.",
challenge = "Satisfaction rose from 62 to 70 percent. Submit the direction of change.",
hints = c(
"Compare the second value with the first.",
"Several words can describe an upward change."
),
checker = function(answer) {
tolower(trimws(as.character(answer))) %in% c("increase", "increased", "up", "rose")
},
success = "The dashboard accepts the trend.",
failure = "Try describing whether the second value is higher or lower than the first."
)Custom checkers should be short and predictable. They must return
TRUE only when the answer should unlock the room.
An accepted contribution must do more than define a pack: it must
also add the pack to the package catalogue and test the student-facing
submission path. The steps below describe the complete route from a
local room pack to rooms and named escapes that are available
immediately after library(escapeR).
Place the pack in a single R source file. A good file name uses the pack theme, for example:
R/room-pack-medicine.R
R/room-pack-agriculture.R
R/room-pack-social.R
The file should define one function that takes no arguments and returns the pack. The function is an internal package factory, so it does not need to be exported. For the medicine example, the submitted file would look like this:
medicine_room_pack <- function() {
bpmean <- new_room(
id = "bpmean",
module = "Medicine",
title = "The Clinic Intake",
learning_goal = "Create a numeric vector and calculate its mean.",
introduction = paste(
"A clinic intake sheet lists systolic blood pressure readings for",
"three patients seen before lunch."
),
challenge = paste(
"Create a vector with the values 120, 124, 111, 182, 130, and 145.",
"What is the average systolic blood pressure? Submit the mean, rounded to 1 decimal."
),
hints = c(
"Use c() to combine multiple readings into one vector.",
"Use mean() to calculate the average of a numeric vector."
),
correct_result = 133.3,
success = "The intake sheet is complete, and the first cabinet opens.",
failure = "Not quite, young doctor. Check that all readings are included before taking the mean."
)
riskcat <- new_room(
id = "riskcat",
module = "Medicine",
title = "The Risk Label",
learning_goal = "Use a logical comparison to classify a value.",
introduction = "A label printer waits beside the triage desk.",
challenge = paste(
"A systolic blood pressure of 145 is considered high if it is above 140.",
"Submit high or normal."
),
hints = c(
"Ask R whether 145 > 140.",
"If the comparison is TRUE, the category requested by the room is high."
),
correct_result = "high",
success = "The label prints clearly, and the triage desk unlocks."
)
posprop <- new_room(
id = "posprop",
module = "Medicine",
title = "The Test Result Board",
learning_goal = "Calculate a proportion from counts.",
introduction = paste(
"A result board shows 8 positive tests out of 40 tests performed.",
"The ward door asks for the positive proportion."
),
challenge = "Submit the proportion of tests that were positive.",
hints = c(
"A proportion is part divided by whole.",
"In R, calculate 8 / 40."
),
correct_result = 0.2,
success = "The board accepts the proportion, and the ward door opens."
)
new_room_pack(
id = "medpack",
title = "Introductory medicine rooms",
description = paste(
"A small set of medicine-themed rooms for practising vectors,",
"means, comparisons, and proportions."
),
rooms = list(bpmean, riskcat, posprop),
escapes = list(
medmini = c("bpmean", "riskcat"),
medfull = c("bpmean", "riskcat", "posprop")
)
)
}Room IDs must be unique across all bundled and contributed rooms. Pack IDs and named escape IDs must likewise avoid existing registered IDs. Keep every ID to at most eight letters, numbers, or underscores, beginning with a letter.
Defining medicine_room_pack() is not enough to make its
rooms available to users. Add a call to the factory inside
.bundled_room_packs() in
R/bundled-packs.R:
If the catalogue already contains packs, retain them and add the new factory as another list element:
When escapeR loads, its .onLoad() hook
registers every pack in this list. Consequently, users do not need to
call register_room_pack() for a bundled contribution: its
rooms appear in list_rooms(), its named sequences appear in
list_escapes(), and those sequences work with
build_escape() immediately.
Do not add a registration call to an example or vignette as a substitute for the catalogue entry. Such a call affects only the R session in which that example happens to run.
If a room needs a small data file, add it under
inst/extdata/ and locate it in the challenge with
escapeR_file(). Use a distinctive file name to avoid
collisions. The room and its tests must not depend on files that exist
only on the contributor’s computer.
For example, a room could direct students to read:
Keep contributed data small, document their origin and licence in the pull request, and include only data that the package is permitted to redistribute.
Add a focused test file such as
tests/testthat/test-room-pack-medicine.R. Tests should
verify permanent registration and should submit answers through the
exported game interface. That is more useful than calling a room’s
internal checker directly because it also tests registration, escape
construction, room order, progress, and submission handling.
testthat is an R package for writing automated checks of
code behaviour. A call to test_that() groups checks for one
behaviour and gives that group a descriptive name. Inside it, functions
beginning with expect_ state what should be true: for
example, expect_equal() compares values,
expect_true() requires a true result, and
expect_error() requires an error. When an expectation
fails, testthat reports the failed expectation and its
location in the test file. This makes it easier to detect when a later
code change breaks a room that previously worked.
For example:
test_that("medicine room pack is registered and playable", {
expect_true(all(
c("bpmean", "riskcat", "posprop") %in% list_rooms()$id
))
expect_true(all(c("medmini", "medfull") %in% list_escapes()$id))
medicine_escape <- build_escape("medfull")
expect_equal(
medicine_escape$room_ids,
c("bpmean", "riskcat", "posprop")
)
player <- paste0("medicine_test_", Sys.getpid())
escape(player = player, reset = TRUE, escape = medicine_escape)
expect_false(submit(100))
expect_true(submit(133.3))
expect_true(submit(" HIGH "))
expect_true(submit(0.2))
})Use a unique test-player name so repeated test runs do not reuse another test’s progress. Test at least one incorrect answer and every correct answer in the sequence. If a room uses a custom checker, include representative accepted and rejected submissions for that checker.
The tests use testthat. Its expectations describe the
required behaviour:
expect_true(x > 0) # a condition holds
expect_false(is.na(x)) # a condition does not hold
expect_equal(result, 4) # two values are equal
expect_error(log("text")) # an error is triggered
expect_warning(sqrt(-1)) # a warning is triggered
expect_type(x, "double") # typeof(x) is "double"Package tests belong under tests/testthat/. Run them
locally with:
If testthat is new to you, see the testthat primer at the end of this
vignette. It explains the structure of a test, the main expectation
functions and their arguments, and how to interpret a failure.
Run the full package checks before opening the pull request:
Also open the rendered vignettes and play the contributed escape once in a fresh R session. Check that the story, hints, failure messages, room order, and all required files make sense from a student’s perspective.
The pull request should contain:
R/bundled-packs.R.inst/extdata/, with source
and licensing details.In the pull-request description, summarize the learning goals, list
the new room and escape IDs, explain any data provenance, and report the
result of devtools::check(). Reviewers can then evaluate
both the teaching design and the technical integration from one
self-contained contribution.
Write rooms for learners, not for answer keys. A useful room should:
The ecology rooms bundled with escapeR are one theme.
The same architecture can support other domains, as long as each room
remains a small, testable R learning step.
testthat is an R package for checking automatically that
code behaves as intended. Each test runs some code and compares the
observed result with an expected result. A passing test is normally
quiet; a failing test identifies the expectation that failed, shows the
difference when possible, and reports the file and line where it
occurred.
Tests are useful because a room that works today can be broken accidentally by a later change to registration, answer checking, or player progress. Re-running the tests checks the old behaviour without requiring someone to play every escape manually.
In an R package, test files conventionally live under
tests/testthat/ and have names beginning with
test-, for example:
tests/testthat/test-room-pack-medicine.R
The package lists testthat under Suggests
in DESCRIPTION. The line
Config/testthat/edition: 3 tells testthat that
the package uses its third edition of testing behaviour.
Most tests follow three small steps:
expect_*()
functions to state the expected result.test_that(description, code) groups related
expectations:
test_that("a correct answer advances the player", {
# Arrange
player <- paste0("medicine_test_", Sys.getpid())
medicine_escape <- build_escape("medmini")
escape(player = player, reset = TRUE, escape = medicine_escape)
# Act and assert
expect_false(submit(100))
expect_true(submit(133.3))
})The first argument is a short description of the behaviour. The
second is the block of R code inside {}. If an expectation
fails, that description helps identify what stopped working.
An expectation receives the value or expression being checked as its first argument. Common expectations include:
expect_true(object) and
expect_false(object) require object to
evaluate to exactly TRUE or FALSE.expect_equal(object, expected, tolerance = NULL)
compares values while allowing small numerical differences. Use
tolerance when floating-point calculations need an explicit
margin.expect_identical(object, expected) is stricter: values
must have the same type and attributes as well as the same
contents.expect_length(object, n) requires an object to have
length n.expect_type(object, type) checks the base type returned
by typeof(), such as "double",
"integer", or "list".expect_s3_class(object, class) checks that an object
inherits from an S3 class.expect_match(object, regexp) checks text against a
regular expression.expect_error(object, regexp = NULL),
expect_warning(), and expect_message() require
an expression to produce the named condition. A regexp can
also check part of its message.expect_no_error(object) requires an expression to
finish without an error.Many expectations also accept info, an optional
explanatory message added to a failure report. It can be helpful when
the reason for an expectation is not obvious:
expect_equal(
medicine_escape$room_ids,
c("bpmean", "riskcat", "posprop"),
info = "The full medicine escape must preserve its teaching order"
)Prefer the most specific expectation that expresses the intended
behaviour. For example, expect_equal(answer, 4) usually
produces a clearer failure than
expect_true(answer == 4).
To test that invalid input is rejected, put the expression that
should fail inside expect_error():
The expression is evaluated by the expectation; it is not run
beforehand. Omitting regexp checks only that some error
occurs. Including it also checks that the message contains the expected
text.
Computers sometimes store calculated decimal values with tiny rounding differences. In those cases, compare with a tolerance:
Do not use a large tolerance merely to make a test pass: it should reflect the precision that the room genuinely promises.
From the package project, the usual command is:
This loads the package’s development code and runs the files under
tests/testthat/. During development,
devtools::test(filter = "medicine") can run only test files
whose names match medicine. The filter value
matches the part after test- and before
.R.
Two lower-level testthat functions are also useful:
testthat::test_file("tests/testthat/test-room-pack-medicine.R")
testthat::test_dir("tests/testthat")test_file(path) runs one file, whereas
test_dir(path, filter = NULL) runs a directory of tests and
can select matching files with filter. Both accept a
reporter argument that controls how results are displayed,
but their defaults are normally suitable for interactive work.
Before a pull request, use devtools::check() as well. It
runs a broader R package check that includes the tests plus checks of
documentation, examples, dependencies, and package structure.
A failure means that observed behaviour did not match an expectation; it does not necessarily mean that the expectation is correct. Read the reported test description, file and line, then compare the actual and expected values. Ask:
Do not replace an expected value merely to silence a failure. First establish which behaviour the package is meant to guarantee.
For contributed room packs, a useful end-to-end test verifies that the rooms and named escapes are registered, confirms their order, starts a uniquely named test player, rejects at least one incorrect answer, and accepts every correct answer. This exercises the same exported functions that a learner uses and is why the example in Step 6 tests more than each room’s checker in isolation.