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.
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?
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.
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💓💀💀💀💀
💀💀💀💀💓💀💀💀
💀💀💓💓💓💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💓💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💓💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀
💀💀💀💀💀💀💀💀