Skip to content

Instantly share code, notes, and snippets.

@ccooke
Created September 23, 2013 01:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ccooke/6665672 to your computer and use it in GitHub Desktop.
Save ccooke/6665672 to your computer and use it in GitHub Desktop.
A dice expression parser
require 'securerandom'
require 'strscan'
require 'pp'
module Dice
class Parser
module Term
class Number
attr_accessor :number, :value, :math_symbol
def initialize( number, sign = :+ )
@number = number.to_i
@math_symbol = sign.to_sym
end
def roll
@value = @number
end
def sum(other)
@value.send(@math_symbol, other )
end
end
class Die
class Modifier
def self.new(match)
if match[:reroll]
Reroll.new(match)
elsif match[:keep]
Keep.new(match)
elsif match[:drop]
Drop.new(match)
else
raise "No such modifier: #{match[0]}"
end
end
class Reroll
def initialize(match)
pp match
if match[:conditional] != ""
@condition_test = match[:conditional] == "=" ? :== : match[:conditional].to_sym
else
@condition_test = :==
end
@condition_num = match[:condition_number].to_i
end
def reroll_with?(number)
number.send( @condition_test, @condition_num )
end
end
class Keep
def initialize(match)
@keep_number = match[:keep_num].nil? ? 1 : match[:keep_num].to_i
@keep_method = match[:keep_lowest] ? :max : :min
end
def process(numbers)
until numbers.count <= @keep_number
remove = numbers.send(@keep_method)
puts "Keep dropping #{remove}"
numbers.delete_at( numbers.index( remove ) )
end
numbers
end
end
class Drop
def initialize(match)
@drop_number = match[:drop_num].nil? ? 1 : match[:drop_num].to_i
@drop_method = match[:drop_lowest] ? :min : :max
end
def process(numbers)
dropped = 0
until dropped == @drop_number or numbers.count == 0
remove = numbers.send(@drop_method)
puts "Dropping #{remove}"
dropped += 1
numbers.delete_at( numbers.index( remove ) )
end
numbers
end
end
end
attr_accessor :count, :size, :math_symbol, :value
def initialize( options = {} )
@math_symbol = options[:math_symbol].to_sym
@modifiers = options[:modifiers]
@size = options[:size]
@exploding = options[:exploding]
@compounding = options[:compounding]
@penetrating = options[:penetrating]
@count = options[:count]
end
def roll
temp = []
count = @count
rolls = 0
number = 0
while rolls < count
catch(:reroll) do
puts "Roll #{rolls} of #{count}"
this_roll = SecureRandom.random_number( @size ) + 1
number += this_roll
puts "Rolled a #{this_roll}. Total is now #{number}"
if number == @size
if @penetrating
puts "Reroll(penetrate)"
number -= 1
throw :reroll
elsif @compounding
puts "Reroll(compound)"
throw :reroll
elsif @exploding
puts "Reroll(explode)"
count += 1
end
end
if @modifiers.select { |m| m.is_a? Modifier::Reroll }.any? { |m| m.reroll_with? number }
puts "Reroll #{number}"
number = 0
throw :reroll
end
temp << number
rolls += 1
number = 0
end
end
@value = process_modifiers( temp )
end
def process_modifiers(numbers)
@modifiers.each do |m|
next unless m.respond_to? :process
numbers = m.process(numbers)
end
numbers
end
def value
@value.inject(:+)
end
end
class FudgeDie < Die
end
end
attr_reader :terms
CONDITIONAL_BASE = %r{
(?<conditional> > | < | = | ){0}
(?<nonzero> [1-9]\d* ){0}
(?<condition> \g<conditional> (?<condition_number> \g<nonzero> ) ){0}
}x
MODIFIERS = %r{
#{CONDITIONAL_BASE}
(?<keep_highest> kh | k ){0}
(?<keep_lowest> kl ){0}
(?<drop_highest> dh ){0}
(?<drop_lowest> dl | d ){0}
(?<keep> (\g<keep_lowest> | \g<keep_highest>) (?<keep_num> \g<nonzero> )?){0}
(?<drop> (\g<drop_highest> | \g<drop_lowest>) (?<drop_num> \g<nonzero> )?){0}
(?<reroll> r \g<condition>? ){0}
(?<die_modifier> \g<drop> | \g<keep> | \g<reroll> ){0}
(?<die_modifiers> \g<die_modifier>* ){0}
}x
MODIFIER_TERMS = %r{
\G
#{MODIFIERS}
\g<die_modifier>
}x
EXPRESSION = %r{
\G
#{MODIFIERS}
(?<penetrating> !p ){0}
(?<compounding> !! ){0}
(?<explode> ! ){0}
(?<decoration> \g<compounding> | \g<penetrating> | \g<explode> ){0}
(?<fudge> f ){0}
(?<die_size> \g<nonzero> | \g<fudge> ){0}
(?<mathlink> \+ | - | \* | / ){0}
(?<die> (?<count> \g<nonzero> ) d \g<die_size> \g<decoration>? \g<die_modifiers>?){0}
(?<constant> (?<constant_number> \g<nonzero> ) ){0}
(?<dice_string>
\g<die>
|
\g<constant>
){0}
\g<mathlink> \g<dice_string>
}ix
def initialize(string, options = {})
string.gsub! /\s+/, ''
unless string.start_with? '+'
string = '+' + string
end
@string = string
@terms = parse
end
def tokenize(regex, string)
index = 0
items = []
while match = string.match( regex, index )
items << match
p match
index = match.end(0)
end
unless index == string.length
raise "Parsing failed at #{string[index,string.length]}"
end
items
end
def parse
tokenize(EXPRESSION, @string).map do |term|
if term[:constant_number]
Term::Number.new term[:constant_number].to_i, term[:mathlink]
elsif term[:die]
options = {
penetrating: !!term[:penetrating],
compounding: !!term[:compounding],
exploding: !!term[:explode],
math_symbol: term[:mathlink].to_sym,
count: term[:count].to_i,
}
if term[:fudge]
Term::FudgeDie.new options
else
options[:size] = term[:die_size].to_i
options[:modifiers] = tokenize( MODIFIER_TERMS, term[:die_modifiers] ).map { |m| Term::Die::Modifier.new( m ) }
Term::Die.new options
end
end
end
end
def roll
@terms.map(&:roll)
end
def value
@terms.inject(0) do |i,t|
i.send(t.math_symbol, t.value)
end
end
end
end
t = Dice::Parser.new ARGV[0].dup
t.roll
t.value
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment