Shiny Monster Maze

Shiny version for mobile

The eerie wails, the apparitions and other supernatural manifestations are an unlimited source for entertainment.

I could not just keep Monster Maze for myself and deprive you of such a joyful game. So I decided to bring it to you and the general public by the hand of Shiny, an R package that makes it easy to build interactive web apps straight from R.

The game is simple enough to be my first Shiny app and the idea of sharing it with you has been a strong motivation to explore and understand shiny and reactive programming beyond the initial examples.

Basically I found everything needed for the refactoring of the console-version of the game into a shiny browser-version in the online version of the book Mastering Shiny (Wickham and autor (2021)).

Below I share a recording on how the game looks on my Android smartphone.

But don’t be shy and give it a go https://ehermo.shinyapps.io/monster-maze-shiny/

Play here

Code

The code is publicly available in Github:

monster-maze-shiny-mobile Download

I kept it very simple, the code consists of the following files:

  • game/monster-maze-all.R: game logic including maze building, characters motion, collision detection, player’s view, ascii art

  • .gitignore
  • app.R: Shiny UI, Server logic and reactive context.
  • monster-maze-shiny.Rproj: RStudio project

How to run it

If you want to run the code locally then you require RSudio.

  • Download and unzip the repo ( or clone it )
  • Open the project in RStudio double clicking on monster-maze-shiny.Rproj
  • In RStudio open the file app.R
  • Click on “Run App” above the file
  • Depending on your choice (Run in Window, Run in Viewer pane or Run External) the game will run in a window in RStudio, a detached Viewer pane or your default browser.

Shinyapps.io

I decided to host the game in Shinyapps.io, a platform as a service (PaaS) specialized in hosting Shiny web apps. Please, refer to this article to learn how to create a shinyapps.io account and deploy your first application to the cloud.

Shinyapps.io Free plan includes:

  • 5 application
  • 25 active hours/month (25 not idle)
  • community support

So, unless this post is a completely unexpected success, the free plan will do for now.

Next I share a number of solutions and findings I have encountered:

The paradigm shift: reactive programming

The main challenge is, of course, shifting from an imperative programming style - where you give a clear command, a step, and you have a clear idea when the command is carried out - its position in a larger sequence of steps - to a declarative one - where you describe the results and dependencies but delegate on something else to decide to what extent and when the instructions are put into action.

Paradigm shift: imperative - declarative

Figure 1: Paradigm shift

The diagram on the left is the game loop in the console version. The diagram was generated with PlantUML from the txt.

The thoughtful developer thanks to Image by jcomp on Freepik.

The diagram on the right is a simplification of the reactive graph used by the shiny version, where:

  • Game holds
    • the lives
    • the level
    • the scene (intro, ghost encounter, zombie encounter, level passed, game over or you won) to display/play.
  • Level holds the details such as:
    • maze: the walls and corridors as well as the exit.
    • forward_vision: sight depth at the front.
    • rear_vision: sight depth at the back.
    • num_ghosts : number of ghosts in the level.
    • num_zombies: number of zombies in the level.
    • ghost_speed: ghost speed expressed as how many times the player moves before the ghost appears/disappears.
    • zombie_speed : zombie speed expressed as how many times the player moves before the zombie moves.
    • radius_to_exit: distance to the exit where the player starts at the beginning or every time the level resets.
  • Positions holds
    • the player’s position in the maze.
    • the player’s direction: which direction the player is looking at.
    • the list of zombie positions in the maze.
    • the list of ghost positions in the maze.
  • Counters holds
    • ghost_moves: number of ghost’s moves in that level.
    • player_moves_since_last_ghost_move: number of player’s moves since the last time a ghost moved.
    • zombie_moves: number of zombie’s moves in that level.
    • player_moves_since_last_zombie_move: number of player’s moves since the last time a zombie moved.
    • player_moves: number of player’s moves in that level.

A detailed reactive graph can be generated by means of the R package reactlog (Schloerke (2022)). However, if you are new to reactive programming like I am, then I encourage you to start drawing a reactive graph after a few reads of the basics to start challenging your comprehension, even if some ideas have not really taken shape yet.

The figure 2 shows one of my first attempts to draw the graph to implement the equivalent to the game loop, it is mostly wrong, but I do believe it helped me to grasp a deeper understanding.

First attemtps to draw the reactive graph

Figure 2: First attempts

How to initialize reactive values before any interaction with the user

The first question I faced that was not directly used by the tutorials and guides I consulted is “how do I initialize reactive values that are not set by any user input?”.

After a little big of digging I solved with an observeEvent that is triggered at the beginning and only once.

 1  # set the initial values 
 2  observeEvent(TRUE, ignoreNULL = FALSE, ignoreInit = FALSE, once = TRUE, {
 3      initial = shuffle(maze=maze(), 
 4                        num_ghosts=num_ghosts(), 
 5                        num_zombies = num_zombies(), 
 6                        radius_to_exit = radius_to_exit())
 7
 8      game_info$scene = "intro"
 9      player_direction(initial$player_direction)
10      player_position(initial$player_position)
11      ghost_positions(initial$ghost_positions)
12      zombie_positions(initial$zombie_positions)
13      console$data <- "** CLICK ANYWHERE TO PLAY THE AUDIO **
14  
1539 years have passed since 
16the unsettling events in that 
17creepy sort of place known as
18'Ghost Maze'.
19The yells and shouts haven’t 
20ceased ever since. But it has 
21only been recently when villagers
22have started to claim that
23something new has nested 
24and dwells inside those walls.
25"
26  })

