games_dice 0.3.12 → 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.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +38 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +53 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +18 -0
  7. data/Gemfile +12 -0
  8. data/README.md +11 -33
  9. data/Rakefile +10 -23
  10. data/ext/games_dice/extconf.rb +4 -22
  11. data/ext/games_dice/games_dice.c +1 -1
  12. data/ext/games_dice/probabilities.c +128 -9
  13. data/ext/games_dice/probabilities.h +1 -1
  14. data/games_dice.gemspec +22 -36
  15. data/lib/games_dice/bunch.rb +203 -247
  16. data/lib/games_dice/bunch_helpers.rb +68 -0
  17. data/lib/games_dice/complex_die.rb +151 -270
  18. data/lib/games_dice/complex_die_helpers.rb +260 -60
  19. data/lib/games_dice/constants.rb +10 -10
  20. data/lib/games_dice/dice.rb +153 -143
  21. data/lib/games_dice/die.rb +101 -97
  22. data/lib/games_dice/die_result.rb +206 -189
  23. data/lib/games_dice/map_rule.rb +72 -70
  24. data/lib/games_dice/marshal.rb +41 -13
  25. data/lib/games_dice/parser.rb +245 -218
  26. data/lib/games_dice/reroll_rule.rb +76 -77
  27. data/lib/games_dice/version.rb +4 -1
  28. data/lib/games_dice.rb +23 -25
  29. data/spec/bunch_spec.rb +399 -420
  30. data/spec/complex_die_spec.rb +314 -305
  31. data/spec/dice_spec.rb +33 -34
  32. data/spec/die_result_spec.rb +174 -181
  33. data/spec/die_spec.rb +81 -81
  34. data/spec/helpers.rb +25 -25
  35. data/spec/map_rule_spec.rb +40 -44
  36. data/spec/parser_spec.rb +106 -82
  37. data/spec/probability_spec.rb +522 -526
  38. data/spec/readme_spec.rb +410 -390
  39. data/spec/reroll_rule_spec.rb +40 -44
  40. metadata +17 -129
  41. data/.travis.yml +0 -14
  42. data/lib/games_dice/prob_helpers.rb +0 -259
  43. data/lib/games_dice/probabilities.rb +0 -244
