SimFrost (#117)

On Mar 9, 2007, at 7:57 PM, Matthew M. wrote:

On 3/9/07, Josef ‘Jupp’ Schugt [email protected] wrote:

  • Matthew M., 09.03.2007 21:38:

Strictly speaking, as this is a torus, you could put it anywhere
and be at “the center”.

Actually it only looks as if we are acting on a torus. The reason is
as follows:

My fault for getting technical… someone else was bound to get even
more technical. =)

No, I think your observation is fundamentally sound. Since the
simulation really does take place on a torus, statistically speaking,
the outcome of the simulation should be independent of where the seed
for freezing is placed.

That this topology does distorts the actual physical situation being
simulated may reduce the simulation’s usefulness for making
inferences about nature, but it does not change the fact that, in the
simulation, any notion of center is an artifact of the coordinate
lattice introduced to facilitate its implementation.

Regards, Morton

On 3/10/07, Rick DeNatale [email protected] wrote:

On 3/10/07, Robert D. [email protected] wrote:

I failed to see one impact of where to put the initial freezer so far.
Now that I have eventually looked at James’ movie - great job James,
really nice - I imagine that it might indeed be a nice thing to start
the freezing process somewhere else than in the center of the
viewscreen that might nicely show how frost growth extends to the
other extremity of the viewscreen.

Although, it’s been pointed out that the reason it’s being modeled as
a torus is to avoid having to have an infinite plane.
But now I got acquainted to the torus idea although I know that an
infinite plane takes a heck of memory ;). (and a heck of computing
time too as Marvin knows)

If you put the seed near(er) an edge, I think that it just means that
you’re going to see some strange frost coming in from the far edge.
This might be avoided if you displayed a portion of the grid through a
clipping window.

Rick I think it is beautiful, well hopefully I can come up with
something this time.


Rick DeNatale

My blog on Ruby
http://talklikeaduck.denhaven2.com/

Robert

On 3/10/07, Robert D. [email protected] wrote:

I failed to see one impact of where to put the initial freezer so far.
Now that I have eventually looked at James’ movie - great job James,
really nice - I imagine that it might indeed be a nice thing to start
the freezing process somewhere else than in the center of the
viewscreen that might nicely show how frost growth extends to the
other extremity of the viewscreen.

Although, it’s been pointed out that the reason it’s being modeled as
a torus is to avoid having to have an infinite plane.

If you put the seed near(er) an edge, I think that it just means that
you’re going to see some strange frost coming in from the far edge.
This might be avoided if you displayed a portion of the grid through a
clipping window.


Rick DeNatale

My blog on Ruby
http://talklikeaduck.denhaven2.com/

Greetings and Salutations to each and all!

This is my first time submitting to the QUIZ. Please critique, I’m
still
learning Ruby, and TASM at school is messing with my brain (can’t think
rubyly).

Here’s the code:

class Snowflake
attr_reader :grid, :vapor
def initialize (y_size=24, x_size=30, vapor_percent=30)
@y_size=y_size/22 #this should take care of those odd numbers
@x_size=x_size/2
2
@vapor_percent=vapor_percent
@vacuum=" "
@vapor="+"
@ice="*"
@offset=1
create_grid
end

def create_grid
@grid=Array.new(@y_size){Array.new(@x_size)}
@grid.collect! do |row|
row.collect! do |square|
rand(100) < @vapor_percent ? @vapor : @vacuum
end
end
@grid[@y_size/2][@x_size/2]=@ice
end

def check_neighborhoods
@offset = (@offset +1)%2
@grid.collect!{|row| row.push(row.slice!(0))}.push(@grid.slice!(0))
if
@offset == 1 #torus me!
(0…@y_size).step(2) do |i|
(0…@x_size).step(2) do |j|
neighborhood=[@grid[i][j], @grid[i][j+1], @grid[i+1][j],
@grid[i+1][j+1]]
if !neighborhood.include?(@vapor)
elsif neighborhood.include?(@ice)
#there’s got to be a rubyer way of doing this…
@grid[i][j] =@ice if @grid[i][j] == @vapor
#top left corner
@grid[i][j+1] =@ice if @grid[i][j+1] == @vapor
#one
right
@grid[i+1][j] =@ice if @grid[i+1][j] == @vapor
#one
down
@grid[i+1][j+1] =@ice if @grid[i+1][j+1] == @vapor
#right
and down
elsif rand(2)==1
@grid[i][j], @grid[i][j+1], @grid[i+1][j], @grid[i+1][j+1] =
@grid[i+1][j], @grid[i][j], @grid[i+1][j+1], @grid[i][j+1]
else #It’s the correct sequence, maybe… I think…
@grid[i][j], @grid[i][j+1], @grid[i+1][j], @grid[i+1][j+1] =
@grid[i][j+1], @grid[i+1][j+1], @grid[i][j], @grid[i+1][j]
end
end
end #pop is to push, as slice!(0) is to ???. Many thanks to James
Edward G.: flip the data!
@grid.reverse!.collect!{|row| row.reverse!.push(row.slice!
(0)).reverse!}.push(@grid.slice!(0)).reverse! if @offset ==1
end

def to_s
@grid.collect{|row| row.join}.join("\n")
end
end

s=Snowflake.new(18,18,10)
while s.grid.collect{|row| true if row.include?(s.vapor)}.include?(true)
puts s
5.times do puts end
sleep(0.1)
s.check_neighborhoods
end
puts s

=begin Running through the finish line

           **
   *       *
   *   *  *
    *   **
     ** **
      **
    *** *
      ***
       * *
        * *
         *
         *

=end

Fun Quiz!

Here’s my solution (ASCII only I’m afraid). I use a wrapper object to
handle a neighbourhood. Since I only allow access one at a time, I
simply recycled a single object. This is unsafe in the general case,
but works nicely here I think.

My code for rotations are explicit than calculated, but I find it
easier to read and understand what is going on.

#!/usr/bin/env ruby -w

module SimFrost

class FrostGrid

 attr_reader :data

 def initialize(width, height, percent)
   @width, @height = width, height
   @data = Array.new(height) { Array.new(width) { rand * 100 <

percent ? ‘.’ : ’ ’ }.join }
self[width / 2, height / 2] = ?*
@neighbourhood = Neighbourhood.new(self)
@tick = 0
end

 def [](x, y)
   @data[y % @height][x % @width]
 end

 def []=(x, y, value)
   @data[y % @height][x % @width] = value
 end

 def tick
   @tick += 1
   vapour = 0
   each_neighbourhood do |neighbourhood|
     neighbourhood.mutate
     vapour += 1 if neighbourhood.contains_vapour?
   end
   vapour
 end

 def draw_freeze
   draw # Before we start freezing
   draw while tick > 0
   draw # After everything is frozen
 end

 def draw
   puts "Tick: #{@tick}"
   puts "+" + "-" * @width + "+"
   @data.each { |row| puts "|#{row}|" }
   puts "+" + "-" * @width + "+"
 end

 def each_neighbourhood
   @tick.step(@tick + @height, 2) do |y|
     @tick.step(@tick + @width, 2) do |x|
       yield @neighbourhood[x, y]
     end
   end
 end

end

class Neighbourhood

 2.times do |y|
   2.times do |x|
     class_eval "def xy#{x}#{y}; @grid[@x + #{x}, @y + #{y}]; end"
     class_eval "def xy#{x}#{y}=(v); @grid[@x + #{x}, @y + #{y}]

= v; end"
end
end

 def initialize(grid)
   @grid = grid
 end

 def [](x, y)
   @x, @y = x, y
   self
 end

 def ccw90
   self.xy00, self.xy10, self.xy01, self.xy11 = xy10, xy11, xy00,

xy01
end

 def cw90
   self.xy00, self.xy10, self.xy01, self.xy11 = xy01, xy00, xy11,

xy10
end

 def each_cell
   @y.upto(@y + 1) { |y| @x.upto(@x + 1) { |x| yield x, y } }
 end

 def contains?(c)
   each_cell { |x, y| return true if @grid[x, y] == c }
   false
 end

 def contains_ice?
   contains? ?*
 end

 def contains_vapour?
   contains? ?.
 end

 def freeze
   each_cell { |x, y| @grid[x, y] = ?* if @grid[x, y] == ?. }
 end

 def rotate_random
   rand < 0.5 ? ccw90 : cw90
 end

 def mutate
   contains_ice? ? freeze : rotate_random
 end

 def to_s
   "+--+\n+" << xy00 << xy10 << "+\n+" << xy01 << xy11 << "+\n+--+"
 end

end

def SimFrost.simfrost(width, height, percent = 50)
FrostGrid.new(width, height, percent).draw_freeze
end

end

if FILE == $PROGRAM_NAME
SimFrost::simfrost(40, 20, 35)
end

right, code got mangled, here’s the file

On 3/11/07, Robert D. [email protected] wrote:

Code’s slow :frowning:
post’s fast :wink:

hopefully I find some time to profile and optimise the whole thing.

Interesting case of the profiler showing bad design, the Neighborhood
class was really bad this one makes the code run twice as fast, still
slow but less so

590/91 > cat torus.rb

class Neighborhood 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 |

±–±–+ ±----------±----------+

def initialize *args
@torus, @top, @bottom, @left, @right = *args
@names = [ [@top, @left], [@top, @right], [@bottom, @right],
[@bottom, @left] ]
end

def [] n
@torus[ *@names[n%4] ]
end
def []= n, val
@torus[ *@names[n%4] ] = 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

end # class Neighborhood

SimFrost

A response to Ruby Q. #117 [ruby-talk:242714]

SimFrost simulates the growth of frost in a finite but unbounded

plane.

The simulation begins with vapor and vacuum cells, and a single ice

cell.

As the simulation progresses, the vapor and vacuum move around, and

vapor

coming into contact with ice becomes ice. Eventually no vapor

remains.

SimFrost is the simulator core, about 50 lines.

SimFrost::Console is a console interface. It parses command-line

options,

runs the simulator, and draws it in ASCII on a terminal.

You can run the script from the command-line:

usage: sim_frost.rb [options]

-w, --width N number of columns

-h, --height N number of rows

-p, --vapor-percentage N % of cells that start as vapor

-d, --delay-per-frame T delay per frame in seconds

