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.
@@ -1,5 +1,4 @@
1
- # Encoding: UTF-8
2
-
1
+ # DiceBag Module
3
2
  module DiceBag
4
3
  # This represents the xDx part of the dice string.
5
4
  class RollPart < SimplePart
@@ -10,42 +9,49 @@ module DiceBag
10
9
  attr_reader :parts
11
10
  attr_reader :options
12
11
  attr_reader :tally
12
+ attr_reader :reroll_count
13
13
 
14
14
  def initialize(part)
15
- @total = nil
16
- @tally = []
17
- @value = part
18
- @count = part[:count]
19
- @sides = part[:sides]
20
- @notes = part[:notes] || []
15
+ super part
21
16
 
22
- # Our Default Options
23
- @options = { explode: 0, drop: 0, keep: 0, reroll: 0, target: 0 }
17
+ @total = nil
18
+ @tally = []
19
+ @count = part[:count]
20
+ @sides = part[:sides]
21
+ @notes = part[:notes]
22
+ @options = default_options
24
23
 
25
24
  @options.update(part[:options]) if part.key?(:options)
26
25
  end
27
26
 
27
+ # Our Default Options
28
+ #
29
+ # Note the absence of :explode, that is handled below.
30
+ def default_options
31
+ {
32
+ drop: 0,
33
+ keep: 0,
34
+ keeplowest: 0,
35
+ reroll: 0,
36
+ target: 0,
37
+ failure: 0
38
+ }
39
+ end
40
+
28
41
  def notes
29
42
  @notes.empty? ? '' : @notes.join("\n")
30
43
  end
31
44
 
32
- # Checks to see if this instance has rolled yet
33
- # or not.
45
+ # Checks to see if this instance has rolled yet or not.
34
46
  def rolled?
35
47
  @total.nil? ? false : true
36
48
  end
37
49
 
38
- # Rolls a single die from the xDx string.
39
- def roll_die
40
- num = 0
41
- num = rand(sides) + 1 while num <= @options[:reroll]
42
-
43
- num
44
- end
45
-
46
50
  def roll
47
51
  generate_results
48
52
 
53
+ return __roll_for_keep_lowest if @options[:keeplowest].positive?
54
+
49
55
  @results.sort!
50
56
  @results.reverse!
51
57
 
@@ -60,12 +66,21 @@ module DiceBag
60
66
 
61
67
  # Set the total.
62
68
  handle_total
69
+ end
63
70
 
64
- self
71
+ def __roll_for_keep_lowest
72
+ @tally = @results.dup
73
+
74
+ @tally.sort!
75
+ @tally.reverse!
76
+ @results.sort!
77
+
78
+ handle_keeplowest
79
+ handle_total
65
80
  end
66
81
 
67
- # Gets the total of the last roll; if there is no
68
- # last roll, it calls roll() first.
82
+ # Gets the total of the last roll; if there is no last roll, it
83
+ # calls roll() first.
69
84
  def total
70
85
  roll if @total.nil?
71
86
 
@@ -81,25 +96,43 @@ module DiceBag
81
96
  def generate_results
82
97
  @results = []
83
98
 
99
+ explode = @options.key?(:explode)
100
+
84
101
  count.times do
85
- r = roll_die
102
+ roll = roll_die
86
103
 
87
- @results.push(r)
104
+ @results.push(roll)
88
105
 
89
- handle_explode(r) unless @options[:explode].zero?
106
+ handle_explode(roll) if explode
90
107
  end
91
108
  end
92
109
 
93
- def handle_explode(r)
94
- while r >= @options[:explode]
95
- r = roll_die
110
+ # Rolls a single die from the xDx string.
111
+ def roll_die
112
+ num = __roll_die
96
113
 
97
- @results.push(r)
114
+ # Handle Reroll
115
+ if options[:reroll].positive?
116
+ num = __roll_die while num <= @options[:reroll]
117
+ end
118
+
119
+ num
120
+ end
121
+
122
+ def handle_explode(roll)
123
+ # If the explode value is nil (allowed!) then default to the
124
+ # number of sides.
125
+ val = @options[:explode] || sides
126
+
127
+ while roll >= val
128
+ roll = roll_die
129
+
130
+ @results.push(roll)
98
131
  end
