Created
December 9, 2012 19:38
-
-
Save jacegu/4246676 to your computer and use it in GitHub Desktop.
Functional take on Conway's Game of Life
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Cell | |
def cell | |
{ state: :alive, neighbours: [] } | |
end | |
def dead_cell | |
{state: :dead, neighbours: []} | |
end | |
def neighbours_of(cell, neighbour_info) | |
cell.merge!(neighbours: neighbour_info[:are]) | |
end | |
def evolve_cell(cell) | |
if underpopulated?(cell) or overpopulated?(cell) | |
dead_cell | |
else | |
cell | |
end | |
end | |
def underpopulated?(cell) | |
neighbour_count(cell) < 2 | |
end | |
def overpopulated?(cell) | |
neighbour_count(cell) > 3 | |
end | |
def neighbour_count(cell) | |
cell[:neighbours].length | |
end | |
end | |
module Organism | |
include Cell | |
def organism(cells = {}) | |
cells | |
end | |
def evolved(organism) | |
organism.each_with_object({}){ |(name, cell), evolved| evolved[name] = evolve_cell(cell) } | |
end | |
def cell_named(name, organism_info) | |
organism_info[:from][name] | |
end | |
end | |
RSpec.configure do |config| | |
config.include(Organism) | |
end | |
RSpec::Matchers.define :be_alive do | |
match do |cell| | |
cell[:state] == :alive | |
end | |
end | |
RSpec::Matchers.define :be_dead do | |
match do |cell| | |
cell[:state] == :dead | |
end | |
end | |
describe 'evolution' do | |
context 'in an organism with a cell without neighbours' do | |
it 'kills that cell' do | |
cell_named(:c1, from: evolved(organism(c1: cell))).should be_dead | |
end | |
end | |
context 'in an organism with a cell with 2 neighbours' do | |
it 'keeps that cell alive' do | |
c1, c2, c3 = cell, cell, cell | |
neighbours_of c1, are: [c2, c3] | |
neighbours_of c2, are: [c1] | |
neighbours_of c3, are: [c1] | |
evolved_organism = evolved(organism(c1: c1, c2: c2, c3: c3)) | |
cell_named(:c1, from: evolved_organism).should be_alive | |
cell_named(:c2, from: evolved_organism).should be_dead | |
cell_named(:c3, from: evolved_organism).should be_dead | |
end | |
end | |
context 'in an organism with a cell with 3 neighbours' do | |
it 'keeps that cell alive' do | |
c1, c2, c3, c4 = cell, cell, cell, cell | |
neighbours_of c1, are: [c2, c3, c4] | |
neighbours_of c2, are: [c1, c3] | |
neighbours_of c3, are: [c1, c2] | |
neighbours_of c4, are: [c1] | |
evolved_organism = evolved(organism(c1: c1, c2: c2, c3: c3, c4: c4)) | |
cell_named(:c1, from: evolved_organism).should be_alive | |
cell_named(:c2, from: evolved_organism).should be_alive | |
cell_named(:c3, from: evolved_organism).should be_alive | |
cell_named(:c4, from: evolved_organism).should be_dead | |
end | |
end | |
context 'in an organism with a cell with 4 neighbours' do | |
it 'kills that cell' do | |
c1, c2, c3, c4, c5 = cell, cell, cell, cell, cell | |
neighbours_of c1, are: [c2, c3, c4, c5] | |
neighbours_of c2, are: [c1, c3] | |
neighbours_of c3, are: [c1, c2] | |
neighbours_of c4, are: [c1] | |
neighbours_of c5, are: [c1] | |
evolved_organism = evolved(organism(c1: c1, c2: c2, c3: c3, c4: c4, c5: c5)) | |
cell_named(:c1, from: evolved_organism).should be_dead | |
cell_named(:c2, from: evolved_organism).should be_alive | |
cell_named(:c3, from: evolved_organism).should be_alive | |
cell_named(:c4, from: evolved_organism).should be_dead | |
cell_named(:c5, from: evolved_organism).should be_dead | |
end | |
end | |
end |
Nice, quite readable.
You have a couple of bugs, though:
- A dead cell with 3 neighbours should come to life, which I don't think your implementation does.
- You don't keep the neighbours on evolving to a dead_cell, so subsequent generations will deviate from the expected results.
Also, there are two things I would do different:
- Neighbourship is reciprocal, so I don't like the fact that you have two define it both ways.
- evolve_cell returns a new dead_cell or the cell itself. It's a bit subtle, since the parameter name is the same as the "new cell" function. Maybe a more symmetrical behaviour would be clearer.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is a functional take on Conway's Game of Life written in Ruby. We ended up taking this approach because of the restriction for that iteration: no properties.