knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
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_ids
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:
.bundled_room_packs <- function() { list( medicine_room_pack() ) }
If the catalogue already contains packs, retain them and add the new factory as another list element:
.bundled_room_packs <- function() { list( existing_room_pack(), medicine_room_pack() ) }
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:
read.csv(escapeR_file("medicine-example.csv"))
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:
devtools::test()
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:
devtools::document() devtools::test() devtools::check()
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():
expect_error( build_escape("unknown_escape"), regexp = "unknown" )
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:
expect_equal(calculated_proportion, 0.2, tolerance = 1e-8)
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:
devtools::test()
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.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.