More Monster Maze

A lot has happened in Monster Maze for the last months and a new chapter has now opened.

(previously seen in this series: Shiny Monster Maze and Monster Maze)

Wanna play?

Play here


The code is publicly available in Github:



In the prior version of the game, time-keeping was turn-based and sequential, I-Go-You-Go. And the speed of any change in the game referred to the player´s speed. Simple put, things took a number of player´s moves to change or happen, most notably, the ghosts appearing and disappearing and the zombies moving.

Now time progresses according to the game clock and the speed of the characters is based on the clock. This is arguably when it comes to the player though, in this case her speed is limited by the player herself and the response time of the game, meaning that is not capped to any specific value (Wikipedia contributors (2023)).

In this case the time is not continuous and passes in discrete units of 1 sec (1000 ms). This can be easily achieved through the function reactiveTimer that invalidates the context next time the interval elapses.


I haven´t settled down on a naming convention yet, moving between camelCase and snake_case, it is difficult to stick to one when the packages I use don't do it.

A few additional reactive values are required to harness the power of time.

  • timerRunning: this flag allows to pause the master clock.
  • afterIntro: this flag indicates the tipping event when game time starts.
  • ghostTimer and zombieTimer are used to determine ghost and zombie speeds based on the master clock.
1  #timer
2  autoInvalidate <- reactiveTimer(1000)
3  timerRunning = reactiveVal(NULL)
4  afterIntro = reactiveVal(NULL)
5  ghostTimer = reactiveVal(0)
6  zombieTimer = reactiveVal(0)

The observeEvent function that initializes the values has changed slightly to accommodate the changes.

 1 # set the initial values 
 2  observeEvent(TRUE, ignoreNULL = FALSE, ignoreInit = FALSE, once = TRUE, {
 3      game_info$scene = "intro"
 4      renderEvent(TRUE)
 5      hide_action_buttons()
 6      timerRunning(FALSE)
 7      afterIntro(FALSE)
 8      console$data <- "Nobody believes me.
 9There is no hope for them...
10...unless I do it myself.
11I couldn´t look myself 
12in the mirror.
13I have to go back 
14and save their souls.
16  })

A new Observe function is invoked every time the reactiveTimer invalidates the context. This function represents the master clock.

 2    observe({
 3    # Invalidate and re-execute this reactive expression every time the
 4    # timer fires.
 5    autoInvalidate()
 6    if (timerRunning()) {
 7      isolate(level_timer(level_timer() + 1))
 8      isolate(ghostTimer(ghostTimer() + 1))
 9      isolate(zombieTimer(zombieTimer() + 1))
10      isolate(move_monsters())
11    }
12  })

And the motion of Ghosts and Zombies is now controlled by the following snippets that check their corresponding clocks against their speeds.

 2    #ghosts move according to ghost speed
 3    if (ghostTimer() == ghost_speed()) {
 4      occupied_positions <- append(zombie_positions(),
 5                                   get_positions_nearby(maze = maze(),
 6                                                        this_position = player_position(),
 7                                                        radius = 1))
 8      ghost_positions(get_random_free_positions(
 9        maze = maze(), num = num_ghosts(), 
10        occupied_positions = occupied_positions))
11      ghost_moves(ghost_moves() +  1)
12     ghostTimer(0)
13    }

Ghosts still disappear and appear on free positions. I have changed their action radius so that now the player has to bump into them to trigger the shuffle.

 2    #zombies move according to zombie speed
 3    if (zombieTimer() == zombie_speed()) {
 4      zombie_positions(move_zombies(
 5        maze=maze(),
 6        zombie_positions = zombie_positions(),
 7        other_positions = append(ghost_positions(),zombie2_positions()), 
 8        player_position = player_position()))
 9      zombie2_positions(move_zombies(
10        maze=maze(),
11        zombie_positions = zombie2_positions(),
12        other_positions = append(ghost_positions(),zombie_positions()), 
13        player_position = player_position()))
14      zombie_moves(zombie_moves() +  1)
15      zombieTimer(0)
16    }

As before, the zombies move towards the player. Considering to limit the distance within which they can sense the player and be aware of her presence. They could stay still or move randomly till her presence is detected.

Image processing and resources

The R package magick for advanced image processing (Ooms (2023)) and the website OpenGameArt.Org have been a galvanizing discovery that has lead to this set of changes and finally moving away from emojis to images.

I came across some amazing 16x16 and 32x32 tiles for the mazes and sprites and a few transformations (crop, scale and animation) from the magick package got me the gifs to render a more dynamic scene.

3tiles <- image_read("images/characters.png")
4char1 <- image_scale(image_crop(tiles,"16x16+48+48"),"32x32")
5char2 <- image_scale(image_crop(tiles,"16x16+64+48"),"32x32")
6char3 <- image_scale(image_crop(tiles,"16x16+80+48"),"32x32")
7char_ani <- image_animate(c(char1,char2,char3, char2), fps=4, loop=0, dispose="background")
1host1 <- image_scale(image_crop(tiles,"16x16+112+0"),"32x32")
2host3 <- image_scale(image_crop(tiles,"16x16+96+16"),"32x32")
3host5 <- image_scale(image_crop(tiles,"16x16+128+32"),"32x32")
4host_ani <- image_animate(c(host1,host3,host1,host5), fps=1, loop=0, dispose="background")
1tiles <- image_read("images/5ZombieSpriteSheet.png")
2z1 <- image_scale(image_crop(tiles,"36x36+0"),"32x32")
3z2 <- image_scale(image_crop(tiles,"36x36+44"),"32x32")
4z3 <- image_scale(image_crop(tiles,"36x36+90"),"32x32")
5zombie_ani <- image_animate(c(z1,z2,z3), fps=4, dispose="background")
6image_write(zombie_ani, "images/zombie_down.gif")

Similarly, the mazes have improved significantly. However, I keep the first person viewpoint. The player doesn’t turn, the wold spins around her but too fast to notice :D. I have to admit that in a 2D, topview game; it may not be the best fit but I want to push this still a bit further.

Level 9
Level 9

New chapter (rescue mission)

The game is shaping itself into a horror survival plot, still working on it, aiming for a simple plot where puzzles, rescuing hostages and more collectables and monsters can easily fit in.

And more subtle changes

  • improved audios, the audio should now work in Safari as well.
  • finally same presentation in iphone and android.
  • coins as first collectable, not sure if they will stay though.
  • added some tests, tried testthat, not great as the game is not a package.
  • And a suprise if the player takes too long to rescue the hostages….


Ooms, Jeroen. 2023. Magick: Advanced Graphics and Image-Processing in r.
Wikipedia contributors. 2023. “Timekeeping in Games — Wikipedia, the Free Encyclopedia.”

Posts in this Series

comments powered by Disqus