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
data/spec/die_spec.rb
CHANGED
@@ -1,82 +1,81 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
before do
|
7
|
-
# Set state of default PRNG
|
8
|
-
srand(4567)
|
9
|
-
end
|
10
|
-
|
11
|
-
describe
|
12
|
-
it
|
13
|
-
die = GamesDice::Die.new(6)
|
14
|
-
die.min.
|
15
|
-
die.max.
|
16
|
-
die.sides.
|
17
|
-
end
|
18
|
-
|
19
|
-
it
|
20
|
-
prng = TestPRNG.new
|
21
|
-
die = GamesDice::Die.new(20,prng)
|
22
|
-
[16,7,3,11,16,18,20,7].each do |expected|
|
23
|
-
die.roll.
|
24
|
-
die.result.
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
describe
|
30
|
-
it "should return results based on Ruby's internal rand() by default" do
|
31
|
-
|
32
|
-
[5,4,10,4,7,8,1,9].each do |expected|
|
33
|
-
die.roll.
|
34
|
-
die.result.
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
describe
|
40
|
-
it
|
41
|
-
die = GamesDice::Die.new(20)
|
42
|
-
die.min.
|
43
|
-
die.max.
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
describe
|
48
|
-
it "should return the die's probability distribution as a GamesDice::Probabilities object" do
|
49
|
-
die = GamesDice::Die.new(6)
|
50
|
-
probs = die.probabilities
|
51
|
-
probs.
|
52
|
-
|
53
|
-
probs.to_h.
|
54
|
-
|
55
|
-
probs.p_eql(1).
|
56
|
-
probs.p_eql(2).
|
57
|
-
probs.p_eql(3).
|
58
|
-
probs.p_eql(4).
|
59
|
-
probs.p_eql(5).
|
60
|
-
probs.p_eql(6).
|
61
|
-
|
62
|
-
probs.expected.
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
describe
|
67
|
-
it
|
68
|
-
die = GamesDice::Die.new(8)
|
69
|
-
die.all_values.
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
describe
|
74
|
-
it
|
75
|
-
die = GamesDice::Die.new(10)
|
76
|
-
arr = []
|
77
|
-
die.each_value { |x| arr << x }
|
78
|
-
arr.
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
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 'should return an object that represents e.g. a six-sided die' do
|
13
|
+
die = GamesDice::Die.new(6)
|
14
|
+
expect(die.min).to eql 1
|
15
|
+
expect(die.max).to eql 6
|
16
|
+
expect(die.sides).to eql 6
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should accept any object with a rand(Integer) method as the second param' do
|
20
|
+
prng = TestPRNG.new
|
21
|
+
die = GamesDice::Die.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 "should return results based on Ruby's internal rand() by default" do
|
31
|
+
die = GamesDice::Die.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 'should calculate correct min, max' do
|
41
|
+
die = GamesDice::Die.new(20)
|
42
|
+
expect(die.min).to eql 1
|
43
|
+
expect(die.max).to eql 20
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#probabilities' do
|
48
|
+
it "should return the die's probability distribution as a GamesDice::Probabilities object" do
|
49
|
+
die = GamesDice::Die.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 'should return array with one result value per side' do
|
68
|
+
die = GamesDice::Die.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 'should iterate through all sides of the die' do
|
75
|
+
die = GamesDice::Die.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,33 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# games_dice/spec/helpers.rb
|
2
4
|
require 'pathname'
|
3
5
|
require 'coveralls'
|
4
|
-
|
5
6
|
Coveralls.wear!
|
6
7
|
|
7
|
-
|
8
|
-
|
8
|
+
require 'games_dice'
|
9
|
+
|
10
|
+
def fixture(name)
|
11
|
+
"#{__dir__}/fixtures/#{name}"
|
9
12
|
end
|
10
13
|
|
11
14
|
# TestPRNG tests short predictable series
|
12
15
|
class TestPRNG
|
13
16
|
def initialize
|
14
|
-
@numbers = [0.123,0.234,0.345,0.999,0.876,0.765,0.543,0.111,0.333,0.777]
|
17
|
+
@numbers = [0.123, 0.234, 0.345, 0.999, 0.876, 0.765, 0.543, 0.111, 0.333, 0.777]
|
15
18
|
end
|
16
|
-
|
17
|
-
|
19
|
+
|
20
|
+
def rand(num)
|
21
|
+
Integer(num * @numbers.pop)
|
18
22
|
end
|
19
23
|
end
|
20
24
|
|
21
25
|
# TestPRNGMax checks behaviour of re-rolls
|
22
26
|
class TestPRNGMax
|
23
|
-
def rand(
|
24
|
-
Integer(
|
27
|
+
def rand(num)
|
28
|
+
Integer(num) - 1
|
25
29
|
end
|
26
30
|
end
|
27
31
|
|
28
32
|
# TestPRNGMin checks behaviour of re-rolls
|
29
33
|
class TestPRNGMin
|
30
|
-
def rand(
|
34
|
+
def rand(_num)
|
31
35
|
1
|
32
36
|
end
|
33
37
|
end
|
@@ -40,13 +44,13 @@ end
|
|
40
44
|
RSpec::Matchers.define :be_valid_distribution do
|
41
45
|
match do |given|
|
42
46
|
@error = nil
|
43
|
-
if !
|
47
|
+
if !given.is_a?(Hash)
|
44
48
|
@error = "distribution should be a Hash, but it is a #{given.class}"
|
45
|
-
elsif given.keys.any? { |k| !
|
46
|
-
bad_key = given.keys.first { |k| !
|
47
|
-
@error = "all keys should be
|
48
|
-
elsif given.values.any? { |v| !
|
49
|
-
bad_value = given.values.find { |v| !
|
49
|
+
elsif given.keys.any? { |k| !k.is_a?(Integer) }
|
50
|
+
bad_key = given.keys.first { |k| !k.is_a?(Integer) }
|
51
|
+
@error = "all keys should be Integers, but found '#{bad_key.inspect}' which is a #{bad_key.class}"
|
52
|
+
elsif given.values.any? { |v| !v.is_a?(Float) }
|
53
|
+
bad_value = given.values.find { |v| !v.is_a?(Float) }
|
50
54
|
@error = "all values should be Floats, but found '#{bad_value.inspect}' which is a #{bad_value.class}"
|
51
55
|
elsif given.values.any? { |v| v < 0.0 || v > 1.0 }
|
52
56
|
bad_value = given.values.find { |v| v < 0.0 || v > 1.0 }
|
@@ -55,18 +59,18 @@ RSpec::Matchers.define :be_valid_distribution do
|
|
55
59
|
total_probs = given.values.inject(:+)
|
56
60
|
@error = "sum of values should be 1.0, but got #{total_probs}"
|
57
61
|
end
|
58
|
-
|
62
|
+
!@error
|
59
63
|
end
|
60
64
|
|
61
|
-
|
62
|
-
@error
|
65
|
+
failure_message do |_given|
|
66
|
+
@error || 'Distribution is valid and complete'
|
63
67
|
end
|
64
68
|
|
65
|
-
|
66
|
-
|
69
|
+
failure_message_when_negated do |_given|
|
70
|
+
@error || 'Distribution is valid and complete'
|
67
71
|
end
|
68
72
|
|
69
|
-
description do |
|
70
|
-
|
73
|
+
description do |_given|
|
74
|
+
'a hash describing a complete probability distribution of integer results'
|
71
75
|
end
|
72
76
|
end
|
data/spec/map_rule_spec.rb
CHANGED
@@ -1,44 +1,40 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
it
|
8
|
-
GamesDice::MapRule.new(
|
9
|
-
GamesDice::MapRule.new(
|
10
|
-
end
|
11
|
-
|
12
|
-
it
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
it
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
rule.
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
rule.
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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 '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
|
+
expect(-> { GamesDice::MapRule.new(5, :member?, -1) }).to raise_error(ArgumentError)
|
14
|
+
expect(-> { GamesDice::MapRule.new((1..5), :>, 12) }).to raise_error(ArgumentError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should reject non-Integer map results' do
|
18
|
+
expect(-> { GamesDice::MapRule.new(5, :>, :reroll_again) }).to raise_error(TypeError)
|
19
|
+
expect(-> { GamesDice::MapRule.new((1..5), :member?, 'foo') }).to raise_error(TypeError)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#map_from' do
|
24
|
+
it 'should return the mapped value for a match' do
|
25
|
+
rule = GamesDice::MapRule.new(5, :>, -1)
|
26
|
+
expect(rule.map_from(4)).to eql(-1)
|
27
|
+
|
28
|
+
rule = GamesDice::MapRule.new((1..5), :member?, 3)
|
29
|
+
expect(rule.map_from(4)).to eql 3
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should return nil for no match' do
|
33
|
+
rule = GamesDice::MapRule.new(5, :>, -1)
|
34
|
+
expect(rule.map_from(6)).to be_nil
|
35
|
+
|
36
|
+
rule = GamesDice::MapRule.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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
'
|
12
|
-
'
|
13
|
-
|
14
|
-
'
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
variations.each do |input,expected_output|
|
44
|
-
parser.parse(
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
it "should parse '
|
49
|
-
variations = {
|
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
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'helpers'
|
4
|
+
|
5
|
+
describe GamesDice::Parser do
|
6
|
+
describe '#parse' do
|
7
|
+
let(:parser) { GamesDice::Parser.new }
|
8
|
+
|
9
|
+
it 'should 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 "should 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 "should 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 "should 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 "should 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 'should 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
|