99
132
  end
100
133
 
101
134
  def handle_drop
102
- return unless @options[:drop] > 0
135
+ return unless @options[:drop].positive?
103
136
 
104
137
  # Note that we invert the drop value here.
105
138
  range = 0...-(@options[:drop])
@@ -108,23 +141,60 @@ module DiceBag
108
141
  end
109
142
 
110
143
  def handle_keep
111
- return unless @options[:keep] > 0
144
+ return unless @options[:keep].positive?
112
145
 
113
146
  range = 0...@options[:keep]
114
147
 
115
148
  @results = @results.slice range
116
149
  end
117
150
 
151
+ def handle_keeplowest
152
+ return unless @options[:keeplowest].positive?
153
+
154
+ range = 0...@options[:keeplowest]
155
+
156
+ @results = @results.slice range
157
+ end
158
+
159
+ # If we have a target number, count how many rolls in the results
160
+ # are >= than this number and subtract the number <= the failure
161
+ # threshold, otherwise we just add up all the numbers.
118
162
  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
163
+ tpos = @options[:target].positive?
164
+ fpos = @options[:failure].positive?
165
+
166
+ # Just add up the results.
167
+ return __simple_total unless tpos || fpos
168
+
169
+ # Add up successes and subtract failures.
170
+ return __target_and_failure_total if tpos
171
+
172
+ # Just tally failures.
173
+ @total = 0 - __failure_total
174
+ end
175
+
176
+ def __roll_die
177
+ rand(sides) + 1
178
+ end
179
+
180
+ def __simple_total
181
+ # I think reduce(:+) is ugly, but it's very fast.
182
+ @total = @results.reduce(:+)
183
+ end
184
+
185
+ def __target_and_failure_total
186
+ tcount = __target_total
187
+ fcount = __failure_total
188
+
189
+ @total = tcount - fcount
190
+ end
191
+
192
+ def __target_total
193
+ @results.count { |r| r >= @options[:target] }
194
+ end
195
+
196
+ def __failure_total
197
+ @results.count { |r| r <= @options[:failure] }
128
198
  end
129
199
  end
130
200
  end
@@ -1,11 +1,8 @@
1
- # Encoding: UTF-8
2
-
3
- # This encapsulates the RollPart string
4
- # generation methods.
1
+ # This encapsulates the RollPart string generation methods.
5
2
  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.
3
+ # This takes the @parts hash and recreates the xDx string. Optionally,
4
+ # passing true to the method will remove spaces from the finished
5
+ # string.
9
6
  def to_s(no_spaces = false)
10
7
  @parts = []
11
8
 
@@ -13,14 +10,20 @@ module RollPartString
13
10
  to_s_explode
14
11
  to_s_drop
15
12
  to_s_keep
13
+ to_s_keeplowest
16
14
  to_s_reroll
17
15
  to_s_target
16
+ to_s_failure
18
17
 
19
18
  join_str = no_spaces ? '' : ' '
20
19
 
21
20
  @parts.join join_str
22
21
  end
23
22
 
23
+ def inspect
24
+ "<#{self.class.name} #{self}>"
25
+ end
26
+
24
27
  private
25
28
 
26
29
  def to_s_xdx
@@ -31,11 +34,11 @@ module RollPartString
31
34
  end
32
35
 
33
36
  def to_s_explode
34
- return if @options[:explode].zero?
37
+ return unless @options.key?(:explode)
35
38
 
36
- e = (@options[:explode] == sides) ? @options[:explode] : ''
39
+ e = @options[:explode].nil? ? 'e' : format('e%s', @options[:explode])
37
40
 
38
- @parts.push format('e%s', e)
41
+ @parts.push e
39
42
  end
40
43
 
41
44
  def to_s_drop
@@ -50,6 +53,12 @@ module RollPartString
50
53
  @parts.push format('k%s', @options[:keep])
51
54
  end
52
55
 
56
+ def to_s_keeplowest
57
+ return if @options[:keeplowest].zero?
58
+
59
+ @parts.push format('kl%s', @options[:keeplowest])
60
+ end
61
+
53
62
  def to_s_reroll
