dicebag 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+