# There it goes, using eval for simplicity, but at least compiling the
# dice into a Proc:

class Integer
  def d(n)                      # evil }:-)
    (1..self).inject(0) { |a,e| a + rand(n) + 1 }
  end
end

class Dice
  def initialize(dice)
    @src = dice.gsub(/d(%|00)(\D|$)/, 'd100\2').
                gsub(/d(\d+)/, 'd(\1)').
                gsub(/(\d+|\))d/, '\1.d').
                gsub(/\d+/) { $&.gsub(/^0+/, '') }

    raise ArgumentError, "invalid dice: `#{dice}'"  if @src =~ /[^-+\/*()d0-9. ]/

    begin
      @dice = eval "lambda{ #@src }"
      roll                      # try the dice
    rescue
      raise ArgumentError, "invalid dice: `#{dice}'"
    end
  end

  def d(n)
    1.d(n)
  end

  def roll
    @dice.call
  end
end

unless $DEBUG
  d = Dice.new(ARGV[0] || "d6")
  puts Array.new((ARGV[1] || 1).to_i) { d.roll }.join("  ")
else
  $DEBUG = false                # only makes test/unit verbose now
  
  warn "This is a heuristic test-suite.  Please re-run (or increase N) on failure."
  
  require 'test/unit'

  N = 100000

  class TestDice < Test::Unit::TestCase
    def test_00_invalid_dice
      assert_raises(ArgumentError) { Dice.new("234%21") }
      assert_raises(ArgumentError) { Dice.new("%d5") }
      assert_raises(ArgumentError) { Dice.new("d5%") }
      assert_raises(ArgumentError) { Dice.new("d%5") }
    end

    def test_10_fixed_expr
      dice_min_max({
        '1'                   => [1, 1],
        '1+2'                 => [3, 3],
        '1+3*4'               => [13, 13],
        '1*2+4/8-1'           => [1, 1],
        'd1'                  => [1, 1],
        '1d1'                 => [1, 1],
        '066d1'               => [66, 66]
      }, 10)
    end

    def test_20_small_dice
      dice_min_max({
        'd10'                 => [1, 10],
        '1d10'                => [1, 10],
        'd3*2'                => [2, 6],
        '2d3+8'               => [10, 14],    # not 22
        '(2d(3+8))'           => [2, 22],    # not 14
        'd3+d3'               => [2, 6],
        'd2*2d4'              => [2, 16],
        'd(2*2)+d4'           => [2, 8]
      })
    end

    def test_30_percent_dice
      dice_min_max({
        'd%'                  => [1, 100],
        '2d%'                 => [2, 200]
      }, 100_000)
    end

    def test_40_complicated_dice
      dice_min_max({
        '10d10'               => [10, 100],
        '5d6d7'               => [5, 210],   # left assoc
        '14+3*10d2'           => [44, 74],
        '(5d5-4)d(16/d4)+3'   => [4, 339],
      }, 1_000_000)
    end

    def dice_min_max(asserts, n=10_000)
      asserts.each { |k, v|
        dice = Dice.new k

        v2 = (1..n).inject([1.0/0.0, 0]) { |(min, max), e|
          r = dice.roll
          [[min, r].min, [max, r].max]
        }
        
        assert_equal v, v2, k
      }
    end
  end
end

__END__
-- 
Christian Neukirchen  <chneukirchen / gmail.com>  http://chneukirchen.org