dicebag 3.2.1 → 3.3.0

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: 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