Test driving Conway's Game of Life in Ruby : Part 2 of 2

Contd... Part 2
Do the evolution (with due apologies to Pearl Jam)
Let’s model our simulation with a file aptly titled ‘GameOfLife.rb’

Rule#1 : Any live cell with fewer than two live neighbours dies, as if by loneliness.
Red
Create new test case class

ts_GameOfLife.rb
require 'test/unit'
require 'Colony'
require 'GameOfLife'

class Test_GameOfLife < Test::Unit::TestCase

def test_Evolve_Loneliness
@obColony = Colony.readFromFile("Colony1.txt");
@obColony = GameOfLife.evolve(@obColony)
assert( !@obColony.isCellAlive(1, 0) )
assert( @obColony.isCellAlive(1, 1) )
assert( !@obColony.isCellAlive(2, 2) )
end
end


Since we want to run all the tests from multiple files now, we can create a “test suite” It is as easy as creating a new file named ts_GameOfLife.rb.
Naming conventions : Prefix ts_ for test suites..
ts_GameOfLife.rb
require 'test/unit'
require 'tc_Colony'
require 'tc_GameOfLife'


We can then run the test suite with
ruby ts_GameOfLife.rb

Green
We traverse each cell in the colony and kill the cell if it is alive
class GameOfLife

def evolve ( obColony )
(0..maxRows).each{ |iRow|
(0..maxCols).each{ |iCol|
killCellIfLonely(obColony, iRow, iCol)
}
}
return obColony;
end

private
def killCellIfLonely(obColony, iRow, iCol)
if (obColony.isCellAlive(iRow, iCol) && ( obColony.getLiveNeighbourCount( iRow, iCol ) < 2 ) )
obColony.markCell( iRow, iCol, false )
end
end

end


Detour
Hmm but we don’t have getLiveNeighbourCount() and markCell(). Let’s test drive
Small red

tc_Colony.rb
def test_GetLiveNeighbourCount()
assert_equal( 2, @obColony.getLiveNeighbourCount(0,0) )
assert_equal( 0, @obColony.getLiveNeighbourCount(0,3) )
assert_equal( 1, @obColony.getLiveNeighbourCount(1,3) )
assert_equal( 0, @obColony.getLiveNeighbourCount(2,3) )
end


Small green
Colony.rb
def getLiveNeighbourCount( iRow, iCol )
iCountOfLiveNeighbours = 0

obCoordinatesToExamine = [
[iRow-1, iCol-1], [iRow-1, iCol], [iRow-1, iCol+1],
[iRow, iCol-1], [iRow, iCol+1],
[iRow+1, iCol-1], [iRow+1, iCol ], [iRow+1, iCol+1] ]

obCoordinatesToExamine.each{ |curRow, curCol|
iCountOfLiveNeighbours = iCountOfLiveNeighbours+1 if (isCellInBounds(curRow, curCol) && isCellAlive(curRow, curCol))
}
return iCountOfLiveNeighbours
end

def isCellInBounds( iRow, iCol)
return ((0...@maxRows) === iRow) &amp;& ((0…@maxCols) === iCol)
end


Small Red
def test_MarkCell()
assert( !@obColony.isCellAlive(0,0) )

@obColony.markCell( 0, 0, true )
assert( @obColony.isCellAlive(0,0), "Should have born now!" )

@obColony.markCell( 0, 0, false )
assert( !@obColony.isCellAlive(0,0), "Should have died now!" )
end


Small Green
def markCell( iRow, iCol, bIsCellAlive )
@colonyGrid[iRow][iCol] = ( bIsCellAlive ? "1" : "0" )
return
end


Refactor
I see some duplication with “1” and “0” – but I’ll refactor with Strike 3.

Hmmm still Red is my evolve test case. Had to use a couple of traces to find the bugger .. I left out a dot in the bold section below. 2 dots – inclusive range, 3 dots – max bound is not part of the range.

def evolve ( obColony )
obNewColony = obColony.clone()

