Skip to content

Instantly share code, notes, and snippets.

@hindmasj
Created July 29, 2010 09:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hindmasj/0fdaaf7ea5340a22786c to your computer and use it in GitHub Desktop.
Save hindmasj/0fdaaf7ea5340a22786c to your computer and use it in GitHub Desktop.
Game Of Life - RPCFN #11
#!/usr/bin/env ruby
# ==Synopsis
# run_game_of_life: Run the game of life in the ncurses client.
#
# == Usage
# run_game_of_life [options]
#
# -?, --help::
# Show help
#
# -s <s>, --size <s>::
# Set the width and height of the universe to <s>. Overrides -x and -y.
#
# -w <x>, --width <x>::
# Set the width of the universe to <x>. Overrides -s. Default is 20
#
# -h <y>, --height <y>::
# Set the height of the universe to <y>. Overrides -s. Default is 20
#
# -l <file>, --load <file>::
# Load the universe from a serialized array. Overrides any sizing.
#
# -d <d>, --density <d>::
# Set the density factor of the universe to <d>. Density is a number between
# between 0 and 100 describing how densely populated on average the new
# universe will be: 0 - unpopulated, 100 - fully populated. Default is 50.
#
# -e <s>, --seed <s>::
# Set the seed for the population randomizer.
#
# -x, --show::
# Do not run the program, simply show the initial universe.
#
# Require getoptlong to parse command line arguments
require 'getoptlong'
# Reguire rdoc/usage to display help
require 'rdoc/usage'
# Require the game of life class and the ncurses visual client
require File.join(File.dirname(__FILE__), 'stevehindmarch')
require File.join(File.dirname(__FILE__), 'life_ncurses')
#
# Coding a game of life solution to ruby challenge:
# http://rubylearning.com/blog/2010/06/28/rpcfn-the-game-of-life-11/
#
# Author:: Stephen J Hindmarch
# Copyright:: British Telecommunications PLC (c) 2010
# License:: Distributed under the terms of the challenge
#
# - $URL: https://svn.coolworld.bt.co.uk/svn/sjh/trunk/ruby/run_game_of_life.rb $
# - $Author: sjh $
# - $Date: 2010-07-28 17:33:55 +0100 (Wed, 28 Jul 2010) $
# - $Revision: 251 $
#
# Run the game of life in a visual client - the ncurses client supplied
# by the challenge.
#
class GameRunner
def initialize
@opts=GetoptLong.new(['--help','-?',GetoptLong::NO_ARGUMENT],
['--size','-s',GetoptLong::REQUIRED_ARGUMENT],
['--height','-h',GetoptLong::REQUIRED_ARGUMENT],
['--width','-w',GetoptLong::REQUIRED_ARGUMENT],
['--load','-l',GetoptLong::REQUIRED_ARGUMENT],
['--density','-d',GetoptLong::REQUIRED_ARGUMENT],
['--seed','-e',GetoptLong::REQUIRED_ARGUMENT],
['--show','-x',GetoptLong::NO_ARGUMENT]
)
end
# Print the current state of the game to screen
def show game
game.state.each do |row|
row.each do |col|
print col
end
puts
end
end
# Parse the command line options and run the game.
def run
#Defaults
matrix=nil
run=true
x=20
y=20
d=50
s=nil
@opts.each do |opt,arg|
case opt
when '--help'
RDoc::usage
when '--size'
size=arg.to_i
x=size
y=size
when '--width'
x=arg.to_i
when '--height'
y=arg.to_i
when '--seed'
s=arg.to_i
when '--density'
d=arg.to_i
when '--load'
matrix=File.open(arg,'r') do |f|
Marshal.load(f)
end
when '--show'
run=false
end
end
game=GameOfLife.new(x,y,d,s)
if matrix
game.state=matrix
end
if run
LifeNcurses.new(game)
else
show game
end
end
end
GameRunner.new.run
#!/usr/bin/env ruby
#
# Coding a game of life solution to ruby challenge:
# http://rubylearning.com/blog/2010/06/28/rpcfn-the-game-of-life-11/
#
# Author:: Stephen J Hindmarch
# Copyright:: British Telecommunications PLC (c) 2010
# License:: Distributed under the terms of the challenge
#
# - $URL: https://svn.coolworld.bt.co.uk/svn/sjh/trunk/ruby/game_of_life.rb $
# - $Author: sjh $
# - $Date: 2010-07-28 17:33:55 +0100 (Wed, 28 Jul 2010) $
# - $Revision: 251 $
#
# This class a takes optional size parameters and creates a rectangular
# universe, randomly populated with cells. If you only supply one size you
# get a square universe.
# The cells live and die according to the well known rules of The Game Of Life.
#
# The current state of the universe is encoded in a 2 dimensional array of
# integers, where +0+ means an empty cell and +1+ is a living cell. You can
# supply the universe with your own array.
#
# In this solution I have concentrated on giving the game a robust algorithm
# and allowed it to cope with badly formed universes. A universe that is badly
# formed in one iteration, such as containing illegal values, or being
# non-rectangular, will evolve into a well formed universe in the next
# iteration.
class GameOfLife
@current_state
@next_state
# Create initial universe and randomly populate it with life.
# +width+:: the width of the universe in cells.
# +height+:: the height of the universe in cells.
# +density+:: average population density, as a percentage.
# +seed+::randon number seed.
# Raises +ArgumentError+ if the size is <0
def initialize width=3,height=width,density=50,seed=nil
if width<0 or height<0
raise ArgumentError.new('Size must be >= 0')
end
populate width,height,density,seed
end
# Reset the game by populating it with a new universe.
# +width+::number of columns.
# +height+::number of rows.
# +density+::density value, between 0 and 100.
# +seed+::randon number seed.
def populate width=20,height=20,density=50,seed=nil
if seed
srand seed
end
@current_state=Array.new(height){Array.new(width){
(rand(100)<density)?1:0
}
}
end
# Take the universe through one iteration of life. Returns the new
# state of the universe as 2D array.
def evolve
# Prepare new next_state matrix same shape as current matrix
@next_state=Array.new(@current_state.length){
Array.new(@current_state[0].length){0}
}
# Run through algorithm
@current_state.each_index{|row|
@current_state[row].each_index{|col| calc_next_state(row,col)}}
# Make next stage of life the current stage
@current_state=@next_state
end
# Set the universe with your own 2D array.
def state=matrix
@current_state=matrix
end
# Returns the current state of the universe as a 2D array.
def state
@current_state
end
# Calculate the future for a single cell. Returns the value this cell will
# take after the next evolve cycle.
def calc_next_state row,col
neighbours=neighbour_count row,col
@next_state[row][col]=next_life_state @current_state[row][col],neighbours
end
# Count the number of live neighbours a cell has.
def neighbour_count row,col
neighbours=0
# iterate around the neighbours
(-1..1).each do |i|
(-1..1).each do |j|
# Wrap lower right edge of matrix back to 0
nrow=(row==@current_state.length-i) ? 0 : row+i
ncol=(col==@current_state[row].length-j) ? 0 : col+j
# Do not count current cell
# Treat illegal values as 0
if (nrow!=row or ncol!=col) and @current_state[nrow][ncol]==1
neighbours += 1
end
end
end
return neighbours
end
# Work out next life state for a cell, given its current state and
# that of its neighbours.
# +current_state+::
# <tt>0</tt> or <tt>1</tt> depending on whether the cell is alive or dead.
# +neighbours+:: number of live neighbours - integer in range <tt>0..8</tt>.
def next_life_state current_state,neighbours
case current_state
when 1 then
(2..3) === neighbours ? 1 : 0
when 0 then
3 === neighbours ? 1 : 0
else
0
end
end
# Save a game pattern by serializing it to a file.
def save file='GOL.sav'
File.open(file,'w') do |f|
Marshal.dump(state,f)
end
end
# Load a game pattern by deserializing it from a file.
def load file='GOL.sav'
self.state=File.open(file,'r') do |f|
Marshal.load(f)
end
end
end
#!/usr/bin/env ruby
require 'test/unit'
require 'stevehindmarch'
#
# Coding a game of life solution to ruby challenge:
# http://rubylearning.com/blog/2010/06/28/rpcfn-the-game-of-life-11/
#
# Author:: Stephen J Hindmarch
# Copyright:: British Telecommunications PLC (c) 2010
# License:: Distributed under the terms of the challenge
#
# - $URL: https://svn.coolworld.bt.co.uk/svn/sjh/trunk/ruby/test_game_of_life.rb $
# - $Author: sjh $
# - $Date: 2010-07-29 10:19:44 +0100 (Thu, 29 Jul 2010) $
# - $Revision: 253 $
#
# Unit testing class for GameOfLife.
# Unit tests begin by ensuring a well formed universe can be created, set
# saved, loaded and examined.
#
# The algorithm was deliberately broken down so that each step could be tested
# in isolation and without the need to create a universe. The evolutionary
# cycle is tested as a whole using the test cases supplied in the challenge.
#
# There are a set of test cases that check edge conditions, such as using
# a trivial or badly formed universe.
#
class GameTest < Test::Unit::TestCase
def setup
@game = GameOfLife.new(3)
end
def measure_universe state,width,height
assert_equal height,state.length
state.each do |row|
assert_equal width,row.length
end
end
def weigh_universe state
weight=0
state.each do |row|
row.each do |x|
weight+=x
end
end
return weight
end
def test_create
state=@game.state
measure_universe state,3,3
end
def test_set_state
state=[[0,0,1],[0,1,1],[1,1,1]]
@game.state=state
assert_equal state,@game.state
end
# Test the save and load features
def test_serialize
first=[[0,0,0],[0,0,0],[0,0,0]]
f1='test_first.sav'
second=[[0,1,0],[1,0,1],[0,1,0]]
f2='test_second.sav'
@game.state=first
@game.save f1
@game.state=second
@game.save f2
@game.load f1
assert_equal first,@game.state
@game.load f2
assert_equal second,@game.state
File.delete('test_first.sav')
File.delete('test_second.sav')
end
# Test that you can create non-square universes
def test_create_rectangle
game=GameOfLife.new(4,6)
measure_universe game.state,4,6
game.populate(7,5)
measure_universe game.state,7,5
end
# Test that the density value gives expected results
def test_density
game=GameOfLife.new(3,3,100)
assert_equal 9,weigh_universe(game.state)
game.populate(3,3,0)
assert_equal 0,weigh_universe(game.state)
# This test will fail just through randomness 2 in 2^100 times
game.populate(10,10,50)
weight=weigh_universe(game.state)
assert weight<100
assert weight>0
end
# Test that setting seed produces predictable universes
def test_seed
@game.populate(3,3,50,0)
zero=@game.state
@game.populate(3,3,50,1)
assert_not_equal zero,@game.state
@game.populate(3,3,50,0)
assert_equal zero,@game.state
end
# Check the rules for what happens in the next generation.
def test_life_algorithm
assert_equal 0,@game.next_life_state(0,0)
assert_equal 0,@game.next_life_state(1,0)
assert_equal 0,@game.next_life_state(2,0)
assert_equal 0,@game.next_life_state(0,2)
assert_equal 1,@game.next_life_state(1,2)
assert_equal 0,@game.next_life_state(2,2)
assert_equal 1,@game.next_life_state(0,3)
assert_equal 1,@game.next_life_state(1,3)
assert_equal 0,@game.next_life_state(2,3)
assert_equal 0,@game.next_life_state(0,4)
assert_equal 0,@game.next_life_state(1,4)
assert_equal 0,@game.next_life_state(2,4)
end
# Check the method for counting up a cell's neighbours. Especially check
# that edge wrapping works as expected.
def test_neighbour_count
# Empty
@game.state=[[0,0,0],[0,0,0],[0,0,0]]
assert_equal 0,@game.neighbour_count(0,0)
assert_equal 0,@game.neighbour_count(1,1)
# One cell, check all neighbours, check proper edge wrapping
@game.state=[[0,0,0],[0,1,0],[0,0,0]]
assert_equal 1,@game.neighbour_count(0,2),'0,2'
assert_equal 1,@game.neighbour_count(0,0),'0,0'
assert_equal 1,@game.neighbour_count(0,1),'0,1'
assert_equal 1,@game.neighbour_count(1,0),'1,0'
assert_equal 0,@game.neighbour_count(1,1),'1,1' # Don't count self
assert_equal 1,@game.neighbour_count(1,2),'1,2'
assert_equal 1,@game.neighbour_count(2,0),'2,0'
assert_equal 1,@game.neighbour_count(2,1),'2,1'
assert_equal 1,@game.neighbour_count(2,2),'2,2'
# Eight cells, check all neighbours, check proper edge wrapping
@game.state=[[1,1,1],[1,0,1],[1,1,1]]
assert_equal 7,@game.neighbour_count(0,2),'0,2'
assert_equal 7,@game.neighbour_count(0,0),'0,0'
assert_equal 7,@game.neighbour_count(0,1),'0,1'
assert_equal 7,@game.neighbour_count(1,0),'1,0'
assert_equal 8,@game.neighbour_count(1,1),'1,1' # Only cell with 8
assert_equal 7,@game.neighbour_count(1,2),'1,2'
assert_equal 7,@game.neighbour_count(2,0),'2,0'
assert_equal 7,@game.neighbour_count(2,1),'2,1'
assert_equal 7,@game.neighbour_count(2,2),'2,2'
end
#Evolve test taken from original challenge code
def test_should_kill_with_no_neighbours
@game.state = [[1,0,0],[0,0,0],[0,0,0]]
after = @game.evolve
assert_equal 0,after[0][0]
end
#Evolve test taken from original challenge code
def test_should_kill_with_just_one_neighbour
@game.state = [[0,0,0],[1,0,0],[1,0,0]]
after = @game.evolve
assert_equal 0,after[1][0]
assert_equal 0,after[2][0]
end
#Evolve test taken from original challenge code
def test_should_kill_with_more_than_3_neighbours
@game.state = [[1,1,1],[1,1,1],[1,1,1]]
after = @game.evolve
assert_equal [[0,0,0],[0,0,0],[0,0,0]],after
end
#Evolve test taken from original challenge code
def test_should_give_birth_if_3_neighbours
@game.state = [[1,0,0],[1,1,0],[0,0,0]]
after = @game.evolve
assert_equal [[1,1,1],[1,1,1],[1,1,1]],after
end
# Test that a rectangular matrix works
def test_rectangle
# A simple rectangle with more rows than columns
@game.state = [[0,0],[0,0],[0,0]]
after = @game.evolve
assert_equal [[0,0],[0,0],[0,0]],after
# A bigger rectangle with more columns than rows
# and some life
@game.state = [[0,0,0,0],[1,1,0,0],[0,0,0,1]]
after = @game.evolve
assert_equal [[1,0,0,0],[1,0,0,0],[1,0,0,0]],after
end
# Test trivial games of size 0 and 1
def test_trivial
@game.state=[]
assert_equal [],@game.evolve
@game.state=[[0]]
assert_equal [[0]],@game.evolve
end
# Test robustness to badly formed games
def test_robust
# expected result
expected=[[0,0,0],[0,0,0],[0,0,0]]
# A non-rectangular game
@game.state=[[0,0,0],[0,0],[0,0,0]]
assert_equal expected,@game.evolve
# A game with illegal values
@game.state=[[0,0,'fred'],[0,nil,0],[0,43,0]]
assert_equal expected,@game.evolve
end
end
@hindmasj
Copy link
Author

This code is my entry for the RPCFN challenge #11 - "Game Of Life"

The game is encoded in a single class which is available in "stevehindmarch.rb". Unit tests are provided in the "test" script, and a runner script is provided in "run". Each file is self documented in RDoc style comments.

I have been programming for over 25 years, but this is my first ruby program beyond the snippets written while doing the tutorials. I started learning ruby on Friday (July 23rd). I would say this program has taken about 20 hours, including research time learning about unit tests, rdoc, getoptlong, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment