games_dice 0.3.12 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +15 -0
- data/.travis.yml +7 -10
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -0
- 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 -32
- data/lib/games_dice/bunch.rb +241 -247
- data/lib/games_dice/complex_die.rb +287 -270
- data/lib/games_dice/complex_die_helpers.rb +68 -60
- 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 -19
- data/spec/bunch_spec.rb +399 -420
- data/spec/complex_die_spec.rb +314 -305
- data/spec/dice_spec.rb +33 -34
- data/spec/die_result_spec.rb +162 -169
- data/spec/die_spec.rb +81 -81
- data/spec/helpers.rb +23 -21
- data/spec/map_rule_spec.rb +40 -44
- data/spec/parser_spec.rb +106 -82
- data/spec/probability_spec.rb +530 -526
- data/spec/readme_spec.rb +404 -390
- data/spec/reroll_rule_spec.rb +40 -44
- metadata +39 -28
- data/lib/games_dice/prob_helpers.rb +0 -259
- data/lib/games_dice/probabilities.rb +0 -244
data/spec/die_spec.rb
CHANGED
@@ -1,81 +1,81 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
die
|
14
|
-
die.
|
15
|
-
die.
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
die.
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
die.
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
die
|
42
|
-
die.
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
probs
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
probs.p_eql(
|
56
|
-
probs.p_eql(
|
57
|
-
probs.p_eql(
|
58
|
-
probs.p_eql(
|
59
|
-
probs.p_eql(
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
die
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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 '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,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# games_dice/spec/helpers.rb
|
2
4
|
require 'pathname'
|
3
5
|
require 'coveralls'
|
@@ -5,31 +7,31 @@ Coveralls.wear!
|
|
5
7
|
|
6
8
|
require 'games_dice'
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
(Pathname.new(__FILE__).dirname + "fixtures" + name).to_s
|
10
|
+
def fixture(name)
|
11
|
+
"#{__dir__}/fixtures/#{name}"
|
11
12
|
end
|
12
13
|
|
13
14
|
# TestPRNG tests short predictable series
|
14
15
|
class TestPRNG
|
15
16
|
def initialize
|
16
|
-
@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]
|
17
18
|
end
|
18
|
-
|
19
|
-
|
19
|
+
|
20
|
+
def rand(num)
|
21
|
+
Integer(num * @numbers.pop)
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
23
25
|
# TestPRNGMax checks behaviour of re-rolls
|
24
26
|
class TestPRNGMax
|
25
|
-
def rand(
|
26
|
-
Integer(
|
27
|
+
def rand(num)
|
28
|
+
Integer(num) - 1
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
30
32
|
# TestPRNGMin checks behaviour of re-rolls
|
31
33
|
class TestPRNGMin
|
32
|
-
def rand(
|
34
|
+
def rand(_num)
|
33
35
|
1
|
34
36
|
end
|
35
37
|
end
|
@@ -42,13 +44,13 @@ end
|
|
42
44
|
RSpec::Matchers.define :be_valid_distribution do
|
43
45
|
match do |given|
|
44
46
|
@error = nil
|
45
|
-
if !
|
47
|
+
if !given.is_a?(Hash)
|
46
48
|
@error = "distribution should be a Hash, but it is a #{given.class}"
|
47
|
-
elsif given.keys.any? { |k| !
|
48
|
-
bad_key = given.keys.first { |k| !
|
49
|
+
elsif given.keys.any? { |k| !k.is_a?(Integer) }
|
50
|
+
bad_key = given.keys.first { |k| !k.is_a?(Integer) }
|
49
51
|
@error = "all keys should be Integers, but found '#{bad_key.inspect}' which is a #{bad_key.class}"
|
50
|
-
elsif given.values.any? { |v| !
|
51
|
-
bad_value = given.values.find { |v| !
|
52
|
+
elsif given.values.any? { |v| !v.is_a?(Float) }
|
53
|
+
bad_value = given.values.find { |v| !v.is_a?(Float) }
|
52
54
|
@error = "all values should be Floats, but found '#{bad_value.inspect}' which is a #{bad_value.class}"
|
53
55
|
elsif given.values.any? { |v| v < 0.0 || v > 1.0 }
|
54
56
|
bad_value = given.values.find { |v| v < 0.0 || v > 1.0 }
|
@@ -57,18 +59,18 @@ RSpec::Matchers.define :be_valid_distribution do
|
|
57
59
|
total_probs = given.values.inject(:+)
|
58
60
|
@error = "sum of values should be 1.0, but got #{total_probs}"
|
59
61
|
end
|
60
|
-
|
62
|
+
!@error
|
61
63
|
end
|
62
64
|
|
63
|
-
failure_message do |
|
64
|
-
@error
|
65
|
+
failure_message do |_given|
|
66
|
+
@error || 'Distribution is valid and complete'
|
65
67
|
end
|
66
68
|
|
67
|
-
failure_message_when_negated do |
|
68
|
-
|
69
|
+
failure_message_when_negated do |_given|
|
70
|
+
@error || 'Distribution is valid and complete'
|
69
71
|
end
|
70
72
|
|
71
|
-
description do |
|
72
|
-
|
73
|
+
description do |_given|
|
74
|
+
'a hash describing a complete probability distribution of integer results'
|
73
75
|
end
|
74
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
|