game-of-life.Rmd
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:
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.
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.
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.