games_dice 0.3.12 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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