Search code examples
rubyarraysparsingconways-game-of-life

Parsing Conway's game Grid


Here is my attempt at writing Conway's Game of Life (http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Rules) in Ruby.

I have a very specific question about the "count_neighbours" method. Basically when I get to the edge of the grid I get some odd behaviour. When I parse Row 0 and reach the last column (Cloumn 4) it does something like this:

Evaluating cell: R: 0 C: 4

  • Evaluating neighbor R: -1 C: 3. State: 0
  • Evaluating neighbor R: -1 C: 4. State: 1
  • Evaluating neighbor R: -1 C: 5. State:
  • Evaluating neighbor R: 0 C: 3. State: 0
  • Evaluating neighbor R: 0 C: 5. State:
  • Evaluating neighbor R: 1 C: 3. State: 1
  • Evaluating neighbor R: 1 C: 4. State: 0
  • Evaluating neighbor R: 1 C: 5. State:

One good thing is that "R: -1" essentially wraps the evaluation around to the bottom of the grid as if the edges to the grid are really connected. I like the idea of an edgeless grid.

The bad thing here is that the method is trying to evaluate "C: 5" (Column 5) which doesn't exist because in this example the grid is columns 0-4. So that is the first problem I am seeking help to fix. Ideally here I want to evaluate column 0.

The next problem is when the method attempts to evaluate the last row on the grid, Row 4. When the method attempts to evaluate "R: 5" there is an error thrown. The error is "undefined method `[ ]' for nil:NilClass (NoMethodError)" because that row does not exist. To avoid the error being thrown I have added the line below the comment "#This line is a hack" but I want to be able to evaluate this row without an error.

The last question I have is, why does going beyond the last row to row 5 throw and error when going beyond the last column does not throw and error?

    #Dimensions for the game grid
    WIDTH = 5
    HEIGHT = 5

    def rand_cell
      rand(2)
    end

    def starting_grid
      #Initialise the playing grid
      @start_grid = Array.new(WIDTH){Array.new(HEIGHT)}
      #Randomly generate starting state for each cell on the grid
      @start_grid.each_with_index do |row, rindex|
        row.each_with_index do |col, cindex|
          @start_grid[rindex][cindex] = rand_cell
        end
      end
    end

    def next_grid
      #build the next generation's grid to load values into
      @next_gen_grid = Array.new(WIDTH){Array.new(HEIGHT)}

      #parse each cell in the start grid to see if it lives in the next round
      @start_grid.each_with_index do |row, rindex|
        row.each_with_index do |col, cindex|
          puts "\n\nEvaluating cell: R: #{rindex} C: #{cindex}"
          @next_gen_grid[rindex][cindex] = will_cell_survive(rindex, cindex)
        end
      end   
      #move the newly generated grid to the start grid as a sterting point for the next round
      @start_grid = @next_gen_grid
    end

    def show_grid(grid)
      #Display the evolving cell structures in the console
      grid.each_with_index do |row, rindex|
        row.each_with_index do |col, cindex|
          if grid[rindex][cindex] == 1
            print "️⬛️ "
          else
            print "⬜️ ️"
      end  
    end
    puts "\n"
  end
end

def count_neighbours(row, col)
  cell_count = 0
  rows = [-1, 0, 1]
  cols = [-1, 0, 1]

  rows.each do |r|
    cols.each do |c|
      #ingnore the cell being evaluated
      unless c == 0 && r == 0

        #This line is a hack to stop an error when evaluating beyond the last row
        if  row != HEIGHT-1
          puts "Evaluating neighbor R: #{row+r} C: #{col+c}. State: #{@start_grid[(row+r)][(col+c)]}" 
          if @start_grid[(row+r)][(col+c)] == 1
            cell_count += 1
          end
        end

      end
    end
  end
  puts "Neighbour count is #{cell_count}"
  return cell_count
end

def will_cell_survive(rindex, cindex)
  count = count_neighbours(rindex, cindex)
  #If the cell being evaluated is currently alive
  if @start_grid[rindex][cindex] == 1
       #test rule 1 
    if alive_rule1(count)
      puts "Rule 1"
      return 0
          #test rule 2
    elsif alive_rule2(count)
      puts "Rule 2"
      return 1
    elsif
      #test rule 3
      puts "Rule 3"
      return 0
    end

  #If the cell being evaluated is currently dead      
  else
    #test rule 4
    alive_rule4(count)
      puts "Rule 4"
      return 1
  end
end

def alive_rule1(neighbour_count)
    neighbour_count < 2
end

def alive_rule2(neighbour_count)
  neighbour_count == 2 || neighbour_count == 3
end

def alive_rule3(neighbour_count)
  neighbour_count > 3
end

def alive_rule4(neighbour_count)
  neighbour_count == 3
end


#Run just one round of the game
system "clear"
starting_grid
show_grid(@start_grid)
puts "\n\n" 
next_grid
show_grid(@next_gen_grid)


#Initiate the game grid
#   system "clear"
#   starting_grid

#Run the game
# 200.times do |t|
#   system "clear"
#   puts "\n\n" 
#   next_grid
#   puts "Grid #{t}"
#   show_grid(@next_gen_grid)
#   sleep(0.25)
# end

[EDIT]: The code with the answer implemented is at https://github.com/AxleMaxGit/ruby-conways-game


Solution

  • If you want to connect the edges to each other (which by the way creates a "torus" shape, or if you prefer is the "asteroids" world model where you can never leave the screen) then the simplest adjustment is to work in modular arithmetic:

    Change:

    if @start_grid[(row+r)][(col+c)] == 1
    

    To:

    if @start_grid[(row+r) % HEIGHT][(col+c) % WIDTH] == 1
    

    The operator symbol % is modular arithmetic, and does the wrap-around logic precisely as you need it.

    The reason why going beyond the last row behaves differently to going beyond the last column is because:

    @start_grid[ 3 ][ 5 ] == nil
    

    which returns false in your check for neighbour, and everything else works as normal.

    However,

    @start_grid[ 5 ][ 3 ]
    

    is a problem, because @start_grid[ 5 ] is nil, so it is effectively

    nil[ 3 ]
    

    the error is thrown because Ruby has no logic for resolving what [] means on a nil.