dicebag 3.2.2 → 3.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: a04e07e333e0dbee195dc20264c31791b3df0e30
4
- data.tar.gz: e142197e3d345460e984497d85daf271468bc8c3
2
+ SHA256:
3
+ metadata.gz: 2c9b311916035f1eaf3e31fd09c0eba523bc426875ca25ea0929d131215df709
4
+ data.tar.gz: 3c7c973630fdc29c654f3b90009d32652da06763a2e6a44fc5d2edb643215b96
5
5
  SHA512:
6
- metadata.gz: f48eea7ecae33fd45c4f8849b74a627b8cbaf13ef01084320d8cc66397c581501a7852be399a7734f2118562c66e383c5c7d149099722b44387e1744b5b4981a
7
- data.tar.gz: 98c8bb9b2e6199487db54d8b11a95f11cac4225a2687a54e356b58c31184e497e633226cf921445f13e5f883fedf92c116d72381dd1af4260a3d3a73bade4959
6
+ metadata.gz: 735fae23c00f6c883f4370bd8ca2a9451b6747264664b820f6f2b36ea062c8f3167197be28e17c83978d82a887c2d71cf5236af5519865913f23a0773145d087
7
+ data.tar.gz: 8c9c8fbc7ad416cf954da26a9118e040863a8cfb8b2ca0e71b9e8b0248d282ef0ff17bb66646433e93c6d55d8c359fc95962ceaba0205f8889500319271cd479
data/bin/dicebag ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ # require 'dicebag'
5
+ require_relative '../lib/dicebag'
6
+
7
+ # Define the Dicebag CLI app
8
+ class DiceBagCLI
9
+ attr_reader :params
10
+
11
+ def self.call
12
+ new.perform
13
+ end
14
+
15
+ def initialize
16
+ @params = { notes: false }
17
+ end
18
+
19
+ def opts
20
+ @opts ||= OptionParser.new do |args|
21
+ args.banner = 'Usage: dicebag [-n | --notes] <dice string>'
22
+
23
+ args.on '-n', '--notes', 'Display any notes for the roll.'
24
+
25
+ args.on_head('-h', 'Displays this help.') do
26
+ puts args
27
+
28
+ exit
29
+ end
30
+ end
31
+ end
32
+
33
+ def perform
34
+ abort(opts.help) if ARGV.empty?
35
+
36
+ nonopts = opts.parse! into: params
37
+
38
+ dstr = nonopts.join(' ')
39
+ roll = DiceBag::Roll.new dstr
40
+ result = roll.roll
41
+
42
+ puts result
43
+
44
+ puts_notes(roll) if params[:notes]
45
+ rescue StandardError => err
46
+ abort err.to_s
47
+ end
48
+
49
+ def puts_notes(roll)
50
+ nstr = roll.notes_to_s
51
+
52
+ return if nstr.empty?
53
+
54
+ puts nstr
55
+ end
56
+ end
57
+
58
+ DiceBagCLI.call
@@ -1,10 +1,12 @@
1
- # Encoding: UTF-8
2
-
3
1
  module DiceBag
4
2
  # The subclass for a label.
5
3
  class LabelPart < SimplePart
6
4
  def to_s
7
5
  format('(%s)', value)
8
6
  end
7
+
8
+ def inspect
9
+ "<#{self.class.name} #{self}>"
10
+ end
9
11
  end
10
12
  end
