games_dice 0.3.12 → 0.4.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/.rubocop.yml +15 -0
- data/.travis.yml +7 -10
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -0
- data/Rakefile +14 -11
- data/ext/games_dice/extconf.rb +4 -22
- data/ext/games_dice/probabilities.c +1 -1
- data/games_dice.gemspec +26 -32
- data/lib/games_dice/bunch.rb +241 -247
- data/lib/games_dice/complex_die.rb +287 -270
- data/lib/games_dice/complex_die_helpers.rb +68 -60
- data/lib/games_dice/constants.rb +10 -10
- data/lib/games_dice/dice.rb +146 -143
- data/lib/games_dice/die.rb +101 -97
- data/lib/games_dice/die_result.rb +193 -189
- data/lib/games_dice/map_rule.rb +72 -70
- data/lib/games_dice/marshal.rb +18 -13
- data/lib/games_dice/parser.rb +219 -218
- data/lib/games_dice/reroll_rule.rb +76 -77
- data/lib/games_dice/version.rb +3 -1
- data/lib/games_dice.rb +19 -19
- data/spec/bunch_spec.rb +399 -420
- data/spec/complex_die_spec.rb +314 -305
- data/spec/dice_spec.rb +33 -34
- data/spec/die_result_spec.rb +162 -169
- data/spec/die_spec.rb +81 -81
- data/spec/helpers.rb +23 -21
- data/spec/map_rule_spec.rb +40 -44
- data/spec/parser_spec.rb +106 -82
- data/spec/probability_spec.rb +530 -526
- data/spec/readme_spec.rb +404 -390
- data/spec/reroll_rule_spec.rb +40 -44
- metadata +39 -28
- data/lib/games_dice/prob_helpers.rb +0 -259
- data/lib/games_dice/probabilities.rb +0 -244
data/lib/games_dice/die.rb
CHANGED
@@ -1,97 +1,101 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
@result =
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GamesDice
|
4
|
+
# This class models the simplest, most-familiar kind of die.
|
5
|
+
#
|
6
|
+
# An object of the class represents a basic die that rolls 1..#sides, with equal weighting for each value.
|
7
|
+
#
|
8
|
+
# @example Create a 6-sided die, and roll it
|
9
|
+
# d = GamesDice::Die.new( 6 )
|
10
|
+
# d.roll # => Integer in range 1..6
|
11
|
+
# d.result # => same Integer value as just returned by d.roll
|
12
|
+
#
|
13
|
+
# @example Create a 10-sided die, that rolls using a monkey-patch to SecureRandom
|
14
|
+
# module SecureRandom
|
15
|
+
# def self.rand n
|
16
|
+
# random_number( n )
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
# d = GamesDice::Die.new( 10, SecureRandom )
|
20
|
+
# d.roll # => (secure) Integer in range 1..10
|
21
|
+
# d.result # => same Integer value as just returned by d.roll
|
22
|
+
#
|
23
|
+
class Die
|
24
|
+
# Creates new instance of GamesDice::Die
|
25
|
+
# @param [Integer] sides the number of sides
|
26
|
+
# @param [#rand] prng random number generator, GamesDice::Die will use Ruby's built-in #rand() by default
|
27
|
+
# @return [GamesDice::Die]
|
28
|
+
def initialize(sides, prng = nil)
|
29
|
+
@sides = Integer(sides)
|
30
|
+
raise ArgumentError, "sides value #{sides} is too low, it must be 1 or greater" if @sides < 1
|
31
|
+
raise ArgumentError, 'prng does not support the rand() method' if prng && !prng.respond_to?(:rand)
|
32
|
+
|
33
|
+
@prng = prng
|
34
|
+
@result = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Integer] number of sides on simulated die
|
38
|
+
attr_reader :sides
|
39
|
+
|
40
|
+
# @return [Integer] result of last call to #roll, nil if no call made yet
|
41
|
+
attr_reader :result
|
42
|
+
|
43
|
+
# @return [Object] random number generator as supplied to constructor, may be nil
|
44
|
+
attr_reader :prng
|
45
|
+
|
46
|
+
# @!attribute [r] min
|
47
|
+
# @return [Integer] minimum possible result from a call to #roll
|
48
|
+
def min
|
49
|
+
1
|
50
|
+
end
|
51
|
+
|
52
|
+
# @!attribute [r] max
|
53
|
+
# @return [Integer] maximum possible result from a call to #roll
|
54
|
+
def max
|
55
|
+
@sides
|
56
|
+
end
|
57
|
+
|
58
|
+
# Calculates probability distribution for this die.
|
59
|
+
# @return [GamesDice::Probabilities] probability distribution of the die
|
60
|
+
def probabilities
|
61
|
+
@probabilities ||= GamesDice::Probabilities.for_fair_die(@sides)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Simulates rolling the die
|
65
|
+
# @return [Integer] selected value between 1 and #sides inclusive
|
66
|
+
def roll
|
67
|
+
@result = if @prng
|
68
|
+
@prng.rand(@sides) + 1
|
69
|
+
else
|
70
|
+
rand(@sides) + 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Iterates through all possible results on die.
|
75
|
+
# @yieldparam [Integer] result A potential result from the die
|
76
|
+
# @return [GamesDice::Die] this object
|
77
|
+
def each_value(&block)
|
78
|
+
(1..@sides).each(&block)
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Array<Integer>] All potential results from the die
|
83
|
+
def all_values
|
84
|
+
(1..@sides).to_a
|
85
|
+
end
|
86
|
+
|
87
|
+
# @!attribute [r] rerolls
|
88
|
+
# Rules for when to re-roll this die.
|
89
|
+
# @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
|
90
|
+
def rerolls
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
|
94
|
+
# @!attribute [r] maps
|
95
|
+
# Rules for when to map return value of this die.
|
96
|
+
# @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
|
97
|
+
def maps
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -1,189 +1,193 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# dr
|
11
|
-
# dr.
|
12
|
-
# dr.
|
13
|
-
#
|
14
|
-
# dr
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# dr
|
24
|
-
# dr.
|
25
|
-
# dr.
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
@
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GamesDice
|
4
|
+
# This class models the output of GamesDice::ComplexDie.
|
5
|
+
#
|
6
|
+
# An object of the class represents the results of a roll of a ComplexDie, including any re-rolls and
|
7
|
+
# value mapping.
|
8
|
+
#
|
9
|
+
# @example Building up a result manually
|
10
|
+
# dr = GamesDice::DieResult.new
|
11
|
+
# dr.add_roll 5
|
12
|
+
# dr.add_roll 4, :reroll_replace
|
13
|
+
# dr.value # => 4
|
14
|
+
# dr.rolls # => [5, 4]
|
15
|
+
# dr.roll_reasons # => [:basic, :reroll_replace]
|
16
|
+
# # dr can behave as dr.value due to coercion and support for some operators
|
17
|
+
# dr + 6 # => 10
|
18
|
+
#
|
19
|
+
# @example Using a result from GamesDice::ComplexDie
|
20
|
+
# # An "exploding" six-sided die that needs a result of 8 to score "1 Success"
|
21
|
+
# d = GamesDice::ComplexDie.new( 6, :rerolls => [[6, :<=, :reroll_add]], :maps => [[8, :<=, 1, 'Success']] )
|
22
|
+
# # Generate result object by rolling the die
|
23
|
+
# dr = d.roll
|
24
|
+
# dr.rolls # => [6, 3]
|
25
|
+
# dr.roll_reasons # => [:basic, :reroll_add]
|
26
|
+
# dr.total # => 9
|
27
|
+
# dr.value # => 1
|
28
|
+
# dr.explain_value # => "[6+3] 9 Success"
|
29
|
+
#
|
30
|
+
class DieResult
|
31
|
+
include Comparable
|
32
|
+
|
33
|
+
# Creates new instance of GamesDice::DieResult. The object can be initialised "empty" or with a first result.
|
34
|
+
# @param [Integer,nil] first_roll_result Value for first roll of the die.
|
35
|
+
# @param [Symbol] first_roll_reason Reason for first roll of the die.
|
36
|
+
# @return [GamesDice::DieResult]
|
37
|
+
def initialize(first_roll_result = nil, first_roll_reason = :basic)
|
38
|
+
unless GamesDice::REROLL_TYPES.key?(first_roll_reason)
|
39
|
+
raise ArgumentError, "Unrecognised reason for roll #{first_roll_reason}"
|
40
|
+
end
|
41
|
+
|
42
|
+
if first_roll_result
|
43
|
+
@rolls = [Integer(first_roll_result)]
|
44
|
+
@roll_reasons = [first_roll_reason]
|
45
|
+
@total = @rolls[0]
|
46
|
+
else
|
47
|
+
@rolls = []
|
48
|
+
@roll_reasons = []
|
49
|
+
@total = nil
|
50
|
+
end
|
51
|
+
@mapped = false
|
52
|
+
@value = @total
|
53
|
+
end
|
54
|
+
|
55
|
+
# The individual die rolls that combined to generate this result.
|
56
|
+
# @return [Array<Integer>] Un-processed values of each die roll used for this result.
|
57
|
+
attr_reader :rolls
|
58
|
+
|
59
|
+
# The individual reasons for each roll of the die. See GamesDice::RerollRule for allowed values.
|
60
|
+
# @return [Array<Symbol>] Reasons for each die roll, indexes match the #rolls Array.
|
61
|
+
attr_reader :roll_reasons
|
62
|
+
|
63
|
+
# Combined result of all rolls, *before* mapping.
|
64
|
+
# @return [Integer,nil]
|
65
|
+
attr_reader :total
|
66
|
+
|
67
|
+
# Combined result of all rolls, *after* mapping.
|
68
|
+
# @return [Integer,nil]
|
69
|
+
attr_reader :value
|
70
|
+
|
71
|
+
# Whether or not #value has been mapped from #total.
|
72
|
+
# @return [Boolean]
|
73
|
+
attr_reader :mapped
|
74
|
+
|
75
|
+
# Adds value from a new roll to the object. GamesDice::DieResult tracks reasons for the roll
|
76
|
+
# and makes the correct adjustment to the total so far. Any mapped value is cleared.
|
77
|
+
# @param [Integer] roll_result Value result from rolling the die.
|
78
|
+
# @param [Symbol] roll_reason Reason for rolling the die.
|
79
|
+
# @return [Integer] Total so far
|
80
|
+
def add_roll(roll_result, roll_reason = :basic)
|
81
|
+
unless GamesDice::REROLL_TYPES.key?(roll_reason)
|
82
|
+
raise ArgumentError, "Unrecognised reason for roll #{roll_reason}"
|
83
|
+
end
|
84
|
+
|
85
|
+
@rolls << Integer(roll_result)
|
86
|
+
@roll_reasons << roll_reason
|
87
|
+
@total = 0 if @rolls.length == 1
|
88
|
+
|
89
|
+
case roll_reason
|
90
|
+
when :basic
|
91
|
+
@total = roll_result
|
92
|
+
when :reroll_add
|
93
|
+
@total += roll_result
|
94
|
+
when :reroll_subtract
|
95
|
+
@total -= roll_result
|
96
|
+
when :reroll_new_die
|
97
|
+
@total = roll_result
|
98
|
+
when :reroll_new_keeper
|
99
|
+
@total = roll_result
|
100
|
+
when :reroll_replace
|
101
|
+
@total = roll_result
|
102
|
+
when :reroll_use_best
|
103
|
+
@total = [@value, roll_result].max
|
104
|
+
when :reroll_use_worst
|
105
|
+
@total = [@value, roll_result].min
|
106
|
+
end
|
107
|
+
|
108
|
+
@mapped = false
|
109
|
+
@value = @total
|
110
|
+
end
|
111
|
+
|
112
|
+
# Sets value arbitrarily, and notes that the value has been mapped. Used by GamesDice::ComplexDie
|
113
|
+
# when there are one or more GamesDice::MapRule objects to process for a die.
|
114
|
+
# @param [Integer] to_value Replacement value.
|
115
|
+
# @param [String] description Description of what the mapped value represents e.g. "Success"
|
116
|
+
# @return [nil]
|
117
|
+
def apply_map(to_value, description = '')
|
118
|
+
@mapped = true
|
119
|
+
@value = to_value
|
120
|
+
@map_description = description
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
|
124
|
+
# Generates a text description of how #value is determined. If #value has been mapped, includes the
|
125
|
+
# map description, but does not include the mapped value.
|
126
|
+
# @return [String] Explanation of #value.
|
127
|
+
def explain_value
|
128
|
+
text = ''
|
129
|
+
if @rolls.length < 2
|
130
|
+
text = @total.to_s
|
131
|
+
else
|
132
|
+
text = "[#{@rolls[0]}"
|
133
|
+
text = (1..@rolls.length - 1).inject(text) do |so_far, i|
|
134
|
+
so_far + GamesDice::REROLL_TYPES[@roll_reasons[i]] + @rolls[i].to_s
|
135
|
+
end
|
136
|
+
text += "] #{@total}"
|
137
|
+
end
|
138
|
+
text += " #{@map_description}" if @mapped && @map_description && @map_description.length.positive?
|
139
|
+
text
|
140
|
+
end
|
141
|
+
|
142
|
+
# @!visibility private
|
143
|
+
# This is mis-named, it doesn't explain the total at all! It is used to generate summaries of keeper dice.
|
144
|
+
def explain_total
|
145
|
+
text = @total.to_s
|
146
|
+
text += " #{@map_description}" if @mapped && @map_description && @map_description.length.positive?
|
147
|
+
text
|
148
|
+
end
|
149
|
+
|
150
|
+
# @!visibility private
|
151
|
+
# all coercions simply use #value (i.e. nil or a Integer)
|
152
|
+
def coerce(thing)
|
153
|
+
@value.coerce(thing)
|
154
|
+
end
|
155
|
+
|
156
|
+
# @!visibility private
|
157
|
+
# addition uses #value
|
158
|
+
def +(other)
|
159
|
+
@value + other
|
160
|
+
end
|
161
|
+
|
162
|
+
# @!visibility private
|
163
|
+
# subtraction uses #value
|
164
|
+
def -(other)
|
165
|
+
@value - other
|
166
|
+
end
|
167
|
+
|
168
|
+
# @!visibility private
|
169
|
+
# multiplication uses #value
|
170
|
+
def *(other)
|
171
|
+
@value * other
|
172
|
+
end
|
173
|
+
|
174
|
+
# @!visibility private
|
175
|
+
# comparison <=> uses #value
|
176
|
+
def <=>(other)
|
177
|
+
value <=> other
|
178
|
+
end
|
179
|
+
|
180
|
+
# This is a deep clone, all attributes are cloned.
|
181
|
+
# @return [GamesDice::DieResult]
|
182
|
+
def clone
|
183
|
+
cloned = GamesDice::DieResult.new
|
184
|
+
cloned.instance_variable_set('@rolls', @rolls.clone)
|
185
|
+
cloned.instance_variable_set('@roll_reasons', @roll_reasons.clone)
|
186
|
+
cloned.instance_variable_set('@total', @total)
|
187
|
+
cloned.instance_variable_set('@value', @value)
|
188
|
+
cloned.instance_variable_set('@mapped', @mapped)
|
189
|
+
cloned.instance_variable_set('@map_description', @map_description)
|
190
|
+
cloned
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|