data/spec/die_spec.rb CHANGED
@@ -1,81 +1,81 @@
1
- require 'helpers'
2
-
3
- describe GamesDice::Die do
4
-
5
- before do
6
- # Set state of default PRNG
7
- srand(4567)
8
- end
9
-
10
- describe "#new" do
11
- it "should return an object that represents e.g. a six-sided die" do
12
- die = GamesDice::Die.new(6)
13
- die.min.should == 1
14
- die.max.should == 6
15
- die.sides.should == 6
16
- end
17
-
18
- it "should accept any object with a rand(Integer) method as the second param" do
19
- prng = TestPRNG.new()
20
- die = GamesDice::Die.new(20,prng)
21
- [16,7,3,11,16,18,20,7].each do |expected|
22
- die.roll.should == expected
23
- die.result.should == expected
24
- end
25
- end
26
- end
27
-
28
- describe "#roll and #result" do
29
- it "should return results based on Ruby's internal rand() by default" do
30
- die = GamesDice::Die.new(10)
31
- [5,4,10,4,7,8,1,9].each do |expected|
32
- die.roll.should == expected
33
- die.result.should == expected
34
- end
35
- end
36
- end
37
-
38
- describe "#min and #max" do
39
- it "should calculate correct min, max" do
40
- die = GamesDice::Die.new(20)
41
- die.min.should == 1
42
- die.max.should == 20
43
- end
44
- end
45
-
46
- describe "#probabilities" do
47
- it "should return the die's probability distribution as a GamesDice::Probabilities object" do
48
- die = GamesDice::Die.new(6)
49
- probs = die.probabilities
50
- probs.should be_a GamesDice::Probabilities
51
-
52
- probs.to_h.should be_valid_distribution
53
-
54
- probs.p_eql(1).should be_within(1e-10).of 1/6.0
55
- probs.p_eql(2).should be_within(1e-10).of 1/6.0
56
- probs.p_eql(3).should be_within(1e-10).of 1/6.0
57
- probs.p_eql(4).should be_within(1e-10).of 1/6.0
58
- probs.p_eql(5).should be_within(1e-10).of 1/6.0
59
- probs.p_eql(6).should be_within(1e-10).of 1/6.0
60
-
61
- probs.expected.should be_within(1e-10).of 3.5
62
- end
63
- end
64
-
65
- describe "#all_values" do
66
- it "should return array with one result value per side" do
67
- die = GamesDice::Die.new(8)
68
- die.all_values.should == [1,2,3,4,5,6,7,8]
69
- end
70
- end
71
-
72
- describe "#each_value" do
73
- it "should iterate through all sides of the die" do
74
- die = GamesDice::Die.new(10)
75
- arr = []
76
- die.each_value { |x| arr << x }
77
- arr.should == [1,2,3,4,5,6,7,8,9,10]
78
- end
79
- end
80
-
81
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'helpers'
4
+
5
+ describe GamesDice::Die do
6
+ before do
7
+ # Set state of default PRNG
8
+ srand(4567)
9
+ end
10
+
11
+ describe '#new' do
12
+ it 'return an object that represents e.g. a six-sided die' do
13
+ die = described_class.new(6)
14
+ expect(die.min).to be 1
15
+ expect(die.max).to be 6
16
+ expect(die.sides).to be 6
17
+ end
18
+
19
+ it 'accept any object with a rand(Integer) method as the second param' do
20
+ prng = TestPRNG.new
21
+ die = described_class.new(20, prng)
22
+ [16, 7, 3, 11, 16, 18, 20, 7].each do |expected|
23
+ expect(die.roll).to eql expected
24
+ expect(die.result).to eql expected
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#roll and #result' do
30
+ it "return results based on Ruby's internal rand() by default" do
31
+ die = described_class.new(10)
32
+ [5, 4, 10, 4, 7, 8, 1, 9].each do |expected|
33
+ expect(die.roll).to eql expected
34
+ expect(die.result).to eql expected
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '#min and #max' do
40
+ it 'calculate correct min, max' do
41
+ die = described_class.new(20)
42
+ expect(die.min).to be 1
43
+ expect(die.max).to be 20
44
+ end
45
+ end
46
+
47
+ describe '#probabilities' do
48
+ it "return the die's probability distribution as a GamesDice::Probabilities object" do
49
+ die = described_class.new(6)
50
+ probs = die.probabilities
51
+ expect(probs).to be_a GamesDice::Probabilities
52
+
53
+ expect(probs.to_h).to be_valid_distribution
54
+
55
+ expect(probs.p_eql(1)).to be_within(1e-10).of 1 / 6.0
56
+ expect(probs.p_eql(2)).to be_within(1e-10).of 1 / 6.0
57
+ expect(probs.p_eql(3)).to be_within(1e-10).of 1 / 6.0
58
+ expect(probs.p_eql(4)).to be_within(1e-10).of 1 / 6.0
59
+ expect(probs.p_eql(5)).to be_within(1e-10).of 1 / 6.0
60
+ expect(probs.p_eql(6)).to be_within(1e-10).of 1 / 6.0
61
+
62
+ expect(probs.expected).to be_within(1e-10).of 3.5
63
+ end
64
+ end
65
+
66
+ describe '#all_values' do
67
+ it 'return array with one result value per side' do
68
+ die = described_class.new(8)
69
+ expect(die.all_values).to eql [1, 2, 3, 4, 5, 6, 7, 8]
70
+ end
71
+ end
72
+
73
+ describe '#each_value' do
74
+ it 'iterate through all sides of the die' do
75
+ die = described_class.new(10)
76
+ arr = []
77
+ die.each_value { |x| arr << x }
78
+ expect(arr).to eql [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
79
+ end
80
+ end
81
+ end
data/spec/helpers.rb CHANGED
@@ -1,35 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # games_dice/spec/helpers.rb
2
4
  require 'pathname'
3
- require 'coveralls'
4
- Coveralls.wear!
5
5
 
6
6
  require 'games_dice'
7
7
 
8
-
9
- def fixture name
10
- (Pathname.new(__FILE__).dirname + "fixtures" + name).to_s
8
+ def fixture(name)
9
+ "#{__dir__}/fixtures/#{name}"
11
10
  end
12
11
 
13
12
  # TestPRNG tests short predictable series
14
13
  class TestPRNG
15
14
  def initialize
16
- @numbers = [0.123,0.234,0.345,0.999,0.876,0.765,0.543,0.111,0.333,0.777]
15
+ @numbers = [0.123, 0.234, 0.345, 0.999, 0.876, 0.765, 0.543, 0.111, 0.333, 0.777]
17
16
  end
18
- def rand(n)
19
- Integer( n * @numbers.pop )
17
+
18
+ def rand(num)
19
+ Integer(num * @numbers.pop)
20
20
  end
21
21
  end
22
22
 
23
23
  # TestPRNGMax checks behaviour of re-rolls
24
24
  class TestPRNGMax
25
- def rand(n)
26
- Integer( n ) - 1
25
+ def rand(num)
26
+ Integer(num) - 1
27
27
  end
28
28
  end
29
29
 
30
30
  # TestPRNGMin checks behaviour of re-rolls
31
31
  class TestPRNGMin
32
- def rand(n)
32
+ def rand(_num)
33
33
  1
34
34
  end
35
35
  end
@@ -42,33 +42,33 @@ end
42
42
  RSpec::Matchers.define :be_valid_distribution do
43
43
  match do |given|
44
44
  @error = nil
45
- if ! given.is_a?(Hash)
45
+ if !given.is_a?(Hash)
46
46
  @error = "distribution should be a Hash, but it is a #{given.class}"
47
- elsif given.keys.any? { |k| ! k.is_a?(Integer) }
48
- bad_key = given.keys.first { |k| ! k.is_a?(Integer) }
47
+ elsif given.keys.any? { |k| !k.is_a?(Integer) }
48
+ bad_key = given.keys.first { |k| !k.is_a?(Integer) }
49
49
  @error = "all keys should be Integers, but found '#{bad_key.inspect}' which is a #{bad_key.class}"
50
- elsif given.values.any? { |v| ! v.is_a?(Float) }
51
- bad_value = given.values.find { |v| ! v.is_a?(Float) }
50
+ elsif given.values.any? { |v| !v.is_a?(Float) }
51
+ bad_value = given.values.find { |v| !v.is_a?(Float) }
52
52
  @error = "all values should be Floats, but found '#{bad_value.inspect}' which is a #{bad_value.class}"
53
53
  elsif given.values.any? { |v| v < 0.0 || v > 1.0 }
54
54
  bad_value = given.values.find { |v| v < 0.0 || v > 1.0 }
55
55
  @error = "all values should be in range (0.0..1.0), but found #{bad_value}"
56
- elsif (1.0 - given.values.inject(:+)).abs > 1e-6
57
- total_probs = given.values.inject(:+)
56
+ elsif (1.0 - given.values.sum).abs > 1e-6
57
+ total_probs = given.values.sum
58
58
  @error = "sum of values should be 1.0, but got #{total_probs}"
59
59
  end
60
- ! @error
60
+ !@error
61
61
  end
62
62
 
63
- failure_message do |given|
64
- @error ? @error : 'Distribution is valid and complete'
63
+ failure_message do |_given|
64
+ @error || 'Distribution is valid and complete'
65
65
  end
66
66
 
67
- failure_message_when_negated do |given|
68
- @error ? @error : 'Distribution is valid and complete'
67
+ failure_message_when_negated do |_given|
68
+ @error || 'Distribution is valid and complete'
69
69
  end
70
70
 
71
- description do |given|
72
- "a hash describing a complete probability distribution of integer results"
71
+ description do |_given|
72
+ 'a hash describing a complete probability distribution of integer results'
73
73
  end
74
74
  end
@@ -1,44 +1,40 @@
1
- require 'helpers'
2
-
3
- describe GamesDice::MapRule do
4
-
5
- describe "#new" do
6
-
7
- it "should accept self-consistent operator/value pairs as a trigger" do
8
- GamesDice::MapRule.new( 5, :>, 1 )
9
- GamesDice::MapRule.new( (1..5), :member?, 17 )
10
- end
11
-
12
- it "should reject inconsistent operator/value pairs for a trigger" do
13
- lambda { GamesDice::MapRule.new( 5, :member?, -1 ) }.should raise_error( ArgumentError )
14
- lambda { GamesDice::MapRule.new( (1..5), :>, 12 ) }.should raise_error( ArgumentError )
15
- end
16
-
17
- it "should reject non-Integer map results" do
18
- lambda { GamesDice::MapRule.new( 5, :>, :reroll_again ) }.should raise_error( TypeError )
19
- lambda { GamesDice::MapRule.new( (1..5), :member?, 'foo' ) }.should raise_error( TypeError )
20
- end
21
-
22
- end
23
-
24
- describe '#map_from' do
25
-
26
- it "should return the mapped value for a match" do
27
- rule = GamesDice::MapRule.new( 5, :>, -1 )
28
- rule.map_from(4).should == -1
29
-
30
- rule = GamesDice::MapRule.new( (1..5), :member?, 3 )
31
- rule.map_from(4).should == 3
32
- end
33
-
34
- it "should return nil for no match" do
35
- rule = GamesDice::MapRule.new( 5, :>, -1 )
36
- rule.map_from(6).should be_nil
37
-
38
- rule = GamesDice::MapRule.new( (1..5), :member?, 3 )
39
- rule.map_from(6).should be_nil
40
- end
41
-
42
- end
43
-
44
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'helpers'
4
+
5
+ describe GamesDice::MapRule do
6
+ describe '#new' do
7
+ it 'accept self-consistent operator/value pairs as a trigger' do
8
+ expect { described_class.new(5, :>, 1) }.not_to raise_error
9
+ expect { described_class.new(1..5, :member?, 17) }.not_to raise_error
10
+ end
11
+
12
+ it 'reject inconsistent operator/value pairs for a trigger' do
13
+ expect { described_class.new(5, :member?, -1) }.to raise_error(ArgumentError)
14
+ expect { described_class.new(1..5, :>, 12) }.to raise_error(ArgumentError)
15
+ end
16
+
17
+ it 'reject non-Integer map results' do
18
+ expect { described_class.new(5, :>, :reroll_again) }.to raise_error(TypeError)
19
+ expect { described_class.new(1..5, :member?, 'foo') }.to raise_error(TypeError)
20
+ end
21
+ end
22
+
23
+ describe '#map_from' do
24
+ it 'return the mapped value for a match' do
25
+ rule = described_class.new(5, :>, -1)
26
+ expect(rule.map_from(4)).to be(-1)
27
+
28
+ rule = described_class.new(1..5, :member?, 3)
29
+ expect(rule.map_from(4)).to be 3
30
+ end
31
+
32
+ it 'return nil for no match' do
33
+ rule = described_class.new(5, :>, -1)
34
+ expect(rule.map_from(6)).to be_nil
35
+
36
+ rule = described_class.new(1..5, :member?, 3)
37
+ expect(rule.map_from(6)).to be_nil
38
+ end
39
+ end
40
+ end
data/spec/parser_spec.rb CHANGED
@@ -1,82 +1,106 @@
1
- require 'helpers'
2
-
3
- describe GamesDice::Parser do
4
-
5
- describe "#parse" do
6
- let(:parser) { GamesDice::Parser.new }
7
-
8
- it "should parse simple dice sums" do
9
- variations = {
10
- '1d6' => { :bunches => [{:ndice=>1, :sides=>6, :multiplier=>1}], :offset => 0 },
11
- '2d8-1d4' => { :bunches => [{:ndice=>2, :sides=>8, :multiplier=>1},{:ndice=>1, :sides=>4, :multiplier=>-1}], :offset => 0 },
12
- '+ 2d10 - 1d4 ' => { :bunches => [{:ndice=>2, :sides=>10, :multiplier=>1},{:ndice=>1, :sides=>4, :multiplier=>-1}], :offset => 0 },
13
- ' + 3d6 + 12 ' => { :bunches => [{:ndice=>3, :sides=>6, :multiplier=>1}], :offset => 12 },
14
- '-7 + 2d4 + 1 ' => { :bunches => [{:ndice=>2, :sides=>4, :multiplier=>1}], :offset => -6 },
15
- '- 3 + 7d20 - 1 ' => { :bunches => [{:ndice=>7, :sides=>20, :multiplier=>1}], :offset => -4 },
16
- ' - 2d4' => { :bunches => [{:ndice=>2, :sides=>4, :multiplier=>-1}], :offset => 0 },
17
- '3d12+5+2d8+1d6' => { :bunches => [{:ndice=>3, :sides=>12, :multiplier=>1},{:ndice=>2, :sides=>8, :multiplier=>1},{:ndice=>1, :sides=>6, :multiplier=>1}], :offset => 5 },
18
- }
19
-
20
- variations.each do |input,expected_output|
21
- parser.parse( input ).should == expected_output
22
- end
23
- end
24
-
25
- it "should parse 'NdXrY' as 'roll N times X-sided dice, re-roll and replace a Y or less (once)'" do
26
- variations = {
27
- '1d6r1' => { :bunches => [{:ndice=>1, :sides=>6, :multiplier=>1, :rerolls=>[ [1,:>=,:reroll_replace] ]}], :offset => 0 },
28
- '2d20r7' => { :bunches => [{:ndice=>2, :sides=>20, :multiplier=>1, :rerolls=>[ [7,:>=,:reroll_replace] ]}], :offset => 0 },
29
- '1d8r2' => { :bunches => [{:ndice=>1, :sides=>8, :multiplier=>1, :rerolls=>[ [2,:>=,:reroll_replace] ]}], :offset => 0 },
30
- }
31
-
32
- variations.each do |input,expected_output|
33
- parser.parse( input ).should == expected_output
34
- end
35
- end
36
-
37
- it "should parse 'NdXmZ' as 'roll N times X-sided dice, a value of Z or more equals 1 (success)'" do
38
- variations = {
39
- '5d6m6' => { :bunches => [{:ndice=>5, :sides=>6, :multiplier=>1, :maps=>[ [6,:<=,1] ]}], :offset => 0 },
40
- '2d10m7' => { :bunches => [{:ndice=>2, :sides=>10, :multiplier=>1, :maps=>[ [7,:<=,1] ]}], :offset => 0 },
41
- }
42
-
43
- variations.each do |input,expected_output|
44
- parser.parse( input ).should == expected_output
45
- end
46
- end
47
-
48
- it "should parse 'NdXkC' as 'roll N times X-sided dice, add together the best C'" do
49
- variations = {
50
- '5d10k3' => { :bunches => [{:ndice=>5, :sides=>10, :multiplier=>1, :keep_mode=>:keep_best, :keep_number=>3}], :offset => 0 },
51
- }
52
-
53
- variations.each do |input,expected_output|
54
- parser.parse( input ).should == expected_output
55
- end
56
- end
57
-
58
- it "should parse 'NdXx' as 'roll N times X-sided *exploding* dice'" do
59
- variations = {
60
- '5d10x' => { :bunches => [{:ndice=>5, :sides=>10, :multiplier=>1, :rerolls=>[ [10,:==,:reroll_add] ]}], :offset => 0 },
61
- '3d6x' => { :bunches => [{:ndice=>3, :sides=>6, :multiplier=>1, :rerolls=>[ [6,:==,:reroll_add] ]}], :offset => 0 },
62
- }
63
-
64
- variations.each do |input,expected_output|
65
- parser.parse( input ).should == expected_output
66
- end
67
- end
68
-
69
- it "should successfully parse combinations of modifiers in any valid order" do
70
- variations = {
71
- '5d10r1x' => { :bunches => [{:ndice=>5, :sides=>10, :multiplier=>1, :rerolls=>[ [1,:>=,:reroll_replace], [10,:==,:reroll_add] ]}], :offset => 0 },
72
- '3d6xk2' => { :bunches => [{:ndice=>3, :sides=>6, :multiplier=>1, :rerolls=>[ [6,:==,:reroll_add] ], :keep_mode=>:keep_best, :keep_number=>2 }], :offset => 0 },
73
- '4d6m8x' => { :bunches => [{:ndice=>4, :sides=>6, :multiplier=>1, :maps=>[ [8,:<=,1] ], :rerolls=>[ [6,:==,:reroll_add] ] }], :offset => 0 },
74
- }
75
-
76
- variations.each do |input,expected_output|
77
- parser.parse( input ).should == expected_output
78
- end
79
- end
80
-
81
- end # describe "#parse"
82
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'helpers'
4
+
5
+ describe GamesDice::Parser do
6
+ describe '#parse' do
7
+ let(:parser) { described_class.new }
8
+
9
+ it 'parse simple dice sums' do
10
+ variations = {
11
+ '1d6' => { bunches: [{ ndice: 1, sides: 6, multiplier: 1 }], offset: 0 },
12
+ '2d8-1d4' => { bunches: [{ ndice: 2, sides: 8, multiplier: 1 }, { ndice: 1, sides: 4, multiplier: -1 }],
13
+ offset: 0 },
14
+ '+ 2d10 - 1d4 ' => {
15
+ bunches: [{ ndice: 2, sides: 10, multiplier: 1 },
16
+ { ndice: 1, sides: 4, multiplier: -1 }], offset: 0
17
+ },
18
+ ' + 3d6 + 12 ' => { bunches: [{ ndice: 3, sides: 6, multiplier: 1 }], offset: 12 },
19
+ '-7 + 2d4 + 1 ' => { bunches: [{ ndice: 2, sides: 4, multiplier: 1 }], offset: -6 },
20
+ '- 3 + 7d20 - 1 ' => { bunches: [{ ndice: 7, sides: 20, multiplier: 1 }], offset: -4 },
21
+ ' - 2d4' => { bunches: [{ ndice: 2, sides: 4, multiplier: -1 }], offset: 0 },
22
+ '3d12+5+2d8+1d6' => {
23
+ bunches: [{ ndice: 3, sides: 12, multiplier: 1 }, { ndice: 2, sides: 8, multiplier: 1 },
24
+ { ndice: 1, sides: 6, multiplier: 1 }], offset: 5
25
+ }
26
+ }
27
+
28
+ variations.each do |input, expected_output|
29
+ expect(parser.parse(input)).to eql expected_output
30
+ end
31
+ end
32
+
33
+ it "parse 'NdXrY' as 'roll N times X-sided dice, re-roll and replace a Y or less (once)'" do
34
+ variations = {
35
+ '1d6r1' => { bunches: [{ ndice: 1, sides: 6, multiplier: 1, rerolls: [[1, :>=, :reroll_replace]] }],
36
+ offset: 0 },
37
+ '2d20r7' => { bunches: [{ ndice: 2, sides: 20, multiplier: 1, rerolls: [[7, :>=, :reroll_replace]] }],
38
+ offset: 0 },
39
+ '1d8r2' => { bunches: [{ ndice: 1, sides: 8, multiplier: 1, rerolls: [[2, :>=, :reroll_replace]] }],
40
+ offset: 0 }
41
+ }
42
+
43
+ variations.each do |input, expected_output|
44
+ expect(parser.parse(input)).to eql expected_output
45
+ end
46
+ end
47
+
48
+ it "parse 'NdXmZ' as 'roll N times X-sided dice, a value of Z or more equals 1 (success)'" do
49
+ variations = {
50
+ '5d6m6' => { bunches: [{ ndice: 5, sides: 6, multiplier: 1, maps: [[6, :<=, 1]] }],
51
+ offset: 0 },
52
+ '2d10m7' => { bunches: [{ ndice: 2, sides: 10, multiplier: 1, maps: [[7, :<=, 1]] }],
53
+ offset: 0 }
54
+ }
55
+
56
+ variations.each do |input, expected_output|
57
+ expect(parser.parse(input)).to eql expected_output
58
+ end
59
+ end
60
+
61
+ it "parse 'NdXkC' as 'roll N times X-sided dice, add together the best C'" do
62
+ variations = {
63
+ '5d10k3' => { bunches: [{ ndice: 5, sides: 10, multiplier: 1, keep_mode: :keep_best, keep_number: 3 }],
64
+ offset: 0 }
65
+ }
66
+
67
+ variations.each do |input, expected_output|
68
+ expect(parser.parse(input)).to eql expected_output
69
+ end
70
+ end
71
+
72
+ it "parse 'NdXx' as 'roll N times X-sided *exploding* dice'" do
73
+ variations = {
74
+ '5d10x' => { bunches: [{ ndice: 5, sides: 10, multiplier: 1, rerolls: [[10, :==, :reroll_add]] }],
75
+ offset: 0 },
76
+ '3d6x' => { bunches: [{ ndice: 3, sides: 6, multiplier: 1, rerolls: [[6, :==, :reroll_add]] }],
77
+ offset: 0 }
78
+ }
79
+
80
+ variations.each do |input, expected_output|
81
+ expect(parser.parse(input)).to eql expected_output
82
+ end
83
+ end
84
+
85
+ it 'successfully parse combinations of modifiers in any valid order' do
86
+ variations = {
87
+ '5d10r1x' => {
88
+ bunches: [{ ndice: 5, sides: 10, multiplier: 1,
89
+ rerolls: [[1, :>=, :reroll_replace], [10, :==, :reroll_add]] }], offset: 0
90
+ },
91
+ '3d6xk2' => {
92
+ bunches: [{ ndice: 3, sides: 6, multiplier: 1, rerolls: [[6, :==, :reroll_add]],
93
+ keep_mode: :keep_best, keep_number: 2 }], offset: 0
94
+ },
95
+ '4d6m8x' => {
96
+ bunches: [{ ndice: 4, sides: 6, multiplier: 1, maps: [[8, :<=, 1]],
97
+ rerolls: [[6, :==, :reroll_add]] }], offset: 0
98
+ }
99
+ }
100
+
101
+ variations.each do |input, expected_output|
102
+ expect(parser.parse(input)).to eql expected_output
103
+ end
104
+ end
105
+ end
106
+ end