Game of Life in Ruby

I think Ruby is kinda interesting, so did this Game of life implement in Ruby. I am wonder is there is some magic in Ruby can let my code more elegant.

I am a python coder and I think my Ruby kinda smell like Python now xD(and with lots end)

def lifegame(grid)
alive = 1
die = 0
while not lifeless(grid, alive)
print grid
print "\n"
next_round = update(grid, alive, die)
if next_round == grid
puts "In stable exiting..."
break
end
grid = next_round
end
end

def lifeless(grid, alive)
0.upto(grid.length-1) do |i|
0.upto(grid[0].length-1) do |j|
if(grid[i][j] == alive)
return false
end
end
end
return true
end

def update(grid, alive, die)
next_round = Array.new(grid.length){Array.new(grid[0].length, die)}
0.upto(grid.length-1) do |i|
0.upto(grid[0].length-1) do |j|
next_round[i][j] = evolve(grid, i, j, alive, die)
end
end
return next_round
end

def evolve(grid, i, j, alive, die)
directions = [[0,1],[0,-1],[1,0],[-1,0],[1,1],[1,-1],[-1,1],[-1,-1]]
t = 0
directions.each do |direction|
if (i+direction[0] >= 0 and i+direction[0] < grid.length and j+direction[1] >= 0 and j+direction[1] < grid[0].length)
if(grid[i+direction[0]][j+direction[1]] == alive)
t += 1
end
end
end
if((grid[i][j] == alive and (t < 2 or t > 3)) or (grid[i][j] == die and t != 3))
return die
else
return alive
end
end

grid = [[0,0,1,0,0],[1,0,1,0,0],[0,1,1,0,0],[0,0,0,0,0],[0,0,0,0,0]]
lifegame grid


class version

Thanks for @Johan Wentholt's advice about class

Here is my update code with custom class

Any advices all welcome!

class Game
WIDTH = 5
HEIGHT = 5
SEEDS = [[0,2],[1,0],[1,2],[2,1],[2,2]]

def initialize
@grid = Grid.new(WIDTH, HEIGHT)
@grid.plant_seeds(SEEDS)
end

def start
while not @grid.lifeless?
puts @grid
next_grid = update()
if(@grid == next_grid)
break
end
@grid = next_grid
end
end

def update
next_round = Grid.new(WIDTH, HEIGHT)
0.upto(WIDTH-1) do |row|
0.upto(HEIGHT-1) do |column|
next_round.update(row, column, evolve(row, column))
end
end
return next_round
end

def evolve(row, column)
directions = [[0,1],[0,-1],[1,0],[-1,0],[1,1],[1,-1],[-1,1],[-1,-1]]
t = 0
directions.each do |i, j|
if (row+i >= 0 and row+i < WIDTH and column+j >= 0 and column+j < HEIGHT)
if(@grid.cell_alive(row+i,column+j))
t += 1
end
end
end

return ((@grid.cell_alive(row,column) and (t == 2 or t == 3)) or (not @grid.cell_alive(row,column) and t == 3))
end

end

class Grid
def initialize(width, height)
@width = width
@height = height
@grid = setup_grid
end

def setup_grid
grid = []
@width.times do |row|
cells = []
@height.times do |column|
cells << Cell.new(false)
end
grid << cells
end
return grid
end

def plant_seeds(seeds)
seeds.each do |x,y|
@grid[x][y].live!
end
end

def update(row, column, value)
@grid[row][column].change_state(value)
end

def cell_alive(row, column)
return @grid[row][column].alive?
end

def lifeless?
not @grid.any?{|row| row.any?{|cell| cell.alive?}}
end

def to_s
rows = []
0.upto(@width-1) do |row|
columns = []
0.upto(@height-1) do |column|
columns << @grid[row][column].to_s
end
rows << columns.join("")
end
return rows.join("\n") + "\n\n"
end

def ==(other)
0.upto(@width-1) do |row|
0.upto(@height-1) do |column|
if cell_alive(row, column) != other.cell_alive(row, column)
return false
end
end
end
return true
end

end

class Cell
def initialize(alive)
@alive = alive
end

def change_state(state)
@alive = state
end

def alive?
@alive
end

def live!
@alive = true
end

def to_s
if @alive
return "x"
else
return "."
end
end

end

game = Game.new()
game.start()

• My first question would be if you avoid custom classes for a reason? I would at least have the classes Grid and Cell, maybe the class Game. You're current solution seems inspired by a functional programming, however Ruby is an object oriented language which should be used to your advantage. I could provided you with an more object oriented example if you want. However if you're not interested in using classes there is no point in working out an answer. Dec 14, 2018 at 12:33
• @JohanWentholt Ah I am not avoid classes, just hmm not think of it, thanks for your advice, I will update my code with classes. I am curious for the reason why "However if you're not interested in using classes there is no point in working out an answer" Dec 14, 2018 at 14:20
• I'll add my answer below somewhere this weekend. The reason I asked if you avoided classes is because it would result in wasted effort to work out an example if you intentionally avoided that approach. Dec 14, 2018 at 17:53
• yup, smells like Python with a scent of Pascal :D Dec 14, 2018 at 21:44

