dicebag 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dicebag.rb ADDED
@@ -0,0 +1,168 @@
1
+ # Copyright (c) 2012 Randy Carnahan <syn at dragonsbait dot com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the "Software"),
5
+ # to deal in the Software without restriction, including without limitation
6
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
7
+ # and/or sell copies of the Software, and to permit persons to whom the
8
+ # Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included
11
+ # in all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
16
+ # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17
+ # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
19
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+ #
21
+ # dicelib.rb -- version: 3.0.1
22
+
23
+ require 'parslet'
24
+
25
+ module DiceBag
26
+
27
+ DefaultRoll = "1d6"
28
+
29
+ class DiceBagError < Exception; end
30
+
31
+ ###
32
+ # Module Methods
33
+ ###
34
+
35
+ # This takes the parsed tree, AFTER it has
36
+ # been through the Transform class, and massages
37
+ # the data a bit more, to ease the iteration that
38
+ # happens in the Roll class. It will convert all
39
+ # values into the correct *Part class.
40
+ def self.normalize_tree(tree)
41
+ return tree.collect do |part|
42
+
43
+ case part
44
+ when Hash
45
+ if part.has_key?(:label)
46
+ part = [:label, LabelPart.new(part[:label])]
47
+ elsif part.has_key?(:start)
48
+ xdx = normalize_xdx(part[:start])
49
+ part = [:start, RollPart.new(xdx)]
50
+ end
51
+
52
+ when Array
53
+ # We swap out the strings for symbols.
54
+ # If the op is not one of the arithimetic
55
+ # operators, then the op itself is returned.
56
+ # (This should only happen on :start arrays.)
57
+ op = case part.first
58
+ when "+" then :add
59
+ when "-" then :sub
60
+ when "*" then :mul
61
+ when "/" then :div
62
+ else part.first
63
+ end
64
+
65
+ val = part.last
66
+
67
+ # If the value is a hash, it's an :xdx hash.
68
+ # Normalize it.
69
+ if val.is_a?(Hash)
70
+ xdx = normalize_xdx(val)
71
+ val = RollPart.new(xdx)
72
+ else
73
+ val = StaticPart.new(val)
74
+ end
75
+
76
+ part = [op, val]
77
+ end
78
+
79
+ part
80
+ end
81
+ end
82
+
83
+ # This further massages the xDx hashes.
84
+ def self.normalize_xdx(xdx)
85
+ count = xdx[:xdx][:count]
86
+ sides = xdx[:xdx][:sides]
87
+ notes = []
88
+
89
+ # Default to at least 1 die.
90
+ count = 1 if count.zero? or count.nil?
91
+
92
+ # Set the :count and :sides keys directly
93
+ # and get ride of the :xdx sub-hash.
94
+ xdx[:count] = count
95
+ xdx[:sides] = sides
96
+ xdx.delete(:xdx)
97
+
98
+ if xdx[:options].empty?
99
+ xdx.delete(:options)
100
+ else
101
+ # VALIDATE ALL THE OPTIONS!!!
102
+
103
+ # Prevent Explosion abuse.
104
+ if xdx[:options].has_key?(:explode)
105
+ explode = xdx[:options][:explode]
106
+
107
+ if explode.nil? or explode.zero? or explode == 1
108
+ xdx[:options][:explode] = sides
109
+ notes.push("Explode set to #{sides}")
110
+ end
111
+ end
112
+
113
+ # Prevent Reroll abuse.
114
+ if xdx[:options].has_key?(:reroll) and xdx[:options][:reroll] >= sides
115
+ xdx[:options][:reroll] = 0
116
+ notes.push("Reroll reset to 0.")
117
+ end
118
+
119
+ # Make sure there are enough dice to
120
+ # handle both Drop and Keep values.
121
+ # If not, both are reset to 0. Harsh.
122
+ drop = xdx[:options][:drop] || 0
123
+ keep = xdx[:options][:keep] || 0
124
+
125
+ if (drop + keep) >= count
126
+ xdx[:options][:drop] = 0
127
+ xdx[:options][:keep] = 0
128
+ notes.push("Drop and Keep Conflict. Both reset to 0.")
129
+ end
130
+
131
+ # Negate :drop. See why in RollPart#roll.
132
+ xdx[:options][:drop] = -(drop)
133
+ end
134
+
135
+ xdx[:notes] = notes unless notes.empty?
136
+
137
+ return xdx
138
+ end
139
+
140
+ # This is the wrapper for the parse, transform,
141
+ # and normalize calls. This is called by the Roll
142
+ # class, but may be called to get the raw returned
143
+ # array of parsed bits for other purposes.
144
+ def self.parse(dstr="")
145
+ begin
146
+ tree = Parser.new.parse(dstr)
147
+ ast = Transform.new.apply(tree)
148
+
149
+ return normalize_tree(ast)
150
+
151
+ rescue Parslet::ParseFailed => reason
152
+ # We're merely re-wrapping the error here to
153
+ # hide implementation from user who doesn't care
154
+ # to read the source.
155
+ raise DiceBagError, "Dice Parse Error for string: #{dstr}"
156
+ end
157
+ end
158
+ end
159
+
160
+ require 'dicebag/parser'
161
+ require 'dicebag/transform'
162
+ require 'dicebag/simple_part'
163
+ require 'dicebag/label_part'
164
+ require 'dicebag/static_part'
165
+ require 'dicebag/roll_part'
166
+ require 'dicebag/roll'
167
+ require 'dicebag/result'
168
+
@@ -0,0 +1,9 @@
1
+ module DiceBag
2
+ # The subclass for a label.
3
+ class LabelPart < SimplePart
4
+ def to_s
5
+ return "(%s)" % self.value
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,86 @@
1
+ module DiceBag
2
+ class Parser < Parslet::Parser
3
+ # Base rules.
4
+ rule(:space?) { str(' ').repeat }
5
+
6
+ # Numbers are limited to 3 digit places. Why?
7
+ # To prevent abuse from people rolling:
8
+ # 999999999D999999999 and 'DOS'-ing the app.
9
+ rule(:number) { match('[0-9]').repeat(1,3) }
10
+ rule(:number?) { number.maybe }
11
+
12
+ # Label rule
13
+ # Labels must match '(<some text here>)' and
14
+ # are not allowed to have commas in the label.
15
+ # This for future use of parsing multiple dice
16
+ # definitions in comma-separated strings.
17
+ # The :label matches anything that ISN'T a
18
+ # parenethesis or a comma.
19
+ rule(:lparen) { str('(') }
20
+ rule(:rparen) { str(')') }
21
+ rule(:label) do
22
+ lparen >>
23
+ match('[^(),]').repeat(1).as(:label) >>
24
+ rparen >>
25
+ space?
26
+ end
27
+
28
+ # count and sides rules.
29
+ # :count is allowed to be nil, which will default
30
+ # to 1.
31
+ rule(:count) { number?.as(:count) }
32
+ rule(:sides) { match('[dD]') >> number.as(:sides) }
33
+
34
+ # xDx Parts.
35
+ # All xDx parts may be followed by none, one, or more
36
+ # options.
37
+ rule(:xdx) { (count >> sides).as(:xdx) >> options? }
38
+
39
+ # xdx Options.
40
+ # Note that :explode is allowed to NOT have a number
41
+ # assigned, which will leave it with a nil value.
42
+ # This is handled in RollPart#initialize.
43
+ rule(:explode) { str('e') >> number?.as(:explode) >> space? }
44
+ rule(:drop) { str('~') >> number.as(:drop) >> space? }
45
+ rule(:keep) { str('!') >> number.as(:keep) >> space? }
46
+ rule(:reroll) { str('r') >> number.as(:reroll) >> space? }
47
+
48
+ # This allows options to be defined in any order and
49
+ # even have more than one of the same option, however
50
+ # only the last option of a given key will be kept.
51
+ rule(:options) {
52
+ space? >> (drop | explode | keep | reroll).repeat >> space?
53
+ }
54
+
55
+ rule(:options?) { options.maybe.as(:options) }
56
+
57
+ # Part Operators.
58
+ rule(:add) { str('+') }
59
+ rule(:sub) { str('-') }
60
+ rule(:mul) { str('*') }
61
+ rule(:div) { str('/') }
62
+ rule(:op) { (add | sub | mul | div).as(:op) }
63
+
64
+ # Part Rule
65
+ # A part is an operator, followed by either an xDx
66
+ # string or a static number value.
67
+ rule(:part) do
68
+ space? >>
69
+ op >>
70
+ space? >>
71
+ (xdx | number).as(:value) >>
72
+ space?
73
+ end
74
+
75
+ # All parts of a dice roll MUST start with an xDx
76
+ # string and then followed by any optional parts.
77
+ # The first xDx string is labeled as :start.
78
+ rule(:parts) { xdx.as(:start) >> part.repeat }
79
+
80
+ # A dice string is an optional label, followed by
81
+ # the defined parts.
82
+ rule(:dice) { label.maybe >> parts }
83
+
84
+ root(:dice)
85
+ end
86
+ end
@@ -0,0 +1,29 @@
1
+ module DiceBag
2
+ # This class merely encapsulates the result,
3
+ # providing convience methods to access the
4
+ # results of each section if desired.
5
+ class Result
6
+ attr_reader :label
7
+ attr_reader :total
8
+ attr_reader :sections
9
+
10
+ def initialize(label, total, sections)
11
+ @label = label
12
+ @total = total
13
+ @sections = sections
14
+ end
15
+
16
+ def each(&block)
17
+ self.sections.each do |section|
18
+ yield section
19
+ end
20
+ return nil
21
+ end
22
+
23
+ def to_s
24
+ return "#{self.label}: #{self.total}" unless self.label.empty?
25
+ return self.total.to_s
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,98 @@
1
+ module DiceBag
2
+ # This is the 'main' class of Dice Bag. This class
3
+ # takes the dice string, parses it, and encapsulates
4
+ # the actual rolling of the dice. If no dice string
5
+ # is given, it defaults to DefaultRoll.
6
+ class Roll
7
+ attr :dstr
8
+ attr :tree
9
+
10
+ alias :parsed :tree
11
+
12
+ def initialize(dstr=nil)
13
+ @dstr = dstr ||= DefaultRoll
14
+ @tree = DiceBag.parse(dstr)
15
+ @result = nil
16
+ end
17
+
18
+ def notes
19
+ s = ""
20
+
21
+ self.tree.each do |op, part|
22
+ if part.is_a?(RollPart)
23
+ n = part.notes
24
+ s += "For: #{part}:\n#{n}\n\n" unless n.empty?
25
+ end
26
+ end
27
+
28
+ return s
29
+ end
30
+
31
+ def result
32
+ self.roll() unless @result
33
+ return @result
34
+ end
35
+
36
+ def roll
37
+ total = 0
38
+ label = ""
39
+ sections = []
40
+
41
+ self.tree.each do |op, part|
42
+ do_push = true
43
+
44
+ # If this is a RollPart instance,
45
+ # ensure fresh results.
46
+ part.roll() if part.is_a?(RollPart)
47
+
48
+ case op
49
+ when :label
50
+ label = part.value()
51
+ do_push = false
52
+ when :start
53
+ total = part.total()
54
+ when :add
55
+ total += part.total()
56
+ when :sub
57
+ total -= part.total()
58
+ when :mul
59
+ total *= part.total()
60
+ when :div
61
+ total /= part.total()
62
+ end
63
+
64
+ sections.push(part) if do_push
65
+ end
66
+
67
+ @result = Result.new(label, total, sections)
68
+
69
+ return @result
70
+ end
71
+
72
+ def to_s(with_space=true)
73
+ s = ""
74
+
75
+ sp = with_space ? ' ' : ''
76
+
77
+ self.tree.each do |op, value|
78
+ case op
79
+ when :label
80
+ s += "#{value}#{sp}"
81
+ when :start
82
+ s += "#{value}#{sp}"
83
+ when :add
84
+ s += "+#{sp}#{value}#{sp}"
85
+ when :sub
86
+ s += "-#{sp}#{value}#{sp}"
87
+ when :mul
88
+ s += "*#{sp}#{value}#{sp}"
89
+ when :div
90
+ s += "/#{sp}#{value}#{sp}"
91
+ end
92
+ end
93
+
94
+ return s.strip
95
+ end
96
+ end
97
+
98
+ end
@@ -0,0 +1,133 @@
1
+ module DiceBag
2
+ # This represents the xDx part of the dice string.
3
+ class RollPart < SimplePart
4
+
5
+ attr :count
6
+ attr :sides
7
+ attr :parts
8
+ attr :options
9
+
10
+ def initialize(part)
11
+ @total = nil
12
+ @tally = []
13
+ @value = part
14
+ @count = part[:count]
15
+ @sides = part[:sides]
16
+ @notes = part[:notes] || []
17
+
18
+ # Our Default Options
19
+ @options = {
20
+ :explode => 0,
21
+ :drop => 0,
22
+ :keep => 0,
23
+ :reroll => 0
24
+ }
25
+
26
+ @options.update(part[:options]) if part.has_key?(:options)
27
+ end
28
+
29
+ def notes
30
+ return @notes.join("\n") unless @notes.empty?
31
+ return ""
32
+ end
33
+
34
+ # Checks to see if this instance has rolled yet
35
+ # or not.
36
+ def has_rolled?
37
+ return @total.nil? ? false : true
38
+ end
39
+
40
+ # Rolls a single die from the xDx string.
41
+ def roll_die()
42
+ num = 0
43
+ reroll = @options[:reroll]
44
+
45
+ while num <= reroll
46
+ num = rand(self.sides) + 1
47
+ end
48
+
49
+ return num
50
+ end
51
+
52
+ def roll
53
+ results = []
54
+ explode = @options[:explode]
55
+
56
+ self.count.times do
57
+ roll = self.roll_die()
58
+
59
+ results.push(roll)
60
+
61
+ unless explode.zero?
62
+ while roll >= explode
63
+ roll = self.roll_die()
64
+ results.push(roll)
65
+ end
66
+ end
67
+ end
68
+
69
+ results.sort!
70
+ results.reverse!
71
+
72
+ # Save the tally in case it's requested later.
73
+ @tally = results.dup()
74
+
75
+ # Drop the low end numbers if :drop is less than zero.
76
+ if @options[:drop] < 0
77
+ results = results[0 ... @options[:drop]]
78
+ end
79
+
80
+ # Keep the high end numbers if :keep is greater than zero.
81
+ if @options[:keep] > 0
82
+ results = results[0 ... @options[:keep]]
83
+ end
84
+
85
+ # I think reduce(:+) is ugly, but it's very fast.
86
+ @total = results.reduce(:+)
87
+
88
+ return self
89
+ end
90
+
91
+ # Returns the tally from the roll. This is the entire
92
+ # tally, even if a :keep or :drop options were given.
93
+ def tally()
94
+ return @tally
95
+ end
96
+
97
+ # Gets the total of the last roll; if there is no
98
+ # last roll, it calls roll() first.
99
+ def total
100
+ self.roll() if @total.nil?
101
+ return @total
102
+ end
103
+
104
+ # This takes the @parts hash and recreates the xDx
105
+ # string. Optionally, passing true to the method will
106
+ # remove spaces form the finished string.
107
+ def to_s(no_spaces=false)
108
+ s = ""
109
+
110
+ sp = no_spaces ? "" : " "
111
+
112
+ s += self.count.to_s unless self.count.zero?
113
+ s += "d"
114
+ s += self.sides.to_s
115
+
116
+ unless @options[:explode].zero?
117
+ s += "#{sp}e"
118
+ s += @options[:explode].to_s unless @options[:explode] == self.sides
119
+ end
120
+
121
+ s += "#{sp}~" + @options[:drop].abs.to_s unless @options[:drop].zero?
122
+ s += "#{sp}!" + @options[:keep].to_s unless @options[:keep].zero?
123
+ s += "#{sp}r" + @options[:reroll].to_s unless @options[:reroll].zero?
124
+
125
+ return s
126
+ end
127
+
128
+ def <=>(other)
129
+ return self.total <=> other.total
130
+ end
131
+ end
132
+
133
+ end
@@ -0,0 +1,21 @@
1
+ module DiceBag
2
+ # The most simplest of a part. If a given part of
3
+ # a dice string is not a Label, Fixnum, or a xDx part
4
+ # it will be an instance of this class, which simply
5
+ # returns the value given to it.
6
+ class SimplePart
7
+ attr :value
8
+
9
+ def initialize(part)
10
+ @value = part
11
+ end
12
+
13
+ def result
14
+ return @value
15
+ end
16
+
17
+ def to_s
18
+ return @value
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module DiceBag
2
+ # This represents a static, non-random number part
3
+ # of the dice string.
4
+ class StaticPart < SimplePart
5
+ def initialize(num)
6
+ num = num.to_i() if num.is_a?(String)
7
+ @value = num
8
+ end
9
+
10
+ def total
11
+ return self.value
12
+ end
13
+
14
+ def to_s
15
+ return self.value.to_s()
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,77 @@
1
+ module DiceBag
2
+ class Transform < Parslet::Transform
3
+
4
+ def Transform.hashify_options(options)
5
+ opts = {}
6
+ options.each {|opt, val| opts[opt] = val} unless options.is_a?(Hash)
7
+ return opts
8
+ end
9
+
10
+ # Option transforms. These are turned into an array of
11
+ # 2-element arrays ('tagged arrays'), which is then
12
+ # hashified later. (There is no way to update the
13
+ # options when these rules are matched.)
14
+ rule(:drop => simple(:x)) { [:drop, Integer(x)] }
15
+ rule(:keep => simple(:x)) { [:keep, Integer(x)] }
16
+ rule(:reroll => simple(:x)) { [:reroll, Integer(x)] }
17
+
18
+ # Explode is special, in that if it is nil, then it
19
+ # must remain that way.
20
+ rule(:explode => simple(:x)) do
21
+ x.nil? ? [:explode, nil] : [:explode, Integer(x)]
22
+ end
23
+
24
+ # Parts {:ops => (:xdx | :number)}
25
+ # These are first-match, so the simple number will
26
+ # be matched before the xdx subtree.
27
+
28
+ # Match an operator followed by a static number.
29
+ rule(:op => simple(:o), :value => simple(:v)) do
30
+ [String(o), Integer(v)]
31
+ end
32
+
33
+ # Match an operator followed by an :xdx subtree.
34
+ rule(:op => simple(:o), :value => subtree(:part)) do
35
+ [String(o),
36
+ {
37
+ :xdx => {
38
+ :count => Integer(part[:xdx][:count]),
39
+ :sides => Integer(part[:xdx][:sides])
40
+ },
41
+ :options => Transform.hashify_options(part[:options])
42
+ }
43
+ ]
44
+ end
45
+
46
+ # Match a label by itself.
47
+ rule(:label => simple(:s)) { {:label => String(s)} }
48
+
49
+ # Match a label followed by a :start subtree.
50
+ rule(:label => simple(:s), :start => subtree(:part)) do
51
+ [
52
+ {:label => String(s)},
53
+ {:start => {
54
+ :xdx => part[:xdx],
55
+ :options => Transform.hashify_options(part[:options])
56
+ }
57
+ }
58
+ ]
59
+ end
60
+
61
+ # Match a :start subtree, with the label not present.
62
+ # Note that this returns a hash, but the final output
63
+ # will still be in an array.
64
+ rule(:start => subtree(:part)) do
65
+ {:start => {
66
+ :xdx => part[:xdx],
67
+ :options => Transform.hashify_options(part[:options])
68
+ }
69
+ }
70
+ end
71
+
72
+ # Convert the count and sides of an :xdx part.
73
+ rule(:count => simple(:c), :sides => simple(:s)) do
74
+ { :count => Integer(c), :sides => Integer(s) }
75
+ end
76
+ end
77
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dicebag
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 3
7
+ - 0
8
+ - 2
9
+ version: 3.0.2
10
+ platform: ruby
11
+ authors:
12
+ - SynTruth
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2012-06-21 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: parslet
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 4
30
+ - 0
31
+ version: 1.4.0
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ description: A very flexible dice rolling library for Ruby.
35
+ email: syntruth@gmail.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files: []
41
+
42
+ files:
43
+ - lib/dicebag/label_part.rb
44
+ - lib/dicebag/parser.rb
45
+ - lib/dicebag/result.rb
46
+ - lib/dicebag/roll.rb
47
+ - lib/dicebag/roll_part.rb
48
+ - lib/dicebag/simple_part.rb
49
+ - lib/dicebag/static_part.rb
50
+ - lib/dicebag/transform.rb
51
+ - lib/dicebag.rb
52
+ has_rdoc: true
53
+ homepage: https://github.com/syntruth/Dice-Bag
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.3.6
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: "Dice Bag: Ruby Dice Rolling Library"
82
+ test_files: []
83
+