[QUIZ] SimFrost (#117)

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

This was one of the Perl Quiz of the Week problems a couple of years ago. It's
also my favorite computer simulation.

The goal is to create a simulation of frost. The end result should look
something like this (13MB Quicktime Movie):

  http://www.rubyquiz.com/SimFrost.mov

Of course, you don't have to create a movie. Using the terminal is just fine.

Here are the rules of the simulation.

First, create a two-dimensional grid to hold our simulation. The width and
height must be even numbers. The top of this grid wraps (or connects) to the
bottom and the left and right sides also touch, so technically we are
representing a torus here.

Each cell in the grid can hold one of three things: vacuum, vapor, or ice. To
begin with, place a single ice particle in the center of the grid and fill the
remaining space with some random mix of vacuum and vapor. You will probably
want to leave yourself a way to easily adjust the starting vapor percentage as
you begin to play with the simulation.

Display the starting grid for the user, then begin a cycle of "ticks" that
change the current state of the grid. Redraw the grid after each tick, so the
user can see the changes. The simulation continues until there are no more
vapor particles. At that time your program should exit.

Each tick changes the grid as follows. First, divide the board into
"neighborhoods." A neighborhood is always a square of four cells. Given that,
on the first tick we would divide a six by four grid as:

  +--+--+--+
  > > > >
  > > > >
  +--+--+--+
  > > > >
  > > > >
  +--+--+--+

However, even numbered ticks divide the board with an offset of one. In other
words, the neighborhoods will be as follows in the second round:

   > > >
  -+--+--+-
   > > >
   > > >
  -+--+--+-
   > > >

Remember that the grid wraps in all directions, so there are still just six
neighborhoods here. Later ticks just alternate these two styles of dividing the
grid.

In each neighborhood, apply one of the following two changes:

  1. If any cell in the neighborhood contains ice, all vapor particles in the
      neighborhood turn to ice.
  2. Otherwise, rotate the contents of the neighborhood 90% clockwise or
      counter-clockwise (50% random chance for either).

For example (using " " for vacuum, "." for vapor, and "*" for ice), the first
rule changes:

  +--+
  > .|
  >* |
  +--+

into:

  +--+
  > *|
  >* |
  +--+

The second rule changes:

  +--+
  >..|
  > >
  +--+

into:

  +--+ +--+
  >. | or | .| 50% chance
  >. | | .|
  +--+ +--+

Time is discrete in these ticks, so given some grid state at tick T all
neighborhood changes appear simultaneously in tick T + 1.

Again, use whatever output you are comfortable with, from ASCII art in the
terminal to pretty graphics.

Quick clarification, I would assume you mean "turn 90 degrees", not 90%, as
that doesn't make any sense.

Jason

···

On 3/9/07, Ruby Quiz <james@grayproductions.net> wrote:

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz
until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby Talk follow the discussion. Please reply to the original quiz
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

This was one of the Perl Quiz of the Week problems a couple of years
ago. It's
also my favorite computer simulation.

The goal is to create a simulation of frost. The end result should look
something like this (13MB Quicktime Movie):

        http://www.rubyquiz.com/SimFrost.mov

Of course, you don't have to create a movie. Using the terminal is just
fine.

Here are the rules of the simulation.

First, create a two-dimensional grid to hold our simulation. The width
and
height must be even numbers. The top of this grid wraps (or connects) to
the
bottom and the left and right sides also touch, so technically we are
representing a torus here.

Each cell in the grid can hold one of three things: vacuum, vapor, or
ice. To
begin with, place a single ice particle in the center of the grid and fill
the
remaining space with some random mix of vacuum and vapor. You will
probably
want to leave yourself a way to easily adjust the starting vapor
percentage as
you begin to play with the simulation.

Display the starting grid for the user, then begin a cycle of "ticks" that
change the current state of the grid. Redraw the grid after each tick, so
the
user can see the changes. The simulation continues until there are no
more
vapor particles. At that time your program should exit.

Each tick changes the grid as follows. First, divide the board into
"neighborhoods." A neighborhood is always a square of four cells. Given
that,
on the first tick we would divide a six by four grid as:

        +--+--+--+
        > > > >
        +--+--+--+
        > > > >
        +--+--+--+

However, even numbered ticks divide the board with an offset of one. In
other
words, the neighborhoods will be as follows in the second round:

         > > >
        -+--+--+-
         > > >
        -+--+--+-
         > > >

Remember that the grid wraps in all directions, so there are still just
six
neighborhoods here. Later ticks just alternate these two styles of
dividing the
grid.

In each neighborhood, apply one of the following two changes:

        1. If any cell in the neighborhood contains ice, all vapor
particles in the
            neighborhood turn to ice.
        2. Otherwise, rotate the contents of the neighborhood 90%
clockwise or
            counter-clockwise (50% random chance for either).

For example (using " " for vacuum, "." for vapor, and "*" for ice), the
first
rule changes:

        +--+
        > .|
        >* |
        +--+

into:

        +--+
        > *|
        >* |
        +--+

The second rule changes:

        +--+
        >..|
        > >
        +--+

into:

        +--+ +--+
        >. | or | .| 50% chance
        >. | | .|
        +--+ +--+

Time is discrete in these ticks, so given some grid state at tick T all
neighborhood changes appear simultaneously in tick T + 1.

Again, use whatever output you are comfortable with, from ASCII art in the
terminal to pretty graphics.

* Ruby Quiz, 09.03.2007 13:58:

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

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

http://rubyforge.org/projects/narray/

Josef 'Jupp' Schugt

···

--
Blog available at http://www.mynetcologne.de/~nc-schugtjo/blog/
PGP key with id 6CC6574F available at http://wwwkeys.de.pgp.net/

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

simfrost.rb (3.15 KB)

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 Shea

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

The three rules of Ruby Quiz:

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

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

Hi,

here is my solution. Runs in a Terminal.
A copy can be viewed here: http://sec.modprobe.de/quiz117.rb.html

Tom

frost2.rb (3.71 KB)

James Gray wrote:

The three rules of Ruby Quiz:

Woah, this quiz was very entertaining. I enjoyed a lot doing it, and
still enjoy watching it every time :smiley:

As the console version wouldn't let me be happy, I tried to do it using
OpenGL. I got it at the end, although it's pretty slow (runs at decent
speed for size of <200*200, obviously the greater values the slowest),
but I'm happy with it for being my first try using GL.