@@ -0,0 +1,173 @@
1
+ # DiceBag Module
2
+ module DiceBag
3
+ # Encapsulate the Normalization Process
4
+ #
5
+ # This takes the parsed tree, AFTER it has been through the Transform
6
+ # class, and massages the data a bit more, to ease the iteration that
7
+ # happens in the Roll class. It will convert all values into the
8
+ # correct *Part classes.
9
+ class Normalize
10
+ # The Abstract Source Tree
11
+ attr_reader :ast
12
+
13
+ def self.call(ast)
14
+ new(ast).perform
15
+ end
16
+
17
+ def initialize(ast)
18
+ # ASTs that only have a :start section will be a single array by
19
+ # itself, with the first element being `:start`, so we need to
20
+ # wrap it once more.
21
+ ast = [ast] unless ast.first.is_a? Array
22
+
23
+ @ast = ast
24
+ end
25
+
26
+ def perform
27
+ ast.map { |part| normalize part }
28
+ end
29
+
30
+ private
31
+
32
+ def normalize(part)
33
+ [op(part.first), value(part.last)]
34
+ end
35
+
36
+ def op(oper)
37
+ # We swap out the strings for symbols. If the oper is not one of
38
+ # the arithimetic operators, then the oper itself is returned.
39
+ # (This should only happen on :start and :label parts.)
40
+ case oper
41
+ when '+' then :add
42
+ when '-' then :sub
43
+ when '*' then :mul
44
+ when '/' then :div
45
+ else
46
+ oper
47
+ end
48
+ end
49
+
50
+ def value(val)
51
+ case val
52
+ when String
53
+ LabelPart.new val
54
+ when Hash
55
+ RollPart.new normalize_xdx(val)
56
+ when Integer
57
+ StaticPart.new val
58
+ else
59
+ val
60
+ end
61
+ end
62
+
63
+ # This further massages the xDx hashes.
64
+ def normalize_xdx(hash)
65
+ count = hash[:xdx][:count]
66
+ sides = hash[:xdx][:sides]
67
+
68
+ # Delete the no longer needed :xdx key.
69
+ hash.delete(:xdx)
70
+
71
+ # Default to at least 1 die.
72
+ count = 1 if count.nil? || count.zero?
73
+
74
+ # Set the :count and :sides keys directly and setup the notes array.
75
+ hash[:count] = count
76
+ hash[:sides] = sides
77
+ hash[:notes] = []
78
+
79
+ normalize_options hash
80
+ end
81
+
82
+ def normalize_options(hash)
83
+ if hash[:options].empty?
84
+ hash.delete(:options)
85
+
86
+ return hash
87
+ end
88
+
89
+ normalize_explode hash
90
+ normalize_reroll hash
91
+ normalize_drop_keep hash
92
+ normalize_target hash
93
+ normalize_failure hash
94
+
95
+ hash
96
+ end
97
+
98
+ # Prevent Explosion abuse.
99
+ def normalize_explode(hash)
100
+ return unless hash[:options].key?(:explode)
101
+
102
+ explode = hash[:options][:explode]
103
+
104
+ # If we did not come with a value, set it to dice sides, but no
105
+ # need to note this, as this is what no values means.
106
+ return __set_explode_to_sides(hash) if explode.negative?
107
+
108
+ # But we do want to note if the explode value was set to 1. >:|
109
+ __set_explode_to_sides if explode == 1
110
+
111
+ hash[:notes].push("Explode set to #{hash[:sides]}")
112
+ end
113
+
114
+ # :nodoc:
115
+ def __set_explode_to_sides(hash)
116
+ hash[:options][:explode] = hash[:sides]
117
+ end
118
+
119
+ # Prevent Reroll abuse.
120
+ def normalize_reroll(hash)
121
+ return unless hash[:options].key?(:reroll) &&
122
+ hash[:options][:reroll] >= hash[:sides]
123
+
124
+ hash[:options][:reroll] = 0
125
+
126
+ hash[:notes].push 'Reroll reset to 0.'
127
+ end
128
+
129
+ # Make sure there are enough dice to handle both Drop and Keep values.
130
+ # If not, both are reset to 0. Harsh.
131
+ def normalize_drop_keep(hash)
132
+ drop = hash[:options].fetch(:drop, 0)
133
+ keep = hash[:options].fetch(:keep, 0)
134
+
135
+ return unless (drop + keep) >= hash[:count]
136
+
137
+ hash[:options][:drop] = 0
138
+ hash[:options][:keep] = 0
139
+
140
+ hash[:notes].push 'Drop and Keep Conflict. Both reset to 0.'
141
+ end
142
+
143
+ # If we have a failure number, make sure it is equal to or less than
144
+ # the dice sides and greater than 0, otherwise, set it to 0 (i.e. no
145
+ # failure number) and add a note.
146
+ def normalize_failure(hash)
147
+ return unless hash[:options].key?(:failure)
148
+
149
+ failure = hash[:options][:failure]
150
+
151
+ return if failure >= 0 && failure <= hash[:sides]
152
+
153
+ hash[:options][:failure] = 0
154
+
155
+ hash[:notes].push 'Failure number too large or is negative; reset to 0.'
156
+ end
157
+
158
+ # Finally, if we have a target number, make sure it is equal to or
159
+ # less than the dice sides and greater than 0, otherwise, set it to 0
160
+ # (i.e. no target number) and add a note.
161
+ def normalize_target(hash)
162
+ return unless hash[:options].key? :target
163
+
164
+ target = hash[:options][:target]
165
+
166
+ return if target >= 0 && target <= hash[:sides]
167
+
168
+ hash[:options][:target] = 0
169
+
170
+ hash[:notes].push 'Target number too large or is negative; reset to 0.'
171
+ end
172
+ end
173
+ end
@@ -1,26 +1,24 @@
1
- # Encoding: UTF-8
2
-
3
1
  module DiceBag
