dicebag 3.1.0 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|