I'll probably spend another little time tuning it up (as I stated again
that premature optimization is evil), and perhaps designing the 3D view
someone suggested :O.

Thanks again for the great quizes!

···

____________

# Quiz 117 : SimFrost
# Ruben Medellin <chubas7@gmail.com>
# Usage> ruby quiz117.rb width height vapor_density

#Based on OpenGL
require 'opengl'
require 'glut'

# Each pixel represents an element.
class Element

  attr_accessor :element

  def initialize(element)
    @element = element
  end

  #Just a change of state. Don't forget to decrement the vapor number.
  def freeze
    if @element == :vapor
      $VAPOR -= 1
      @element = :ice
    end
  end
end

# Main class
class Freeze_Simulator

  #Standard initializer. It prepares the windows to be called.
  def initialize

    $WIDTH = ARGV[0] && ARGV[0].to_i || 100
    $HEIGHT = ARGV[1] && ARGV[1].to_i || 100

    $WIDTH += 1 if $WIDTH % 2 != 0
    $HEIGHT += 1 if $HEIGHT % 2 != 0

    $DENSITY = ARGV[2] && ARGV[2].to_i || 30

    $VAPOR = 0

    # We create a matrix, assigning randomly (according to the
    # percentage) the density of vapor. Vacuum is represented by nil.
    $GRID = Array.new($HEIGHT) do
      Array.new($WIDTH) do
        if rand(100) > $DENSITY
          nil
        else
          # We need this counter if we want a nice quick
          # checker for all_frozen? method
          $VAPOR += 1
          Element.new(:vapor)
        end
      end
    end

    #We set the center to be frozen
    ($GRID[$HEIGHT/2][$WIDTH/2] = Element.new(:vapor)).freeze

    $TICK_COUNT = 0

    $PIXELS =

    #Standard GL methods
    GLUT.Init
    GLUT.InitDisplayMode(GLUT::SINGLE)
    GLUT.InitWindowSize($WIDTH, $HEIGHT)
    GLUT.InitWindowPosition(100, 100)
    GLUT.CreateWindow('SimFrost : Quiz #117 - by CHubas')
    GL.ShadeModel(GL::FLAT)

    make_matrix
    GLUT.DisplayFunc(method(:display).to_proc)
    GLUT.KeyboardFunc(Proc.new{|k, x, y| exit if k == 27})
    GLUT.ReshapeFunc(method(:reshape).to_proc)

    # IdleFunc takes a proc object and calls it continously whenever it