4
- # This class parses the dice string into the individual
5
- # components. To understand this code, please refer to
6
- # the Parslet library's documentation.
2
+ # This class parses the dice string into the individual components. To
3
+ # understand this code, please refer to the Parslet library's
4
+ # documentation.
7
5
  class Parser < Parslet::Parser
8
6
  # Base rules.
9
7
  rule(:space?) { str(' ').repeat }
10
8
 
11
- # Numbers are limited to 3 digit places. Why?
12
- # To prevent abuse from people rolling:
13
- # 999999999D999999999 and 'DOS'-ing the app.
9
+ # Numbers are limited to 3 digit places.
10
+ #
11
+ # Why? To prevent abuse from people rolling: 999999999D999999999 and
12
+ # 'DOS'-ing the app.
14
13
  rule(:number) { match('[0-9]').repeat(1, 3) }
15
14
  rule(:number?) { number.maybe }
16
15
 
17
16
  # Label rule
18
- # Labels must match '(<some text here>)' and
19
- # are not allowed to have commas in the label.
20
- # This for future use of parsing multiple dice
21
- # definitions in comma-separated strings.
22
- # The :label matches anything that ISN'T a
23
- # parenethesis or a comma.
17
+ #
18
+ # Labels must match '(<some text here>)' and are not allowed to have
19
+ # commas in the label. This for future use of parsing multiple dice
20
+ # definitions in comma-separated strings. The :label matches
21
+ # anything that ISN'T a parenethesis or a comma.
24
22
  rule(:lparen) { str('(') }
25
23
  rule(:rparen) { str(')') }
26
24
  rule(:label) do
@@ -28,35 +26,62 @@ module DiceBag
28
26
  end
29
27
 
30
28
  # count and sides rules.
31
- # :count is allowed to be nil, which will default
32
- # to 1.
29
+ #
30
+ # :count is allowed to be nil, which will default to 1.
33
31
  rule(:count) { number?.as(:count) }
34
32
  rule(:sides) { match('[dD]') >> number.as(:sides) }
35
33
 
36
34
  # xDx Parts.
37
- # All xDx parts may be followed by none, one, or more
38
- # options.
39
35
  #
40
- # TODO: Remove the .as(:xdx) and rework the Transform
41
- # sub-class to account for it. It'll make the
42
- # resulting data much cleaner.
36
+ # All xDx parts may be followed by none, one, or more options.
37
+ #
38
+ # TODO: Remove the .as(:xdx) and rework the Transform sub-class to
39
+ # account for it. It'll make the resulting data much cleaner.
43
40
  rule(:xdx) { (count >> sides).as(:xdx) >> options? }
44
41
 
45
42
  # xdx Options.
46
- # Note that :explode is allowed to NOT have a number
47
- # assigned, which will leave it with a nil value. This
48
- # is handled in the Transform class.
43
+ #
44
+ # Note that :explode is allowed to NOT have a number assigned, which
45
+ # will leave it with a nil value. This is handled in the Transform
46
+ # class.
47
+ #
48
+ # For whatever reason ri doesn't work here, so I'm ordering the
49
+ # indefinite options as ir
50
+
51
+ # Explode!
49
52
  rule(:explode) { str('e') >> number?.as(:explode) >> space? }
50
- rule(:drop) { str('d') >> number.as(:drop) >> space? }
51
- rule(:keep) { str('k') >> number.as(:keep) >> space? }
52
- rule(:reroll) { str('r') >> number.as(:reroll) >> space? }
53
- rule(:target) { str('t') >> number.as(:target) >> space? }
54
-
55
- # This allows options to be defined in any order and
56
- # even have more than one of the same option, however
57
- # only the last option of a given key will be kept.
53
+
54
+ # Drop number of lowest rolled dice equal to drop value.
55
+ rule(:drop) { str('d') >> number.as(:drop) >> space? }
56
+
57
+ # Keep dice which are greater than the keep value.
58
+ rule(:keep) { str('k') >> number.as(:keep) >> space? }
59
+
60
+ # Only keep dice that are lower than the keeplowest value.
61
+ rule(:keeplowest) { str('kl') >> number.as(:keeplowest) >> space? }
62
+
63
+ # Reroll if die is less than reroll value.
64
+ rule(:reroll) { str('r') >> number.as(:reroll) >> space? }
65
+
66
+ # Don't tally the dice, but count how many rolled >= the target
67
+ # value.
68
+ rule(:target) { str('t') >> number.as(:target) >> space? }
69
+
70
+ # Opposite of :target, dice that roll below the failure value are
71
+ # not considered successes and are subtraced from successes.
72
+ rule(:failure) { str('f') >> number.as(:failure) >> space? }
73
+
74
+ # This allows options to be defined in any order and even have more
75
+ # than one of the same option, however only the last option of a
76
+ # given key will be kept.
58
77
  rule(:option) do