Play sound on the browser side: Howler.js

The package beepr (BĂ„Ă„th (2018)) served me well in the console version to play basic sounds on any platform. In the browser version, however, the users are too far away from the server to hear a thing, the server may not even have speakers. So, the sound must be played on the browser side.

After a little bit or research I opted for the package howler (Baldry and Simpson (2022)) that enables the communication between the audio player on the browser side and the server and allows the latter to control the audio. I also resorted to the package shinyjs (Attali (2021)) to assist me with Javascript operations.

  • So it is as simple as importing the aforementioned packages.
1library(shiny)
2library("howler")
3library("shinyjs")
  • Adding the path to the beepr audio files as a resource path in order to reuse them.
1audio_files_dir <- system.file("sounds", package = "beepr")
2addResourcePath("sample_audio", audio_files_dir)
3audio_files <- file.path("sample_audio", list.files(audio_files_dir, ".wav$"))
  • Making sure the javascript files are imported when defining the UI. In the case of shinyjs you can simply use useShinyjs(), I could not find a similar way for howler so I just created a howler element. However, I initialise it with the dummy values, I defer that to a later point. Perhaps something to polish, advices are welcome in the comments.
 1# Define UI for application 
 2ui<- fluidPage(
 3  useShinyjs(),  
 4  howler(
 5    elementId = "sound", 
 6    tracks = list("Track 1" = "sample_audio/smb_stage_clear.wav"),
 7    auto_continue = FALSE,
 8    auto_loop = FALSE,
 9    seek_ping_rate = 1000
10  ),
11  ...
  • And finally, add the corresponding wav file to that particular scene when repainting the player’s view in the reactive expression.
 1players_view <- reactive({
 2    isolate(scene_to_play <- game_info$scene)
 3    if (scene_to_play != "") {
 4      scene <- scene_map$get(scene_to_play)
 5      sound <- scene$sound
 6      wav_file = sound$wav
 7      isolate(game_info$scene <- "")
 8      shinyjs::runjs(paste0("var music = new Howl({src: ['",wav_file,"']}); music.play();"))
 9      duration <- sound$duration
10      result <- div(pre(HTML(scene$ascii),style=scene$style),style="background-color:black;color:green;text-align=center;")
11      if(scene$invalidate) {
12        if(duration != 0 ) {
13          invalidateLater(duration * 1000 + 500)
14        }
15      }
16      return(result)
17    }

I was delighted to see how easy was to get the same files playing on the browser and on my phone.

However, the feeling diminished slightly as soon as I realized that the audio was not played correctly at the start on Chrome. Then I came to know that Chrome’s autoplay policies changed in April of 2018 in order to improve the user experience. Basically Autoplay with sound is allowed if the user has interacted with the domain (BeaufortDives (2017)). The issue was not observed on the Viewer pane in RStudio and at that point I had no intention to change the flow to force a user interaction before starting to play the game. Below I share the warning prompted on the Chrome console for your information.

1howler.min.js:2 The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page. https://goo.gl/7K7WLu

Monospace on Android Chrome

I could not get a monospace font working on Android Chrome. It seems that the issue has been around for a long time. It works smoothly on Desktop Chrome and Safari. The visual impact is tolerable and therefore I have not really invested a big deal of time. I tried different fonts but none of them work so I think the fix is something else. Online you can find more suggestions. Please leave me a comment if you know how to fix it.

The figure 3 shows the difference between a monospace font and a non-monospace.

Monospace v Non-monospace

Figure 3: Monospace v Non-monospace

Emojis

Again I rely heavily on Emojis to render the board and characters. I faced similar issues to those in the console version, with emojis overlapping or not rendering correctly when a particular emoji is in the proximity. In this case I did not resort to combination of invisible characters to separate them, a brittle solution I came up through trial-and-error in the console version, this time a simple HTML table resolved the issue, regrettably no without bringing a few of its own. Especially in terms of controlling the distance between cells, it sounds simple, doesn’t it? But Android Chrome and Safari do not render CSS exactly in the same way. Once more I had to settle for a compromise solution. Overall it is acceptable on both Android Chrome and Safari but not quite what I wanted.

The figure 4 shows the difference between an Android smartphone and an iPhone.

Android v iPhone

Figure 4: Android v iPhone

Final notes

The game is not responsive in that it does not resize, re-arrange accordingly to the device or orientation. I am also working on a desktop version with keyboard input and a different layout. Later I would like to check if Shiny is capable of merging both. I will keep you updated.

References

Attali, Dean. 2021. Shinyjs: Easily Improve the User Experience of Your Shiny Apps in Seconds. https://CRAN.R-project.org/package=shinyjs.
BĂ„Ă„th, Rasmus. 2018. Beepr: Easily Play Notification Sounds on Any Platform. https://CRAN.R-project.org/package=beepr.
Baldry, Ashley, and James Simpson. 2022. Howler: ’Shiny’ Extension of ’Howler.js’. https://CRAN.R-project.org/package=howler.
BeaufortDives, François. 2017. “Autoplay Policy in Chrome.” Chrome Developers. https://developer.chrome.com/blog/autoplay/.
Schloerke, Barret. 2022. Reactlog: Reactivity Visualizer for ’Shiny’. https://CRAN.R-project.org/package=reactlog.
Wickham, Hadley, and autor. 2021. Mastering Shiny: Build Interactive Apps, Reports, and Dashboards Powered by r. O’Reilly Media, Incorporated.

Posts in this Series

comments powered by Disqus