games_dice 0.5.0 → 0.6.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 +4 -4
- data/.github/workflows/ci.yml +11 -1
- data/.gitignore +0 -1
- data/.rubocop.yml +13 -48
- data/CHANGELOG.md +19 -0
- data/Gemfile +5 -3
- data/README.md +46 -5
- data/Rakefile +12 -5
- data/ext/games_dice/extconf.rb +0 -3
- data/ext/games_dice/probabilities.c +293 -193
- data/ext/games_dice/probabilities.h +2 -12
- data/games_dice.gemspec +1 -1
- data/lib/games_dice/bunch.rb +50 -36
- data/lib/games_dice/bunch_helpers.rb +35 -14
- data/lib/games_dice/complex_die.rb +64 -22
- data/lib/games_dice/complex_die_helpers.rb +122 -144
- data/lib/games_dice/dice.rb +61 -17
- data/lib/games_dice/dice_helpers.rb +37 -0
- data/lib/games_dice/die_result.rb +6 -8
- data/lib/games_dice/marshal.rb +2 -7
- data/lib/games_dice/parser.rb +30 -0
- data/lib/games_dice/probability_budget.rb +90 -0
- data/lib/games_dice/probability_calculation.rb +95 -0
- data/lib/games_dice/probability_shortcuts.rb +65 -0
- data/lib/games_dice/recipe_cache.rb +155 -0
- data/lib/games_dice/version.rb +1 -1
- data/lib/games_dice.rb +4 -0
- data/script/check_documentation.rb +10 -0
- data/spec/games_dice/bunch_regressions_spec.rb +46 -0
- data/spec/{bunch_spec.rb → games_dice/bunch_spec.rb} +65 -112
- data/spec/games_dice/complex_die_budget_spec.rb +60 -0
- data/spec/games_dice/complex_die_limits_spec.rb +119 -0
- data/spec/games_dice/complex_die_spec.rb +228 -0
- data/spec/games_dice/dice_mutation_spec.rb +165 -0
- data/spec/games_dice/dice_regressions_spec.rb +50 -0
- data/spec/games_dice/dice_spec.rb +61 -0
- data/spec/games_dice/die_result_spec.rb +221 -0
- data/spec/{die_spec.rb → games_dice/die_spec.rb} +15 -14
- data/spec/{map_rule_spec.rb → games_dice/map_rule_spec.rb} +8 -1
- data/spec/{parser_spec.rb → games_dice/parser_spec.rb} +12 -2
- data/spec/games_dice/probabilities_boundaries_spec.rb +161 -0
- data/spec/games_dice/probabilities_marshal_spec.rb +62 -0
- data/spec/games_dice/probabilities_spec.rb +484 -0
- data/spec/games_dice/probability_budget_spec.rb +17 -0
- data/spec/{reroll_rule_spec.rb → games_dice/reroll_rule_spec.rb} +5 -1
- data/spec/games_dice_spec.rb +382 -0
- data/spec/helpers.rb +64 -2
- data/spec/simple_cov_spec.rb +42 -0
- data/spec/support/coverage.rb +12 -0
- data/spec/support/validation.rb +14 -0
- data/spec/yard/cli/stats_spec.rb +43 -0
- metadata +29 -12
- data/spec/complex_die_spec.rb +0 -314
- data/spec/dice_spec.rb +0 -33
- data/spec/die_result_spec.rb +0 -254
- data/spec/fixtures/probs_fair_die_6.dat +0 -2
- data/spec/probability_spec.rb +0 -584
- data/spec/readme_spec.rb +0 -410
|
@@ -10,22 +10,12 @@
|
|
|
10
10
|
void init_probabilities_class(void);
|
|
11
11
|
|
|
12
12
|
typedef struct _pd {
|
|
13
|
+
struct _pd *next;
|
|
14
|
+
struct _pd **owner_link;
|
|
13
15
|
int offset;
|
|
14
16
|
int slots;
|
|
15
17
|
double *probs;
|
|
16
18
|
double *cumulative;
|
|
17
19
|
} ProbabilityList;
|
|
18
20
|
|
|
19
|
-
ProbabilityList *pl_add_distributions( ProbabilityList *pl_a, ProbabilityList *pl_b );
|
|
20
|
-
|
|
21
|
-
ProbabilityList *pl_add_distributions_mult( int mul_a, ProbabilityList *pl_a, int mul_b, ProbabilityList *pl_b );
|
|
22
|
-
|
|
23
|
-
ProbabilityList *pl_given_ge( ProbabilityList *pl, int target );
|
|
24
|
-
|
|
25
|
-
ProbabilityList *pl_given_le( ProbabilityList *pl, int target );
|
|
26
|
-
|
|
27
|
-
ProbabilityList *pl_repeat_sum( ProbabilityList *pl, int n );
|
|
28
|
-
|
|
29
|
-
ProbabilityList *pl_repeat_n_sum_k( ProbabilityList *pl, int n, int k, int kbest );
|
|
30
|
-
|
|
31
21
|
#endif
|
data/games_dice.gemspec
CHANGED
|
@@ -24,7 +24,7 @@ Gem::Specification.new do |gem|
|
|
|
24
24
|
|
|
25
25
|
gem.add_dependency 'parslet', '~> 2.0'
|
|
26
26
|
|
|
27
|
-
gem.files = `git ls-files -z`.split("\0").select { |file| File.file?(file) }
|
|
27
|
+
gem.files = `git ls-files -z`.split("\0").select { |file| File.file?(file) && file != 'Gemfile.lock' }
|
|
28
28
|
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
29
29
|
gem.extensions = gem.files.grep(%r{/extconf\.rb$})
|
|
30
30
|
gem.require_paths = ['lib']
|
data/lib/games_dice/bunch.rb
CHANGED
|
@@ -23,6 +23,8 @@ module GamesDice
|
|
|
23
23
|
# d.explain_result # => "4, 9, 2, 9, 1. Keep: 9 + 9 = 18"
|
|
24
24
|
#
|
|
25
25
|
class Bunch
|
|
26
|
+
include RecipeQueries
|
|
27
|
+
include ProbabilityHelpers
|
|
26
28
|
include KeepHelpers
|
|
27
29
|
include ExplainHelpers
|
|
28
30
|
|
|
@@ -51,6 +53,7 @@ module GamesDice
|
|
|
51
53
|
else
|
|
52
54
|
GamesDice::Die.new(@sides, options[:prng])
|
|
53
55
|
end
|
|
56
|
+
initialize_recipe
|
|
54
57
|
end
|
|
55
58
|
|
|
56
59
|
# Name to help identify bunch
|
|
@@ -75,7 +78,10 @@ module GamesDice
|
|
|
75
78
|
|
|
76
79
|
# Result of most-recent roll, or nil if no roll made yet.
|
|
77
80
|
# @return [Integer,nil]
|
|
78
|
-
|
|
81
|
+
def result
|
|
82
|
+
refresh_recipe
|
|
83
|
+
@result
|
|
84
|
+
end
|
|
79
85
|
|
|
80
86
|
# @!attribute [r] label
|
|
81
87
|
# Description that will be used in explanations with more than one bunch
|
|
@@ -105,80 +111,88 @@ module GamesDice
|
|
|
105
111
|
# allowing inspection of how the result was obtained.
|
|
106
112
|
# @return [Array<GamesDice::DieResult>, nil] Sequence of GamesDice::DieResult objects.
|
|
107
113
|
def result_details
|
|
114
|
+
refresh_recipe
|
|
108
115
|
return nil unless @raw_result_details
|
|
109
116
|
|
|
110
117
|
@raw_result_details.map { |r| r.is_a?(Integer) ? GamesDice::DieResult.new(r) : r }
|
|
111
118
|
end
|
|
112
119
|
|
|
113
120
|
# @!attribute [r] min
|
|
114
|
-
#
|
|
121
|
+
# Lower bound on the value returned by #roll. No roll will be below this value.
|
|
122
|
+
# For some complex dice, this may be lower than any value that can actually
|
|
123
|
+
# be rolled, because calculating the exact minimum would take too much work.
|
|
115
124
|
# @return [Integer]
|
|
116
125
|
def min
|
|
126
|
+
refresh_recipe
|
|
117
127
|
n = @keep_mode ? [@keep_number, @ndice].min : @ndice
|
|
118
128
|
n * @single_die.min
|
|
119
129
|
end
|
|
120
130
|
|
|
121
131
|
# @!attribute [r] max
|
|
122
|
-
#
|
|
132
|
+
# Upper bound on the value returned by #roll. No roll will exceed this value.
|
|
133
|
+
# For some complex dice, this may be higher than any value that can actually
|
|
134
|
+
# be rolled, because calculating the exact maximum would take too much work.
|
|
123
135
|
# @return [Integer]
|
|
124
136
|
def max
|
|
137
|
+
refresh_recipe
|
|
125
138
|
n = @keep_mode ? [@keep_number, @ndice].min : @ndice
|
|
126
139
|
n * @single_die.max
|
|
127
140
|
end
|
|
128
141
|
|
|
129
|
-
# Calculates the probability distribution for the bunch.
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
142
|
+
# Calculates the probability distribution for the bunch.
|
|
143
|
+
#
|
|
144
|
+
# Very unlikely sequences of rerolls may be left out to keep calculation
|
|
145
|
+
# time reasonable. This changes the probability of any result by at most
|
|
146
|
+
# 1e-10 (one in ten billion) across the whole bunch, in addition to normal
|
|
147
|
+
# floating-point rounding.
|
|
148
|
+
#
|
|
149
|
+
# Raises ProbabilityLimitError if this accuracy cannot be achieved within
|
|
150
|
+
# the calculation's work limits.
|
|
151
|
+
# @return [GamesDice::Probabilities] Probability distribution of the bunch.
|
|
133
152
|
def probabilities
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
@probabilities = if @keep_mode && @ndice > @keep_number
|
|
137
|
-
@single_die.probabilities.repeat_n_sum_k(@ndice, @keep_number, @keep_mode)
|
|
138
|
-
else
|
|
139
|
-
@single_die.probabilities.repeat_sum(@ndice)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
@probabilities
|
|
153
|
+
probabilities_for_budget(ProbabilityBudget::LIMIT)
|
|
143
154
|
end
|
|
144
155
|
|
|
145
156
|
# Simulates rolling the bunch of identical dice
|
|
146
157
|
# @return [Integer] Sum of all rolled dice, or sum of all keepers
|
|
147
158
|
def roll
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
when :keep_worst then @raw_result_details.sort[0..(@keep_number - 1)]
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
@result = use_dice.sum
|
|
159
|
+
with_recipe do
|
|
160
|
+
generate_raw_results
|
|
161
|
+
@used_result_details = select_keepers
|
|
162
|
+
@result = @used_result_details.sum
|
|
163
|
+
end
|
|
157
164
|
end
|
|
158
165
|
|
|
159
166
|
# @!attribute [r] explain_result
|
|
160
167
|
# Explanation of result, or nil if no call to #roll yet.
|
|
161
168
|
# @return [String,nil]
|
|
162
169
|
def explain_result
|
|
170
|
+
refresh_recipe
|
|
163
171
|
return nil unless @result
|
|
164
172
|
|
|
165
|
-
|
|
166
|
-
used_dice = result_details
|
|
167
|
-
used_dice, = find_used_dice_due_to_keep_mode(result_details) if @keep_mode && @keep_number < @ndice
|
|
168
|
-
|
|
173
|
+
used_dice = @used_result_details.map { |result| result.is_a?(Integer) ? DieResult.new(result) : result }
|
|
169
174
|
build_explanation(used_dice)
|
|
170
175
|
end
|
|
171
176
|
|
|
172
177
|
private
|
|
173
178
|
|
|
174
|
-
def
|
|
175
|
-
@
|
|
176
|
-
|
|
179
|
+
def recipe_sources
|
|
180
|
+
[[[@single_die], @single_die.class]]
|
|
181
|
+
end
|
|
177
182
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
def recipe_labels
|
|
184
|
+
[@name]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def select_keepers
|
|
188
|
+
return @raw_result_details if !@keep_mode || @keep_number >= @ndice
|
|
189
|
+
return @raw_result_details.sort[-@keep_number..] if @keep_mode == :keep_best
|
|
190
|
+
|
|
191
|
+
@raw_result_details.sort[0...@keep_number]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def generate_raw_results
|
|
195
|
+
@raw_result_details = Array.new(@ndice) { @single_die.roll }
|
|
182
196
|
end
|
|
183
197
|
|
|
184
198
|
def name_number_sides_from_hash(options)
|
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
module GamesDice
|
|
4
4
|
class Bunch
|
|
5
|
+
# @!visibility private
|
|
6
|
+
# Private distribution and aggregate-error helpers.
|
|
7
|
+
module ProbabilityHelpers
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def probabilities_for_budget(limit)
|
|
11
|
+
refresh_recipe
|
|
12
|
+
return @probabilities if valid_probability_cache?(limit)
|
|
13
|
+
|
|
14
|
+
with_recipe do
|
|
15
|
+
distribution = single_distribution(limit)
|
|
16
|
+
result = repeated_distribution(distribution)
|
|
17
|
+
@probability_error = ProbabilityBudget.check(ProbabilityBudget.scale(single_error, @ndice), limit)
|
|
18
|
+
@probabilities = result
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def repeated_distribution(distribution)
|
|
23
|
+
return distribution.repeat_sum(@ndice) unless @keep_mode && @ndice > @keep_number
|
|
24
|
+
|
|
25
|
+
distribution.repeat_n_sum_k(@ndice, @keep_number, @keep_mode)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def single_error
|
|
29
|
+
@single_die.is_a?(ComplexDie) ? @single_die.instance_variable_get(:@probability_error) : 0.0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def single_distribution(limit)
|
|
33
|
+
return @single_die.probabilities unless @single_die.is_a?(ComplexDie)
|
|
34
|
+
|
|
35
|
+
@single_die.__send__(:probabilities_for_budget, ProbabilityBudget.share(limit, @ndice))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
5
39
|
# @!visibility private
|
|
6
40
|
# Private extension methods for GamesDice::Bunch keep rules
|
|
7
41
|
module KeepHelpers
|
|
@@ -14,25 +48,12 @@ module GamesDice
|
|
|
14
48
|
@keep_mode = nil
|
|
15
49
|
when :keep_best, :keep_worst
|
|
16
50
|
@keep_number = Integer(options[:keep_number] || 1)
|
|
51
|
+
raise ArgumentError, ":keep_number must be 1 or more, but got #{@keep_number}" unless @keep_number.positive?
|
|
17
52
|
else
|
|
18
53
|
raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{options[:keep_mode].inspect}"
|
|
19
54
|
end
|
|
20
55
|
end
|
|
21
56
|
|
|
22
|
-
def find_used_dice_due_to_keep_mode(used_dice, unused_dice = [])
|
|
23
|
-
full_dice = result_details.sort_by(&:total)
|
|
24
|
-
case @keep_mode
|
|
25
|
-
when :keep_best
|
|
26
|
-
used_dice = full_dice[-@keep_number..]
|
|
27
|
-
unused_dice = full_dice[0..(full_dice.length - 1 - @keep_number)]
|
|
28
|
-
when :keep_worst
|
|
29
|
-
used_dice = full_dice[0..(@keep_number - 1)]
|
|
30
|
-
unused_dice = full_dice[@keep_number..]
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
[used_dice, unused_dice]
|
|
34
|
-
end
|
|
35
|
-
|
|
36
57
|
def explain_kept_dice(used_dice)
|
|
37
58
|
separator = @single_die.maps ? ', ' : ' + '
|
|
38
59
|
". Keep: #{used_dice.map(&:explain_total).join(separator)}"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'games_dice/complex_die_helpers'
|
|
4
|
+
require 'games_dice/probability_calculation'
|
|
4
5
|
|
|
5
6
|
module GamesDice
|
|
6
7
|
# This class models a die that is built up from a simpler unit by adding rules to re-roll
|
|
@@ -25,6 +26,7 @@ module GamesDice
|
|
|
25
26
|
#
|
|
26
27
|
class ComplexDie
|
|
27
28
|
include RollHelpers
|
|
29
|
+
include RecipeQueries
|
|
28
30
|
include ProbabilityHelpers
|
|
29
31
|
include MinMaxHelpers
|
|
30
32
|
|
|
@@ -51,6 +53,7 @@ module GamesDice
|
|
|
51
53
|
@total = nil
|
|
52
54
|
@result = nil
|
|
53
55
|
|
|
56
|
+
initialize_recipe
|
|
54
57
|
@probabilities_complete = true
|
|
55
58
|
end
|
|
56
59
|
|
|
@@ -65,14 +68,25 @@ module GamesDice
|
|
|
65
68
|
attr_reader :maps
|
|
66
69
|
|
|
67
70
|
# @return [GamesDice::DieResult, nil] Result of last call to #roll, nil if no call made yet
|
|
68
|
-
|
|
71
|
+
def result
|
|
72
|
+
refresh_recipe
|
|
73
|
+
@result
|
|
74
|
+
end
|
|
69
75
|
|
|
70
|
-
# Whether
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
+
# Whether the probability calculation included all possible sequences of rerolls.
|
|
77
|
+
# After calling #probabilities, true means none were left out. False means
|
|
78
|
+
# very unlikely sequences were skipped to keep calculation time reasonable.
|
|
79
|
+
# Even when false, this changes the probability of any result by at most
|
|
80
|
+
# 1e-10 (one in ten billion), in addition to normal floating-point rounding.
|
|
81
|
+
#
|
|
82
|
+
# Call #probabilities before checking this value. A new die starts with true,
|
|
83
|
+
# even though no calculation has run yet.
|
|
84
|
+
# @return [Boolean, nil] Whether the calculation is complete, or nil if its
|
|
85
|
+
# result was cleared after a recipe change or a failed calculation.
|
|
86
|
+
def probabilities_complete
|
|
87
|
+
refresh_recipe
|
|
88
|
+
@probabilities_complete
|
|
89
|
+
end
|
|
76
90
|
|
|
77
91
|
# @!attribute [r] sides
|
|
78
92
|
# @return [Integer] Number of sides.
|
|
@@ -83,49 +97,77 @@ module GamesDice
|
|
|
83
97
|
# @!attribute [r] explain_result
|
|
84
98
|
# @return [String,nil] Explanation of result, or nil if no call to #roll yet.
|
|
85
99
|
def explain_result
|
|
86
|
-
|
|
100
|
+
refresh_recipe
|
|
101
|
+
@result&.explain_value
|
|
87
102
|
end
|
|
88
103
|
|
|
89
|
-
#
|
|
90
|
-
#
|
|
104
|
+
# Lower bound on the numeric value of a roll (#roll.value).
|
|
105
|
+
# No roll will have a value below this bound.
|
|
106
|
+
# For some complex dice, this may be lower than any value that can actually
|
|
107
|
+
# be rolled, because calculating the exact minimum would take too much work.
|
|
108
|
+
#
|
|
109
|
+
# Raises ProbabilityLimitError if a lower bound cannot be found within
|
|
110
|
+
# the calculation's work limits.
|
|
91
111
|
# @!attribute [r] min
|
|
92
112
|
# @return [Integer]
|
|
93
113
|
def min
|
|
114
|
+
refresh_recipe
|
|
94
115
|
return @min_result if @min_result
|
|
95
116
|
|
|
96
|
-
calc_minmax
|
|
117
|
+
with_recipe { calc_minmax }
|
|
97
118
|
@min_result
|
|
98
119
|
end
|
|
99
120
|
|
|
121
|
+
# Upper bound on the numeric value of a roll (#roll.value).
|
|
122
|
+
# No roll will have a value above this bound.
|
|
123
|
+
# For some complex dice, this may be higher than any value that can actually
|
|
124
|
+
# be rolled, because calculating the exact maximum would take too much work.
|
|
125
|
+
#
|
|
126
|
+
# Raises ProbabilityLimitError if an upper bound cannot be found within
|
|
127
|
+
# the calculation's work limits.
|
|
100
128
|
# @!attribute [r] max
|
|
101
|
-
# @return [Integer]
|
|
129
|
+
# @return [Integer]
|
|
102
130
|
def max
|
|
131
|
+
refresh_recipe
|
|
103
132
|
return @max_result if @max_result
|
|
104
133
|
|
|
105
|
-
calc_minmax
|
|
134
|
+
with_recipe { calc_minmax }
|
|
106
135
|
@max_result
|
|
107
136
|
end
|
|
108
137
|
|
|
109
|
-
# Calculates the probability distribution for the die.
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
138
|
+
# Calculates the probability distribution for the die.
|
|
139
|
+
#
|
|
140
|
+
# Very unlikely sequences of rerolls may be left out to keep calculation
|
|
141
|
+
# time reasonable. This changes the probability of any result by at most
|
|
142
|
+
# 1e-10 (one in ten billion), in addition to normal floating-point rounding.
|
|
143
|
+
#
|
|
144
|
+
# Raises ProbabilityLimitError if this accuracy cannot be achieved within
|
|
145
|
+
# the calculation's work limits.
|
|
146
|
+
# @return [GamesDice::Probabilities] Probability distribution of the die.
|
|
113
147
|
def probabilities
|
|
114
|
-
|
|
148
|
+
probabilities_for_budget(ProbabilityBudget::LIMIT)
|
|
115
149
|
end
|
|
116
150
|
|
|
117
151
|
# Simulates rolling the die
|
|
118
152
|
# @param [Symbol] reason Assign a reason for rolling the first die.
|
|
119
153
|
# @return [GamesDice::DieResult] Detailed results from rolling the die, including resolution of rules.
|
|
120
154
|
def roll(reason = :basic)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
with_recipe do
|
|
156
|
+
rerolls = @rerolls&.dup
|
|
157
|
+
maps = @maps&.dup
|
|
158
|
+
@result = GamesDice::DieResult.new(@basic_die.roll, reason)
|
|
159
|
+
roll_apply_rerolls(rerolls)
|
|
160
|
+
roll_apply_maps(maps)
|
|
161
|
+
@result
|
|
162
|
+
end
|
|
125
163
|
end
|
|
126
164
|
|
|
127
165
|
private
|
|
128
166
|
|
|
167
|
+
def recipe_sources
|
|
168
|
+
[[@rerolls, RerollRule], [@maps, MapRule]]
|
|
169
|
+
end
|
|
170
|
+
|
|
129
171
|
def construct_rerolls(rerolls_input)
|
|
130
172
|
check_and_construct rerolls_input, GamesDice::RerollRule, 'rerolls'
|
|
131
173
|
end
|