59
- (drop | explode | keep | reroll | target)
78
+ drop |
79
+ explode |
80
+ keep |
81
+ keeplowest |
82
+ reroll |
83
+ target |
84
+ failure
60
85
  end
61
86
 
62
87
  rule(:options) { space? >> option.repeat >> space? }
@@ -70,19 +95,19 @@ module DiceBag
70
95
  rule(:op) { (add | sub | mul | div).as(:op) }
71
96
 
72
97
  # Part Rule
73
- # A part is an operator, followed by either an xDx
74
- # string or a static number value.
98
+ #
99
+ # A part is an operator, followed by either an xDx string or a
100
+ # static number value.
75
101
  rule(:part) do
76
102
  space? >> op >> space? >> (xdx | number).as(:value) >> space?
77
103
  end
78
104
 
79
- # All parts of a dice roll MUST start with an xDx
80
- # string and then followed by any optional parts.
81
- # The first xDx string is labeled as :start.
105
+ # All parts of a dice roll MUST start with an xDx string and then
106
+ # followed by any optional parts. The first xDx string is labeled as
107
+ # :start.
82
108
  rule(:parts) { xdx.as(:start) >> part.repeat }
83
109
 
84
- # A dice string is an optional label, followed by
85
- # the defined parts.
110
+ # A dice string is an optional label, followed by the defined parts.
86
111
  rule(:dice) { label.maybe >> parts }
87
112
 
88
113
  root(:dice)
@@ -1,10 +1,9 @@
1
- # Encoding: UTF-8
2
-
3
1
  module DiceBag
4
- # This class merely encapsulates the result,
5
- # providing convience methods to access the
6
- # results of each section if desired.
2
+ # This class merely encapsulates the result, providing convience
3
+ # methods to access the results of each section if desired.
7
4
  class Result
5
+ include Comparable
6
+
8
7
  attr_reader :label
9
8
  attr_reader :total
10
9
  attr_reader :sections
@@ -15,8 +14,8 @@ module DiceBag
15
14
  @sections = sections
16
15
  end
17
16
 
18
- def each
19
- sections.each { |section| yield section }
17
+ def each(&block)
18
+ sections.each { |section| block.call section }
20
19
  end
21
20
 
22
21
  def to_s
@@ -24,5 +23,13 @@ module DiceBag
24
23
 
25
24
  total.to_s
26
25
  end
26
+
27
+ def inspect
28
+ "<#{self.class.name} #{self}>"
29
+ end
30
+
31
+ def <=>(other)
32
+ @total <=> other.total
33
+ end
27
34
  end
28
35
  end
data/lib/dicebag/roll.rb CHANGED
@@ -1,10 +1,7 @@
1
- # Encoding: UTF-8
2
-
3
1
  module DiceBag
4
- # This is the 'main' class of Dice Bag. This class
5
- # takes the dice string, parses it, and encapsulates
6
- # the actual rolling of the dice. If no dice string
7
- # is given, it defaults to DefaultRoll.
2
+ # This is the 'main' class of Dice Bag. This class takes the dice
3
+ # string, parses it, and encapsulates the actual rolling of the dice.
4
+ # If no dice string is given, it defaults to DiceBag.default_roll
8
5
  class Roll
9
6
  include RollString
10
7
 
@@ -14,21 +11,28 @@ module DiceBag
14
11
  alias parsed tree
15
12
 
16
13
  def initialize(dstr = nil)
17
- @dstr = dstr ||= DEFAULT_ROLL
14
+ @dstr = dstr ||= DiceBag.default_roll
18
15
  @tree = DiceBag.parse dstr
19
16
  @result = nil
20
17
  end
21
18
 
22
19
  def notes
23
- str = ''
20
+ arr = []
21
+ fmt = "For %s: %s\n"
24
22
 
25
23
  tree.each do |_op, part|
26
- next unless part.is_a?(RollPart) || !part.notes.empty?
27
-
28
- str += format('For: %s\n%s\n\n', part, part.notes)
24
+ if part.is_a?(RollPart) && !part.notes.empty?
25
+ arr.push format(fmt, part, part.notes)
26
+ end
29
27
  end
30
28
 
31
- str
29
+ arr
30
+ end
31
+
32
+ def notes_to_s
33
+ n = notes
34
+
35
+ n.empty? ? '' : n.join("\n")
32
36
  end
33
37
 
34
38
  def result
@@ -62,8 +66,8 @@ module DiceBag
62
66
  end
63
67
  end
64
68
 
65
- def handle_op(op, part)
66
- case op
69
+ def handle_op(oper, part)
70
+ case oper
67
71
  when :start then @total = part.total
68
72
  when :add then @total += part.total
69
73
  when :sub then @total -= part.total