-
-
Save hindmasj/0fdaaf7ea5340a22786c to your computer and use it in GitHub Desktop.
Game Of Life - RPCFN #11
This file contains 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
#!/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 |
This file contains 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
#!/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 |
This file contains 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
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.