games_dice 0.3.9 → 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 +7 -0
- data/.rubocop.yml +15 -0
- data/.travis.yml +9 -12
- data/CHANGELOG.md +29 -13
- data/Gemfile +2 -0
- data/README.md +5 -5
- 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 -28
- data/lib/games_dice/bunch.rb +241 -247
- data/lib/games_dice/complex_die.rb +287 -303
- data/lib/games_dice/complex_die_helpers.rb +68 -0
- 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 -16
- data/spec/bunch_spec.rb +399 -421
- data/spec/complex_die_spec.rb +314 -306
- data/spec/dice_spec.rb +33 -34
- data/spec/die_result_spec.rb +163 -170
- data/spec/die_spec.rb +81 -82
- data/spec/helpers.rb +26 -22
- data/spec/map_rule_spec.rb +40 -44
- data/spec/parser_spec.rb +106 -82
- data/spec/probability_spec.rb +530 -527
- data/spec/readme_spec.rb +404 -384
- data/spec/reroll_rule_spec.rb +40 -44
- metadata +63 -74
- data/lib/games_dice/probabilities.rb +0 -445
@@ -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
|
data/lib/games_dice/map_rule.rb
CHANGED
@@ -1,70 +1,72 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
#
|
11
|
-
# rule.
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# rule
|
15
|
-
|
16
|
-
#
|
17
|
-
# rule.
|
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
|
-
return
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GamesDice
|
4
|
+
# This class models rules that convert numbers shown on a die to values used in a game. A
|
5
|
+
# common use for this is to count "successes" - dice that score a certain number or higher.
|
6
|
+
#
|
7
|
+
# An object of the class represents a single rule, such as "count a die result of 5 or more as 1
|
8
|
+
# _success_".
|
9
|
+
#
|
10
|
+
# @example A rule for counting successes
|
11
|
+
# rule = GamesDice::MapRule.new( 6, :<=, 1, 'Success' )
|
12
|
+
# # Test how the rule applies . . .
|
13
|
+
# rule.map_from 4 # => nil
|
14
|
+
# rule.map_from 6 # => 1
|
15
|
+
#
|
16
|
+
# @example A rule for counting "fumbles" which reduce total successes
|
17
|
+
# rule = GamesDice::MapRule.new( 1, :==, -1, 'Fumble' )
|
18
|
+
# # Test how the rule applies . . .
|
19
|
+
# rule.map_from 7 # => nil
|
20
|
+
# rule.map_from 1 # => -1
|
21
|
+
#
|
22
|
+
class MapRule
|
23
|
+
# Creates new instance of GamesDice::MapRule. The rule will be assessed as
|
24
|
+
# trigger_value.send( trigger_op, x )
|
25
|
+
# where x is the Integer value shown on a die.
|
26
|
+
# @param [Integer,Range<Integer>,Object] trigger_value Any object is allowed, but typically an Integer
|
27
|
+
# @param [Symbol] trigger_op A method of trigger_value that takes an Integer param and returns Boolean
|
28
|
+
# @param [Integer] mapped_value The value to use in place of the trigger value
|
29
|
+
# @param [String] mapped_name Name of mapped value, for use in descriptions
|
30
|
+
# @return [GamesDice::MapRule]
|
31
|
+
def initialize(trigger_value, trigger_op, mapped_value = 0, mapped_name = '')
|
32
|
+
unless trigger_value.respond_to?(trigger_op)
|
33
|
+
raise ArgumentError,
|
34
|
+
"trigger_value #{trigger_value.inspect} cannot respond to trigger_op #{trigger_value.inspect}"
|
35
|
+
end
|
36
|
+
|
37
|
+
@trigger_value = trigger_value
|
38
|
+
@trigger_op = trigger_op
|
39
|
+
raise TypeError unless mapped_value.is_a? Numeric
|
40
|
+
|
41
|
+
@mapped_value = Integer(mapped_value)
|
42
|
+
@mapped_name = mapped_name.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
# Trigger operation. How the rule is assessed against #trigger_value.
|
46
|
+
# @return [Symbol] Method name to be sent to #trigger_value
|
47
|
+
attr_reader :trigger_op
|
48
|
+
|
49
|
+
# Trigger value. An object that will use #trigger_op to assess a die result for a reroll.
|
50
|
+
# @return [Integer,Range,Object] Object that receives (#trigger_op, die_result)
|
51
|
+
attr_reader :trigger_value
|
52
|
+
|
53
|
+
# Value that a die will use after the value has been mapped.
|
54
|
+
# @return [Integer]
|
55
|
+
attr_reader :mapped_value
|
56
|
+
|
57
|
+
# Name for mapped value, used in explanations.
|
58
|
+
# @return [String]
|
59
|
+
attr_reader :mapped_name
|
60
|
+
|
61
|
+
# Assesses the rule against a die result value.
|
62
|
+
# @param [Integer] test_value Value that is result of rolling a single die.
|
63
|
+
# @return [Integer,nil] Replacement value, or nil if this rule doesn't apply
|
64
|
+
def map_from(test_value)
|
65
|
+
op_result = @trigger_value.send(@trigger_op, test_value)
|
66
|
+
return nil unless op_result
|
67
|
+
return @mapped_value if op_result == true
|
68
|
+
|
69
|
+
op_result
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/games_dice/marshal.rb
CHANGED
@@ -1,13 +1,18 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GamesDice
|
4
|
+
# Probability calculations
|
5
|
+
class Probabilities
|
6
|
+
# @!visibility private
|
7
|
+
# Adds support for Marshal, via to_h and from_h methods
|
8
|
+
def _dump(*_ignored)
|
9
|
+
Marshal.dump to_h
|
10
|
+
end
|
11
|
+
|
12
|
+
# @!visibility private
|
13
|
+
def self._load(buf)
|
14
|
+
h = Marshal.load buf
|
15
|
+
from_h h
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|