54
63
  return if @options[:reroll].zero?
55
64
 
@@ -61,4 +70,10 @@ module RollPartString
61
70
 
62
71
  @parts.push format('t%s', @options[:target])
63
72
  end
73
+
74
+ def to_s_failure
75
+ return if @options[:failure].zero?
76
+
77
+ @parts.push format('f%s', @options[:failure])
78
+ end
64
79
  end
@@ -1,7 +1,4 @@
1
- # Encoding: UTF-8
2
-
3
- # This encapsulates the Roll class' string
4
- # generation methods.
1
+ # This encapsulates the Roll class' string generation methods.
5
2
  module RollString
6
3
  def to_s(no_spaces = false)
7
4
  @parts = []
@@ -13,6 +10,10 @@ module RollString
13
10
  no_spaces ? str.tr(' ', '') : str
14
11
  end
15
12
 
13
+ def inspect
14
+ "<#{self.class.name} #{self}>"
15
+ end
16
+
16
17
  private
17
18
 
18
19
  def to_s_tree
@@ -30,22 +31,22 @@ module RollString
30
31
  end
31
32
 
32
33
  def to_s_add(value)
33
- _op_value '+', value
34
+ __op_value '+', value
34
35
  end
35
36
 
36
37
  def to_s_sub(value)
37
- _op_value '-', value
38
+ __op_value '-', value
38
39
  end
39
40
 
40
41
  def to_s_mul(value)
41
- _op_value '*', value
42
+ __op_value '*', value
42
43
  end
43
44
 
44
45
  def to_s_div(value)
45
- _op_value '/', value
46
+ __op_value '/', value
46
47
  end
47
48
 
48
- def _op_value(op, value)
49
- "#{op}#{value}"
49
+ def __op_value(oper, value)
50
+ "#{oper}#{value}"
50
51
  end
51
52
  end
@@ -1,8 +1,7 @@
1
1
  module DiceBag
2
- # The most simplest of a part. If a given part of
3
- # a dice string is not a Label, Fixnum, or a xDx part
4
- # it will be an instance of this class, which simply
5
- # returns the value given to it.
2
+ # The most simplest of a part. If a given part of a dice string is not
3
+ # a Label, Fixnum, or a xDx part it will be an instance of this class,
4
+ # which simply returns the value given to it.
6
5
  class SimplePart
7
6
  attr_reader :value
8
7
 
@@ -17,5 +16,9 @@ module DiceBag
17
16
  def to_s
18
17
  value
19
18
  end
19
+
20
+ def inspect
21
+ "<#{self.class.name} #{self}>"
22
+ end
20
23
  end
21
24
  end
@@ -1,10 +1,11 @@
1
+ # DiceBag module
1
2
  module DiceBag
2
- # This represents a static, non-random number part
3
- # of the dice string.
3
+ # This represents a static, non-random number part of the dice string.
4
4
  class StaticPart < SimplePart
5
5
  def initialize(num)
6
- num = num.to_i if num.is_a?(String)
7
- @value = num
6
+ num = num.to_i if num.is_a?(String)
7
+
8
+ super num
8
9
  end
9
10
 
10
11
  def total
@@ -14,5 +15,9 @@ module DiceBag
14
15
  def to_s
15
16
  value.to_s
16
17
  end
18
+
19
+ def inspect
20
+ "<#{self.class.name} #{self}>"
21
+ end
17
22
  end
18
23
  end
