life.rb |
|
---|---|
Life is a class that implements Conway’s Game of life, a simple cellular automaton. The Game of Life is “played” on a two-dimensional grid of square cells, each of which is in one of two possible states, alive or dead. The game plays out in time-steps called ticks. At every tick, each cell interacts with its eight neighours. At each step, the following transitions occur:
I have implemented this code in a Test Driven manner in order to practice TDD and to play with the new Ruby testing library Minitest. The tests can be found here. There is also a simple Shoes app for visualizing the game. The code has been tested using Ruby 1.9.2 and 1.8.7. Github Repo: /stungeye/tdd_gam_of_life |
|
In the Beginning was the Word |
|
Our |
class Life
attr_reader :cells, :width, :height |
|
class OutOfBoundsError < ArgumentError; end |
When constructing a So, for example, if you had two live cells in the grid, one at x=1 and y=0 and another at x=2 and y=3, the array would be:
Side Note: Based on the number of times |
def initialize(cells, width, height)
@cells = cells
@width = width
@height = height
end |
These are the People in Your Neighbourhood |
|
Given two sets of x, y coordinates, are they |
def neighbour?(x1, y1, x2, y2) |
Remember, you are not your own neighbour! |
return false if x1 == x2 && y1 == y2 |
A neighbour is an adjecent cell. In otherwords, neither our delta x or our delta y can be greater than one. |
(x1 - x2).abs <= 1 && (y1 - y2).abs <= 1
end |
The |
def neighbours(x, y) |
If the x, y arguments are out of bounds we raise our custom |
raise OutOfBoundsError if out_of_bounds x, y
neighbours_found = 0 |
This method is compuationally expensive. As such, the performance degrades as our cells count grows. |
@cells.each do |cx, cy|
neighbours_found += 1 if neighbour? cx, cy, x, y
end
neighbours_found
end |
Life, The Next Generation |
|
The |
def next_gen |
We start with an empty cell array… |
@next_cells = []
@height.times do |y|
@width.times do |x| |
…and we add cells depending on the rules of the game. |
@next_cells << [x,y] if should_remain_alive?(x, y) ||
should_come_to_life?(x, y)
end
end |
Finally we return a new Life object instantiated with the next generation of cells. |
Life.new @next_cells, @width, @height
end |
The Rules of Life |
|
Cells that are alive should remain so if they have 2 or 3 neighbours. |
def should_remain_alive?(x,y)
alive?(x,y) && (2..3).include?(neighbours(x,y))
end |
Cells that are dead should come to life if they have exactly 3 neighbours. |
def should_come_to_life?(x, y)
dead?(x,y) && neighbours(x,y) == 3
end |
Helper Methods |
|
A cell at a particular x,y coordinate is alive if it can be found in our
array of |
def alive?(x, y)
@cells.include?([x, y])
end |
A cell at a particular x,y coordinate is dead if it is not alive. :) |
def dead?(x,y)
!alive?(x, y)
end |
Allow us to bring specific cells to life, as long as they aren’t already alive. This method is only used in the Shoes GUI. |
def set_cell_alive!(x, y)
@cells << [x, y] if !alive?(x,y)
end
|
Our grid bounaries are zero-based and depend on our |
def out_of_bounds(x, y)
x < 0 || x >= @width || y < 0 || y >= @height
end
|
Our Life object can output a string representation of itself. Live cells are shown as asteriks and dead cells as minus signs. |
def to_s
output = ""
@height.times do |y|
@width.times do |x|
output << (@cells.include?([x,y]) ? '*' : '-')
end
output << "\n"
end
output
end
end |
UNLICENSEThis is free and unencumbered software released into the public domain. |
|