dicebag 3.2.1 → 3.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: dc6f9c347f892aeb166fcae63db74f852f4f78d5
4
- data.tar.gz: 4b719b9fa625bef4c4cc5a9bf531249d1cd366b6
2
+ SHA256:
3
+ metadata.gz: b36691cc7b34c8637154bac79f610b1867d55902b904e83b390b12ae1e6ebf37
4
+ data.tar.gz: d3fb57f5f563a1e599beb75831953ff3e8bc6dd2e60fcc82aee66a609ac96b1a
5
5
  SHA512:
6
- metadata.gz: 78bf99b571914d1a28c26f964cdaf23ffbdb1bbb923527bacf1f613785f46b82cbfba283471cc080bdcc53a5c95263550472294ee4e3c22f6e5eaa77d3907769
7
- data.tar.gz: da473d03f4524f4e0824d02b2875b705a2ef41c5254f6ad002ec1816696f2d78e348a5fe1ed3b636199b91587e6d8da4614b5d70f2ec6f2e67eedf03b84d8b03
6
+ metadata.gz: d0367e80fdfc838a465a5b431c766a3418dcb35ae99f8cce9a6ee9f040e6dd4b859b1c561a36f6bfbed16e88a4e9e817e9f9c077263de8721e72498ab9e6e63d
7
+ data.tar.gz: d1cf650ccb9d0a75fcfb9d278198b949c17291ebf488878f3c7df3a3accb63ac8c9e9ed8c00b1ffb6222369fd6ed9c0ad1bad8a5c5c8b471cefd59e7cffa63d7
data/bin/dicebag ADDED
@@ -0,0 +1,47 @@
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.on '-n', '--notes', 'Display any notes for the roll.'
22
+ end
23
+ end
24
+
25
+ def perform
26
+ nonopts = opts.parse! into: params
27
+
28
+ roll = DiceBag::Roll.new nonopts.first
29
+ result = roll.roll
30
+
31
+ puts result
32
+
33
+ puts_notes(roll) if params[:notes]
34
+ rescue StandardError => err
35
+ abort err.to_s
36
+ end
37
+
38
+ def puts_notes(roll)
39
+ nstr = roll.notes_to_s
40
+
41
+ return if nstr.empty?
42
+
43
+ puts nstr
44
+ end
45
+ end
46
+
47
+ 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,165 @@
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
+ return if explode.nil? || explode >= 2
105
+
106
+ hash[:options][:explode] = nil
107
+
108
+ hash[:notes].push("Explode set to #{hash[:sides]}")
109
+ end
110
+
111
+ # Prevent Reroll abuse.
112
+ def normalize_reroll(hash)
113
+ return unless hash[:options].key?(:reroll) &&
114
+ hash[:options][:reroll] >= hash[:sides]
115
+
116
+ hash[:options][:reroll] = 0
117
+
118
+ hash[:notes].push 'Reroll reset to 0.'
119
+ end
120
+
121
+ # Make sure there are enough dice to handle both Drop and Keep values.
122
+ # If not, both are reset to 0. Harsh.
123
+ def normalize_drop_keep(hash)
124
+ drop = hash[:options].fetch(:drop, 0)
125
+ keep = hash[:options].fetch(:keep, 0)
126
+
127
+ return unless (drop + keep) >= hash[:count]
128
+
129
+ hash[:options][:drop] = 0
130
+ hash[:options][:keep] = 0
131
+
132
+ hash[:notes].push 'Drop and Keep Conflict. Both reset to 0.'
133
+ end
134
+
135
+ # If we have a failure number, make sure it is equal to or less than
136
+ # the dice sides and greater than 0, otherwise, set it to 0 (i.e. no
137
+ # failure number) and add a note.
138
+ def normalize_failure(hash)
139
+ return unless hash[:options].key?(:failure)
140
+
141
+ failure = hash[:options][:failure]
142
+
143
+ return if failure >= 0 && failure <= hash[:sides]
144
+
145
+ hash[:options][:failure] = 0
146
+
147
+ hash[:notes].push 'Failure number too large or is negative; reset to 0.'
148
+ end
149
+
150
+ # Finally, if we have a target number, make sure it is equal to or
151
+ # less than the dice sides and greater than 0, otherwise, set it to 0
152
+ # (i.e. no target number) and add a note.
153
+ def normalize_target(hash)
154
+ return unless hash[:options].key? :target
155
+
156
+ target = hash[:options][:target]
157
+
158
+ return if target >= 0 && target <= hash[:sides]
159
+
160
+ hash[:options][:target] = 0
161
+
162
+ hash[:notes].push 'Target number too large or is negative; reset to 0.'
163
+ end
164
+ end
165
+ end
@@ -1,61 +1,87 @@
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
27
- lparen >>
28
- match('[^(),]').repeat(1).as(:label) >>
29
- rparen >>
30
- space?
25
+ lparen >> match('[^(),]').repeat(1).as(:label) >> rparen >> space?
31
26
  end
32
27
 
33
28
  # count and sides rules.
34
- # :count is allowed to be nil, which will default
35
- # to 1.
29
+ #
30
+ # :count is allowed to be nil, which will default to 1.
36
31
  rule(:count) { number?.as(:count) }
37
32
  rule(:sides) { match('[dD]') >> number.as(:sides) }
38
33
 
39
34
  # xDx Parts.
40
- # All xDx parts may be followed by none, one, or more
41
- # options.
35
+ #
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.
42
40
  rule(:xdx) { (count >> sides).as(:xdx) >> options? }
43
41
 
44
42
  # xdx Options.
45
- # Note that :explode is allowed to NOT have a number
46
- # assigned, which will leave it with a nil value.
47
- # This is handled in RollPart#initialize.
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!
48
52
  rule(:explode) { str('e') >> number?.as(:explode) >> space? }
49
- rule(:drop) { str('d') >> number.as(:drop) >> space? }
50
- rule(:keep) { str('k') >> number.as(:keep) >> space? }
51
- rule(:reroll) { str('r') >> number.as(:reroll) >> space? }
52
- rule(:target) { str('t') >> number.as(:target) >> space? }
53
-
54
- # This allows options to be defined in any order and
55
- # even have more than one of the same option, however
56
- # 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.
57
77
  rule(:option) do
58
- (drop | explode | keep | reroll | target)
78
+ drop |
79
+ explode |
80
+ keep |
81
+ keeplowest |
82
+ reroll |
83
+ target |
84
+ failure
59
85
  end
60
86
 
61
87
  rule(:options) { space? >> option.repeat >> space? }
@@ -69,19 +95,19 @@ module DiceBag
69
95
  rule(:op) { (add | sub | mul | div).as(:op) }
70
96
 
71
97
  # Part Rule
72
- # A part is an operator, followed by either an xDx
73
- # 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.
74
101
  rule(:part) do
75
102
  space? >> op >> space? >> (xdx | number).as(:value) >> space?
76
103
  end
77
104
 
78
- # All parts of a dice roll MUST start with an xDx
79
- # string and then followed by any optional parts.
80
- # 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.
81
108
  rule(:parts) { xdx.as(:start) >> part.repeat }
82
109
 
83
- # A dice string is an optional label, followed by
84
- # the defined parts.
110
+ # A dice string is an optional label, followed by the defined parts.
85
111
  rule(:dice) { label.maybe >> parts }
86
112
 
87
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