Like I said in the comments Ruby is an object oriented language. However in your first attempt you don't make use of custom classes and object at all. In your second attempt you do use custom classes, but in my opinion the design can be done better.

Simple way to spot "bad" Ruby code

One of the simplest ways to spot "bad" Ruby code is by the use of manual iteration. Ruby provides plenty of iterators that could be used instead of manually iterating over an collection. Examples are each, each_with_index, map, none?, all?, any? and many others.

In some cases you may not be able to work around manual iteration, but most scenarios have a build-in solution.

In case you need the index with map you can make use of the enumerator returned if no block is provided.

array.map.with_index { |element, index| ... }


Game rules

Let's first address the rules of the game:

1. Any live cell with fewer than two live neighbours dies, as if by under-population.
2. Any live cell with two or three live neighbours lives on to the next generation.
3. Any live cell with more than three live neighbours dies, as if by overpopulation.
4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.

Rule evaluation

The rules are all about the amount of neighbours that is alive. A cell could check this for himself if he knew who his neighbours are. For this reason I would leave the placement and neighbour assignment up to the Grid, but I would leave the state checking to the Cell itself. This would also eliminate a lot of the coordinate usage, since a cell doesn't care if a neighbour lives above, next or under him. The only thing that matters is the amount of neighbours alive. The only place where you still need the coordinates is when placing the cells and when assigning the neighbours of each cell.

In my opinion the code becomes a lot more readable when it speaks for itself.

Advantages of working with classes

Working with classes comes most of the time with some overhead (which can be seen in my example below), but has several advantages.

• When working with classes the methods are namespaced in the class. Keeping the global namespace free from clutter.
• You can assign certain classes certain responsibilities. This makes it easier to maintain code since you know where you should look for certain problems.

Responsibilities

I've chosen the following responsibilities for the different classes:

• Cell

A cell is responsible for its own state and the transition to the next state. It has references to its neighbours to check this.

• Grid

The grid is responsible for creating the grid, creating initially activated cells and assigning each cell its neighbours.

• Game

The game is responsible for grid instantiation and manages the game cycles to progress the grid further.

Code Example

class Cell
RELATIVE_NEIGHBOUR_COORDINATES = {
north: [-1, 0].freeze, north_east: [-1, 1].freeze,
east:  [0, 1].freeze,  south_east: [1, 1].freeze,
south: [1, 0].freeze,  south_west: [1, -1].freeze,
west:  [0, -1].freeze, north_west: [-1, -1].freeze,
}.freeze

NEIGHBOUR_DIRECTIONS = RELATIVE_NEIGHBOUR_COORDINATES.keys.freeze

attr_accessor(*NEIGHBOUR_DIRECTIONS)

def initialize(alive = false)
@alive = !!alive # "!!" converts alive value to boolean
end

def alive?
@alive
end

def live!
@alive = true
end

def die! # currently unused
@alive = false
end

##
# Queues the next state. Returns true if the state is going to change and
# false if it stays the same.
def queue_evolve
@queued_alive = alive_next_cycle?

@alive != @queued_alive
end

##
# Applies the queued state. Returns true if the state changed and false if the
# state stayed the same.
def apply_queued_evolve
old_alive = @alive

@alive = @queued_alive

old_alive != @alive
end

def alive_next_cycle?
alive_neighbours = neighbours.count(&:alive?)

if alive?
(2..3).cover?(alive_neighbours)
else
alive_neighbours == 3
end
end

def going_to_change?
alive? != alive_next_cycle?
end

##
# Used to get a neighbour in dynamic fashion. Returns the neighbouring cell or
# nil if there is no neighbour on the provided direction.
#
#     cell[:north]
#     #=> neighbouring_cell_or_nil
#
def [](direction)
validate_direction(direction)
send(direction)
end

##
# Used to set a neighbour in dynamic fashion. Returns the provided neighbour.
#
#     cell[:south] = other_cell
#     #=> other_cell
#
def []=(direction, neighbour)
validate_direction(direction)
send("#{direction}=", neighbour)
end

##
# Returns a list of all present neighbours.
def neighbours
NEIGHBOUR_DIRECTIONS.map(&method(:[])).compact
end

##
# Returns a hash of neighbours and their positions.
#
#     cell.neighbours_hash
#     #=> {
#       north: nil,
#       north_east: nil,
#       east: some_cell,
#       south_east: some_other_cell,
#       # ...
#     }
#
def neighbours_hash # currently unused
NEIGHBOUR_DIRECTIONS.map { |dir| [dir, self[dir]] }.to_h
end

##
# Returns "x" if the cell is alive and "." if the cell is not.
def to_s
alive? ? 'x' : '.'
end

