games_dice 0.4.0 → 0.5.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 +92 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +41 -3
- data/.yardopts +1 -1
- data/CHANGELOG.md +19 -0
- data/Gemfile +11 -0
- data/README.md +11 -33
- data/Rakefile +79 -14
- data/ext/games_dice/extconf.rb +29 -0
- data/ext/games_dice/games_dice.c +2 -2
- data/ext/games_dice/probabilities.c +178 -29
- data/ext/games_dice/probabilities.h +1 -17
- data/games_dice.gemspec +11 -19
- data/lib/games_dice/bunch.rb +27 -65
- data/lib/games_dice/bunch_helpers.rb +68 -0
- data/lib/games_dice/complex_die.rb +10 -146
- data/lib/games_dice/complex_die_helpers.rb +238 -46
- data/lib/games_dice/dice.rb +17 -10
- data/lib/games_dice/die.rb +2 -2
- data/lib/games_dice/die_result.rb +84 -71
- data/lib/games_dice/marshal.rb +26 -3
- data/lib/games_dice/parser.rb +128 -102
- data/lib/games_dice/version.rb +2 -1
- data/lib/games_dice.rb +7 -9
- data/spec/bunch_spec.rb +72 -72
- data/spec/complex_die_spec.rb +158 -158
- data/spec/dice_spec.rb +5 -5
- data/spec/die_result_spec.rb +98 -98
- data/spec/die_spec.rb +19 -19
- data/spec/helpers.rb +2 -4
- data/spec/map_rule_spec.rb +17 -17
- data/spec/parser_spec.rb +7 -7
- data/spec/probability_spec.rb +248 -194
- data/spec/readme_spec.rb +28 -22
- data/spec/reroll_rule_spec.rb +15 -15
- metadata +16 -139
- data/.travis.yml +0 -11
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module GamesDice
|
|
4
|
+
# Module supports using DieResults directly in calculations
|
|
5
|
+
# @!visibility private
|
|
6
|
+
module ExpressionHelpers
|
|
7
|
+
# all coercions simply use #value (i.e. nil or a Integer)
|
|
8
|
+
def coerce(thing)
|
|
9
|
+
@value.coerce(thing)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# addition uses #value
|
|
13
|
+
def +(other)
|
|
14
|
+
@value + other
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# subtraction uses #value
|
|
18
|
+
def -(other)
|
|
19
|
+
@value - other
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# multiplication uses #value
|
|
23
|
+
def *(other)
|
|
24
|
+
@value * other
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# comparison <=> uses #value
|
|
28
|
+
def <=>(other)
|
|
29
|
+
value <=> other
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
4
33
|
# This class models the output of GamesDice::ComplexDie.
|
|
5
34
|
#
|
|
6
35
|
# An object of the class represents the results of a roll of a ComplexDie, including any re-rolls and
|
|
@@ -29,6 +58,7 @@ module GamesDice
|
|
|
29
58
|
#
|
|
30
59
|
class DieResult
|
|
31
60
|
include Comparable
|
|
61
|
+
include ExpressionHelpers
|
|
32
62
|
|
|
33
63
|
# Creates new instance of GamesDice::DieResult. The object can be initialised "empty" or with a first result.
|
|
34
64
|
# @param [Integer,nil] first_roll_result Value for first roll of the die.
|
|
@@ -40,13 +70,9 @@ module GamesDice
|
|
|
40
70
|
end
|
|
41
71
|
|
|
42
72
|
if first_roll_result
|
|
43
|
-
|
|
44
|
-
@roll_reasons = [first_roll_reason]
|
|
45
|
-
@total = @rolls[0]
|
|
73
|
+
init_with_result(first_roll_result, first_roll_reason)
|
|
46
74
|
else
|
|
47
|
-
|
|
48
|
-
@roll_reasons = []
|
|
49
|
-
@total = nil
|
|
75
|
+
init_empty
|
|
50
76
|
end
|
|
51
77
|
@mapped = false
|
|
52
78
|
@value = @total
|
|
@@ -78,32 +104,13 @@ module GamesDice
|
|
|
78
104
|
# @param [Symbol] roll_reason Reason for rolling the die.
|
|
79
105
|
# @return [Integer] Total so far
|
|
80
106
|
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
|
|
107
|
+
raise ArgumentError, "Unrecognised roll reason #{roll_reason}" unless GamesDice::REROLL_TYPES.key?(roll_reason)
|
|
84
108
|
|
|
85
109
|
@rolls << Integer(roll_result)
|
|
86
110
|
@roll_reasons << roll_reason
|
|
87
111
|
@total = 0 if @rolls.length == 1
|
|
88
112
|
|
|
89
|
-
|
|
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
|
|
113
|
+
apply_roll_to_total(roll_reason, roll_result)
|
|
107
114
|
|
|
108
115
|
@mapped = false
|
|
109
116
|
@value = @total
|
|
@@ -125,17 +132,12 @@ module GamesDice
|
|
|
125
132
|
# map description, but does not include the mapped value.
|
|
126
133
|
# @return [String] Explanation of #value.
|
|
127
134
|
def explain_value
|
|
128
|
-
text =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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?
|
|
135
|
+
text = if @rolls.length < 2
|
|
136
|
+
@total.to_s
|
|
137
|
+
else
|
|
138
|
+
explain_value_multiple_rolls
|
|
139
|
+
end
|
|
140
|
+
text += " #{@map_description}" if @mapped && @map_description&.length&.positive?
|
|
139
141
|
text
|
|
140
142
|
end
|
|
141
143
|
|
|
@@ -143,51 +145,62 @@ module GamesDice
|
|
|
143
145
|
# This is mis-named, it doesn't explain the total at all! It is used to generate summaries of keeper dice.
|
|
144
146
|
def explain_total
|
|
145
147
|
text = @total.to_s
|
|
146
|
-
text += " #{@map_description}" if @mapped && @map_description
|
|
148
|
+
text += " #{@map_description}" if @mapped && @map_description&.length&.positive?
|
|
147
149
|
text
|
|
148
150
|
end
|
|
149
151
|
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
def
|
|
153
|
-
|
|
152
|
+
# This is a deep clone, all attributes are also cloned.
|
|
153
|
+
# @return [GamesDice::DieResult]
|
|
154
|
+
def clone
|
|
155
|
+
cloned = GamesDice::DieResult.new
|
|
156
|
+
cloned.instance_variable_set(:@rolls, @rolls.clone)
|
|
157
|
+
cloned.instance_variable_set(:@roll_reasons, @roll_reasons.clone)
|
|
158
|
+
cloned.instance_variable_set(:@total, @total)
|
|
159
|
+
cloned.instance_variable_set(:@value, @value)
|
|
160
|
+
cloned.instance_variable_set(:@mapped, @mapped)
|
|
161
|
+
cloned.instance_variable_set(:@map_description, @map_description)
|
|
162
|
+
cloned
|
|
154
163
|
end
|
|
155
164
|
|
|
156
|
-
|
|
157
|
-
# addition uses #value
|
|
158
|
-
def +(other)
|
|
159
|
-
@value + other
|
|
160
|
-
end
|
|
165
|
+
private
|
|
161
166
|
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
# Splitting this method up further, or flattening it will not make it read better. If
|
|
168
|
+
# we had a few more reroll reasons, they could maybe be grouped and method split up.
|
|
169
|
+
# rubocop:disable Metrics/MethodLength
|
|
170
|
+
def apply_roll_to_total(roll_reason, roll_result)
|
|
171
|
+
case roll_reason
|
|
172
|
+
when :basic, :reroll_new_die, :reroll_new_keeper, :reroll_replace
|
|
173
|
+
@total = roll_result
|
|
174
|
+
when :reroll_add
|
|
175
|
+
@total += roll_result
|
|
176
|
+
when :reroll_subtract
|
|
177
|
+
@total -= roll_result
|
|
178
|
+
when :reroll_use_best
|
|
179
|
+
@total = [@value, roll_result].max
|
|
180
|
+
when :reroll_use_worst
|
|
181
|
+
@total = [@value, roll_result].min
|
|
182
|
+
end
|
|
166
183
|
end
|
|
184
|
+
# rubocop:enable Metrics/MethodLength
|
|
167
185
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
@
|
|
186
|
+
def init_with_result(first_roll_result, first_roll_reason)
|
|
187
|
+
@rolls = [Integer(first_roll_result)]
|
|
188
|
+
@roll_reasons = [first_roll_reason]
|
|
189
|
+
@total = @rolls[0]
|
|
172
190
|
end
|
|
173
191
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
192
|
+
def init_empty
|
|
193
|
+
@rolls = []
|
|
194
|
+
@roll_reasons = []
|
|
195
|
+
@total = nil
|
|
178
196
|
end
|
|
179
197
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
198
|
+
def explain_value_multiple_rolls
|
|
199
|
+
text = "[#{@rolls[0]}"
|
|
200
|
+
text = (1..(@rolls.length - 1)).inject(text) do |so_far, i|
|
|
201
|
+
so_far + GamesDice::REROLL_TYPES[@roll_reasons[i]] + @rolls[i].to_s
|
|
202
|
+
end
|
|
203
|
+
text + "] #{@total}"
|
|
191
204
|
end
|
|
192
205
|
end
|
|
193
206
|
end
|
data/lib/games_dice/marshal.rb
CHANGED
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module GamesDice
|
|
4
|
-
#
|
|
4
|
+
# This class models probability distributions for dice systems.
|
|
5
|
+
#
|
|
6
|
+
# An object of this class represents a single distribution, which might be the result of a complex
|
|
7
|
+
# combination of dice.
|
|
8
|
+
#
|
|
9
|
+
# @example Distribution for a six-sided die
|
|
10
|
+
# probs = GamesDice::Probabilities.for_fair_die( 6 )
|
|
11
|
+
# probs.min # => 1
|
|
12
|
+
# probs.max # => 6
|
|
13
|
+
# probs.expected # => 3.5
|
|
14
|
+
# probs.p_ge( 4 ) # => 0.5
|
|
15
|
+
#
|
|
16
|
+
# @example Adding two distributions
|
|
17
|
+
# pd6 = GamesDice::Probabilities.for_fair_die( 6 )
|
|
18
|
+
# probs = GamesDice::Probabilities.add_distributions( pd6, pd6 )
|
|
19
|
+
# probs.min # => 2
|
|
20
|
+
# probs.max # => 12
|
|
21
|
+
# probs.expected # => 7.0
|
|
22
|
+
# probs.p_ge( 10 ) # => 0.16666666666666669
|
|
23
|
+
#
|
|
5
24
|
class Probabilities
|
|
6
25
|
# @!visibility private
|
|
7
26
|
# Adds support for Marshal, via to_h and from_h methods
|
|
8
|
-
def
|
|
9
|
-
|
|
27
|
+
def marshal_dump
|
|
28
|
+
to_h
|
|
10
29
|
end
|
|
11
30
|
|
|
12
31
|
# @!visibility private
|
|
13
32
|
def self._load(buf)
|
|
33
|
+
# Use of Marshal for general-purpose object serialisation is discouraged. However, this class does support
|
|
34
|
+
# it for backwards-compatibility.
|
|
35
|
+
# rubocop:disable Security/MarshalLoad
|
|
14
36
|
h = Marshal.load buf
|
|
37
|
+
# rubocop:enable Security/MarshalLoad
|
|
15
38
|
from_h h
|
|
16
39
|
end
|
|
17
40
|
end
|
data/lib/games_dice/parser.rb
CHANGED
|
@@ -46,9 +46,11 @@ module GamesDice
|
|
|
46
46
|
rule(:condition_type_and_num) do
|
|
47
47
|
opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) >> comma >> integer.as(:num)
|
|
48
48
|
end
|
|
49
|
+
|
|
49
50
|
rule(:condition_num_and_output) do
|
|
50
51
|
opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) >> comma >> output_string.as(:output)
|
|
51
52
|
end
|
|
53
|
+
|
|
52
54
|
rule(:num_and_type) { integer.as(:num) >> comma >> ctl_string.as(:type) }
|
|
53
55
|
|
|
54
56
|
rule(:reroll_params) { condition_type_and_num | condition_and_type | condition_only }
|
|
@@ -78,25 +80,41 @@ module GamesDice
|
|
|
78
80
|
def parse(dice_description)
|
|
79
81
|
dice_description = dice_description.to_s.strip
|
|
80
82
|
# Force first item to start '+' for simpler parse rules
|
|
81
|
-
dice_description = "+#{dice_description}" unless
|
|
82
|
-
dice_expressions = super
|
|
83
|
-
{
|
|
83
|
+
dice_description = "+#{dice_description}" unless /\A[+-]/.match?(dice_description)
|
|
84
|
+
dice_expressions = super
|
|
85
|
+
{
|
|
86
|
+
bunches: ParseTreeProcessor.collect_bunches(dice_expressions),
|
|
87
|
+
offset: ParseTreeProcessor.collect_offset(dice_expressions)
|
|
88
|
+
}
|
|
84
89
|
end
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
# Class converts parse tree to GamesDice hash model
|
|
92
|
+
# @!visibility private
|
|
93
|
+
class ParseTreeProcessor
|
|
94
|
+
class << self
|
|
95
|
+
def collect_bunches(dice_expressions)
|
|
96
|
+
dice_expressions[:bunches].select { |h| h[:ndice] }.map do |in_hash|
|
|
97
|
+
out_hash = {}
|
|
98
|
+
collect_bunch_basics(in_hash, out_hash)
|
|
99
|
+
collect_bunch_multiplier(in_hash, out_hash) if in_hash[:op]
|
|
100
|
+
|
|
101
|
+
in_hash[:mods]&.each do |mod|
|
|
102
|
+
collect_bunch_modifier(mod, out_hash)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
out_hash
|
|
106
|
+
end
|
|
107
|
+
end
|
|
87
108
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# Convert integers
|
|
92
|
-
%i[ndice sides].each do |s|
|
|
93
|
-
next unless in_hash[s]
|
|
109
|
+
def collect_bunch_basics(in_hash, out_hash)
|
|
110
|
+
%i[ndice sides].each do |s|
|
|
111
|
+
next unless in_hash[s]
|
|
94
112
|
|
|
95
|
-
|
|
113
|
+
out_hash[s] = in_hash[s].to_i
|
|
114
|
+
end
|
|
96
115
|
end
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
if in_hash[:op]
|
|
117
|
+
def collect_bunch_multiplier(in_hash, out_hash)
|
|
100
118
|
optype = in_hash[:op].to_s
|
|
101
119
|
out_hash[:multiplier] = case optype
|
|
102
120
|
when '+' then 1
|
|
@@ -104,116 +122,124 @@ module GamesDice
|
|
|
104
122
|
end
|
|
105
123
|
end
|
|
106
124
|
|
|
107
|
-
|
|
108
|
-
in_hash[:mods]&.each do |mod|
|
|
125
|
+
def collect_bunch_modifier(mod, out_hash)
|
|
109
126
|
if mod[:alias]
|
|
110
|
-
collect_alias_modifier mod, out_hash
|
|
127
|
+
ParseTreeBunchModifier.collect_alias_modifier mod, out_hash
|
|
111
128
|
elsif mod[:keep]
|
|
112
|
-
collect_keeper_rule mod, out_hash
|
|
129
|
+
ParseTreeBunchModifier.collect_keeper_rule mod, out_hash
|
|
113
130
|
elsif mod[:map]
|
|
114
|
-
|
|
115
|
-
collect_map_rule mod, out_hash
|
|
131
|
+
ParseTreeBunchModifier.collect_map_rule mod, out_hash
|
|
116
132
|
elsif mod[:reroll]
|
|
117
|
-
|
|
118
|
-
collect_reroll_rule mod, out_hash
|
|
133
|
+
ParseTreeBunchModifier.collect_reroll_rule mod, out_hash
|
|
119
134
|
end
|
|
120
135
|
end
|
|
121
136
|
|
|
122
|
-
|
|
137
|
+
def collect_offset(dice_expressions)
|
|
138
|
+
dice_expressions[:bunches].select { |h| h[:constant] }.inject(0) do |total, in_hash|
|
|
139
|
+
c = in_hash[:constant].to_i
|
|
140
|
+
optype = in_hash[:op].to_s
|
|
141
|
+
if optype == '+'
|
|
142
|
+
total + c
|
|
143
|
+
else
|
|
144
|
+
total - c
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
123
148
|
end
|
|
124
149
|
end
|
|
125
150
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
# Class for collating bunch data into bunch construction hash
|
|
152
|
+
# @!visibility private
|
|
153
|
+
class ParseTreeBunchModifier
|
|
154
|
+
class << self
|
|
155
|
+
# Called when we have a single letter convenient alias for common dice adjustments
|
|
156
|
+
def collect_alias_modifier(alias_mod, out_hash)
|
|
157
|
+
alias_name = alias_mod[:alias].to_s
|
|
158
|
+
case alias_name
|
|
159
|
+
when 'x' # Exploding re-roll
|
|
160
|
+
out_hash[:rerolls] ||= []
|
|
161
|
+
out_hash[:rerolls] << [out_hash[:sides], :==, :reroll_add]
|
|
162
|
+
end
|
|
134
163
|
end
|
|
135
|
-
total
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
164
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
147
|
-
end
|
|
165
|
+
# Called for any parsed reroll rule
|
|
166
|
+
def collect_reroll_rule(reroll_mod, out_hash)
|
|
167
|
+
out_hash[:rerolls] ||= []
|
|
168
|
+
if reroll_mod[:simple_value]
|
|
169
|
+
out_hash[:rerolls] << [reroll_mod[:simple_value].to_i, :>=, :reroll_replace]
|
|
170
|
+
return
|
|
171
|
+
end
|
|
148
172
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
out_hash[:rerolls] ||= []
|
|
152
|
-
if reroll_mod[:simple_value]
|
|
153
|
-
out_hash[:rerolls] << [reroll_mod[:simple_value].to_i, :>=, :reroll_replace]
|
|
154
|
-
return
|
|
155
|
-
end
|
|
173
|
+
collect_complex_reroll_rule(reroll_mod, out_hash)
|
|
174
|
+
end
|
|
156
175
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
def collect_complex_reroll_rule(reroll_mod, out_hash)
|
|
177
|
+
# Typical reroll_mod: {:reroll=>"r"@5, :condition=>{:compare_num=>"10"@7}, :type=>"add"@10}
|
|
178
|
+
op = get_op_symbol(reroll_mod[:condition][:comparison] || '==')
|
|
179
|
+
v = reroll_mod[:condition][:compare_num].to_i
|
|
180
|
+
type = :"reroll_#{reroll_mod[:type] || 'replace'}"
|
|
181
|
+
|
|
182
|
+
out_hash[:rerolls] << if reroll_mod[:num]
|
|
183
|
+
[v, op, type, reroll_mod[:num].to_i]
|
|
184
|
+
else
|
|
185
|
+
[v, op, type]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
161
188
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
[v, op, type]
|
|
166
|
-
end
|
|
167
|
-
end
|
|
189
|
+
# Called for any parsed keeper mode
|
|
190
|
+
def collect_keeper_rule(keeper_mod, out_hash)
|
|
191
|
+
raise 'Cannot set keepers for a bunch twice' if out_hash[:keep_mode]
|
|
168
192
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
193
|
+
if keeper_mod[:simple_value]
|
|
194
|
+
out_hash[:keep_mode] = :keep_best
|
|
195
|
+
out_hash[:keep_number] = keeper_mod[:simple_value].to_i
|
|
196
|
+
return
|
|
197
|
+
end
|
|
172
198
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
end
|
|
199
|
+
# Typical keeper_mod: {:keep=>"k"@5, :num=>"1"@7, :type=>"worst"@9}
|
|
200
|
+
out_hash[:keep_number] = keeper_mod[:num].to_i
|
|
201
|
+
out_hash[:keep_mode] = :"keep_#{keeper_mod[:type] || 'best'}"
|
|
202
|
+
end
|
|
178
203
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
204
|
+
# Called for any parsed map mode
|
|
205
|
+
def collect_map_rule(map_mod, out_hash)
|
|
206
|
+
out_hash[:maps] ||= []
|
|
207
|
+
if map_mod[:simple_value]
|
|
208
|
+
out_hash[:maps] << [map_mod[:simple_value].to_i, :<=, 1]
|
|
209
|
+
return
|
|
210
|
+
end
|
|
183
211
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
out_hash[:maps] ||= []
|
|
187
|
-
if map_mod[:simple_value]
|
|
188
|
-
out_hash[:maps] << [map_mod[:simple_value].to_i, :<=, 1]
|
|
189
|
-
return
|
|
190
|
-
end
|
|
212
|
+
collect_complex_map_rule(map_mod, out_hash)
|
|
213
|
+
end
|
|
191
214
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
215
|
+
def collect_complex_map_rule(map_mod, out_hash)
|
|
216
|
+
# Typical map_mod: {:map=>"m"@4, :condition=>{:compare_num=>"5"@6}, :num=>"2"@8, :output=>"Qwerty"@10}
|
|
217
|
+
op = get_op_symbol(map_mod[:condition][:comparison] || '>=')
|
|
218
|
+
v = map_mod[:condition][:compare_num].to_i
|
|
219
|
+
out_val = 1
|
|
220
|
+
out_val = map_mod[:num].to_i if map_mod[:num]
|
|
221
|
+
|
|
222
|
+
out_hash[:maps] << if map_mod[:output]
|
|
223
|
+
[v, op, out_val, map_mod[:output].to_s]
|
|
224
|
+
else
|
|
225
|
+
[v, op, out_val]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
204
228
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
# The dice description language uses (r).op.x, whilst GamesDice::RerollRule uses x.op.(r), so
|
|
230
|
+
# as well as converting to a symbol, we must reverse sense of input to constructor
|
|
231
|
+
OP_CONVERSION = {
|
|
232
|
+
'==' => :==,
|
|
233
|
+
'>=' => :<=,
|
|
234
|
+
'>' => :<,
|
|
235
|
+
'<' => :>,
|
|
236
|
+
'<=' => :>=
|
|
237
|
+
}.freeze
|
|
238
|
+
|
|
239
|
+
def get_op_symbol(parsed_op_string)
|
|
240
|
+
OP_CONVERSION[parsed_op_string.to_s]
|
|
241
|
+
end
|
|
242
|
+
end
|
|
217
243
|
end
|
|
218
244
|
end
|
|
219
245
|
end
|
data/lib/games_dice/version.rb
CHANGED
data/lib/games_dice.rb
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'games_dice/version'
|
|
4
|
-
begin
|
|
5
|
-
require 'games_dice/games_dice'
|
|
6
|
-
rescue LoadError
|
|
7
|
-
require 'games_dice/probabilities'
|
|
8
|
-
end
|
|
9
4
|
require 'games_dice/constants'
|
|
10
5
|
require 'games_dice/die'
|
|
11
6
|
require 'games_dice/die_result'
|
|
@@ -15,20 +10,23 @@ require 'games_dice/complex_die'
|
|
|
15
10
|
require 'games_dice/bunch'
|
|
16
11
|
require 'games_dice/dice'
|
|
17
12
|
require 'games_dice/parser'
|
|
13
|
+
require 'games_dice/games_dice'
|
|
18
14
|
require 'games_dice/marshal'
|
|
19
15
|
|
|
16
|
+
# GamesDice is a library for simulating dice combinations used in dice and board games.
|
|
20
17
|
module GamesDice
|
|
21
|
-
# @!visibility private
|
|
22
|
-
@@parser = GamesDice::Parser.new
|
|
23
|
-
|
|
24
18
|
# Creates an instance of GamesDice::Dice from a string description.
|
|
25
19
|
# @param [String] dice_description Uses a variation of common game notation, examples: '1d6', '3d8+1d4+7', '5d10k2'
|
|
26
20
|
# @param [#rand] prng Optional random number generator, default is to use Ruby's built-in #rand()
|
|
27
21
|
# @return [GamesDice::Dice] A new dice object.
|
|
28
22
|
#
|
|
29
23
|
def self.create(dice_description, prng = nil)
|
|
30
|
-
parsed =
|
|
24
|
+
parsed = parser.parse(dice_description)
|
|
31
25
|
parsed[:bunches].each { |bunch| bunch.merge!(prng: prng) } if prng
|
|
32
26
|
GamesDice::Dice.new(parsed[:bunches], parsed[:offset])
|
|
33
27
|
end
|
|
28
|
+
|
|
29
|
+
def self.parser
|
|
30
|
+
@parser ||= GamesDice::Parser.new
|
|
31
|
+
end
|
|
34
32
|
end
|