-i, --ice S ice cell

-v, --vapor S vapor cell

-0, --vacuum S vacuum cell

–help show this message

Author: [email protected]

Created: 10 Mar 2007

Last modified: 11 Mar 2007

class SimFrost

attr_reader :width, :height, :cells

def initialize(width, height, vapor_percentage)
unless width > 0 && width % 2 == 0 &&
height > 0 && height % 2 == 0
throw ArgumentError, “width and height must be even, positive
numbers”
end
@width = width
@height = height
@cells = Array.new(width) do
Array.new(height) do
:vapor if rand * 100 <= vapor_percentage
end
end
@cells[width / 2][height / 2] = :ice
@offset = 0
end

def step
@offset ^= 1
@new_cells = Array.new(width) { Array.new(height) }
@offset.step(width - 1, 2) do |x|
@offset.step(height - 1, 2) do |y|
process_neighbourhood(x, y)
end
end
@cells = @new_cells
nil
end

def contains_vapor?
@cells.any? {|column| column.include? :vapor }
end

private

 def process_neighbourhood(x0, y0)
   x1 = (x0 + 1) % width
   y1 = (y0 + 1) % height
   hood = [[x0, y0], [x0, y1], [x1, y1], [x1, y0]]
   if hood.any? {|x, y| @cells[x][y] == :ice }
     hood.each do |x, y|
       @new_cells[x][y] = @cells[x][y] && :ice
     end
   else
     hood.reverse! if rand < 0.5
     4.times do |i|
       j = (i + 1) % 4
       @new_cells[hood[i][0]][hood[i][1]] =

@cells[hood[j][0]][hood[j][1]]
end
end
nil
end

