crueber
10/29/2012 - 4:37 AM

Ruby Expression based Die Roller - Accepts plus, minus, numeric dice and variable number of dice

Ruby Expression based Die Roller - Accepts plus, minus, numeric dice and variable number of dice

# The MIT License
# Copyright (c) 2012 Christopher WJ Rueber
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 
# and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all copies or substantial
# portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
# TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

class Fixnum
  def d(sides=6)
    rolls = []
    self.times { rolls.push(1 + rand(sides)) }
    
    { rolls: rolls, total: rolls.inject(:+) }
  end
end
# Usage: 5.d(20)

# This module is maint to be used as a mix-in where needed
module Dice
  DICE_REGEX = /([\+-]?)(\d*[d]?\d*|\d*)/ 
  STANDARD_DIE_SIDES = [2, 3, 4, 6, 8, 10, 12, 20, 100, 1000]

  def self.roll(formula)
    raise "die formula must be a string" unless formula.class == String

    formula = formula.gsub(' ', '').downcase
    raise "formula may only contain numbers, d, +, and -." unless /[d\d\+-]*/.match(formula).to_s == formula

    results = []
    formula.scan(DICE_REGEX).each do |exp|
      operator = exp[0]
      dice     = exp[1]

      next if operator.blank? and dice.blank?
      operator = operator.empty? ? '+' : operator

      possible_dice = dice.split('d').delete_if {|x| x.blank? }
      if possible_dice.size == 1
        if possible_dice[0] == dice
          results.push "#{operator}#{possible_dice[0]}".to_i
        else
          raise "must specify both a number of dice and the sides: 1d20"
        end
      elsif possible_dice.size == 2
        number_of_dice = possible_dice[0].blank? ? 1 : possible_dice[0].to_i
        die_sides      = possible_dice[1].to_i

        die_result = number_of_dice.d(die_sides)
        die_result[:rolls].each {|roll| results.push "#{operator}#{roll}".to_i }
      else
        raise "this die expression is not possible"
      end
    end

    { individualResults: results, total: results.inject(:+) }
  end
end

# examples: 
# Dice.roll "1d20"
# Dice.roll "20d20"
# Dice.roll "6d6"
# Dice.roll "1d20+12"
# Dice.roll "1d20+3d10+2d6"
# Dice.roll "1d20-10"
# Dice.roll "1d20"