(0...obColony.maxRows).each{ |iRow|
(0...obColony.maxCols).each{ |iCol|
#print "Count = " + obColonySnapshot.getLiveNeighboursCount( iRow, iCol ).to_s
if ( obColony.getLiveNeighboursCount( iRow, iCol ) < 2 )
obNewColony.markCell( iRow, iCol, false )
end
}
}

return obNewColony;
end

Colony.rb
def clone()
#return Colony.new( @maxRows, @maxCols, Array.new(@colonyGrid) )
return Colony.new( @maxRows, @maxCols, @colonyGrid.map{ |elem| elem.clone } )
end


Refactor

Well I got bit with the ranges. I used 0..obColony.maxRows instead of 0…obColony.maxRows and it blew up my test. I think traversing the Colony should be handled / provided by the colony itself. I don’t want anyone making the same mistake.

Let’s move that into a ruby iterator style Colony.each() method. So here it is
Colony.rb
def each
(0...@maxRows).each{ |iRow|
(0...@maxCols).each{ |iCol|
yield iRow, iCol
}
}
end


so now I can use this in GameOfLife#evolve, Colony#to_s and Test_Colony#test_IsCellAlive. Code is even smaller!!

def evolve ( obColony )
obNewColony = obColony.clone()

obColony.each{ |iRow, iCol|
if ( obColony.getLiveNeighboursCount( iRow, iCol ) < 2 )
obNewColony.markCell( iRow, iCol, false )
end
}

return obNewColony;
end

Colony.rb
def to_s
sGridListing = ""
each{ |iRow, iCol|
sGridListing += ( isCellAlive( iRow, iCol) ? "@ " : ". " )
sGridListing += "\n" if iCol.succ == @maxCols
}
return sGridListing
end


Next we need a test for Colony#clone that we sneakily added in to fix this test. We need to check if the cloned colony is similar to the original ..
tc_Colony.rb
def test_Clone
clonedColony = @obColony.clone()
assert_not_equal( clonedColony.object_id, @obColony.object_id, "Cloned object is the same as original" )

@obColony.each{ |iRow, iCol|
assert_equal( @obColony.isCellAlive(iRow, iCol),
clonedColony.isCellAlive(iRow, iCol),
"Contents differ at " + iRow.to_s + ", " + iCol.to_s )
}
end


Ok let’s take a look at our next rule.
Rule#2 - Any live cell with more than three live neighbours dies, as if by overcrowding.
Let’s create a test colony for this rule.

Colony_Overcrowding.txt
3, 4
0 0 1 0
1 1 1 1
0 0 1 1


Red
tc_GameOfLife.rb
def test_Evolve_Overcrowding
arrExpected = [false, false, true, false,
false, false, false, false,
false, false, false, true]

@obColony = Colony.readFromFile("Colony_Overcrowding.txt");
@obColony = GameOfLife.new.evolve(@obColony)

iLooper = 0
@obColony.each{ |iRow, iCol|
assert_equal( arrExpected[iLooper], @obColony.isCellAlive(iRow, iCol),
"Mismatch at " + iRow.to_s + ", " + iCol.to_s )
iLooper += 1
}
end


Green
Ok so we kill em off even if there are too many of them in an area ! Just another OR clause …
def evolve ( obColony )
obNewColony = obColony.clone()

obColony.each{ |iRow, iCol|
if obColony.isCellAlive(iRow, iCol) and
( (obColony.getLiveNeighboursCount(iRow,iCol) < 2) or (obColony.getLiveNeighboursCount(iRow,iCol) > 3) )
obNewColony.markCell( iRow, iCol, false )
end
}
return obNewColony;
end



Rule#3 - Any dead cell with exactly three live neighbours comes to life.
Red

Colony_Birth.txt
5, 5
0 0 0 0 1
1 1 0 0 1
0 0 1 0 0
0 0 1 0 0
0 0 1 0 0

