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