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 hereCode
The code is publicly available in Github:
monster-maze-shiny-mobile DownloadI 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.
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.
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.
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.
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.