module Console

 DEFAULT_RUN_OPTIONS = {
   :width => 78,
   :height => 24,
   :vapor_percentage => 30,
   :delay_per_frame => 0.1,
   :ice => " ",
   :vapor => "O",
   :vacuum => "#"
 }

 def self.run(options = {})
   opts = DEFAULT_RUN_OPTIONS.merge(options)
   sim = SimFrost.new(opts[:width], opts[:height],

opts[:vapor_percentage])
puts sim_to_s(sim, opts)
i = 0
while sim.contains_vapor?
sleep opts[:delay_per_frame]
sim.step
puts sim_to_s(sim, opts)
i += 1
end
puts “All vapor frozen in #{i} steps.”
end

 def self.sim_to_s(sim, options = {})
   sim.cells.transpose.map do |column|
     column.map do |cell|
       case cell
       when :ice:   options[:ice] || "*"
       when :vapor: options[:vapor] || "."
       else         options[:vacuum] || " "
       end
     end.join(options[:column_separator] || "")
   end.join(options[:row_separator] || "\n")
 end

 def self.parse_options(argv)
   require 'optparse'
   opts = {}
   op = OptionParser.new do |op|
     op.banner = "usage: #{$0} [options]"
     op.on("-w","--width N",Integer,"number of

columns"){|w|opts[:width] = w}
op.on("-h","–height N",Integer,“number of rows”)
{|h|opts[:height] = h}
op.on("-p", “–vapor-percentage N”, Integer,
“% of cells that start as vapor”){|p|
opts[:vapor_percentage] = p}
op.on("-d", “–delay-per-frame T”, Float,
“delay per frame in seconds”) {|d| opts[:delay_per_frame]
= d }
op.on("-i", “–ice S”, String, “ice cell”) {|i| opts[:ice] = i
}
op.on("-v", “–vapor S”, String, “vapor cell”) {|v|
opts[:vapor] = v }
op.on("-0", “–vacuum S”, String, “vacuum cell”){|z|
opts[:vacuum] = z }
op.on_tail("–help", “just show this message”) { puts op; exit
}
end

   begin
     op.parse!(ARGV)
   rescue OptionParser::ParseError => e
     STDERR.puts "#{$0}: #{e}"
     STDERR.puts op
     exit
   end
   opts
 end

end
end

if $0 == FILE
SimFrost::Console.run SimFrost::Console.parse_options(ARGV)
end

This is the solution I used to build the quiz movie. It works in a
Unix terminal and it can generate PPM images:

#!/usr/bin/env ruby -w

class SimFrost
def initialize(width, height, vapor)
@ticks = 0
@grid = Array.new(height) do
Array.new(width) { rand(100) < vapor ? “.” : " " }
end
@grid[height / 2][width / 2] = “*”
end

attr_reader :ticks

def width
@grid.first.size
end

def height
@grid.size
end

def tick
(tick_start…height).step(2) do |y|
(tick_start…width).step(2) do |x|
cells = [ [x, y ],
[wrap_x(x + 1), y ],
[wrap_x(x + 1), wrap_y(y + 1)],
[x, wrap_y(y + 1)] ]
if cells.any? { |xy| cell(xy) == “" }
cells.select { |xy| cell(xy) == “.” }.each { |xy| cell(xy,
"
”) }
else
rotated = cells.dup
if rand(2).zero?
rotated.push(rotated.shift)
else
rotated.unshift(rotated.pop)
end
new_cells = rotated.map { |xy| cell(xy) }
cells.zip(new_cells) { |xy, value| cell(xy, value) }
end
end
end
@ticks += 1
end

def complete?
not @grid.flatten.include? “.”
end

def to_s
@grid.map { |row| row.join }.join("\n")
end

private

def tick_start; (@ticks % 2).zero? ? 0 : 1 end

def wrap_x(x) x % width end
def wrap_y(y) y % height end

def cell(xy, value = nil)
if value
@grid[xy.last][xy.first] = value
else
@grid[xy.last][xy.first]
end
end
end

class UnixTerminalDisplay
BLUE = “\e[34m”
WHITE = “\e[37m”
ON_BLACK = “\e[40m”
CLEAR = “\e[0m”

def initialize(simulator)
@simulator = simulator
end

def clear
@clear ||= clear
end

def display
print clear
puts @simulator.to_s.gsub(/.+/, “#{BLUE + ON_BLACK}\&#{CLEAR}”).
gsub(/*+/, “#{WHITE + ON_BLACK}\&#{CLEAR}”).
gsub(/ +/, “#{ ON_BLACK}\&#{CLEAR}”)
end
end

class PPMImageDisplay
BLUE = [0, 0, 255].pack(“C*”)
WHITE = [255, 255, 255].pack(“C*”)
BLACK = [0, 0, 0 ].pack(“C*”)

def initialize(simulator, directory)
@simulator = simulator
@directory = directory

 Dir.mkdir directory unless File.exist? directory

end

def display
File.open(file_name, “w”) do |image|
image.puts “P6”
image.puts “#{@simulator.width} #{@simulator.height} 255”
@simulator.to_s.each_byte do |cell|
case cell.chr
when “.” then image.print BLUE
when “*” then image.print WHITE
when " " then image.print BLACK
else next
end
end
end
end

private

def file_name
File.join(@directory, “%04d.ppm” % @simulator.ticks)
end
end

if FILE == $PROGRAM_NAME
require “optparse”

options = { :width => 80,
:height => 22,
:vapor => 30,
:output => UnixTerminalDisplay,
:directory => “frost_images” }

ARGV.options do |opts|
opts.banner = “Usage: #{File.basename($PROGRAM_NAME)} [OPTIONS]”

 opts.separator ""
 opts.separator "Specific Options:"

 opts.on( "-w", "--width EVEN_INT", Integer,
          "Sets the width for the simulation." ) do |width|
   options[:width] = width
 end
 opts.on( "-h", "--height EVEN_INT", Integer,
          "Sets the height for the simulation." ) do |height|
   options[:height] = height
 end
 opts.on( "-v", "--vapor PERCENT_INT", Integer,
          "The percent of the grid filled with vapor." ) do |vapor|
   options[:vapor] = vapor
 end
 opts.on( "-t", "--terminal",
          "Unix terminal display (default)." ) do
   options[:output] = UnixTerminalDisplay
 end
 opts.on( "-i", "--image",
          "PPM image series display." ) do
   options[:output] = PPMImageDisplay
 end
 opts.on( "-d", "--directory", String,
          "Where to place PPM image files.  ",
          %Q{Defaults to "frost_images".} ) do |directory|
   options[:directory] = directory
 end

 opts.separator "Common Options:"

 opts.on( "-?", "--help",
          "Show this message." ) do
   puts opts
   exit
 end

 begin
   opts.parse!
 rescue
   puts opts
   exit
 end

end

simulator = SimFrost.new(options[:width], options[:height], options
[:vapor])
setup = options[:output] == PPMImageDisplay ?
[simulator, options[:directory]] :
[simulator]
terminal = options[:output].new(*setup)

terminal.display
until simulator.complete?
sleep 0.5 if options[:output] == UnixTerminalDisplay
simulator.tick
terminal.display
end
end

END

James Edward G. II

I used RMagick to create animated gifs. I’ve never programatically
generated images before, so I was very pleased to discover that it’s
as easy as I hoped it would be.

On my (slowly dying) 1.3GHz Celeron M, a 200 x 200 movie with 40%
vapor fill takes 3 minutes to make, resulting in a movie of about 25
seconds and 700KB. The memory usage while running gets up to about
80MB. An example output is here:
http://www.tie-rack.org/images/frost.gif

I decided to place the initial point of ice at a random location
instead of the center.

The vapor isn’t in the images, as it would dramatically increase the
time it takes to render each frame (or at least it would the way I’m
doing it).

There’s still some ugliness in there, but I became more interested in
tweaking than beautifying.

-Chris S.

frost.rb

require ‘RMagick’

module Frost
ICE = 0
NEWICE = 1
VAPOR = 2
VACUUM = 3
ICECOLOR = ‘blue’

class Window
def initialize(width, height, vapor_chance)
unless width % 2 == 0 and height % 2 == 0
raise ArgumentError, “divisible by 2”
end
@width = width
@height = height
row = Array.new(width, Frost::VACUUM)
@glass = Array.new(height) { row.dup }
@image = Magick::ImageList.new

  #place random vapor
  0.upto(height - 1) do |row|
    0.upto(width - 1) do |col|
      @glass[row][col] = Frost::VAPOR if rand < vapor_chance
    end
  end

  #place first ice
  #@glass[height / 2][width / 2] = Frost::NEWICE
  @glass[rand(height)][rand(width)] = Frost::NEWICE

  @step = 0
  make_gif
end

def step
  neighborhood_starts.each do |start|
      n = find_neighbors(start)
      n.step
      @glass[start[0]][start[1]] = n.layout[0]
      @glass[start[0]][(start[1]+1) % @width] = n.layout[1]
      @glass[(start[0]+1) % @height][start[1]] = n.layout[2]
      @glass[(start[0]+1) % @height][(start[1]+1) % @width] =

n.layout[3]
end
@step += 1
end

def neighborhood_starts
  starts = []
  offset = @step % 2
  offset.step(@height - 1, 2) do |row|
    offset.step(@width - 1, 2) do |col|
      starts << [row,col]
    end
  end
  starts
end

def find_neighbors(start)
  one = @glass[start[0]][start[1]]
  two = @glass[start[0]][(start[1] + 1) % @width]
  three = @glass[(start[0] + 1) % @height][start[1]]
  four = @glass[(start[0] + 1) % @height][(start[1] + 1) % @width]
  Frost::Neighborhood.new(one,two,three,four)
end

def done?
  @glass.each do |row|
    return false if row.include? Frost::VAPOR
  end
  true
end

def make_gif
  if @image.empty?
    @image.new_image(@width, @height)
  else
    @image << @image.last.copy
  end

  @glass.each_with_index do |row, y|
    row.each_with_index do |cell, x|
      if cell == Frost::NEWICE
        point = Magick::Draw.new
        point.fill(Frost::ICECOLOR)
        point.point(x,y)
        point.draw(@image)
      end
    end
  end
end

def create_animation
  @image.write("frost_#{Time.now.strftime("%H%M")}.gif")
end

def go
  until done?
    step
    make_gif
    print '.'
  end
  print "\ncreating animation... "
  create_animation
  puts 'done'
end

end

class Neighborhood
def initialize(one,two,three,four)
@layout = [one,two,three,four]
transform(Frost::NEWICE, Frost::ICE)
end

attr_reader :layout

def step
  if ice?
    ice_over
  else
    rotate
  end
end

def ice?
  @layout.include? Frost::ICE
end

def rotate
  if rand(2).zero?
    @layout = [@layout[1],@layout[3],@layout[0],@layout[2]]
  else
    @layout = [@layout[2],@layout[0],@layout[3],@layout[1]]
  end
end

def transform(from, to)
  @layout.map! {|cell| cell == from ? to : cell}
end

def ice_over
  transform(Frost::VAPOR, Frost::NEWICE)
end

end

end

if FILE == $0
if ARGV.size != 3
puts “frost.rb <vapor chance (float)>”
puts “This shouldn’t take too long: frost.rb 100 100 0.3”
exit
end
width = ARGV[0].to_i
height = ARGV[1].to_i
vapor_percent = ARGV[2].to_f
window = Frost::Window.new(width,height,vapor_percent).go
end

Code’s slow :frowning:
post’s fast :wink:

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 :wink:

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 :wink:

###############################################################
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

On Mar 11, 11:02 am, “Harrison R.” [email protected] wrote:

Thethree rulesof Ruby Q.:

Hello, everypeoples. First time here; please bear with me.

Oops, I forgot to add the win.getch call.

Also, I’d like to add that it’s especially cool to set your terminal
to a 1-pt font and resize it to about 150x150 (much more than that
bogs it down tremendously) before running the script. It gets you
close enough to an actual animation to be satisfied.

– Harrison R.

The three rules of Ruby Q.:

Hello, everypeoples. First time here; please bear with me.

My solution class is at the bottom of this message.
One could run the sim with something as simple as this:

frost = SimFrost.new(80,24)
puts frost while frost.step

but this will cause jerkiness due to the sheer mass of text.
So, in my solution script, quiz117.rb, I used curses instead:

require ‘simfrost’
require ‘curses’

win = Curses.init_screen

columns = win.maxx
lines = win.maxy

ensure even numbers

columns -= columns % 2
lines -= lines % 2

frost = SimFrost.new(columns, lines)

while frost.step
win.setpos(0,1)
win << frost.to_s
win.refresh
end

Of course, one could also use frost.to_a and translate frost symbols
(:vapor, :ice, :vacuum) into pixels, but I haven’t had the time to
play with a Ruby graphics library yet.

Anyway, this is the class:

SimFrost, solution to RubyQuiz #117

by Harrison R. 2007-03-10

class SimFrost
def initialize(width, height, vapor_ratio = 0.25)
@height = height.to_i
@width = width.to_i
vapor_ratio = vapor_ratio.to_f

raise "height must be even" if height % 2 == 1
raise "width must be even" if width % 2 == 1

# fill the matrix with random vapor
@grid = Array.new(height) do |row|
  row = Array.new(width) { |x| x = rand <

vapor_ratio ? :vapor : :vacuum }
end

# seed it with an ice particle
@grid[height/2][width/2] = :ice

@offset = 0

end

advances the frost simulation by one tick

or returns false if it has already finished.

def step
# confirm the presence of vapor
return false if @grid.each do |row|
break unless row.each { |sq| break if sq == :vapor }
end

# for each 2x2 box in the grid
(0...@height/2).each do |i|
  (0...@width/2).each do |j|
    # get the coordinates of the corners
    y0 = i + i + @offset
    x0 = j + j + @offset
    y1 = (y0 + 1) % @height
    x1 = (x0 + 1) % @width

    # check for ice
    if @grid[y0][x0] == :ice or @grid[y0][x1] == :ice or
       @grid[y1][x0] == :ice or @grid[y1][x1] == :ice
      # freeze nearby vapor
      @grid[y0][x0] = :ice if @grid[y0][x0] == :vapor
      @grid[y0][x1] = :ice if @grid[y0][x1] == :vapor
      @grid[y1][x0] = :ice if @grid[y1][x0] == :vapor
      @grid[y1][x1] = :ice if @grid[y1][x1] == :vapor
    else
      if rand < 0.5
        # rotate right-hand
        temp = @grid[y0][x0]
        @grid[y0][x0] = @grid[y1][x0]
        @grid[y1][x0] = @grid[y1][x1]
        @grid[y1][x1] = @grid[y0][x1]
        @grid[y0][x1] =  temp
      else
        # rotate left-hand
        temp = @grid[y0][x0]
        @grid[y0][x0] = @grid[y0][x1]
        @grid[y0][x1] = @grid[y1][x1]
        @grid[y1][x1] = @grid[y1][x0]
        @grid[y1][x0] = temp
      end
    end
  end
end

# toggle the offset
@offset = @offset ^ 1
true # report that progress has been made

end

def to_a; @grid; end

def to_s
@grid.map { |row| row.map { |sq| @@asciifrost[sq] }.join }.join
end

maps frost symbols to characters

@@asciifrost = { :vapor => ‘.’, :ice => ‘*’, :vacuum => ’ ’ }
end

Here’s my solution. I only provide text console output, which is
pretty effective when you pause a little bit between “frames”.
Glancing over some of the other solutions, the one thing I may have
done differently is pre-compute the two grid overlays.

Eric

Are you interested in on-site Ruby training that’s been highly
reviewed by former students? http://LearnRuby.com

====

class SimFrost

A Cell keeps track of its contents. It is essentially a mutable

Symbol with some extra knowledge to convert into a string.

class Cell
attr_accessor :contents

@@strings = { :space => ' ', :ice => '*', :vapor => '-' }

def initialize(contents)
  @contents = contents
end

def to_s
  @@strings[@contents]
end

end # class SimFrost::Cell

A Grid overlays the space dividing it up into 2-by-2 Boxes.

Different Grids can cover the same space if the offsets are

different.

class Grid

# A Box is a 2-by-2 slice of the space containing 4 cells, and a
# Grid contains a set of Boxes that cover the entire space.
class Box
  def initialize
    @cells = []
  end

  # Appends a cell to this box
  def <<(cell)
    @cells << cell
  end

  # Adjust the cell contents by the following rules: if any cell
  # contains Ice then all vapor in the Box will be transformed to
  # ice.  Otherwise rotate the four cells clockwise or
  # counter-clockwise with a 50/50 chance.
  def tick
    if @cells.any? { |cell| cell.contents == :ice }
      @cells.each do
        |cell| cell.contents = :ice if cell.contents == :vapor
      end
    else
      if rand(2) == 0  # rotate counter-clockwise
        @cells[0].contents, @cells[1].contents,
          @cells[2].contents, @cells[3].contents =
            @cells[1].contents, @cells[3].contents,
              @cells[0].contents, @cells[2].contents
      else  # rotate clockwise
        @cells[0].contents, @cells[1].contents,
          @cells[2].contents, @cells[3].contents =
            @cells[2].contents, @cells[0].contents,
              @cells[3].contents, @cells[1].contents
      end
    end
  end
end  # class SimFrost::Grid::Box


# Creates a Grid over the space provided with the given offset.
# Offset should be either 0 or 1.
def initialize(space, offset)
  @boxes = []
  rows = space.size
  cols = space[0].size

  # move across the space Box by Box
  (rows / 2).times do |row0|
    (cols / 2).times do |col0|

      # create a Box and add it to the list
      box = Box.new
      @boxes << box

      # add the four neighboring Cells to the Box
      (0..1).each do |row1|
        (0..1).each do |col1|
          # compute the indexes and wrap around at the far edges
          row_index = (2*row0 + row1 + offset) % rows
          col_index = (2*col0 + col1 + offset) % cols
          # add the indexed Cell to the Box
          box << space[row_index][col_index]
        end
      end
    end
  end
end

# Tick each box in this Grid.
def tick()
  @boxes.each { |box| box.tick }
end

end # class SimFrost::Grid

Creates the space and the two alternate Grids and initializes the

time counter to 0.

def initialize(rows, columns, vapor_rate)
# argument checks
raise ArgumentError, “rows and columns must be positive” unless
rows > 0 && columns > 0
raise ArgumentError, “rows and columns must be even” unless
rows % 2 == 0 && columns % 2 == 0
raise ArgumentError, “vapor rate must be from 0.0 to 1.0” unless
vapor_rate >= 0.0 && vapor_rate <= 1.0

# Create the space with the proper vapor ratio.
@space = Array.new(rows) do
  Array.new(columns) do
    Cell.new(rand <= vapor_rate ? :vapor : :space)
  end
end

# Put one ice crystal in the middle.
@space[rows/2][columns/2].contents = :ice

# Create the two Grids by using different offsets.
@grids = [Grid.new(@space, 0), Grid.new(@space, 1)]

@time = 0

end

Returns true if there’s any vapor left in @space

def contains_vapor?
@space.flatten.any? { |cell| cell.contents == :vapor }
end

Alternates which Grid is used during each tick and adjust the

Cells in each Box.

def tick
@grids[@time % 2].tick
@time += 1
end

def to_s
@space.map do |row|
row.map { |cell| cell.to_s }.join(‘’)
end.join(“\n”)
end
end # class SimFrost

if FILE == $0

choose command-line arguments or default values

rows = ARGV[0] && ARGV[0].to_i || 30
columns = ARGV[1] && ARGV[1].to_i || 60
vapor_rate = ARGV[2] && ARGV[2].to_f || 0.15
pause = ARGV[3] && ARGV[3].to_f || 0.025

s = SimFrost.new(rows, columns, vapor_rate)
puts s.to_s
while s.contains_vapor?
sleep(pause)
s.tick
puts “=” * columns # separator
puts s.to_s
end
end

I’m still working on a version that has fancy graphical output.
This is the ascii output version.

Raj S.

RubyQuiz #117

Frost Simulation

USAGE: ruby frost.rb [height] [width] [vapor_percentage]

class Fixnum
def even?
self%2 == 0
end

def odd?
not self.even?
end

def prev
self -1
end

end

#The order ROWxCOL is kept throughout the program

for any type of matrix/grid format.

class Torus
attr_reader :width, :height
attr_accessor :grid

def initialize(row, col)
raise “Width and Height must be even integers” unless row.even? and
col.even?
@width = col
@height = row
@grid = Array.new(row){Array.new(col)}
end

def
@grid[row]
end

def []=(row, value)
@grid[row] = value
end

def next_row(row)
row.next == @height ? 0 : row.next
end

def next_col(col)
col.next == @width ? 0 : col.next
end

def prev_row(row)
row == 0 ? @height.prev : row.prev
end

def prev_col(col)
col == 0 ? @width.prev : col.prev
end
end

class FrostSimulation
#Initialize with the number of rows and columns

and the percentage of the grid(an Integer from 0-100)

that should be vapor

def initialize(rows, cols, percentage)

@torus = Torus.new(rows, cols)
@torus.grid.each{|row| row.collect!{|n| rand(99) < percentage

?(:vapor):(:vacuum)}}
center = [rows/2, cols/2]
@torus[center[0]][center[1]] = :ice

end

def display
@torus.width.times{print ‘#’}; print “\n”
@torus.grid.each do |row|
row.each do |n|
if n == :vapor then print(’.’)
elsif n == :vacuum then print(’ ‘)
elsif n == :ice then print(’*’)
end
end
print “\n”
end
end

def extract_groups_at(tick)
ptr = tick.even? ? [0, 0] : [1, 1]
width, height = @torus.width/2, @torus.height/2
#Neighborhood array is formatted counterclockwise from starting
point
#Eg. one element of neighborhood shows [top_left, bottom_left,
bottom_right, top_right]
groups = Array.new(width*height){Array.new(4)}
groups.each_index do |index|
groups[index][0] = @torus.grid[ptr[0]][ptr[1]] #set top_left
ptr[0] = @torus.next_row(ptr[0]) #move pointer down
a row
groups[index][1] = @torus.grid[ptr[0]][ptr[1]] #set bottom_left
ptr[1] = @torus.next_col(ptr[1]) # move pointer
over a col
groups[index][2] = @torus.grid[ptr[0]][ptr[1]] # set bottom_right
ptr[0] = @torus.prev_row(ptr[0]) # move pointer up
a row
groups[index][3] = @torus.grid[ptr[0]][ptr[1]] #set top_right
ptr[1] = @torus.next_col(ptr[1]) # move pointer
over a col
#if we are at the end of a row, move the pointer down 2 rows
2.times{ptr[0] = @torus.next_row(ptr[0])} if index.next%width == 0
end
end

def process_groups(groups)
groups.each do |group|
if group.include?(:ice)
group.collect!{|n| n == :vapor ? :ice : n}
else
rand(100) < 51 ? group.unshift(group.pop) :
group.push(group.shift)
end
end
end

def inject_groups(tick, groups)
#this is the same algorithm as extraction
ptr = tick.even? ? [0, 0] : [1, 1]
width, height = @torus.width/2, @torus.height/2
groups.each_index do |index|
@torus.grid[ptr[0]][ptr[1]] = groups[index][0] #set top_left
ptr[0] = @torus.next_row(ptr[0]) #move pointer
down a row
@torus.grid[ptr[0]][ptr[1]] = groups[index][1] #set bottom_left
ptr[1] = @torus.next_col(ptr[1]) # move pointer
over a col
@torus.grid[ptr[0]][ptr[1]] = groups[index][2] # set bottom_right
ptr[0] = @torus.prev_row(ptr[0]) # move pointer
up a row
@torus.grid[ptr[0]][ptr[1]] = groups[index][3] #set top_right
ptr[1] = @torus.next_col(ptr[1]) # move pointer
over a col
#if we are at the end of a row, move the pointer down 2 rows
2.times{ptr[0] = @torus.next_row(ptr[0])} if index.next%width == 0
end
end

def run
tick = 0
continue = true
display
while continue
groups = inject_groups(tick,
process_groups(extract_groups_at(tick)))
display
continue = @torus.grid.flatten.detect{|n| n == :vapor}
tick = tick.next
sleep(0.15)
end
end
end

if $0 == FILE
rows = ARGV[0].nil? ? 24 : ARGV[0].to_i
cols = ARGV[1].nil? ? 40 : ARGV[1].to_i
percentage = ARGV[2].nil? ? 30 : ARGV[2].to_i
sim = FrostSimulation.new(rows, cols, percentage)
sim.run
end

It doesn’t get any faster
at least I got it shorter

649/150 > 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 :wink:

###############################################################
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_line
@values.each{ |line| yield line }
end

def each_nbh
r = c = @generation % 2
loop do
yield @nhoods[ r * @lines + 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 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 |
@values[index/@lines][index%@lines] = VAPOR
end
@values[@lines-1][@columns-1] = @values[@start.first][@start.last]
@values[@start.first][@start.last] = ICE
end # def set_vapors

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
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 |

±–±--+ ±----------±----------+

def initialize *args
@torus, @top, @bottom, @left, @right = *args
@names = [ [@top, @left], [@top, @right], [@bottom, @right],
[@bottom, @left] ]
end

def [] n
@torus[ *@names[n%4] ]
end
def []= n, val
@torus[ *@names[n%4] ] = 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

end # class Neighborhood
650/151 > 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
651/152 > 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 :wink:

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

On Mar 11, 1:20 pm, “Eric I.” [email protected] wrote:

Here’s my solution. I only provide text console output, which is
pretty effective when you pause a little bit between “frames”.
Glancing over some of the other solutions, the one thing I may have
done differently is pre-compute the two grid overlays.

Eric

I don’t have time to write an entire implementation, but I wanted to
do some graphics. I borrowed Eric’s code and used the rubysdl library
with it. I’m sure there is large room for improvement, but my wife
wants me to clean out the garage:)

require ‘sdl’
require ‘simfrost’

class SimFrost

class Cell
attr_accessor :contents

@@colors = {
  :vapor  => 65535,
  :space => 0,
  :ice  => 31
}
def to_sdl
  @@colors[@contents]
end

end

def to_sdl(screen)
@space.each_with_index do |row, i|
row.each_with_index { |cell, j|
screen.put_pixel(i,j,cell.to_sdl) }
end
screen.flip
end

end

rows = ARGV[0] && ARGV[0].to_i || 160
columns = ARGV[1] && ARGV[1].to_i || 120
vapor_rate = ARGV[2] && ARGV[2].to_f || 0.25
pause = ARGV[3] && ARGV[3].to_f || 0.025

SDL.init( SDL::INIT_VIDEO )

screen = SDL::setVideoMode(rows,columns,16,SDL::SWSURFACE)
SDL::WM::setCaption $0, $0

s = SimFrost.new(rows, columns, vapor_rate)
s.to_sdl(screen)
while s.contains_vapor?
sleep(pause)
s.tick
s.to_sdl(screen)
end

while true
while event = SDL::Event2.poll
case event
when SDL::Event2::KeyDown, SDL::Event2::Quit
exit
end
end

end

On Sat, 10 Mar 2007 04:45:26 +0900, Josef ‘Jupp’ Schugt wrote:

  1. Enjoy!

What about using using nonstandard ruby packages? To in particular I am
talking about NArray:

NArray is an n-dimensional numerical array class. Data types:
integer/float/complexe/Ruby object. Methods: array manipulation
including multi-dimensional slicing, fast arithmetic/matrix operations,
etc.

By all means, use it.

It’s interesting that it looks like everyone populated their grid
using a randomizer for each position in the grid.

This is is obviously fast, but for small grids (and low percentages),
the percentage of actual generated vapour particles may be off by
quite a bit.

For a 10x10 grid and 10% vapour, the amount of particles typically
range between 5 and 15, which in turn makes the finished frost look
very different from run to run.

I was thinking of ways to solve this. Obviously trying to randomly
put vapour particles into an array - and retry if it already
cointains vapur - is not ideal…

My best trick is this one:

require ‘enumerator’
percentage = 30
width = 30
height = 20
vapour = width * height * percentage / 100
vacuum = width * height - vapour
grid = []
(Array.new(vacuum, ’ ') + Array.new(vapour, ‘.’)).sort_by
{ rand }.each_slice(width) { |s| grid << s }

This gives us a grid that is guaranteed to have the correct initial
proportion of vapour.

Anyone else with a more elegant solution to the problem?

Christoffer

On Mar 11, 2007, at 3:10 PM, Ken B. wrote:

http://www.rubyquiz.com/
operations,
etc.

By all means, use it.

Ah, I somehow glazed over this message when it originally came in,
but Ken summed my opinion right up. I’m not a restrictions kind of guy.

James Edward G. II