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