Code’s slow 
post’s fast 
hopefully I find some time to profile and optimise the whole thing.
run.rb is the shell cmdline frontend
torus.rb is the freezer
ascii.rb and ppm.rb are two output plugins.
actually the output of the follwoing is nice already
rubr run.rb -f ascii 80 80 0.5 &&
for i in output/run-80-80.000000*.txt ; do clear; cat $i; sleep 1; done
GQView shows the ppm files nicely too and I guess lot’s of other
viewers do but I did not mange to create movies on Linux yet, any
hints would be appreciated.
And before I forget
YET ANOTHER GREAT RUBY QUIZ :))
534/35 > cat run.rb
vim: sw=2 sts=2 nu tw=0 expandtab:
require ‘fileutils’
require ‘torus’
def usage msg = nil
$stderr.puts msg if msg
$stderr.puts <<-EOS
usage:
#{$0} [options] height width vapor_probability
options and their defaults
-s|–start /2@/2 where to put the initial freezer
please use Smalltalk syntax here
-n|–name run-- name of the output file
-v|–vapor 255/0/255 rgb value for PPM
O use strings for ASCII
-0|–vacuum 0/0/0 idem
-i|–ice 255/255/255 idem
*
-f|–format ppm ppm or ascii are supported
write your own plugins 
have fun
EOS
exit -1
end
@start = @name = nil
@vapor = nil
@vacuum = nil
@ice = nil
@format = “ppm”
options = { /^-f|^–format/ => :format,
/^-s|^–start/ => :start,
/^-n|^–name/ => :name,
/^-v|^–vapor/ => :vapor,
/^-0|^–vacuum/ => :vacuum,
/^-i|^–ice/ => :ice }
loop do
break if ARGV.empty?
break if ARGV.first == “–”
break unless /^-/ === ARGV.first
illegal_option = true
options.each do
| opt_reg, opt_sym |
if opt_reg === ARGV.first then
usage “Missing argument for option #{ARGV}” if ARGV.length < 2
instance_variable_set( “@#{opt_sym}”, ARGV[1] )
ARGV.slice!( 0, 2 )
illegal_option = false
end
end
usage ARGV.first if illegal_option
end
usage ARGV.join(", ") unless ARGV.size == 3
require @format rescue usage
begin
mkdir( “output” ) unless File.directory?( “output” )
rescue
$stderr.puts ‘Cannot create output directory “output”’
usage
end
t = Torus( *(ARGV << @start) )
t.name = @name || “run-#{ARGV[0…1].join(”-")}"
t.formatter = Formatter.new( ICE => @ice, VAPOR => @vapor, VACUUM =>
@vacuum )
t.start_sim
535/36 > cat torus.rb
vim: sw=2 sts=2 nu tw=0 expandtab nowrap:
ICE = Class.new
VAPOR = Class.new
VACUUM = Class.new
###############################################################
a small reference to Python
###############################################################
def Torus( rows, cols, vapors, start = nil )
Torus_.new( rows.to_i, cols.to_i, vapors.to_f, start )
end
class Torus_
###############################################################
Torus_
###############################################################
attr_reader :lines, :columns
attr_reader :generation
attr_accessor :formatter, :name
def initialize rows, cols, vapors, start
@lines = rows
@columns = cols
@vapors = vapors
@generation = 0
if start then
@start = start.split("@").map{|e| e.to_i}
else
@start ||= [ rows/2, cols /2 ]
end
@nhoods = [] # we will store neighborhoods identified by
# their upper left corner index, odd is for even generations
# and even is for odd generations, which might seem odd.
reset_values
set_vapors
end
def [] line, col=nil
return @values[line] unless col
@values[line][col]
end # def [](line, col=nil)
def []= line, col, val
@values[line][col] = val
end
def each_cell
(1…@lines).each do
| line |
(1…@columns).each do
| column |
yield @values[line-1][column-1], line-1, column-1
end # (0…@columns).each do
end # (0…@lines).each do
end # def each_cell &blk
def each_line
@values.each{ |line| yield line }
end
def each_nbh
r = c = @generation % 2
loop do
yield @nhoods[ linear_idx( r, c ) ] ||=
Neighborhood.new( self, r, r.succ % @lines, c, c.succ %
@columns )
c += 2
r += 2 unless c < @columns
return unless r < @lines
c %= @columns
r %= @lines
end
end
def set_from_str str
@values = []
str.strip.split("\n").each do
| line_str |
@values << []
line_str.each_byte do
| char |
@values.last << case char.chr
when ICE.to_s
ICE
when VACUUM.to_s
VACUUM
when VAPOR.to_s
VAPOR
end
end
end
end
def start_sim
until no_more_vapor? do
tick
write
end
end # def start_sim
def tick
puts “Simulation #{@name} generation #{@generation}:”
@generation += 1
each_nbh do
| nbh |
nbh.recalc
end
end
private
def no_more_vapor?
! @values.any?{ |line|
line.any?{ |v| v == VAPOR }
}
end
def reset_values
@values = Array.new(@lines){
Array.new(@columns){
VACUUM
}
}
end
def set_vapors
total = @lines * @columns
v = ( @vapors * (total-1) ).to_i
x = [*0…total-2]
at = []
v.times do
at << x.delete_at( rand(x.size) )
end
at.each do
| index |
l,c = matrix_idx index
@values[l][c] = VAPOR
end
@values[@lines-1][@columns-1] = @values[@start.first][@start.last]
@values[@start.first][@start.last] = ICE
end # def set_vapors
def linear_idx r, c
r * @columns + c
end
def matrix_idx l
return l / @columns, l % @columns
end
def write
@formatter.to_file self, “output/#{@name}.%08d” % @generation
end # def write
end # class Torus_
###############################################################
Neighborhood is implementing a 2x2 window to any object
that responds to #[]n,m and #[]=n,m,value
It implements the operation of rotation.
###############################################################
class Neighborhood < Struct.new( :torus, :top, :bottom, :left, :right )
include Enumerable
Neighborhood gives us the following indexed view to the underlying
torus
±–±--+ ±----------±----------+
| 0 | 1 | | @top,@lft | @top,@rgt |
±–±--+ ±----------±----------+
| 3 | 2 | | @bot,@lft | @bot,@rgt |
±–±--+ ±----------±----------+
The Name and the Indexer implement that mapping
Names = [
%w{ top left },
%w{ top right },
%w{ bottom right },
%w{ bottom left }
]
def initialize *args
super *args
end
alias_method :access, :[] # Needed b/c/o the limited access
abilities of Struct
def [] n
access(“torus”)[ *resolve_idx( n ) ]
end
def []= n, val
access(“torus”)[ *resolve_idx( n ) ] = val
end
def each
4.times do
| idx |
yield self[idx]
end
end
def recalc
if any?{|v| v == ICE} then
4.times do
| idx |
self[ idx ] = ICE if self[ idx ] == VAPOR
end
else
rotate( rand(2) )
end
end
def rotate dir
x = self[0]
3.times do
| n |
self[ n + 2dirn ] = self[ n + 1 + dir2n.succ ]
end # 3.times do
self[ 3 + 2 * dir ] = x
end # def rotate dir
private
def resolve_idx n
[
access( Names[ n % 4 ].first ),
access( Names[ n % 4 ].last)
]
end # def resolv_idx
end # class Neighborhood
538/39 > cat ascii.rb
vim: sw=2 sts=2 nu tw=0 expandtab nowrap:
class Formatter
@@default = { ICE => “*”,
VAPOR => “0”,
VACUUM => " "
}
def initialize chars={}
@chars =
Hash[ *chars.to_a.map{ |(k,v)| [k, v || @@default[k] ] }.flatten ]
end # def initialize colors={}
def to_file( source, file, comment = nil )
File.open( “#{file}.txt”, “w” ) do
| f |
source.each_line{
|line|
line.each do
| cell |
f.print @chars[cell]
end
f.puts
}
end
end
end
539/40 > cat ppm.rb
vim: sw=2 sts=2 nu tw=0 expandtab nowrap:
class Formatter
@@default = { ICE => “255/255/255”,
VAPOR => “255/0/255”,
VACUUM => “0/0/0”
}
def initialize colors={}
@colors = {}
colors.each do
| element, color |
color ||= @@default[element]
@colors[ element ] = " " << color.gsub("/", " ") << " "
end # colors.each do
end # def initialize colors={}
def to_file( source, file, comment = nil )
comment ||= file
File.open( “#{file}.ppm”, “w” ) do
| f |
f.puts “P3 #{source.columns} #{source.lines} 255”
f.puts “#”
f.puts “# #{comment}”
f.puts “#”
source.each_line{
|line|
count = 0
line.each do
| cell |
s = @colors[cell]
if count + s.size > 70 then
f.puts
count = 0
end
count += s.size
f.print s
end
f.puts unless count.zero?
}
end
end
end