#
# 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: dave / burt.id.au
# Created: 10 Mar 2007
# Last modified: 11 Mar 2007
#
class SimFrost
attr_reader :width, :height, :cells
def initialize(width, height, vapor_percentage)
unless width > 0 && width % 2 == 0 &&
height > 0 && height % 2 == 0
throw ArgumentError, "width and height must be even, positive
numbers"
end
@width = width
@height = height
@cells = Array.new(width) do
Array.new(height) do
:vapor if rand * 100 <= vapor_percentage
end
end
@cells[width / 2][height / 2] = :ice
@offset = 0
end
def step
@offset ^= 1
@new_cells = Array.new(width) { Array.new(height) }
@offset.step(width - 1, 2) do |x|
@offset.step(height - 1, 2) do |y|
process_neighbourhood(x, y)
end
end
@cells = @new_cells
nil
end
def contains_vapor?
@cells.any? {|column| column.include? :vapor }
end
private
def process_neighbourhood(x0, y0)
x1 = (x0 + 1) % width
y1 = (y0 + 1) % height
hood = [[x0, y0], [x0, y1], [x1, y1], [x1, y0]]
if hood.any? {|x, y| @cells[x][y] == :ice }
hood.each do |x, y|
@new_cells[x][y] = @cells[x][y] && :ice
end
else
hood.reverse! if rand < 0.5
4.times do |i|
j = (i + 1) % 4
@new_cells[hood[i][0]][hood[i][1]] =
@cells[hood[j][0]][hood[j][1]]
end
end
nil
end
module Console
DEFAULT_RUN_OPTIONS = {
:width => 78,
:height => 24,
:vapor_percentage => 30,
:delay_per_frame => 0.1,
:ice => " ",
:vapor => "O",
:vacuum => "#"
}
def self.run(options = {})
opts = DEFAULT_RUN_OPTIONS.merge(options)
sim = SimFrost.new(opts[:width], opts[:height],
opts[:vapor_percentage])
puts sim_to_s(sim, opts)
i = 0
while sim.contains_vapor?
sleep opts[:delay_per_frame]
sim.step
puts sim_to_s(sim, opts)
i += 1
end
puts "All vapor frozen in #{i} steps."
end
def self.sim_to_s(sim, options = {})
sim.cells.transpose.map do |column|
column.map do |cell|
case cell
when :ice: options[:ice] || "*"
when :vapor: options[:vapor] || "."
else options[:vacuum] || " "
end
end.join(options[:column_separator] || "")
end.join(options[:row_separator] || "\n")
end
def self.parse_options(argv)
require 'optparse'
opts = {}
op = OptionParser.new do |op|
op.banner = "usage: #{$0} [options]"
op.on("-w","--width N",Integer,"number of
columns"){|w|opts[:width] = w}
op.on("-h","--height N",Integer,"number of rows")
{|h|opts[:height] = h}
op.on("-p", "--vapor-percentage N", Integer,
"% of cells that start as vapor"){|p|
opts[:vapor_percentage] = p}
op.on("-d", "--delay-per-frame T", Float,
"delay per frame in seconds") {|d| opts[:delay_per_frame]
= d }
op.on("-i", "--ice S", String, "ice cell") {|i| opts[:ice] = i }
op.on("-v", "--vapor S", String, "vapor cell") {|v|
opts[:vapor] = v }
op.on("-0", "--vacuum S", String, "vacuum cell"){|z|
opts[:vacuum] = z }
op.on_tail("--help", "just show this message") { puts op; exit }
end
begin
op.parse!(ARGV)
rescue OptionParser::ParseError => e
STDERR.puts "#{$0}: #{e}"
STDERR.puts op
exit
end
opts
end
end
end
if $0 == __FILE__
SimFrost::Console.run SimFrost::Console.parse_options(ARGV)
end