Search code examples
arraysrubybooleanconways-game-of-life

Why does this function produce chaotic results instead of calculating Conway's Game Of Life?


I am writing Conway's Game Of Life in Ruby. The game is based on a 2d array of booleans where true represents an alive cell and false represents a dead cell. (e. g.

arr = [
    [false, false, false, false, false, false, false],
    [false, false, false, false, false, false, false],
    [false, false, false, true, false,  false, false],
    [false, false, false, false, true,  false, false],
    [false, false, true,  true,  true,  false, false],
    [false, false, false, false, false, false, false],
    [false, false, false, false, false, false, false],
    [false, false, false, false, false, false, false],
    [false, false, false, false, false, false, false],
    [false, false, false, false, false, false, false],
    [false, false, false, false, false, false, false],
    [false, false, false, false, false, false, false],
    [false, false, false, false, false, false, false]
]

would be for making a glider) According to the rules, as I'm sure most of you know, it should create a pattern that repeatedly reforms to the right and bottom. However, when I run this code, it produces a chaotic pattern that explodes. Gif with the chaotic result. Here's my main function, which calculates the new board state:

def iterate(arr) 
  arr = arr.dup
  new_arr = arr
  y_counter = 0
  arr.each do |elem|
    x_counter = 0
    elem.each do 
      count = 0
      neighbors = [
        (arr[y_counter-1] || [false, false, false])[x_counter-1], 
        (arr[y_counter-1] || [false, false, false])[x_counter], 
        (arr[y_counter-1]|| [false, false, false])[x_counter+1],
        (arr[y_counter]|| [false, false, false])[x_counter-1],
        (arr[y_counter]|| [false, false, false])[x_counter+1],
        (arr[y_counter+1]|| [false, false, false])[x_counter-1],
        (arr[y_counter+1]|| [false, false, false])[x_counter],
        (arr[y_counter+1]|| [false, false, false])[x_counter+1],
      ]
      neighbors.each do |elem|
        count += 1 if elem
      end
      new_arr[y_counter][x_counter] = (count == 2) || (count == 3 && arr[y_counter][x_counter])
      x_counter+=1
  
    end
    
    y_counter+=1
  end
  new_arr
end

Why doesn't it work, and what can I do to fix it?


Solution

  • Consider writing your code with a few methods to perform different tasks. Firstly, we will need to display a picture of the grid in a terminal. We might do that as follows.

    ALIVE = "💓" 
    DEAD = "💀"
    
    def display_grid(grid)
      last_col = grid.first.size
      system("clear")
      grid.each_index do |i|
        puts (0..last_col).map { |j| grid[i][j] ? ALIVE : DEAD }.join
      end
    end
    
    display_grid(grid)      
    
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💓💀💀💀💀
    💀💀💀💀💓💀💀💀
    💀💀💓💓💓💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    

    system("clear") clears the terminal in OS X and Linux. system("cls") does the same in Windows. I understand Ruby v2.7 has introduced a cross-platform way to do that:

    require 'io/console'
    $stdout.clear_screen # or STDOUT.clear_screen
    

    Next, for each cell in the grid we will need to count the number of its immediate neighbors who are alive.

    def neighbors_alive(grid, (row, col))
       rows = ([row-1, 0].max..[row+1, arr.size-1].min).to_a
       cols = ([col-1, 0].max..[col+1, arr.first.size-1].min).to_a
       neighbors = rows.product(cols) - [cell]
       neighbors.count { |row, col| grid[row][col] }
    end
    

    For example:

    neighbors_alive(grid, [0, 0])  #=> 0 
    neighbors_alive(grid, [1, 1])  #=> 0
    neighbors_alive(grid, [2, 2])  #=> 1
    neighbors_alive(grid, [3, 3])  #=> 5
    neighbors_alive(grid, [4, 4])  #=> 2
    neighbors_alive(grid, [5, 5])  #=> 1
    neighbors_alive(grid, [6, 6])  #=> 0
    

    The calculations are as follows for the cell [4, 3], which is centered in the following 3x3 subarray:

    [false, false, true],  # 💀💀💓 
    [true,  true,  true],  # 💓💓💓
    [false, false, false]] # 💀💀💀
    

    so we expect that number to be 3.

    row, col = [4, 3]
    row
      #=> 4 
    col
      #=> 3 
    rows = ([row-1, 0].max..[row+1, arr.size-1].min).to_a
      #=> [3, 4, 5] 
    cols = ([col-1, 0].max..[col+1, arr.first.size-1].min).to_a
      #=> [2, 3, 4] 
    neighbors = rows.product(cols) - [[row, col]]
      #=> [[3, 2], [3, 3], [3, 4], [4, 2], [4, 4], [5, 2], [5, 3], [5, 4]]
      #      💀       💀      💓      💓      💓       💀      💀      💀
    neighbors.count { |row, col| grid[row][col] }
      #=> 3 
    

    At each iteration we need to construct an array of live cells that will die (to_die) and dead cells that will become alive (to_alive). This can be done as follows.

    def transitions(grid)
      rows = 0..grid.size - 1
      cols = 0..grid.first.size - 1
      rows.each_with_object([]) do |i, cells_to_flip|
        cols.each do |j|
          cell = [i, j]
          n = neighbors_alive(grid, cell)
          if grid[i][j] # alive
            cells_to_flip << cell unless [2, 3].include?(cell)
          else          # dead
            cells_to_flip << cell if n == 3
          end
        end
      end
    end
    
    cells_to_flip = transitions(grid)
      #=> [[2, 3], [3, 2], [3, 4], [4, 2], [4, 3], [4, 4], [5, 3]] 
    

    We then need to update the grid.

    def update_grid(grid, cells_to_flip)
      cells_to_flip.each { |i, j| grid[i][j] = ! grid[i][j] }
    end
    

    Let's try this with a deep copy of grid (so we don't modify the original grid, which will use below in conway(grid)), using the values of to_die and to_alive above.

    arr = grid.dup.map(&:dup)
    update_grid(arr, cells_to_flip)
    display_grid(arr)
    
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💓💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💓💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    

    We see that all live cells die and the dead cells at [3, 2] and [5, 3] are revived.


    We may now write a method to play the game. Recall that at each iteration a live cell dies if has fewer than two or more than three live neighbors and a dead cell springs back to life if it has exactly three live neighbors.

    def conway(grid, delay, max_iterations)
       rows = 0..grid.size - 1
       cols = 0..grid.first.size - 1
       iterations = 0
       display_grid(grid)
       sleep(delay)
       loop do
         break if iterations == max_iterations
         iterations += 1
         cells_to_flip = transitions(grid)
         break if cells_to_flip.empty?
         update_grid(grid, cells_to_flip)
         display_grid(grid)
         sleep(delay)
       end
    end
    

    Let's try it.

    conway(grid, 2, 1_000_000)
    

    This displays the grid three times, two seconds apart, and then exits as there were no further changes to be made after the last iteration.

    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💓💀💀💀💀
    💀💀💀💀💓💀💀💀
    💀💀💓💓💓💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💓💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💓💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀
    💀💀💀💀💀💀💀💀