@@ -0,0 +1,65 @@
1
+ # This models a D20 roll used in various D&D versions for rolling a d20
2
+ # +/-mod to equal or exceed a DC value.
3
+ #
4
+ # This is a very simple version, but could easily be expanded on.
5
+ #
6
+ # Usage:
7
+ #
8
+ # die = D20.new
9
+ #
10
+ # die.roll 5, 15 => [:success, 17]
11
+ # die.roll -3, 12 => [:fail, 9]
12
+ class D20
13
+ attr_reader :mod
14
+ attr_reader :dc
15
+
16
+ def self.roll(mod = 0, difficulty = 10)
17
+ new(mod, difficulty).roll
18
+ end
19
+
20
+ def initialize(mod = 0, difficulty = 10)
21
+ @mod = mod.to_i
22
+ @dc = difficulty.to_i
23
+ @dstr = "1d20 #{stringify_mod}"
24
+ @roll = DiceBag::Roll.new @dstr
25
+ end
26
+
27
+ def roll
28
+ total = roll_for_result.total
29
+ sym = total >= dc ? :success : :failure
30
+
31
+ [sym, total]
32
+ end
33
+
34
+ def stringify_mod
35
+ return "+#{@mod}" if @mod.positive?
36
+
37
+ return @mod.to_s if @mod.negative?
38
+
39
+ ''
40
+ end
41
+
42
+ def roll_for_result
43
+ @roll.roll
44
+ end
45
+ end
46
+
47
+ # Roll a d20 with Advantage
48
+ class D20Advantage < D20
49
+ def roll_for_result
50
+ r1 = @roll.roll
51
+ r2 = @roll.roll
52
+
53
+ r1 > r2 ? r1 : r2
54
+ end
55
+ end
56
+
57
+ # Roll a d20 with Disadvantage
58
+ class D20Disadvantage < D20
59
+ def roll_for_result
60
+ r1 = @roll.roll
61
+ r2 = @roll.roll
62
+
63
+ r1 < r2 ? r1 : r2
64
+ end
65
+ end
@@ -0,0 +1,80 @@
1
+ require 'dicebag'
2
+
3
+ # This handles Fudge RPG type of rolls.
4
+ #
5
+ # This probably isn't (it *totally* isn't!) the most effecient way to do
6
+ # this, but shows how to examine DiceBag objects to get what you need
7
+ # for wonky dice systems.
8
+ module Fudge
9
+ # This models a standard Fudge RPG dice pool.
10
+ class Roll < DiceBag::Roll
11
+ def initialize(number = 4)
12
+ @number = number
13
+ @total = nil
14
+ @tally = nil
15
+
16
+ # This is a very silly way to do this, since there is no need to
17
+ # actually add the dice together here. But we need all of the d6's
18
+ # together in the same roll.
19
+ dstr = (['1d6'] * number).join(' + ')
20
+
21
+ super(dstr)
22
+ end
23
+
24
+ def roll
25
+ super
26
+
27
+ generate_tally
28
+
29
+ @total = @tally.count('+') - @tally.count('-')
30
+
31
+ [@total, tally_to_s]
32
+ end
33
+
34
+ def total
35
+ roll unless @total
36
+
37
+ @total
38
+ end
39
+
40
+ def tally
41
+ roll unless @tally
42
+
43
+ @tally
44
+ end
45
+
46
+ def to_s
47
+ base = "#{@number}dF"
48
+
49
+ "#{base} #{tally_to_s} => #{@total}" if @total
50
+
51
+ base
52
+ end
53
+
54
+ private
55
+
56
+ def generate_tally
57
+ @tally = @sections.map { |s| gen_symbol s.total }.sort.reverse
58
+ end
59
+
60
+ def gen_symbol(total)
61
+ case total
62
+ when 1, 2 then '-'
63
+ when 3, 4 then ' '
64
+ when 5, 6 then '+'
65
+ end
66
+ end
67
+
68
+ def tally_to_s
69
+ return '[]' unless @tally
70
+
71
+ "[#{@tally.join('][')}]"
72
+ end
73
+ end
74
+
75
+ DF = Roll.new
76
+
77
+ def self.roll(num = 4)
78
+ Roll.new(num).roll
79
+ end
80
+ end
@@ -0,0 +1,52 @@
1
+ require 'dicebag'
2
+
3
+ # This models the standard GURPS 3d6 dice pool used for attribute/skill
4
+ # tests.
5
+ #
6
+ # This will return an array of [status, result] where:
7
+ # - status is one of:
8
+ # :success, :failure, :critical_success, or :critical_failure
9
+ # - result is the actual dice roll total.
10
+ class GURPS < DiceBag::Roll
11
+ def self.roll(target, mod = 0)
12
+ new.roll(target, mod)
13
+ end
14
+
15
+ def initialize
16
+ super('3d6')
17
+ end
18
+
19
+ def roll(target, mod = 0)
20
+ mod = 0 unless mod.is_a?(Integer)
21
+
22
+ @total_target = target + mod
23
+ @total = super().total
24
+
25
+ figure_success
26
+ figure_failure
27
+
28
+ [figure_result, @total]
29
+ end
30
+
31
+ private
32
+
33
+ def figure_success
34
+ @crit_success = [3, 4]
35
+
36
+ @crit_success.push(5) if @total_target >= 15
37
+ @crit_success.push(6) if @total_target >= 16
38
+ end
39
+
40
+ def figure_failure
41
+ @crit_failure = [18]
42
+
43
+ @crit_failure.push(17) if @total_target <= 15
44
+ end
45
+
46
+ def figure_result
47
+ return :critical_success if @crit_success.include?(@total)
48
+ return :critical_failure if @crit_failure.include?(@total)
49
+
50
+ @total <= @total_target ? :success : :failure
51
+ end
52
+ end
@@ -0,0 +1,31 @@
1
+ require 'dicebag'
2
+
3
+ module SavageWorlds
4
+ # Models a single Savage World die, with attributes for max and half
5
+ # values.
6
+ #
7
+ # Since all Savage Worlds Trait (Attributes + Skills) dice can
8
+ # explode, that option is included automatically.
9
+ class SWDie < DiceBag::Roll
10
+ attr_reader :maximum
11
+ attr_reader :half
12
+
13
+ def initialize(sides, mod = 0)
14
+ @maximum = sides
15
+ @half = (sides / 2) + (mod / 2)
16
+
17
+ dstr = mod.zero? ? "1d#{sides}e" : "1d#{sides}e #{mod}"
18
+
19
+ super(dstr)
20
+ end
21
+ end
22
+
23
+ D4 = SWDie.new(4)
24
+ D6 = SWDie.new(6)
25
+ D8 = SWDie.new(8)
26
+ D10 = SWDie.new(10)
27
+ D12 = SWDie.new(12)
28
+ WildDie = SWDie.new(6)
29
+ NoTrait = SWDie.new(4, -2)
30
+ NoTraitWildDie = SWDie.new(6, -2)
31
+ end
@@ -0,0 +1,19 @@
1
+ require 'dicebag'
2
+
3
+ # Module for standard polyhedral dice types.
4
+ module Standard
5
+ # Models a single, simple die.
6
+ class Die < DiceBag::Roll
7
+ def initialize(sides)
8
+ super("1d#{sides}")
9
+ end
10
+ end
11
+
12
+ D4 = Die.new(4)
13
+ D6 = Die.new(6)
14
+ D8 = Die.new(8)
15
+ D10 = Die.new(10)
16
+ D12 = Die.new(12)
17
+ D20 = Die.new(20)
18
+ D100 = Die.new(100)
19
+ end
@@ -0,0 +1,45 @@
1
+ require 'dicebag'
2
+
3
+ # This is actually modeling the "Storytelling" system dice, not the
4
+ # older "Storyteller" system dice, but I personally find "Storytelling"
5
+ # kind of a silly name, so I prefer the older name. :D
6
+ module Storyteller
7
+ # This is a models a pool of Storyteller dice.
8
+ class Pool < DiceBag::Roll
9
+ def initialize(number = 1, success = 8)
10
+ @number = number
11
+ @success = success
12
+ @result = nil
13
+
14
+ super("#{number}d10e t#{success}")
15
+ end
16
+
17
+ def roll
18
+ @result = super
19
+ end
20
+
21
+ def successes
22
+ roll unless @result
23
+
24
+ @result.total
25
+ end
26
+
27
+ def tally
28
+ roll unless @result
29
+
30
+ @result.sections[0].tally
31
+ end
32
+
33
+ def to_s
34
+ "#{@number}d10/#{@success}"
35
+ end
36
+ end
37
+
38
+ def self.roll(number = 1, success = 8)
39
+ Pool.new(number, success).roll
40
+ end
41
+
42
+ def self.chance
43
+ Pool.new(1, 10).roll
44
+ end
45
+ end