games_dice 0.4.0 → 0.4.2
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 +38 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +41 -3
- data/.yardopts +1 -1
- data/CHANGELOG.md +12 -0
- data/Gemfile +10 -0
- data/README.md +11 -33
- data/Rakefile +2 -18
- data/ext/games_dice/games_dice.c +1 -1
- data/ext/games_dice/probabilities.c +127 -8
- data/ext/games_dice/probabilities.h +1 -1
- 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 +188 -196
- data/spec/readme_spec.rb +28 -22
- data/spec/reroll_rule_spec.rb +15 -15
- metadata +16 -139
- data/.travis.yml +0 -11
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
|