[QUIZ] SimFrost (#117)

R

Rick DeNatale

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

Robert Dober

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
 
C

Christoffer Lernö

--Apple-Mail-6--109314657
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed

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



--Apple-Mail-6--109314657
Content-Transfer-Encoding: 7bit
Content-Type: text/x-ruby-script;
x-unix-mode=0755;
name=simfrost.rb
Content-Disposition: attachment;
filename=simfrost.rb

#!/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
--Apple-Mail-6--109314657--
 
D

Dave Burt

#
# SimFrost
#
# A response to Ruby Quiz #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: (e-mail address removed)
# 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[0]][hood[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
 
J

James Edward Gray II

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,
:eek:utput => 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[:eek:utput] = UnixTerminalDisplay
end
opts.on( "-i", "--image",
"PPM image series display." ) do
options[:eek:utput] = 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[:eek:utput] == PPMImageDisplay ?
[simulator, options[:directory]] :
[simulator]
terminal = options[:eek:utput].new(*setup)

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

__END__

James Edward Gray II
 
C

Chris Shea

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
 
H

Harrison Reiser

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
 
H

Harrison Reiser

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

Eric I.

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
 
R

Raj Sahae

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
 
G

Gordon Thiesfeld

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
 
C

Christoffer Lernö

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
 
K

Ken Bloom

* Ruby Quiz, 09.03.2007 13:58:

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

James Edward Gray II

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

Robert Dober

It's interesting that it looks like everyone populated their grid
no Sir not me ;) 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 =3D 30
width =3D 30
height =3D 20
vapour =3D width * height * percentage / 100
vacuum =3D width * height - vapour
grid =3D []
(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
--=20
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 thin=
gs.
-Anonymous
 
J

James Edward Gray II

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

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

What's a "small grid" and what's "quite a bit"? ;)

#!/usr/bin/env ruby -w

TRIALS =3D 10_000
SIZE =3D 80*22

sum =3D 0

high =3D low =3D nil

TRIALS.times do
grid =3D Array.new(SIZE) { rand < 0.3 ? "." : " " }
percent =3D (grid.grep(/\./).size / SIZE.to_f * 100).round

puts "Actual percentage: #{percent}" if $DEBUG

sum +=3D percent
low =3D percent if low.nil? or percent < low
high =3D percent if high.nil? or percent > high
end

puts <<END_OVERALL
Average: #{sum / TRIALS}
High: #{high}
Low: #{low}
END_OVERALL
# >> Average: 29
# >> High: 35
# >> Low: 26

James Edward Gray II=
 
F

F. Senault

Le 11 mars 2007 à 21:07, Christoffer Lernö a écrit :
Anyone else with a more elegant solution to the problem?

I'd use this (only moderately tested) :

#! /usr/local/bin/ruby

p = 0.3
w = 100
h = 100
t = h * w
v = (t * p).floor

g = Array.new(v, '.') + Array.new(t - v, '.')

0.upto(t - 1) do |i|
j = i + rand(t - i)
z = g[j] ; g[j] = g ; g = z
# g[j], g = g, g[j]
end

(And then split the grid in w chunks.)

The fun part is that the version using a parallel assignment (commented)
is actually much slower (about two times) than the version using the
temporary variable, to the point that it was beaten by sort_by (which is
supposed to be O(n*log(n)) versus O(n) for my versions).

200*200 :
user system total real
scramble / temp : 15.281250 0.000000 15.281250 ( 15.587794)
scramble / swap : 31.398438 0.007812 31.406250 ( 32.220921)
sort_by rand : 28.625000 0.101562 28.726562 ( 29.168337)

500*500 :
user system total real
scramble / temp : 97.195312 0.000000 97.195312 ( 98.037395)
scramble / swap : 297.937500 0.929688 298.867188 (302.746914)
sort_by rand : 213.906250 0.468750 214.375000 (217.172667)

Fred
 
H

Harrison Reiser

I'm gonna have to agree here that it matters very little whether the
proper ratio is enforced on the scale in which SimFrost produces
actually interesting results. +/- 5% in all 10,000 trials on a
terminal-window sized sim is plenty accurate for my purposes.

Harrison Reiser
 
T

Tom Rauchenwald

--=-=-=

Hi,

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

Tom

--=-=-=
Content-Type: text/x-ruby; charset=iso-8859-1
Content-Disposition: inline; filename=frost2.rb
Content-Transfer-Encoding: quoted-printable

#!/usr/bin/env ruby

# frost.rb
# see http://rubyquiz.com/quiz117.html for details
# Author: Tom Rauchenwald <[email protected]>

