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