dicebag 3.1.0 → 3.2.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 +7 -0
- data/lib/dicebag/label_part.rb +3 -2
- data/lib/dicebag/parser.rb +32 -17
- data/lib/dicebag/result.rb +8 -9
- data/lib/dicebag/roll.rb +46 -67
- data/lib/dicebag/roll_part.rb +80 -92
- data/lib/dicebag/roll_part_string.rb +63 -0
- data/lib/dicebag/roll_string.rb +44 -0
- data/lib/dicebag/simple_part.rb +3 -3
- data/lib/dicebag/static_part.rb +3 -4
- data/lib/dicebag/transform.rb +48 -39
- data/lib/dicebag.rb +132 -118
- metadata +13 -15
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 755294b13e5e461c7cefc63493214beb228d539a
|
4
|
+
data.tar.gz: 340241ef792274c0da3a67b97f64ba81f18e6163
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5efb90bebf321dd2a721f367ec7b1a3c4e57963858210393a0f246646d4cb3865c6f36260c390639007d824100fac44cd16707c9c539db41cf92c833d2073827
|
7
|
+
data.tar.gz: d2b2871d3940d219091daedc85c63771b69a1a789d087e972cb50ae41a0279880733abfabb61199b4d3bb9a35f8f52d00f202d97a4bad8ca47c3e6d5ac7c7a6a
|
data/lib/dicebag/label_part.rb
CHANGED
data/lib/dicebag/parser.rb
CHANGED
@@ -1,12 +1,27 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
1
3
|
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
7
|
class Parser < Parslet::Parser
|
8
|
+
# We override the #parse method, so that we can
|
9
|
+
# assure that we always return an Array.
|
10
|
+
def parse(dstr)
|
11
|
+
result = super dstr
|
12
|
+
|
13
|
+
result = [result] unless result.is_a? Array
|
14
|
+
|
15
|
+
result
|
16
|
+
end
|
17
|
+
|
3
18
|
# Base rules.
|
4
19
|
rule(:space?) { str(' ').repeat }
|
5
20
|
|
6
21
|
# Numbers are limited to 3 digit places. Why?
|
7
|
-
# To prevent abuse from people rolling:
|
22
|
+
# To prevent abuse from people rolling:
|
8
23
|
# 999999999D999999999 and 'DOS'-ing the app.
|
9
|
-
rule(:number) { match('[0-9]').repeat(1,3) }
|
24
|
+
rule(:number) { match('[0-9]').repeat(1, 3) }
|
10
25
|
rule(:number?) { number.maybe }
|
11
26
|
|
12
27
|
# Label rule
|
@@ -14,15 +29,15 @@ module DiceBag
|
|
14
29
|
# are not allowed to have commas in the label.
|
15
30
|
# This for future use of parsing multiple dice
|
16
31
|
# definitions in comma-separated strings.
|
17
|
-
# The :label matches anything that ISN'T a
|
32
|
+
# The :label matches anything that ISN'T a
|
18
33
|
# parenethesis or a comma.
|
19
34
|
rule(:lparen) { str('(') }
|
20
35
|
rule(:rparen) { str(')') }
|
21
36
|
rule(:label) do
|
22
|
-
lparen >>
|
23
|
-
|
24
|
-
|
25
|
-
|
37
|
+
lparen >>
|
38
|
+
match('[^(),]').repeat(1).as(:label) >>
|
39
|
+
rparen >>
|
40
|
+
space?
|
26
41
|
end
|
27
42
|
|
28
43
|
# count and sides rules.
|
@@ -49,9 +64,9 @@ module DiceBag
|
|
49
64
|
# This allows options to be defined in any order and
|
50
65
|
# even have more than one of the same option, however
|
51
66
|
# only the last option of a given key will be kept.
|
52
|
-
rule(:options)
|
67
|
+
rule(:options) do
|
53
68
|
space? >> (drop | explode | keep | reroll | target).repeat >> space?
|
54
|
-
|
69
|
+
end
|
55
70
|
|
56
71
|
rule(:options?) { options.maybe.as(:options) }
|
57
72
|
|
@@ -61,16 +76,16 @@ module DiceBag
|
|
61
76
|
rule(:mul) { str('*') }
|
62
77
|
rule(:div) { str('/') }
|
63
78
|
rule(:op) { (add | sub | mul | div).as(:op) }
|
64
|
-
|
79
|
+
|
65
80
|
# Part Rule
|
66
81
|
# A part is an operator, followed by either an xDx
|
67
82
|
# string or a static number value.
|
68
|
-
rule(:part)
|
69
|
-
space?
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
83
|
+
rule(:part) do
|
84
|
+
space? >>
|
85
|
+
op >>
|
86
|
+
space? >>
|
87
|
+
(xdx | number).as(:value) >>
|
88
|
+
space?
|
74
89
|
end
|
75
90
|
|
76
91
|
# All parts of a dice roll MUST start with an xDx
|
@@ -80,7 +95,7 @@ module DiceBag
|
|
80
95
|
|
81
96
|
# A dice string is an optional label, followed by
|
82
97
|
# the defined parts.
|
83
|
-
rule(:dice) { label.maybe >> parts
|
98
|
+
rule(:dice) { label.maybe >> parts }
|
84
99
|
|
85
100
|
root(:dice)
|
86
101
|
end
|
data/lib/dicebag/result.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
1
3
|
module DiceBag
|
2
4
|
# This class merely encapsulates the result,
|
3
5
|
# providing convience methods to access the
|
4
6
|
# results of each section if desired.
|
5
|
-
class Result
|
7
|
+
class Result
|
6
8
|
attr_reader :label
|
7
9
|
attr_reader :total
|
8
10
|
attr_reader :sections
|
@@ -13,17 +15,14 @@ module DiceBag
|
|
13
15
|
@sections = sections
|
14
16
|
end
|
15
17
|
|
16
|
-
def each
|
17
|
-
|
18
|
-
yield section
|
19
|
-
end
|
20
|
-
return nil
|
18
|
+
def each
|
19
|
+
sections.each { |section| yield section }
|
21
20
|
end
|
22
21
|
|
23
22
|
def to_s
|
24
|
-
return "#{
|
25
|
-
|
23
|
+
return "#{label}: #{total}" unless label.empty?
|
24
|
+
|
25
|
+
total.to_s
|
26
26
|
end
|
27
27
|
end
|
28
|
-
|
29
28
|
end
|
data/lib/dicebag/roll.rb
CHANGED
@@ -1,98 +1,77 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
1
3
|
module DiceBag
|
2
4
|
# This is the 'main' class of Dice Bag. This class
|
3
5
|
# takes the dice string, parses it, and encapsulates
|
4
6
|
# the actual rolling of the dice. If no dice string
|
5
7
|
# is given, it defaults to DefaultRoll.
|
6
8
|
class Roll
|
7
|
-
|
8
|
-
|
9
|
+
include RollString
|
10
|
+
|
11
|
+
attr_reader :dstr
|
12
|
+
attr_reader :tree
|
9
13
|
|
10
|
-
alias
|
14
|
+
alias parsed tree
|
11
15
|
|
12
|
-
def initialize(dstr=nil)
|
13
|
-
@dstr = dstr ||=
|
14
|
-
@tree = DiceBag.parse
|
16
|
+
def initialize(dstr = nil)
|
17
|
+
@dstr = dstr ||= DEFAULT_ROLL
|
18
|
+
@tree = DiceBag.parse dstr
|
15
19
|
@result = nil
|
16
20
|
end
|
17
21
|
|
18
22
|
def notes
|
19
|
-
|
23
|
+
str = ''
|
20
24
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
25
|
+
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)
|
26
29
|
end
|
27
30
|
|
28
|
-
|
31
|
+
str
|
29
32
|
end
|
30
33
|
|
31
34
|
def result
|
32
|
-
|
33
|
-
|
35
|
+
roll unless @result
|
36
|
+
|
37
|
+
@result
|
34
38
|
end
|
35
39
|
|
36
40
|
def roll
|
37
|
-
|
38
|
-
|
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
|
41
|
+
@label = ''
|
42
|
+
@total = 0
|
43
|
+
@sections = []
|
66
44
|
|
67
|
-
|
45
|
+
handle_tree
|
68
46
|
|
69
|
-
|
47
|
+
@result = Result.new(@label, @total, @sections)
|
70
48
|
end
|
71
49
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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}"
|
50
|
+
private
|
51
|
+
|
52
|
+
def handle_tree
|
53
|
+
tree.each do |op, part|
|
54
|
+
if op == :label
|
55
|
+
@label = part.value
|
56
|
+
next
|
91
57
|
end
|
58
|
+
|
59
|
+
part.roll if part.is_a?(RollPart) # ensure fresh results.
|
60
|
+
|
61
|
+
handle_op op, part
|
92
62
|
end
|
63
|
+
end
|
93
64
|
|
94
|
-
|
65
|
+
def handle_op(op, part)
|
66
|
+
case op
|
67
|
+
when :start then @total = part.total
|
68
|
+
when :add then @total += part.total
|
69
|
+
when :sub then @total -= part.total
|
70
|
+
when :mul then @total *= part.total
|
71
|
+
when :div then @total /= part.total
|
72
|
+
end
|
73
|
+
|
74
|
+
@sections.push part
|
95
75
|
end
|
96
76
|
end
|
97
|
-
|
98
77
|
end
|
data/lib/dicebag/roll_part.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
1
3
|
module DiceBag
|
2
4
|
# This represents the xDx part of the dice string.
|
3
5
|
class RollPart < SimplePart
|
6
|
+
include RollPartString
|
4
7
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
attr_reader :count
|
9
|
+
attr_reader :sides
|
10
|
+
attr_reader :parts
|
11
|
+
attr_reader :options
|
12
|
+
attr_reader :tally
|
9
13
|
|
10
14
|
def initialize(part)
|
11
15
|
@total = nil
|
@@ -16,127 +20,111 @@ module DiceBag
|
|
16
20
|
@notes = part[:notes] || []
|
17
21
|
|
18
22
|
# Our Default Options
|
19
|
-
@options = {
|
20
|
-
|
21
|
-
|
22
|
-
:keep => 0,
|
23
|
-
:reroll => 0,
|
24
|
-
:target => 0
|
25
|
-
}
|
26
|
-
|
27
|
-
@options.update(part[:options]) if part.has_key?(:options)
|
23
|
+
@options = { explode: 0, drop: 0, keep: 0, reroll: 0, target: 0 }
|
24
|
+
|
25
|
+
@options.update(part[:options]) if part.key?(:options)
|
28
26
|
end
|
29
27
|
|
30
28
|
def notes
|
31
|
-
|
32
|
-
return ""
|
29
|
+
@notes.empty? ? '' : @notes.join("\n")
|
33
30
|
end
|
34
31
|
|
35
32
|
# Checks to see if this instance has rolled yet
|
36
33
|
# or not.
|
37
|
-
def
|
38
|
-
|
34
|
+
def rolled?
|
35
|
+
@total.nil? ? false : true
|
39
36
|
end
|
40
37
|
|
41
38
|
# Rolls a single die from the xDx string.
|
42
|
-
def roll_die
|
43
|
-
num
|
44
|
-
|
39
|
+
def roll_die
|
40
|
+
num = 0
|
41
|
+
num = rand(sides) + 1 while num <= @options[:reroll]
|
45
42
|
|
46
|
-
|
47
|
-
num = rand(self.sides) + 1
|
48
|
-
end
|
49
|
-
|
50
|
-
return num
|
43
|
+
num
|
51
44
|
end
|
52
45
|
|
53
46
|
def roll
|
54
|
-
|
55
|
-
explode = @options[:explode]
|
56
|
-
|
57
|
-
self.count.times do
|
58
|
-
roll = self.roll_die()
|
47
|
+
generate_results
|
59
48
|
|
60
|
-
|
61
|
-
|
62
|
-
unless explode.zero?
|
63
|
-
while roll >= explode
|
64
|
-
roll = self.roll_die()
|
65
|
-
results.push(roll)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
results.sort!
|
71
|
-
results.reverse!
|
49
|
+
@results.sort!
|
50
|
+
@results.reverse!
|
72
51
|
|
73
52
|
# Save the tally in case it's requested later.
|
74
|
-
@tally = results.dup
|
53
|
+
@tally = @results.dup
|
75
54
|
|
76
|
-
# Drop the low end numbers if :drop is
|
77
|
-
|
78
|
-
results = results[0 ... @options[:drop]]
|
79
|
-
end
|
55
|
+
# Drop the low end numbers if :drop is not zero.
|
56
|
+
handle_drop
|
80
57
|
|
81
58
|
# Keep the high end numbers if :keep is greater than zero.
|
82
|
-
|
83
|
-
results = results[0 ... @options[:keep]]
|
84
|
-
end
|
59
|
+
handle_keep
|
85
60
|
|
86
|
-
#
|
87
|
-
|
88
|
-
# we just add up all the numbers.
|
89
|
-
if @options[:target] and @options[:target] > 0
|
90
|
-
@total = results.count {|r| r >= @options[:target]}
|
91
|
-
else
|
92
|
-
# I think reduce(:+) is ugly, but it's very fast.
|
93
|
-
@total = results.reduce(:+)
|
94
|
-
end
|
95
|
-
|
96
|
-
return self
|
97
|
-
end
|
61
|
+
# Set the total.
|
62
|
+
handle_total
|
98
63
|
|
99
|
-
|
100
|
-
# tally, even if a :keep or :drop options were given.
|
101
|
-
def tally()
|
102
|
-
return @tally
|
64
|
+
self
|
103
65
|
end
|
104
66
|
|
105
|
-
# Gets the total of the last roll; if there is no
|
67
|
+
# Gets the total of the last roll; if there is no
|
106
68
|
# last roll, it calls roll() first.
|
107
69
|
def total
|
108
|
-
|
109
|
-
|
70
|
+
roll if @total.nil?
|
71
|
+
|
72
|
+
@total
|
73
|
+
end
|
74
|
+
|
75
|
+
def <=>(other)
|
76
|
+
total <=> other.total
|
110
77
|
end
|
111
78
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
unless @options[:explode].zero?
|
125
|
-
s += "#{sp}e"
|
126
|
-
s += @options[:explode].to_s unless @options[:explode] == self.sides
|
79
|
+
private
|
80
|
+
|
81
|
+
def generate_results
|
82
|
+
@results = []
|
83
|
+
|
84
|
+
count.times do
|
85
|
+
r = roll_die
|
86
|
+
|
87
|
+
@results.push(r)
|
88
|
+
|
89
|
+
handle_explode(r) unless @options[:explode].zero?
|
127
90
|
end
|
91
|
+
end
|
128
92
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
s += "#{sp}r" + @options[:reroll].to_s unless @options[:reroll].zero?
|
93
|
+
def handle_explode(r)
|
94
|
+
while r >= @options[:explode]
|
95
|
+
r = roll_die
|
133
96
|
|
134
|
-
|
97
|
+
@results.push(r)
|
98
|
+
end
|
135
99
|
end
|
136
100
|
|
137
|
-
def
|
138
|
-
return
|
101
|
+
def handle_drop
|
102
|
+
return unless @options[:drop] > 0
|
103
|
+
|
104
|
+
# Note that we invert the drop value here.
|
105
|
+
range = 0...-(@options[:drop])
|
106
|
+
|
107
|
+
@results = @results.slice range
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_keep
|
111
|
+
return unless @options[:keep] > 0
|
112
|
+
|
113
|
+
range = 0...@options[:keep]
|
114
|
+
|
115
|
+
@results = @results.slice range
|
139
116
|
end
|
140
|
-
end
|
141
117
|
|
118
|
+
def handle_total
|
119
|
+
# If we have a target number, count how many rolls
|
120
|
+
# in the results are >= than this number, otherwise
|
121
|
+
# we just add up all the numbers.
|
122
|
+
@total = if @options[:target] && @options[:target] > 0
|
123
|
+
@results.count { |r| r >= @options[:target] }
|
124
|
+
else
|
125
|
+
# I think reduce(:+) is ugly, but it's very fast.
|
126
|
+
@results.reduce(:+)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
142
130
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
3
|
+
# This encapsulates the RollPart string
|
4
|
+
# generation methods.
|
5
|
+
module RollPartString
|
6
|
+
# This takes the @parts hash and recreates the xDx
|
7
|
+
# string. Optionally, passing true to the method will
|
8
|
+
# remove spaces form the finished string.
|
9
|
+
def to_s(no_spaces = false)
|
10
|
+
@string = ''
|
11
|
+
@space = (no_spaces ? '' : ' ')
|
12
|
+
|
13
|
+
to_s_xdx
|
14
|
+
to_s_explode
|
15
|
+
to_s_drop
|
16
|
+
to_s_keep
|
17
|
+
to_s_reroll
|
18
|
+
to_s_target
|
19
|
+
|
20
|
+
@string
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def to_s_xdx
|
26
|
+
c = count.zero? ? '' : count.to_s
|
27
|
+
s = sides.to_s
|
28
|
+
|
29
|
+
@string += format('%sd%s', c, s)
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s_explode
|
33
|
+
return unless @options[:explode].zero?
|
34
|
+
|
35
|
+
e = (@options[:explode] == sides) ? @options[:explode] : ''
|
36
|
+
|
37
|
+
@string += format('%se%s', @space, e)
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s_drop
|
41
|
+
return unless @options[:drop].zero?
|
42
|
+
|
43
|
+
@string += format('%sd%d', @space, @options[:drop])
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s_keep
|
47
|
+
return unless @options[:keep].zero?
|
48
|
+
|
49
|
+
@string += format('%sk%s', @space, @options[:keep])
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s_reroll
|
53
|
+
return unless @options[:reroll].zero?
|
54
|
+
|
55
|
+
@string += format('%sr%s', @space, @options[:reroll])
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s_target
|
59
|
+
return unless @options[:target].zero?
|
60
|
+
|
61
|
+
@string += format('%st%s', @space, @options[:target])
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
3
|
+
# This encapsulates the Roll class' string
|
4
|
+
# generation methods.
|
5
|
+
module RollString
|
6
|
+
def to_s(no_spaces = false)
|
7
|
+
@string = ''
|
8
|
+
@space = no_spaces ? '' : ' '
|
9
|
+
|
10
|
+
to_s_tree
|
11
|
+
|
12
|
+
@string.strip
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def to_s_tree
|
18
|
+
tree.each { |op, value| @string += send("to_s_#{op}", value) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s_label(value)
|
22
|
+
"#{value}#{@space}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s_start(value)
|
26
|
+
"#{value}#{@space}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s_add(value)
|
30
|
+
"+#{@space}#{value}#{@space}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s_sub(value)
|
34
|
+
"-#{@space}#{value}#{@space}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s_mul(value)
|
38
|
+
"*#{@space}#{value}#{@space}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s_div(value)
|
42
|
+
"/#{@space}#{value}#{@space}"
|
43
|
+
end
|
44
|
+
end
|
data/lib/dicebag/simple_part.rb
CHANGED
@@ -4,18 +4,18 @@ module DiceBag
|
|
4
4
|
# it will be an instance of this class, which simply
|
5
5
|
# returns the value given to it.
|
6
6
|
class SimplePart
|
7
|
-
|
7
|
+
attr_reader :value
|
8
8
|
|
9
9
|
def initialize(part)
|
10
10
|
@value = part
|
11
11
|
end
|
12
12
|
|
13
13
|
def result
|
14
|
-
|
14
|
+
value
|
15
15
|
end
|
16
16
|
|
17
17
|
def to_s
|
18
|
-
|
18
|
+
value
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
data/lib/dicebag/static_part.rb
CHANGED
@@ -3,17 +3,16 @@ module DiceBag
|
|
3
3
|
# of the dice string.
|
4
4
|
class StaticPart < SimplePart
|
5
5
|
def initialize(num)
|
6
|
-
num = num.to_i
|
6
|
+
num = num.to_i if num.is_a?(String)
|
7
7
|
@value = num
|
8
8
|
end
|
9
9
|
|
10
10
|
def total
|
11
|
-
|
11
|
+
value
|
12
12
|
end
|
13
13
|
|
14
14
|
def to_s
|
15
|
-
|
15
|
+
value.to_s
|
16
16
|
end
|
17
17
|
end
|
18
|
-
|
19
18
|
end
|
data/lib/dicebag/transform.rb
CHANGED
@@ -1,25 +1,32 @@
|
|
1
|
+
# Encoding: UTF-8
|
2
|
+
|
3
|
+
# This continues definining the DiceBag module.
|
1
4
|
module DiceBag
|
5
|
+
# This is the Transform subclass that takes the
|
6
|
+
# parsed tree and transforms it into its (almost)
|
7
|
+
# final form. (It gets a normalization pass later.)
|
2
8
|
class Transform < Parslet::Transform
|
3
|
-
|
4
|
-
def Transform.hashify_options(options)
|
9
|
+
def self.hashify_options(options)
|
5
10
|
opts = {}
|
6
|
-
|
7
|
-
|
11
|
+
|
12
|
+
options.each { |opt, val| opts[opt] = val } if options.is_a?(Array)
|
13
|
+
|
14
|
+
opts
|
8
15
|
end
|
9
16
|
|
10
17
|
# Option transforms. These are turned into an array of
|
11
18
|
# 2-element arrays ('tagged arrays'), which is then
|
12
|
-
# hashified later. (There is no way to update the
|
19
|
+
# hashified later. (There is no way to update the
|
13
20
|
# options when these rules are matched.)
|
14
|
-
rule(:
|
15
|
-
rule(:
|
16
|
-
rule(:
|
17
|
-
rule(:
|
18
|
-
|
21
|
+
rule(drop: simple(:x)) { [:drop, Integer(x)] }
|
22
|
+
rule(keep: simple(:x)) { [:keep, Integer(x)] }
|
23
|
+
rule(reroll: simple(:x)) { [:reroll, Integer(x)] }
|
24
|
+
rule(target: simple(:x)) { [:target, Integer(x)] }
|
25
|
+
|
19
26
|
# Explode is special, in that if it is nil, then it
|
20
27
|
# must remain that way.
|
21
|
-
rule(:
|
22
|
-
|
28
|
+
rule(explode: simple(:x)) do
|
29
|
+
[:explode, (x ? Integer(x) : nil)]
|
23
30
|
end
|
24
31
|
|
25
32
|
# Parts {:ops => (:xdx | :number)}
|
@@ -27,52 +34,54 @@ module DiceBag
|
|
27
34
|
# be matched before the xdx subtree.
|
28
35
|
|
29
36
|
# Match an operator followed by a static number.
|
30
|
-
|
37
|
+
# TODO: find out why this is not matching simple
|
38
|
+
# op => integers! -- 2016-04-18
|
39
|
+
rule(op: simple(:o), value: simple(:v)) do
|
31
40
|
[String(o), Integer(v)]
|
32
41
|
end
|
33
42
|
|
34
43
|
# Match an operator followed by an :xdx subtree.
|
35
|
-
rule(:
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
44
|
+
rule(op: simple(:o), value: subtree(:part)) do
|
45
|
+
value = if part.is_a? Hash
|
46
|
+
count = Integer(part[:xdx][:count])
|
47
|
+
sides = Integer(part[:xdx][:sides])
|
48
|
+
options = Transform.hashify_options(part[:options])
|
49
|
+
|
50
|
+
{ xdx: { count: count, sides: sides }, options: options }
|
51
|
+
else
|
52
|
+
Integer(part)
|
53
|
+
end
|
54
|
+
|
55
|
+
[String(o), value]
|
45
56
|
end
|
46
57
|
|
47
58
|
# Match a label by itself.
|
48
|
-
rule(:
|
59
|
+
rule(label: simple(:s)) { { label: String(s) } }
|
49
60
|
|
50
61
|
# Match a label followed by a :start subtree.
|
51
|
-
rule(:
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
}
|
58
|
-
}
|
59
|
-
]
|
62
|
+
rule(label: simple(:s), start: subtree(:part)) do
|
63
|
+
label = String(s)
|
64
|
+
xdx = part[:xdx]
|
65
|
+
options = Transform.hashify_options(part[:options])
|
66
|
+
|
67
|
+
[{ label: label }, { start: { xdx: xdx, options: options } }]
|
60
68
|
end
|
61
69
|
|
62
70
|
# Match a :start subtree, with the label not present.
|
63
71
|
# Note that this returns a hash, but the final output
|
64
72
|
# will still be in an array.
|
65
|
-
rule(:
|
66
|
-
{
|
67
|
-
:
|
68
|
-
|
73
|
+
rule(start: subtree(:part)) do
|
74
|
+
{
|
75
|
+
start: {
|
76
|
+
xdx: part[:xdx],
|
77
|
+
options: Transform.hashify_options(part[:options])
|
69
78
|
}
|
70
79
|
}
|
71
80
|
end
|
72
81
|
|
73
82
|
# Convert the count and sides of an :xdx part.
|
74
|
-
rule(:
|
75
|
-
{ :
|
83
|
+
rule(count: simple(:c), sides: simple(:s)) do
|
84
|
+
{ count: Integer(c), sides: Integer(s) }
|
76
85
|
end
|
77
86
|
end
|
78
87
|
end
|
data/lib/dicebag.rb
CHANGED
@@ -1,31 +1,33 @@
|
|
1
1
|
# Copyright (c) 2012 Randy Carnahan <syn at dragonsbait dot com>
|
2
2
|
#
|
3
|
-
# Permission is hereby granted, free of charge, to any person obtaining
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
4
|
# a copy of this software and associated documentation files (the "Software"),
|
5
|
-
# to deal in the Software without restriction, including without limitation
|
5
|
+
# to deal in the Software without restriction, including without limitation
|
6
6
|
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
7
7
|
# and/or sell copies of the Software, and to permit persons to whom the
|
8
8
|
# Software is furnished to do so, subject to the following conditions:
|
9
9
|
#
|
10
|
-
# The above copyright notice and this permission notice shall be included
|
10
|
+
# The above copyright notice and this permission notice shall be included
|
11
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
|
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
19
|
# USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
20
|
#
|
21
|
-
# dicelib.rb -- version: 3.
|
21
|
+
# dicelib.rb -- version: 3.2.0
|
22
22
|
|
23
23
|
require 'parslet'
|
24
24
|
|
25
|
+
# This defined the main DiceBag module.
|
25
26
|
module DiceBag
|
27
|
+
DEFAULT_ROLL = '1d6'.freeze
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
+
# This is our generic DiceBagError
|
30
|
+
# exception subclass.
|
29
31
|
class DiceBagError < Exception; end
|
30
32
|
|
31
33
|
###
|
@@ -38,148 +40,160 @@ module DiceBag
|
|
38
40
|
# happens in the Roll class. It will convert all
|
39
41
|
# values into the correct *Part class.
|
40
42
|
def self.normalize_tree(tree)
|
41
|
-
|
42
|
-
|
43
|
+
tree.collect do |part|
|
43
44
|
case part
|
44
45
|
when Hash
|
45
|
-
|
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
|
-
|
46
|
+
normalize_hash part
|
52
47
|
when Array
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
# (This should only happen on :start arrays.)
|
57
|
-
|
58
|
-
op = part.first
|
59
|
-
val = part.last
|
60
|
-
|
61
|
-
op = case op
|
62
|
-
when "+" then :add
|
63
|
-
when "-" then :sub
|
64
|
-
when "*" then :mul
|
65
|
-
when "/" then :div
|
66
|
-
end
|
67
|
-
|
68
|
-
# If the value is a hash, it's an :xdx hash.
|
69
|
-
# Normalize it.
|
70
|
-
if val.is_a?(Hash)
|
71
|
-
xdx = normalize_xdx(val)
|
72
|
-
val = RollPart.new(xdx)
|
73
|
-
else
|
74
|
-
val = StaticPart.new(val)
|
75
|
-
end
|
76
|
-
|
77
|
-
part = [op, val]
|
48
|
+
normalize_array part
|
49
|
+
else
|
50
|
+
part
|
78
51
|
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.normalize_hash(part)
|
56
|
+
return [:label, LabelPart.new(part[:label])] if part.key? :label
|
57
|
+
|
58
|
+
if part.key? :start
|
59
|
+
xdx = normalize_xdx(part[:start])
|
60
|
+
|
61
|
+
return [:start, RollPart.new(xdx)]
|
62
|
+
end
|
63
|
+
|
64
|
+
part
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.normalize_array(part)
|
68
|
+
[normalize_op(part.first), normalize_value(part.last)]
|
69
|
+
end
|
79
70
|
|
80
|
-
|
71
|
+
def self.normalize_op(op)
|
72
|
+
# We swap out the strings for symbols.
|
73
|
+
# If the op is not one of the arithimetic
|
74
|
+
# operators, then the op itself is returned.
|
75
|
+
# (This should only happen on :start arrays.)
|
76
|
+
case op
|
77
|
+
when '+' then :add
|
78
|
+
when '-' then :sub
|
79
|
+
when '*' then :mul
|
80
|
+
when '/' then :div
|
81
|
+
else
|
82
|
+
op
|
81
83
|
end
|
82
84
|
end
|
83
85
|
|
86
|
+
def self.normalize_value(val)
|
87
|
+
val.is_a?(Hash) ? RollPart.new(normalize_xdx(val)) : StaticPart.new(val)
|
88
|
+
end
|
89
|
+
|
84
90
|
# This further massages the xDx hashes.
|
85
91
|
def self.normalize_xdx(xdx)
|
86
92
|
count = xdx[:xdx][:count]
|
87
93
|
sides = xdx[:xdx][:sides]
|
88
|
-
|
94
|
+
|
95
|
+
# Delete the no longer needed :xdx key.
|
96
|
+
xdx.delete(:xdx)
|
89
97
|
|
90
98
|
# Default to at least 1 die.
|
91
|
-
count = 1 if count.zero?
|
99
|
+
count = 1 if count.zero? || count.nil?
|
92
100
|
|
93
101
|
# Set the :count and :sides keys directly
|
94
|
-
# and
|
102
|
+
# and set the notes array.
|
95
103
|
xdx[:count] = count
|
96
104
|
xdx[:sides] = sides
|
97
|
-
xdx
|
105
|
+
xdx[:notes] = []
|
106
|
+
|
107
|
+
normalize_options xdx
|
108
|
+
end
|
98
109
|
|
110
|
+
def self.normalize_options(xdx)
|
99
111
|
if xdx[:options].empty?
|
100
112
|
xdx.delete(:options)
|
101
113
|
else
|
102
|
-
|
114
|
+
normalize_explode xdx
|
115
|
+
normalize_reroll xdx
|
116
|
+
normalize_drop_keep xdx
|
117
|
+
normalize_target xdx
|
118
|
+
end
|
103
119
|
|
104
|
-
|
105
|
-
|
106
|
-
explode = xdx[:options][:explode]
|
120
|
+
xdx
|
121
|
+
end
|
107
122
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
end
|
112
|
-
end
|
123
|
+
# Prevent Explosion abuse.
|
124
|
+
def self.normalize_explode(xdx)
|
125
|
+
return unless xdx[:options].key? :explode
|
113
126
|
|
114
|
-
|
115
|
-
if xdx[:options].has_key?(:reroll) and xdx[:options][:reroll] >= sides
|
116
|
-
xdx[:options][:reroll] = 0
|
117
|
-
notes.push("Reroll reset to 0.")
|
118
|
-
end
|
127
|
+
explode = xdx[:options][:explode]
|
119
128
|
|
120
|
-
|
121
|
-
|
122
|
-
# If not, both are reset to 0. Harsh.
|
123
|
-
drop = xdx[:options][:drop] || 0
|
124
|
-
keep = xdx[:options][:keep] || 0
|
129
|
+
if explode.nil? || explode.zero? || explode == 1
|
130
|
+
xdx[:options][:explode] = xdx[:sides]
|
125
131
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
notes.push("Drop and Keep Conflict. Both reset to 0.")
|
130
|
-
end
|
132
|
+
xdx[:notes].push("Explode set to #{xdx[:sides]}")
|
133
|
+
end
|
134
|
+
end
|
131
135
|
|
132
|
-
|
133
|
-
|
136
|
+
# Prevent Reroll abuse.
|
137
|
+
def self.normalize_reroll(xdx)
|
138
|
+
return unless xdx[:options].key? :reroll
|
134
139
|
|
135
|
-
|
136
|
-
|
137
|
-
# set it to 0 (aka no target number) and add a note.
|
138
|
-
if xdx[:options].has_key?(:target)
|
139
|
-
target = xdx[:options][:target]
|
140
|
+
if xdx[:options][:reroll] >= xdx[:sides]
|
141
|
+
xdx[:options][:reroll] = 0
|
140
142
|
|
141
|
-
|
142
|
-
xdx[:options][:target] = 0
|
143
|
-
notes.push("Target number too large or is negative; reset to 0.")
|
144
|
-
end
|
145
|
-
end
|
143
|
+
xdx[:notes].push 'Reroll reset to 0.'
|
146
144
|
end
|
145
|
+
end
|
147
146
|
|
148
|
-
|
147
|
+
# Make sure there are enough dice to
|
148
|
+
# handle both Drop and Keep values.
|
149
|
+
# If not, both are reset to 0. Harsh.
|
150
|
+
def self.normalize_drop_keep(xdx)
|
151
|
+
drop = xdx[:options].fetch(:drop, 0)
|
152
|
+
keep = xdx[:options].fetch(:keep, 0)
|
149
153
|
|
150
|
-
|
154
|
+
if (drop + keep) >= xdx[:count]
|
155
|
+
xdx[:options][:drop] = 0
|
156
|
+
xdx[:options][:keep] = 0
|
157
|
+
|
158
|
+
xdx[:notes].push 'Drop and Keep Conflict. Both reset to 0.'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Finally, if we have a target number, make sure it is equal
|
163
|
+
# to or less than the dice sides and greater than 0, otherwise,
|
164
|
+
# set it to 0 (aka no target number) and add a note.
|
165
|
+
def self.normalize_target(xdx)
|
166
|
+
return unless xdx[:options].key? :target
|
167
|
+
|
168
|
+
target = xdx[:options][:target]
|
169
|
+
|
170
|
+
return if target >= 0 && target <= xdx[:sides]
|
171
|
+
|
172
|
+
xdx[:options][:target] = 0
|
173
|
+
|
174
|
+
xdx[:notes].push 'Target number too large or is negative; reset to 0.'
|
151
175
|
end
|
152
176
|
|
153
177
|
# This is the wrapper for the parse, transform,
|
154
178
|
# and normalize calls. This is called by the Roll
|
155
179
|
# class, but may be called to get the raw returned
|
156
180
|
# array of parsed bits for other purposes.
|
157
|
-
def self.parse(dstr=
|
158
|
-
|
159
|
-
|
160
|
-
ast = Transform.new.apply(tree)
|
161
|
-
|
162
|
-
# Sometimes, we get a hash back, so wrap it as
|
163
|
-
# a single element array.
|
164
|
-
ast = [ast] unless ast.is_a?(Array)
|
165
|
-
|
166
|
-
return normalize_tree(ast)
|
167
|
-
|
168
|
-
rescue Parslet::ParseFailed => reason
|
169
|
-
# We're merely re-wrapping the error here to
|
170
|
-
# hide implementation from user who doesn't care
|
171
|
-
# to read the source.
|
172
|
-
raise DiceBagError, "Dice Parse Error for string: #{dstr}"
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
require 'dicebag/parser'
|
178
|
-
require 'dicebag/transform'
|
179
|
-
require 'dicebag/simple_part'
|
180
|
-
require 'dicebag/label_part'
|
181
|
-
require 'dicebag/static_part'
|
182
|
-
require 'dicebag/roll_part'
|
183
|
-
require 'dicebag/roll'
|
184
|
-
require 'dicebag/result'
|
181
|
+
def self.parse(dstr = '')
|
182
|
+
tree = Parser.new.parse(dstr)
|
183
|
+
ast = Transform.new.apply(tree)
|
185
184
|
|
185
|
+
normalize_tree ast
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Our sub-modules.
|
190
|
+
require_relative './dicebag/roll_string'
|
191
|
+
require_relative './dicebag/roll_part_string'
|
192
|
+
require_relative './dicebag/parser'
|
193
|
+
require_relative './dicebag/transform'
|
194
|
+
require_relative './dicebag/simple_part'
|
195
|
+
require_relative './dicebag/label_part'
|
196
|
+
require_relative './dicebag/static_part'
|
197
|
+
require_relative './dicebag/roll_part'
|
198
|
+
require_relative './dicebag/roll'
|
199
|
+
require_relative './dicebag/result'
|
metadata
CHANGED
@@ -1,30 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dicebag
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
5
|
-
prerelease:
|
4
|
+
version: 3.2.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- SynTruth
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2016-04-18 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: parslet
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - ">="
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: 1.4.0
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- -
|
24
|
+
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: 1.4.0
|
30
27
|
description: A very flexible dice rolling library for Ruby.
|
@@ -33,37 +30,38 @@ executables: []
|
|
33
30
|
extensions: []
|
34
31
|
extra_rdoc_files: []
|
35
32
|
files:
|
33
|
+
- lib/dicebag.rb
|
36
34
|
- lib/dicebag/label_part.rb
|
37
35
|
- lib/dicebag/parser.rb
|
38
36
|
- lib/dicebag/result.rb
|
39
37
|
- lib/dicebag/roll.rb
|
40
38
|
- lib/dicebag/roll_part.rb
|
39
|
+
- lib/dicebag/roll_part_string.rb
|
40
|
+
- lib/dicebag/roll_string.rb
|
41
41
|
- lib/dicebag/simple_part.rb
|
42
42
|
- lib/dicebag/static_part.rb
|
43
43
|
- lib/dicebag/transform.rb
|
44
|
-
- lib/dicebag.rb
|
45
44
|
homepage: https://github.com/syntruth/Dice-Bag
|
46
45
|
licenses: []
|
46
|
+
metadata: {}
|
47
47
|
post_install_message:
|
48
48
|
rdoc_options: []
|
49
49
|
require_paths:
|
50
50
|
- lib
|
51
51
|
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
-
none: false
|
53
52
|
requirements:
|
54
|
-
- -
|
53
|
+
- - ">="
|
55
54
|
- !ruby/object:Gem::Version
|
56
55
|
version: '0'
|
57
56
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
-
none: false
|
59
57
|
requirements:
|
60
|
-
- -
|
58
|
+
- - ">="
|
61
59
|
- !ruby/object:Gem::Version
|
62
60
|
version: '0'
|
63
61
|
requirements: []
|
64
62
|
rubyforge_project:
|
65
|
-
rubygems_version:
|
63
|
+
rubygems_version: 2.4.5.1
|
66
64
|
signing_key:
|
67
|
-
specification_version:
|
68
|
-
summary:
|
65
|
+
specification_version: 4
|
66
|
+
summary: 'Dice Bag: Ruby Dice Rolling Library'
|
69
67
|
test_files: []
|