class Cell
attr_reader :kind
=20=20
def initialize kind
if kind!=3D:vapor && kind !=3D:ice && kind !=3D :vacuum
raise "Cell has to be either :ice, :water or :vacuum!"
end
@kind=3Dkind
end

def kind=3D(kind)
if kind!=3D:vapor && kind !=3D:ice && kind !=3D :vacuum
raise "Cell has to be either :ice, :water or :vacuum!"
end
@kind=3Dkind
end
=20=20
def kind? kind
kind=3D=3D@kind
end

def to_s
case kind
when :vacuum; " "
when :ice; "*"
when :vapor; "`"
end
end
end
=20
class Board
def initialize x,y,s,pv
@x=3Dx; @y=3Dy; @sleeptime=3Ds.to_f/1000
# create Board and fill it with Cells
@board=3DArray.new(@x) { Array.new(@y) {Cell.new :vacuum } }
@board[@x]=3D@board[0] # Make last and first column the same object
# same for last and first row
(@x+1).times do |x|
@board[x][@y]=3D@board[x][0]
end
# this will track how man vapor particles are on the board
# scanning the board each tick would be a waste of time
@n_vapor=3D0
if pv < 100 && pv > 0
@p_vapor=3Dpv.to_f/100
else
@p_vapor=3D0.3
end
# this will toggle between 0 and 1, more is not necessary as
# outlined in the quiz description=20
@tick=3D0
end

def init_board
# clear board
@x.times do |x|
@y.times do |y|
@board[x][y].kind=3D:vacuum
end
end
@n_vapor=3D0;
# place the ice at the center
@board[@x/2][@y/2].kind=3D:ice
# place vapor randomly
while @n_vapor.to_f/(@x*@y) < @p_vapor
x,y =3D rand(@x), rand(@y)
if @board[x][y].kind?:)vacuum)
@board[x][y].kind=3D:vapor; @n_vapor +=3D 1
end
end
end
=20=20
def to_s
res =3D "=B4" + "-"*@x + "`\n"
(@y).times do |y|
res << "|"
(@x).times do |x|
res << @board[x][y].to_s
end
res << "|\n"
end
res +=3D "`" + "_"*@x + "=B4\n"
return res
end

def tick
# copy first column to last column
(@tick...(@y+@tick)).step(2) do |y|
(@tick...(@x+@tick)).step(2) do |x|
# check if ice is in the neighbourhood
if [@board[x][y], @board[x+1][y], @board[x][y+1], @board[x+1][y+1]]=
any? { |i| i.kind? :ice }
# there is, change vapor to ice
[@board[x][y], @board[x+1][y], @board[x][y+1], @board[x+1][y+1]].=
each do |i|
if i.kind? :vapor
i.kind=3D:ice
@n_vapor-=3D1=20
end
end
else
# no ice, rotate neighbourhood clockwise or counter clockwise
if rand(2)=3D=3D0
@board[x][y].kind, @board[x+1][y].kind, @board[x+1][y+1].kind, =
@board[x][y+1].kind =3D=20
@board[x][y+1].kind, @board[x][y].kind, @board[x+1][y].kind, =
@board[x+1][y+1].kind
else
@board[x][y].kind, @board[x+1][y].kind, @board[x+1][y+1].kind, =
@board[x][y+1].kind =3D=20
@board[x+1][y].kind, @board[x+1][y+1].kind, @board[x][y+1].ki=
nd, @board[x][y].kind
end
end
end
end
@tick=3D(@tick+1)%2 # toggle tick
end

def start
puts "\033[2J" # clear terminal and move cursor to upper left
while @n_vapor>0
puts "\033[0;0f" # move cursor to upper left
puts self.to_s
tick
sleep @sleeptime
end
puts "\033[0;0f"
puts self.to_s
end
end

if ARGV.length<2
puts "Usage: " + __FILE__ + " <sizeX> <sizeY> <sleeptime>"
puts "Sleeptime is optional (in milliseconds). X and Y must be even numbe=
rs."
exit 1
end

x =3D ARGV[0].to_i;=20
y =3D ARGV[1].to_i;
s =3D !ARGV[2] ? 20 : ARGV[2].to_i;
pv =3D !ARGV[3] ? 30 : ARGV[3].to_i;

b=3DBoard.new(x, y, s, pv)
loop do
b.init_board
b.start
print "Again? (y/n): "
break if STDIN.gets.chomp.downcase !=3D "y"
end

--=-=-=--
 
E

Eric I.

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

Cool! Thanks!

Eric
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,982
Messages
2,570,185
Members
46,736
Latest member
AdolphBig6

Latest Threads

Top