can.
    GLUT.IdleFunc(method(:tick).to_proc)
  end

  # Here we create the pixel information.
  # Open GL takes an array of integers and splits it in groups of three
  # that represent one color component each.
  def make_matrix
    for i in 0..$HEIGHT-1
          for j in 0..$WIDTH-1
        index = (i * $WIDTH + j) * 3
        if particle = $GRID[i][j]
          case particle.element
            when :vapor
              # A blue-ish color
              $PIXELS[index] = 50
              $PIXELS[index+1] = 100
              $PIXELS[index+2] = 255
            when :ice
              # White ice
              $PIXELS[index] = 255
              $PIXELS[index+1] = 255
              $PIXELS[index+2] = 255
          end
        else
          # Black
          $PIXELS[index] = 0
          $PIXELS[index+1] = 0
          $PIXELS[index+2] = 0
        end
      end
      end
  end

  # Some basic window behavior
  def reshape(width, height)
    GL.PixelZoom(width / $WIDTH.to_f, height / $HEIGHT.to_f)
    display
  end

  # Draws the pixel bitmap
  def display
     GL.DrawPixels($WIDTH, $HEIGHT, GL::RGB, GL::UNSIGNED_BYTE,
$PIXELS.pack("C*"))
     GL.Flush
  end

  # We split the board into 2*2 squares with this method
  def each_square( tick_number )
    start = 0
    w_end = $WIDTH
    h_end = $HEIGHT

    if tick_number % 2 != 0 #odd
      start -= 1
      w_end -= 1
      h_end -= 1
    end

    (start...h_end).step(2) do |row|
      (start...w_end).step(2) do |column|
        s = yield *get_square_at(row, column)
        set_square_at(row, column, s)
      end
    end

  end

  # Checks for each 2*2 square and does the proper transformation
  def tick
    each_square( ($TICK_COUNT += 1) ) do |*square|
      if square.any?{|e| e != nil && e.element == :ice}
        for e in square
          e.freeze
        end
        square
      else
        rotate(square)
      end
    end

    # Having modified the matrix, now we have to rebuild the pixel map
    make_matrix
    GLUT.PostRedisplay
    GLUT.SwapBuffers

    #Stop doing this if everything is frozen already
    GLUT.IdleFunc(nil) if all_frozen?
  end

  # Some dirty methods
  def get_square_at(row, column)
    [$GRID[row][column],$GRID[row][column+1],$GRID[row+1][column],$GRID[row+1][column+1]]
  end

  def set_square_at(row, column, new_square)
    $GRID[row][column],$GRID[row][column+1],$GRID[row+1][column],$GRID[row+1][column+1]
= new_square
  end

  # Rotates elements in
  # | 0 1 |
  # | 2 3 |
  def rotate(square)
    if rand(2) == 0
      square.values_at(1,3,0,2)
    else
      square.values_at(2,0,3,1)
    end
  end

  # Validates if there is any vapor particle
  def all_frozen?
    if $VAPOR > 0
      return false
    else
      puts "Welcome to the ice age!"
      puts "All frozen in #{$TICK_COUNT} thicks"
      return true
    end
  end

  # Starts the main loop
  def start
    GLUT.MainLoop
  end

end

#Let the fun begin
Freeze_Simulator.new.start

________

Now I wonder how to make a movie :open_mouth:

--
Posted via http://www.ruby-forum.com/\.

Hi!

* Ruby Quiz, 09.03.2007 13:58:

