Deuxième partie du jeu de la vie en Ruby/Opal.rb, on va calculer le prochain
état d’une cellule, et extraire un voisinage de cellules d’une génération.
Après l’avoir écrit en Javascript, j’avoue que cette partie est quelque peu
ennuyeuse à reproduire. Je vais montrer du code, mais il y aura peu
d’explications, la logique étant la même qu’en Javascript (quoiqu’à base de
classes cette fois-ci).
Premier test et première classe pour spécifier une API.
require './app/neighborhood.rb'
describe Neighborhood do
let ( :alive ) { [ 1 , 1 , 1 , 0 , 0 , 0 , 0 , 0 , 0 ] }
describe '#next_state' do
it 'returns 1 when it will be alive' do
neighborhood = Neighborhood . new ( alive )
expect ( neighborhood . next_state ). to eq 1
end
end
end
class Neighborhood
def initialize ( cells )
@cells = cells
end
def next_state
1
end
end
next_state
doit être capable de determiner que la cellule va mourrir.
require './app/neighborhood.rb'
describe Neighborhood do
let ( :alive ) { [ 1 , 1 , 1 , 0 , 0 , 0 , 0 , 0 , 0 ] }
let ( :dead ) { [ 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 ] }
let ( :dead2 ) { [ 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] }
let ( :dead3 ) { [ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] }
describe '#next_state' do
...
it 'returns 0 when it will be dead' do
[ dead , dead2 , dead3 ]. each do | cells |
neighborhood = Neighborhood . new ( cells )
expect ( neighborhood . next_state ). to eq 0
end
end
class Neighborhood
ALIVE = 3
def initialize ( cells )
@sum = cells . reduce (: + )
end
def next_state
@sum == ALIVE ? 1 : 0
end
end
Quand le nombre de cellules vivantes du voisinage est 4, le prochain état de
la cellule est le même que l’état actuel.
require './app/neighborhood.rb'
describe Neighborhood do
...
let ( :status_quo1 ) { [ 1 , 1 , 1 , 1 , 0 , 0 , 0 , 0 , 0 ] }
let ( :status_quo2 ) { [ 0 , 1 , 1 , 1 , 1 , 0 , 0 , 0 , 0 ] }
describe '#next_state' do
...
it 'returns old state in other cases' do
neighborhood = Neighborhood . new ( status_quo1 )
expect ( neighborhood . next_state ). to eq 0
neighborhood = Neighborhood . new ( status_quo2 )
expect ( neighborhood . next_state ). to eq 1
end
end
end
class Neighborhood
ALIVE = 3
STATUS_QUO = 4
def initialize ( cells )
@subject = cells [ 4 ]
@sum = cells . reduce (: + )
end
def next_state
case @sum
when ALIVE then 1
when STATUS_QUO then @subject
else
0
end
end
end
Il faut pouvoir extraire un ensemble de 9 cellules (le voisinage ) d’une
génération.
require './app/neighborhood_extractor.rb'
describe NeighborhoodExtractor do
let ( :generation ) do
[
[ 0 , 1 , 0 , 1 ],
[ 1 , 0 , 1 , 0 ],
[ 0 , 1 , 1 , 0 ]
]
end
it 'returns 9 cells' do
x , y = 1 , 1
extractor = NeighborhoodExtractor . new ( generation , x , y )
expect ( extractor . cells . size ). to eq 9
end
end
Ça, c’est juste la mise en train.
class NeighborhoodExtractor
def initialize ( generation , x , y )
end
def cells
Array . new ( 9 )
end
end
Là, on commence à faire quelque chose d’utile.
describe NeighborhoodExtractor do
let ( :generation ) do
[
[ 0 , 1 , 0 , 1 ],
[ 1 , 0 , 1 , 0 ],
[ 0 , 1 , 1 , 0 ]
]
end
...
describe 'inner position' do
specify 'x=1 y=1' do
extractor = NeighborhoodExtractor . new ( generation , 1 , 1 )
expect ( extractor . cells ). to eq [ 0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 , 1 ]
end
end
end
class NeighborhoodExtractor < Struct . new ( :generation , :x , :y )
def cells
[
* generation [ y - 1 ][ x - 1 .. x + 1 ],
* generation [ y ][ x - 1 .. x + 1 ],
* generation [ y + 1 ][ x - 1 .. x + 1 ],
]
end
end
Maintenant, voyons le problème des bordures.
describe 'borders' do
specify 'x=1 y=0' do
extractor = NeighborhoodExtractor . new ( generation , 1 , 0 )
expect ( extractor . cells ). to eq [ 0 , 0 , 0 , 0 , 1 , 0 , 1 , 0 , 1 ]
end
end
La manière dont le test échoue est intéressante. C’est du à la façon dont Ruby
gère les indexs négatifs pour les tableaux, ceux-cis sont parfaitement
autorisés.
Failures:
1) NeighborhoodExtractor borders x=1 y=0
Failure/Error: expect(extractor.cells).to eq [0, 0, 0, 0, 1, 0, 1, 0, 1]
expected: [0, 0, 0, 0, 1, 0, 1, 0, 1]
got: [0, 1, 1, 0, 1, 0, 1, 0, 1]
(compared using ==)
# ./spec/neighborhood_extractor_spec.rb:29:in `block (3 levels) in <top (required)>'
class NeighborhoodExtractor < Struct . new ( :generation , :x , :y )
def cells
[
* row1 ,
* generation [ y ][ x - 1 .. x + 1 ],
* generation [ y + 1 ][ x - 1 .. x + 1 ],
]
end
def row1
if y == 0
[ 0 , 0 , 0 ]
else
generation [ y - 1 ][ x - 1 .. x + 1 ]
end
end
end
Testons avec la bordure du bas.
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
class NeighborhoodExtractor < Struct . new ( :generation , :x , :y )
...
def row3
if y == generation . size - 1
[ 0 , 0 , 0 ]
else
generation [ y + 1 ][ x - 1 .. x + 1 ]
end
end
end
Un peu de refactoring.
class NeighborhoodExtractor < Struct . new ( :generation , :x , :y )
def cells
[ * group_of_tree ( y - 1 ), * group_of_tree ( y ), * group_of_tree ( y + 1 ) ]
end
def group_of_tree ( row )
if row < 0 || row == generation . size
[ 0 , 0 , 0 ]
else
generation [ row ][ x - 1 .. x + 1 ]
end
end
end
La bordure de gauche.
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
class NeighborhoodExtractor < Struct . new ( :generation , :x , :y )
def cells
[ * group_of_tree ( y - 1 ), * group_of_tree ( y ), * group_of_tree ( y + 1 ) ]
end
def group_of_tree ( row )
if row < 0 || row == generation . size
[ 0 , 0 , 0 ]
else
if x == 0
[ 0 , * generation [ row ][ x .. x + 1 ] ]
else
generation [ row ][ x - 1 .. x + 1 ]
end
end
end
end
Et enfin celle de droite.
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
Ok, c’est moche, mais ça fonctionne.
class NeighborhoodExtractor < Struct . new ( :generation , :x , :y )
def cells
[ * group_of_tree ( y - 1 ), * group_of_tree ( y ), * group_of_tree ( y + 1 ) ]
end
def group_of_tree ( row_index )
if row_index < 0 || 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
Je devrais refactorer ce code, mais comme je sais qu’il va bientôt changer
(quand on va supprimer les bordures de la surface de jeu) je me dis qu’on verra
bien à ce moment là.
À noter pour finir que je ne teste pas les cas des cellules de coin. Nous avons
vu dans la version Javascript que si les cellules des bords droits, gauches,
hauts et bas fonctionnent, alors les coins fonctionnent aussi.
La prochaine fois on verra la classe Game
et une petite astuce pour faire
un sleep
like en Opal.rb.