Il est temps de tout assembler, pour ça on va écrire une classe Game qui va jouer le rôle de chef d’orchestre.

La classe Game

class Game

  def initialize(generation, canvas, iterations)
    @iterations = iterations
    @height = generation.size
    @width = generation.first.size
    @generation = generation
    @canvas = canvas
  end

  def start
    draw
    @iterations -= 1
    if @iterations > 0
      update
      after_ms(500) { start }
    end
  end

  def draw
    @canvas.clear
    @generation.each_with_index do |line, y|
      line.each_with_index do |cell, x|
        @canvas.pixel(x, y) if cell == 1
      end
    end
  end

  def update
    new_generation = (0...@height).map do |y|
      (0...@width).map do |x|
        extractor = NeighborhoodExtractor.new(@generation, x, y)
        Neighborhood.new(extractor.cells).next_state
      end
    end
    @generation = new_generation
  end

end

Rien d’exceptionnel dans ce code, à part la ligne suivante, extraite de la méthode start:

      after_ms(500) { start }

Qu’est-ce que c’est que cette méthode after_ms ?

Je ne peux pas faire une bête boucle loop, ou un appel récursif à start puisqu’on est en Opal.rb, et pas vraiment en Ruby. Le code qui tourne, au final, sera du Javascript. Et si on n’insère pas des petites pauses, le navigateur ne va pas aimer du tout. Et puisqu’en Javascript il n’existe pas de fonction pause, il n’y en a pas non plus en Opal.rb.

J’avoue que je me suis gratter un peu la tête avant de trouver une solution toute simple. Il suffit d’écrire un wrapper autour de la fonction Javascript setTimeout:

module Kernel

  def after_ms(n, &block)
    `setTimeout(function() {`
      block.call
    `}, n);`
  end

end

Mise à l’échelle de l’affichage

Ça, c’est très simple.

<!DOCTYPE html>
<html>
  ...
  <body>
    <canvas width="400" height="400" id="canvas"></canvas>
    <script src="build.js"></script>
  </body>
</html>
class Canvas

  SCALE = 4

  def initialize
    @canvas  = `document.getElementById('canvas')`
    @context = `#@canvas.getContext('2d')`
    @height  = `#@canvas.height`
    @width   = `#@canvas.width`
  end

  def clear
    draw_rect(0, 0, @width, @height, 'black')
  end

  def pixel(x, y)
    draw_rect(x * SCALE, y * SCALE, SCALE, SCALE, 'white')
  end

  private

  def draw_rect(x, y, w, h, color)
    `#@context.fillStyle = #{color}`
    `#@context.fillRect(#{x}, #{y}, #{w}, #{h})`
  end

end

Supprimer les bordures

Ça, c’est très ennuyeux, vous pouvez sauter directement à la fin de l’article.

Je désactive les tests des bordures, puis je les réécrit un par un.

require './app/neighborhood_extractor.rb'

describe NeighborhoodExtractor do

  let(:generation) do
    [
      [0, 1, 0, 1],
      [1, 0, 1, 0],
      [0, 1, 1, 0]
    ]
  end

  ...

  describe 'borders' do
    specify 'x=1 y=0' do
      extractor = NeighborhoodExtractor.new(generation, 1, 0)
      expect(extractor.cells).to eq [0, 1, 1, 0, 1, 0, 1, 0, 1]
    end

    # specify 'x=2 y=2' do
    #   extractor = NeighborhoodExtractor.new(generation, 2, 2)
    #   expect(extractor.cells).to eq [0, 1, 0, 1, 1, 0, 0, 0, 0]
    # end

    # specify 'x=0 y=1' do
    #   extractor = NeighborhoodExtractor.new(generation, 0, 1)
    #   expect(extractor.cells).to eq [0, 0, 1, 0, 1, 0, 0, 0, 1]
    # end

    # specify 'x=3 y=1' do
    #   extractor = NeighborhoodExtractor.new(generation, 3, 1)
    #   expect(extractor.cells).to eq [0, 1, 0, 1, 0, 0, 1, 0, 0]
    # end

  end
end
  def group_of_tree(row_index)
    if row_index < 0
      generation[generation.size-1][x-1..x+1]
    elsif row_index == generation.size
      [0, 0, 0]
    else
      if x == 0
        [ 0, *generation[row_index][x..x+1] ]
      elsif x == generation.first.size - 1
        [*generation[row_index][x-1..x], 0]
      else
        generation[row_index][x-1..x+1]
      end
    end
  end

Après refactoring

class NeighborhoodExtractor < Struct.new(:generation, :x, :y)

  def cells
    [ *extract(y - 1), *extract(y), *extract(y + 1) ]
  end

  private

  def extract(row_index)
    row_index = generation.size - 1 if row_index < 0
    group_of_tree(row_index)
  end

  def group_of_tree(row_index)
    if row_index == generation.size
      [0, 0, 0]
    else
      if x == 0
        [ 0, *generation[row_index][x..x+1] ]
      elsif x == generation.first.size - 1
        [*generation[row_index][x-1..x], 0]
      else
        generation[row_index][x-1..x+1]
      end
    end
  end

end
  describe 'borders' do
    
    ...

    specify 'x=2 y=2' do
      extractor = NeighborhoodExtractor.new(generation, 2, 2)
      expect(extractor.cells).to eq [0, 1, 0, 1, 1, 0, 1, 0, 1]
    end
class NeighborhoodExtractor < Struct.new(:generation, :x, :y)

  def cells
    [ *extract(y - 1), *extract(y), *extract(y + 1) ]
  end

  private

  def extract(row_index)
    group_of_tree(ensure_overlapping(row_index))
  end

  def ensure_overlapping(index)
    if index < 0
      generation.size - 1
    elsif index == generation.size
      0
    else
      index
    end
  end

  def group_of_tree(row_index)
    if x == 0
      [ 0, *generation[row_index][x..x+1] ]
    elsif x == generation.first.size - 1
      [*generation[row_index][x-1..x], 0]
    else
      generation[row_index][x-1..x+1]
    end
  end

end
  describe 'borders' do

    ...

    specify 'x=0 y=1' do
      extractor = NeighborhoodExtractor.new(generation, 0, 1)
      expect(extractor.cells).to eq [1, 0, 1, 0, 1, 0, 0, 0, 1]
    end

    specify 'x=3 y=1' do
      extractor = NeighborhoodExtractor.new(generation, 3, 1)
      expect(extractor.cells).to eq [0, 1, 0, 1, 0, 1, 1, 0, 0]
    end

Ça y est, on y voit plus clair.

class NeighborhoodExtractor < Struct.new(:generation, :x, :y)

  def cells
    [ *extract(y - 1), *extract(y), *extract(y + 1) ]
  end

  private

  def extract(row_index)
    group_of_tree(ensure_overlapping(row_index))
  end

  def ensure_overlapping(index)
    if index < 0
      generation.size - 1
    elsif index == generation.size
      0
    else
      index
    end
  end

  def group_of_tree(row_index)
    row = generation[row_index]
    if x == 0
      [row[-1], *row[x..x+1] ]
    elsif x == generation.first.size - 1
      [*row[x-1..x], row[0]]
    else
      row[x-1..x+1]
    end
  end

end

Voilà, Ruby/Opal.rb c’est fait. Vous pouvez trouver le code sur Github si vous êtes intéressés : Le jeu de la vie en ruby/opal.rb.

La prochaine version sera écrite en Racket, un dialecte de Lisp.