The goal is to create a simulation of frost.

My aim was not to solve the task in a very object-oriened way or to
have a shiny output. My major objectives were

* Writing a Ruby program that can easily be ported to C, porting it
   and then compare the performance. It turned out that the C
   implementation is faster by a factor of about 100.

* Finding a simple way of animating the produced data without
   having to implement a GUI.

For the implementation see source below, let's turn to animation:

I use numbered PGM files as output format. For tick 0 (initial state
tick_00000.pgm is written, for tick 1 tick_00001.pgm and so forth.
For large simulations - I ran a C 1280x1024 C simulation with 25%
vapor that took about 7 minutes (with the bottleneck being the
Journaling File System) that took 3472 ticks meaning 3473 output
frames - it can be a good idea not to display all of the frames but
only every 10th or so. This goal can easily achieved by not looking
at all frames but only at those matching "tick_*0.pgm". For every
20th one could use "tick_*[02468]*.pgm" and so on.

One way of animating the output is using "convert" to create an
animated gif in the following manner:

convert -delay 0 -loop 0 tick_*.pgm simfrost.gif

Beware that this can require quite a lot of RAM!

A less demanding way of animtin the output is using an appropriate
display program. Personally I perfer qiv. To use this program to
animate the output all one has to do is issue

qiv -s -d 0 tick_*pgm

Where '-s' orders qiv to display the images as a slideshow and '-d'
provides a delay in seconds between the individual images, for I was
using the program for 1280x1024 images I set this delay to 0.

qiv is available at http://www.klografx.net/qiv/

The second major advantage of using PGM or PBM or PGM besides the
simplicity of output is that they can be converted to virtually any
other graphics format because they are the generic formats used by
the netpbm tools, see http://netpbm.sourceforge.net/

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Follows Ruby implementation
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
#!/usr/bin/ruby -w

#####################################################################
# Ruby Quiz 117, SimFrost
#
# The simulation uses an array of integers.
#
# It seems to make sense to use
#
# 0 to represent vacuum
# 1 to represent vapor
# 2 to represent ice
#
# Note that your terminal emulation should support ANSI escape
# sequences
#
#####################################################################

#####################################################################
# Integer#even - see if Integer is even
#####################################################################

class Integer
  def even?
    self/2*2 == self
  end
end

#####################################################################
# cls - clear screen
#####################################################################

def cls
  print "\e[2J"
end

#####################################################################
# home - move cursor to home position
#####################################################################

def home
  print "\e[1;1H"
end

#####################################################################
# Get even positive number
#####################################################################

def get_even_positive(desc)
  n = 0
  until n > 0 && n.even?
    print "Please enter #{desc} (must be even and positive): "
    n = gets.to_i
  end
  return n
end

#####################################################################
#
# Read probability
#
# Input is probability in percent, return value is probability
#
#####################################################################

def get_probability(desc)
  p = -1.0
  while p < 0.0 or p > 100.0
    print "Please enter probability for #{desc} (in %, float): "
    p = gets.to_f
  end
  return p / 100.0
end

#####################################################################
#
# Read settings
#
#####################################################################

def get_settings
  okay = "no"
  while okay != "yes"
    cls
    cols = get_even_positive("number of columns")
    rows = get_even_positive("number of rows")
    prob = get_probability("vapor")
    puts <<-EOF
You want:
\t#{cols}\tcolums
\t#{rows}\trows
\t#{prob*100.0}\tas the initial probabilty for vapor in percent
IS THAT CORRECT? If so please answer with: yes
    EOF
    okay = gets.chomp
    puts "Please re-enter data." unless okay == "yes"
  end
  return { "cols" => cols, "rows" => rows, "prob" => prob }
end

#####################################################################
#
# generate initial state for simulation
#
#####################################################################

def initial_state(cols, rows, prob)
  a =
  Array.new(rows) do |row|
    Array.new(cols) do |elem|
      rand < prob ? 1 : 0
    end
  end
  a[rows/2][cols/2] = 2
  return a
end

#####################################################################
#
# output current simulation state
#
#####################################################################

def output_state(state, tick)
  home
  puts "Simulation tick #{tick}"
  filename = "tick_#{'%05d' % tick}.pgm"
  File.open(filename, 'w') do |file|
    file.puts <<-EOF
P2
# #{filename}
#{state.first.length} #{state.length}
2
    EOF
    state.each do |row|
      row.each do |elem|
        file.puts elem.to_s
      end
    end
  end
end

#####################################################################
# see if state is frozen out (i.e. no more vapor is present)
#####################################################################

class Array
  def frozen_out?
    not self.flatten.member?(1)
  end
end

#####################################################################
# the simulation itself
#####################################################################

settings = get_settings
cols = settings["cols"],
rows = settings["rows"],
prob = settings["prob"]
state = initial_state(cols, rows, prob)
tick = 0
cls
while true
  output_state(state, tick)
  break if state.frozen_out?
  tick += 1
  offset = (tick + 1) % 2
  i = offset
  while i < rows
    i1 = (i + 1) % rows
    j = offset
    while j < cols
      j1 = (j + 1) % cols
      if [ state[i][j],
           state[i][j1],
           state[i1][j],
           state[i1][j1] ].member?(2)
        state[i][j] = 2 if state[i][j] == 1
        state[i][j1] = 2 if state[i][j1] == 1
        state[i1][j] = 2 if state[i1][j] == 1
        state[i1][j1] = 2 if state[i1][j1] == 1
      else
        if rand < 0.5
          state[i][j], state[i][j1], state[i1][j], state[i1][j1] =
          state[i][j1], state[i1][j1], state[i][j], state[i1][j]
        else
          state[i][j], state[i][j1], state[i1][j], state[i1][j1] =
          state[i1][j], state[i][j], state[i1][j1], state[i][j1]
        end
      end
      j += 2
    end
    i += 2
  end
end

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Follows C implementation
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void cls(void)
{
  printf("\033[2J");
}

void home(void)
{
  printf("\033[1;1H");
}

int get_even_positive(char* desc)
{
  int n = 0;
  char s[21];
  while( n <= 0 || n/2*2 != n)
  {
    printf("Please enter %s (must be even and positive): ", desc);
    scanf("%20s", s);
    n = atoi(s);
  }
  return n;
}

double get_probability(char* desc)
{
  double p = -1.0;
  char s[21];
  while (p < 0.0 || p > 100.0)
  {
    printf("Please enter probability for %s (in percent, float): ",
           desc);
    scanf("%20s", s);
    p = atof(s);
  }
  return p / 100.0;
}

int **initialize_state(int cols, int rows, double prob)
{
  int i;
  int j;
  int **a;

  a = (int **) calloc(rows, sizeof(int *));
  for (i = 0; i < rows; i++)
  {
    a[i] = (int *) calloc(cols, sizeof(int));
  }
  for (i = 0; i < rows; i++)
  {
    for (j = 0; j < cols; j++)
    {
      a[i][j] = (rand() < RAND_MAX * prob) ? 1 : 0;
    }
  }
  a[rows/2][cols/2] = 2;
  return a;
}

void display_state(int **state, int tick, int cols, int rows)
{
  int i;
  int j;
  char filename[15];
  FILE *file;

  home();
  printf("Simulation tick %d\n", tick);
  sprintf(filename, "tick_%05d.pgm", tick);
  file = fopen(filename, "w");
  fprintf(file, "P2\n");
  fprintf(file, "# %s\n", filename);
  fprintf(file, "%d %d\n", cols, rows);
  fprintf(file, "2/n");
  for (i = 0; i < rows; i++)
  {
    for (j = 0; j < cols; j++)
    {
      putc("012"[state[i][j]], file);
      putc('\n', file);
    }
  }
  fclose(file);
}

int frozen_out(int **state, int cols, int rows)
{
  int i;
  int j;

  for (i = 0; i < rows; i++)
  {
    for (j = 0; j < cols; j++)
    {
      if (state[i][j] == 1)
      {
        return 0;
      }
    }
  }
  return 1;
}

int main(void)
{
  int okay = 0;
  int tick = 0;
  int offset;
  int cols;
  int rows;
  int i, i1;
  int j, j1;
  int h00, h01, h10, h11;
  double prob;
  char s[21];
  int **state;

  while (!okay)
  {
    cls();
    cols = get_even_positive("number of columns");
    rows = get_even_positive("number of rows");
    prob = get_probability("vapor");
    printf("You want:\n");
    printf("\t%d\tcolums\n", cols);
    printf("\t%d\trows\n", rows);
    printf("\t%f\tas the initial probabilty for vapor in percent\n",
           prob * 100.0);
    printf("IS THAT CORRECT? If so please answer with: yes\n");
    scanf("%20s", s);
    okay = !strcmp(s, "yes");
    if (!okay)
    {
      puts("Please re-enter data.");
    }
  }
  state = initialize_state(cols, rows, prob);
  cls();
  while(1)
  {
    display_state(state, tick, cols, rows);
    if (frozen_out(state, cols, rows))
    {
      return 0;
    }
    offset = (tick++ + 1) % 2;
    for (i = offset; i < rows; i += 2)
    {
      i1 = (i + 1) % rows;
      for (j = offset; j < cols; j += 2)
      {
        j1 = (j + 1) % cols;
        if (state[i][j] == 2 ||
            state[i][j1] == 2 ||
            state[i1][j] == 2 ||
            state[i1][j1] == 2)
        {
          if (state[i][j] == 1) state[i][j] = 2;
          if (state[i][j1] == 1) state[i][j1] = 2;
          if (state[i1][j] == 1) state[i1][j] = 2;
          if (state[i1][j1] == 1) state[i1][j1] = 2;
        }
        else
        {
          h00 = state[i][j];
          h01 = state[i][j1];
          h10 = state[i1][j];
          h11 = state[i1][j1];
          if (rand() < RAND_MAX/2)
          {
            state[i][j] = h01;
            state[i][j1] = h11;
            state[i1][j] = h00;
            state[i1][j1] = h10;
          }
          else
          {
            state[i][j] = h10;
            state[i][j1] = h00;
            state[i1][j] = h11;
            state[i1][j1] = h01;
          }
        }
      }
    }
  }
  return 0;
}
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

Even if you've never seen C before you ought to be able to understand
the C implementation by comparing it to the Ruby one.

Josef 'Jupp' Schugt
--
Blog available at http://www.mynetcologne.de/~nc-schugtjo/blog/
PGP key with id 6CC6574F available at http://wwwkeys.de.pgp.net/

Ick, yes. Good catch. I'll fix it on the web site...

James Edward Gray II

···

On Mar 9, 2007, at 8:04 AM, Jason Roelofs wrote:

Quick clarification, I would assume you mean "turn 90 degrees", not 90%, as
that doesn't make any sense.

right, code got mangled, here's the file

snowflake.rb (2.47 KB)

···

On 3/11/07, Christoffer Lernö <lerno@dragonascendant.com> wrote:

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

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

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 Reiser

···

On Mar 11, 11:02 am, "Harrison Reiser" <y2kbugx...@gmail.com> wrote:

> Thethree rulesof Ruby Quiz:

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

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

···

#
# Raj Sahae
# 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 [](row)
    @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

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 Mar 11, 1:20 pm, "Eric I." <rubytrain...@gmail.com> 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
----

By all means, use it.

···

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

* Ruby Quiz, 09.03.2007 13:58:

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this
quiz until 48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

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

--
Ken Bloom. PhD candidate. Linguistic Cognition Laboratory.
Department of Computer Science. Illinois Institute of Technology.
http://www.iit.edu/~kbloom1/

It's interesting that it looks like everyone populated their grid

no Sir not me :wink: but my code is longer and I wonder if it was worth
it, now that I have looked at the outcomes of 55,60,65,70,75 and 80%
vapor

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

Almost interminable, I was using it too, look at Torus_#set_vapors in
my solution

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

Robert

···

On 3/11/07, Christoffer Lernö <lerno@dragonascendant.com> wrote:
--
We have not succeeded in answering all of our questions.
In fact, in some ways, we are more confused than ever.
But we feel we are confused on a higher level and about more important things.
-Anonymous