tc_GameOfLife.rb
def test_Evolve_Birth
arrExpected = [false, false, false, false, false,
false, true, false, true, false,
false, false, true, true, false,
false, true, true, true, false,
false, false,false, false, false
]

@obColony = Colony.readFromFile("Colony_Birth.txt");
@obColony = GameOfLife.new.evolve(@obColony)

iLooper = 0
@obColony.each{ |iRow, iCol|
assert_equal( arrExpected[iLooper], @obColony.isCellAlive(iRow, iCol),
"Mismatch at " + iRow.to_s + ", " + iCol.to_s )
iLooper += 1
}
end


Green
GameOfLife.rb
def evolve ( obColony )
obNewColony = obColony.clone()

obColony.each{ |iRow, iCol|
if obColony.isCellAlive(iRow, iCol)
if ( (obColony.getLiveNeighboursCount(iRow,iCol) < 2) or (obColony.getLiveNeighboursCount(iRow,iCol) > 3) ) then
obNewColony.markCell( iRow, iCol, false )
end
else
obNewColony.markCell( iRow, iCol, true ) if ( obColony.getLiveNeighboursCount(iRow,iCol) == 3 )
end
}
return obNewColony;
end


Hmm… that broke my overcrowding test. Oh yeah, as per the new rule, there is a newborn in that colony. Let me just update the expected results as

arrExpected = [false, false, true, true,
false, false, false, false,
false, false, false, true]

Refactor
The tests are duplicating code to traverse and check the Colony with the expected results/array. Extract method
private
def assert_colonies_are_similar( arrExpectedValues, obColony )
iLooper = 0
obColony.each{ |iRow, iCol|
assert_equal( arrExpectedValues[iLooper], obColony.isCellAlive(iRow, iCol),
"Mismatch at " + iRow.to_s + ", " + iCol.to_s )
iLooper += 1
}
end


refactored test looks like :
def test_Evolve_Overcrowding
arrExpected = [false, false, true, true,
false, false, false, false,
false, false, false, true]

obColony = Colony.readFromFile("Colony_Overcrowding.txt");

assert_colonies_are_similar( arrExpected, GameOfLife.new.evolve(obColony) )
end


And that’s a wrap !!
Finally I quickly write up what I learn is called a “Driver”
ARGV is an array that contains the command line parameters. I’ll take two.. thank you!
One for the input colony text file and the second for the number of generations.
We then just evolve and keep printing the colony.

if (ARGV.length != 2) then
puts "Usage : ruby GameOfLife.rb [ColonyTextFile] [No of generations]"
exit
end
if (!File.exist?(ARGV[0])) then
puts "Specified Colony File does not exist"
exit
end


obColony = Colony.readFromFile(ARGV[0]);

0.upto(ARGV[1].to_i) { |iGenNo|
puts obColony.to_s + "Gen:" + iGenNo.to_s

obColony = GameOfLife.new.evolve(obColony)
}

Run it as
L:\Gishu\Ruby\GameOfLife>ruby GameOfLife.rb TestColony.txt 100

You can also redirect output to a file
L:\Gishu\Ruby\GameOfLife>ruby GameOfLife.rb TestColony.txt 100 > a.txt


Open it up in Notepad for example… resize the height such that you can see one line below the Gen : X line. Now use the PgUp or PgDown keys to see evolution in action!!

By the way the TestColony.txt file contains the colony called Gosper’s glider gun. Watch the output and read the wiki to know why ! 

Hey this program is slow for 100+ generations.. I am at chapter 2 and find that there is another better way to implement this… a different perspective on this problem that cuts down the grid traversal for better performance.

Till next time….
Gishu
Feb 18, 2006

[Update : ]
A special Thanks to http://blog.wolfman.com/articles/2006/05/26/howto-format-ruby-code-for-blogs
for a good tip and an even cooler ruby script on pretty-formatting ruby code for html display.

Also the ruby-185-21 windows package seems to be broken. the gem install fails with a
"getaddrinfo: no address associated with hostname". I went back to 1.8.2 to get this done today !

No comments:

Post a Comment