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.
- 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
|