Introduction

Conway’s Game of Life is a cellular automaton where the evolution of the grid state is determined entirely by the initially state of the grid.

If we consider the grid to be black pixels on a white background, at each time step the boart updates by following these rules:

  1. Any live cell with two or three live neighbours survives.
  2. Any dead cell with three live neighbours becomes a live cell.
  3. All other live cells die in the next generation. Similarly, all other dead cells stay dead.

General implementation notes for R

A naive approach to implemention of this game would be to have nested for loops and iterate over every cell. At every cell then, interrogate all the neighbours to decide on what the cell should do for the next time step.

Nested for loops in R are too slow to produce a smoothly animating result. Instead, a matrix-based approach is taken where the board state is shifted in the 8 directions, and then these shifted boards are stacked and summed efficiently to determine the next board state.

Rendering in the Event Loop

At every frame, the board is cleared and the entire board state is updated, and then drawn from scratch using grid::grid.raster() to plot a matrix to the graphics output device.

Controls

  • Press and hold the left mouse button to draw
    • the board updates are paused, and the pixels nearest the mouse position are set to “active”.
  • Press ESC to quit

Game of Life in R

library(eventloop)
library(grid)


#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Global State
#  - 'N'     size of board
#  - 'g'     the game board is an NxN integer matrix of 0/1 values
#  - 'mouse_button_pressed' is set to TRUE whenever the user presses-and-holds a mouse button
#            and then set to FALSE when the mouse button is released
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
N <- 100
g <- matrix(sample(c(0L, 1L), N*N, replace = TRUE), N, N)
mouse_button_pressed <- FALSE


#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Standard blank vectors needed when shifting the current board in 8 
# different directions
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
blankN   <- rep(0L, N   )
blankNm1 <- rep(0L, N-1L)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#' The gave of life 
#'
#' @param event The event from the graphics device. Is NULL when no event
#'        occurred.  Otherwise has `type` element set to:
#'        `event$type = 'mouse_down'` 
#'               - an event in which a mouse button was pressed
#'               - `event$button` gives the index of the button
#'        `event$type = 'mouse_up'`   
#'               - a mouse button was released
#'        `event$type = 'mouse_move'`   
#'               - mouse was moved 
#'        `event$type = 'key_press'`  
#'               - a key was pressed
#'               - `event$str` String describing which key was pressed. 
#'                  See \code{grDevices::setGraphicsEventHandlers} for more information.
#' @param mouse_x,mouse_y current location of mouse within window in normalised 
#'        coordinates in the range [0, 1]. If mouse is 
#'        not within window, this will be set to the last available coordinates
#' @param frame_num Current frame number (integer)
#' @param fps_actual,fps_target the curent framerate and the framerate specified
#'        by the user
#' @param dev_width,dev_height the width and height of the output device. Note:
#'        this does not cope well if you resize the window
#' @param ... any extra arguments ignored
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
game_of_life <- function(event, mouse_x, mouse_y, ...) {

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Set the 'mouse_button_pressed' variable depending on whether or not a button is pressed
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  if (!is.null(event)) {
    if (event$type == 'mouse_down') {
      mouse_button_pressed <<- TRUE
    } else if (event$type == 'mouse_up') {
      mouse_button_pressed <<- FALSE
    }
  }

  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # if 'mouse_button_pressed' is TRUE, then set the pixel under the mouse to 'black'
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  if (mouse_button_pressed) {
    g[as.integer((1 - mouse_y) * (N-1) + 1), as.integer(mouse_x * (N-1) + 1)] <<- 1L
  } 
  
  # Neighbours, everybody needs good neighbours....
  #  1 2 3
  #  4   5
  #  6 7 8
  gsum <- cbind(blankN, rbind(blankNm1, g[-N, -N])) +  # 1
    rbind(blankN, g[-N,]) +                            # 2
    cbind(rbind(blankNm1, g[-N, -1]), blankN) +        # 3
    cbind(blankN, g[,-N]) +                            # 4
    cbind(g[,-1], blankN) +                            # 5
    cbind(blankN, rbind(g[-1, -N], blankNm1)) +        # 6
    rbind(g[-1, ], blankN) +                           # 7
    cbind(rbind(g[-1, -1], blankNm1), blankN)          # 8
  
  # Standard Conway rules
  g[] <<- (g == 1 & (gsum == 2L | gsum == 3)) | (g == 0 & gsum == 3)
  
  
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  # Clear the screen (Note: grid.rect() is faster than a grid.newpage())
  # Draw the GameOfLife grid
  #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  grid::grid.rect(gp = grid::gpar(fill = 'black'))
  grid::grid.raster(1L - g, interpolate = FALSE)
}


#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Run the loop
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
run_loop(game_of_life, fps_target = 30, show_fps = TRUE)

Since an interactive window cannot be captured in a vignette, a video screen capture has been taken of the window and included below.