5
\$\begingroup\$

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()
\$\endgroup\$
  • \$\begingroup\$ 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. \$\endgroup\$ – 3limin4t0r Dec 14 '18 at 12:33
  • \$\begingroup\$ @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" \$\endgroup\$ – Aries_is_there Dec 14 '18 at 14:20
  • \$\begingroup\$ 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. \$\endgroup\$ – 3limin4t0r Dec 14 '18 at 17:53
  • \$\begingroup\$ yup, smells like Python with a scent of Pascal :D \$\endgroup\$ – Jenny T-Type Dec 14 '18 at 21:44
3
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ 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. \$\endgroup\$ – 3limin4t0r Dec 17 '18 at 10:22
  • \$\begingroup\$ 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 ^^) \$\endgroup\$ – Aries_is_there Dec 20 '18 at 7:03
  • \$\begingroup\$ @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. \$\endgroup\$ – 3limin4t0r Dec 22 '18 at 19:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.