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 +5 -5
- data/bin/dicebag +47 -0
- data/lib/dicebag/label_part.rb +4 -2
- data/lib/dicebag/normalize.rb +165 -0
- data/lib/dicebag/parser.rb +67 -41
- data/lib/dicebag/result.rb +14 -7
- data/lib/dicebag/roll.rb +18 -14
- data/lib/dicebag/roll_part.rb +111 -41
- data/lib/dicebag/roll_part_string.rb +25 -10
- data/lib/dicebag/roll_string.rb +11 -10
- data/lib/dicebag/simple_part.rb +7 -4
- data/lib/dicebag/static_part.rb +9 -4
- data/lib/dicebag/systems/dnd.rb +65 -0
- data/lib/dicebag/systems/fudge.rb +80 -0
- data/lib/dicebag/systems/gurps.rb +52 -0
- data/lib/dicebag/systems/savage_worlds.rb +31 -0
- data/lib/dicebag/systems/standard.rb +19 -0
- data/lib/dicebag/systems/storyteller.rb +45 -0
- data/lib/dicebag/transform.rb +58 -47
- data/lib/dicebag.rb +16 -166
- metadata +18 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b36691cc7b34c8637154bac79f610b1867d55902b904e83b390b12ae1e6ebf37
|
4
|
+
data.tar.gz: d3fb57f5f563a1e599beb75831953ff3e8bc6dd2e60fcc82aee66a609ac96b1a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/dicebag/label_part.rb
CHANGED
@@ -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
|
data/lib/dicebag/parser.rb
CHANGED
@@ -1,61 +1,87 @@
|
|
1
|
-
# Encoding: UTF-8
|
2
|
-
|
3
1
|
module DiceBag
|
4
|
-
# This class parses the dice string into the individual
|
5
|
-
#
|
6
|
-
#
|
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.
|
12
|
-
#
|
13
|
-
#
|
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
|
-
#
|
19
|
-
# are not allowed to have
|
20
|
-
# This for future use of parsing multiple dice
|
21
|
-
# definitions in comma-separated strings.
|
22
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
46
|
-
#
|
47
|
-
# This is handled in
|
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
|
-
|
50
|
-
|
51
|
-
rule(:
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
#
|
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
|
-
|
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
|
-
#
|
73
|
-
# string or a
|
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
|
-
#
|
80
|
-
#
|
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)
|
data/lib/dicebag/result.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
# Encoding: UTF-8
|
2
|
-
|
3
1
|
module DiceBag
|
4
|
-
# This class merely encapsulates the result,
|
5
|
-
#
|
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|
|
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
|
-
#
|
6
|
-
#
|
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 ||=
|
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
|
-
|
20
|
+
arr = []
|
21
|
+
fmt = "For %s: %s\n"
|
24
22
|
|
25
23
|
tree.each do |_op, part|
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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(
|
66
|
-
case
|
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
|