dicebag 3.2.2 → 3.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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[: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) unless explode.nil?
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) && @options[:explode].positive?
35
38
 
36
- e = (@options[:explode] == sides) ? @options[:explode] : ''
39
+ 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