##
# Since neighbours point to each other the default inspect results in an
# endless loop. Therefore this is overwritten with a simpler representation.
#
#     #<Cell dead> or #<Cell alive>
#
def inspect
"#<#{self.class} #{alive? ? 'alive' : 'dead'}>"
end

private

def validate_direction(direction)
unless NEIGHBOUR_DIRECTIONS.map(&:to_s).include?(direction.to_s)
raise "unsupported direction #{direction}"
end
end
end

class Grid
def initialize(width, height, seeds = [])
@cells = Array.new(width * height).map { Cell.new }
@grid  = @cells.each_slice(width).to_a

seeds.each { |coordinate| @grid.dig(*coordinate).live! }

assign_cell_neighbours
end

##
# Returns true if the resulting grid changed after evolution.
def evolve
# Keep in mind that any? short circuits after the first truethy evaluation.
# Therefore the following line would yield incorrect results.
#
#     @cells.each(&:queue_evolve).any?(&:apply_queued_evolve)
#

@cells.each(&:queue_evolve).map(&:apply_queued_evolve).any?
end

##
# Returns true if the next evolutions doesn't change anything.
def lifeless?
@cells.none?(&:going_to_change?)
end

##
# Returns the grid in string format. Placing an "x" if a cell is alive and "."
# if a cell is dead. Rows are separated with newline characters.
def to_s
@grid.map { |row| row.map(&:to_s).join }.join("\n")
end

private

##
# Assigns every cell its neighbours. @grid must be initialized.
def assign_cell_neighbours
@grid.each_with_index do |row, row_index|
row.each_with_index do |cell, column_index|
Cell::RELATIVE_NEIGHBOUR_COORDINATES.each do |dir, rel_coord|
(rel_row_index, rel_column_index) = rel_coord
neighbour_row_index    = row_index    + rel_row_index
neighbour_column_index = column_index + rel_column_index

next if neighbour_row_index.negative? ||
neighbour_column_index.negative?

cell[dir] = @grid.dig(neighbour_row_index, neighbour_column_index)
end
end
end
end
end

class Game
def initialize(width, height, seeds)
@width  = width
@height = height
@seeds  = seeds
end

def reset
@grid = Grid.new(@width, @height, @seeds)
end

def start
reset

puts @grid

until @grid.lifeless?
@grid.evolve

puts
puts @grid
end
end
end

game = Game.new(5, 5, [[0,2], [1,0], [1,2], [2,1], [2,2]])
game.start


The reason cell needs to update its state in two steps is simple. It can't depend upon the new state of one of its neighbours. For this reason all cells prepare their new state first before applying the prepared state.

References

Most things speak for themselves, however I still think some references are needed for the not so obvious (Ruby specific) code.

• The splat operator (*) used to use the contents of an array as individual arguments. Used in the lines:

@grid.dig(*coordinate)
# and
attr_accessor(*NEIGHBOUR_DIRECTIONS)

• attr_accessor is used to create getters and setters for the different neighbour directions.

attr_accessor(:north) # or attr_accessor :north
# is the same as
def north
@north
end

def north=(value)
@north = value
end


This allows cell.north to fetch the north neighbour and cell.north = neighbour to set the north neighbour.

• The use of send to dynamically call methods inside the Cell class.

• Array decomposition assignment done in the following line:

(rel_row_index, rel_column_index) = rel_coord

• Block passing. I currently can't find a reference for this. But the following things yield the same result.

numbers = [1, 2, 3, 4]

numbers.map { |number| number.to_s }
#=> ["1", "2", "3", "4"]
# is the same as
numbers.map(&:to_s)
#=> ["1", "2", "3", "4"]
#===========================================

def some_method(number)
number.to_s
end

numbers.map { |number| some_method(number) }
#=> ["1", "2", "3", "4"]
# is the same as
number.map(&method(:some_method))
#=> ["1", "2", "3", "4"]


Most other methods I use (e.g. none?, each_slice) can be found in the Enumerable module.

• I've thrown my my on spin on your solution. If there is any code you don't understand just ask and I'll add some clarification. Dec 17, 2018 at 10:22
• Thanks again. hmm yup some of your improvments are overhead to me, but I think will try to practice more to well understand them ^^, splat operator and decomposition also exist in python, I used write swift, so attr_accessor and block are not so new to me, but the send is really new, and the iteration ! I am curious what you plan to do with neighbours_hash, as I am not used to do so in python(maybe because I am a bad coder ^^) Dec 20, 2018 at 7:03
• @Aries_is_there I used Cell#neighbours_hash when coding to check if the neighbours where set correctly. But I left it because it might become in handy when you wan't to do more dynamic programming. When you call cell[:with_a_key_that_does_not_exist] it will raise a RuntimeError. While converting it to a hash and than calling neighbours_hash[:with_a_key_that_does_not_exist] will result in #=> nil. If you where to add the method Cell#assign_neighbours you might want to use an hash as parameter. This could be in the same format as the output of Cell#neighbours_hash. Dec 22, 2018 at 19:15