games_dice 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +2 -2
- data/Rakefile +5 -0
- data/games_dice.gemspec +3 -1
- data/lib/games_dice.rb +1 -0
- data/lib/games_dice/complex_die.rb +60 -35
- data/lib/games_dice/die.rb +43 -16
- data/lib/games_dice/version.rb +1 -1
- data/spec/die_spec.rb +3 -18
- data/spec/helpers.rb +15 -0
- metadata +39 -6
data/.travis.yml
CHANGED
data/Rakefile
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "rspec/core/rake_task"
|
3
|
+
require "yard"
|
3
4
|
|
4
5
|
task :default => [:test]
|
5
6
|
|
@@ -8,3 +9,7 @@ RSpec::Core::RakeTask.new(:test) do |t|
|
|
8
9
|
t.pattern = "spec/*_spec.rb"
|
9
10
|
t.verbose = false
|
10
11
|
end
|
12
|
+
|
13
|
+
YARD::Rake::YardocTask.new do |t|
|
14
|
+
t.files = ['lib/**/*.rb']
|
15
|
+
end
|
data/games_dice.gemspec
CHANGED
@@ -16,8 +16,10 @@ Gem::Specification.new do |gem|
|
|
16
16
|
|
17
17
|
gem.add_development_dependency "rspec", ">= 2.13.0"
|
18
18
|
gem.add_development_dependency "rake", ">= 1.9.1"
|
19
|
+
gem.add_development_dependency "yard", ">= 0.8.6"
|
20
|
+
gem.add_development_dependency "redcarpet", ">=2.3.0"
|
19
21
|
|
20
|
-
gem.add_dependency "parslet", "
|
22
|
+
gem.add_dependency "parslet", ">= 1.5.0"
|
21
23
|
|
22
24
|
gem.files = `git ls-files`.split($/)
|
23
25
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
data/lib/games_dice.rb
CHANGED
@@ -1,59 +1,82 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
1
|
+
# This class models a die that is built up from a simpler unit by adding rules to re-roll
|
2
|
+
# and interpret the value shown.
|
3
|
+
#
|
4
|
+
# An object of this class represents a single complex die. It rolls 1..#sides, with equal weighting
|
5
|
+
# for each value. The value from a roll may be used to trigger yet more rolls that combine together.
|
6
|
+
# After any re-rolls, the value can be interpretted ("mapped") as another integer, which is used as
|
7
|
+
# the final result.
|
8
|
+
#
|
9
|
+
# @example An open-ended percentile die from a popular RPG
|
10
|
+
# d = GamesDice::ComplexDie.new( 100, :rerolls => [[96, :<=, :reroll_add],[5, :>=, :reroll_subtract]] )
|
11
|
+
# d.roll # => #<GamesDice::DieResult:0x007ff03a2415f8 @rolls=[4, 27], ...>
|
12
|
+
# d.result.value # => -23
|
13
|
+
# d.explain_result # => "[4-27] -23"
|
14
|
+
#
|
15
|
+
# @example An "exploding" six-sided die with a target number
|
16
|
+
# d = GamesDice::ComplexDie.new( 6, :rerolls => [[6, :<=, :reroll_add]], :maps => [[8, :<=, 1, 'Success']] )
|
17
|
+
# d.roll # => #<GamesDice::DieResult:0x007ff03a1e8e08 @rolls=[6, 5], ...>
|
18
|
+
# d.result.value # => 1
|
19
|
+
# d.explain_result # => "[6+5] 11 Success"
|
20
|
+
#
|
21
|
+
|
6
22
|
class GamesDice::ComplexDie
|
7
23
|
|
8
|
-
#
|
9
|
-
#
|
24
|
+
# @!visibility private
|
25
|
+
# arbitrary limit to speed up probability calculations. It should
|
26
|
+
# be larger than anything seen in real-world tabletop games.
|
10
27
|
MAX_REROLLS = 1000
|
11
28
|
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
def initialize(sides,
|
20
|
-
@basic_die = GamesDice::Die.new(sides,
|
29
|
+
# Creates new instance of GamesDice::ComplexDie
|
30
|
+
# @param [Integer] sides Number of sides on a single die, passed to GamesDice::Die's constructor
|
31
|
+
# @param [Hash] options
|
32
|
+
# @option options [Array<GamesDice::RerollRule,Array>] :rerolls The rules that cause the die to roll again
|
33
|
+
# @option options [Array<GamesDice::MapRule,Array>] :maps The rules to convert a value into a final result for the die
|
34
|
+
# @option options [#rand] :prng An alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
|
35
|
+
# @return [GamesDice::ComplexDie]
|
36
|
+
def initialize( sides, options = {} )
|
37
|
+
@basic_die = GamesDice::Die.new(sides, options[:prng])
|
21
38
|
|
22
|
-
@rerolls = construct_rerolls(
|
23
|
-
@maps = construct_maps(
|
39
|
+
@rerolls = construct_rerolls( options[:rerolls] )
|
40
|
+
@maps = construct_maps( options[:maps] )
|
24
41
|
|
25
42
|
@total = nil
|
26
43
|
@result = nil
|
27
44
|
end
|
28
45
|
|
29
|
-
#
|
46
|
+
# The simple component used by this complex one
|
47
|
+
# @return [GamesDice::Die] Object used to make individual dice rolls for the complex die
|
30
48
|
attr_reader :basic_die
|
31
49
|
|
32
|
-
#
|
50
|
+
# @return [Array<GamesDice::RerollRule>, nil] Sequence of re-roll rules, or nil if re-rolls are not required.
|
33
51
|
attr_reader :rerolls
|
34
52
|
|
35
|
-
#
|
53
|
+
# @return [Array<GamesDice::MapRule>, nil] Sequence of map rules, or nil if mapping is not required.
|
36
54
|
attr_reader :maps
|
37
55
|
|
38
|
-
#
|
56
|
+
# @return [GamesDice::DieResult, nil] Result of last call to #roll, nil if no call made yet
|
39
57
|
attr_reader :result
|
40
58
|
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
59
|
+
# Whether or not #probabilities includes all possible outcomes.
|
60
|
+
# True if all possible results are represented and assigned a probability. Dice with open-ended re-rolls
|
61
|
+
# may have calculations cut short, and will result in a false value of this attribute. Even when this
|
62
|
+
# attribute is false, probabilities should still be accurate to nearest 1e-9.
|
63
|
+
# @return [Boolean, nil] Depending on completeness when generating #probabilites
|
44
64
|
attr_reader :probabilities_complete
|
45
65
|
|
46
|
-
#
|
66
|
+
# @!attribute [r] sides
|
67
|
+
# @return [Integer] Number of sides.
|
47
68
|
def sides
|
48
69
|
@basic_die.sides
|
49
70
|
end
|
50
71
|
|
51
|
-
#
|
72
|
+
# @!attribute [r] explain_result
|
73
|
+
# @return [String,nil] Explanation of result, or nil if no call to #roll yet.
|
52
74
|
def explain_result
|
53
75
|
@result.explain_value
|
54
76
|
end
|
55
77
|
|
56
|
-
#
|
78
|
+
# @!attribute [r] min
|
79
|
+
# @return [Integer] Minimum possible result from a call to #roll
|
57
80
|
def min
|
58
81
|
return @min_result if @min_result
|
59
82
|
@min_result, @max_result = [probabilities.min, probabilities.max]
|
@@ -63,8 +86,8 @@ class GamesDice::ComplexDie
|
|
63
86
|
@min_result
|
64
87
|
end
|
65
88
|
|
66
|
-
#
|
67
|
-
#
|
89
|
+
# @!attribute [r] max
|
90
|
+
# @return [Integer] Maximum possible result from a call to #roll
|
68
91
|
def max
|
69
92
|
return @max_result if @max_result
|
70
93
|
@min_result, @max_result = [probabilities.min, probabilities.max]
|
@@ -74,9 +97,10 @@ class GamesDice::ComplexDie
|
|
74
97
|
@max_result
|
75
98
|
end
|
76
99
|
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
100
|
+
# Calculates the probability distribution for the die. For open-ended re-roll rules, there are some
|
101
|
+
# arbitrary limits imposed to prevent large amounts of recursion. Probabilities should be to nearest
|
102
|
+
# 1e-9 at worst.
|
103
|
+
# @return [GamesDice::Probabilities] Probability distribution of die.
|
80
104
|
def probabilities
|
81
105
|
return @probabilities if @probabilities
|
82
106
|
@probabilities_complete = true
|
@@ -106,8 +130,9 @@ class GamesDice::ComplexDie
|
|
106
130
|
@probabilities = GamesDice::Probabilities.new( prob_hash )
|
107
131
|
end
|
108
132
|
|
109
|
-
#
|
110
|
-
#
|
133
|
+
# Simulates rolling the die
|
134
|
+
# @param [Symbol] reason Assign a reason for rolling the first die.
|
135
|
+
# @return [GamesDice::DieResult] Detailed results from rolling the die, including resolution of rules.
|
111
136
|
def roll( reason = :basic )
|
112
137
|
# Important bit - actually roll the die
|
113
138
|
@result = GamesDice::DieResult.new( @basic_die.roll, reason )
|
data/lib/games_dice/die.rb
CHANGED
@@ -1,12 +1,28 @@
|
|
1
|
-
#
|
2
|
-
#
|
1
|
+
# This class models the simplest, most-familiar kind of die.
|
2
|
+
#
|
3
|
+
# An object of the class represents a basic die that rolls 1..#sides, with equal weighting for each value.
|
4
|
+
#
|
5
|
+
# @example Create a 6-sided die, and roll it
|
6
|
+
# d = GamesDice::Die.new( 6 )
|
3
7
|
# d.roll # => Integer in range 1..6
|
4
|
-
# d.result # => same Integer value as returned by d.roll
|
8
|
+
# d.result # => same Integer value as just returned by d.roll
|
9
|
+
#
|
10
|
+
# @example Create a 10-sided die, that rolls using a monkey-patch to SecureRandom
|
11
|
+
# module SecureRandom
|
12
|
+
# def self.rand n
|
13
|
+
# random_number( n )
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
# d = GamesDice::Die.new( 10, SecureRandom )
|
17
|
+
# d.roll # => (secure) Integer in range 1..10
|
18
|
+
# d.result # => same Integer value as just returned by d.roll
|
19
|
+
|
5
20
|
class GamesDice::Die
|
6
|
-
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
21
|
+
|
22
|
+
# Creates new instance of GamesDice::Die
|
23
|
+
# @param [Integer] sides the number of sides
|
24
|
+
# @param [#rand] prng random number generator, GamesDice::Die will use Ruby's built-in #rand() by default
|
25
|
+
# @return [GamesDice::Die]
|
10
26
|
def initialize( sides, prng=nil )
|
11
27
|
@sides = Integer(sides)
|
12
28
|
raise ArgumentError, "sides value #{sides} is too low, it must be 1 or greater" if @sides < 1
|
@@ -15,29 +31,36 @@ class GamesDice::Die
|
|
15
31
|
@result = nil
|
16
32
|
end
|
17
33
|
|
18
|
-
# number of sides
|
34
|
+
# @return [Integer] number of sides on simulated die
|
19
35
|
attr_reader :sides
|
20
36
|
|
21
|
-
#
|
37
|
+
# @return [Integer] result of last call to #roll, nil if no call made yet
|
22
38
|
attr_reader :result
|
23
39
|
|
24
|
-
#
|
40
|
+
# @return [Object] random number generator as supplied to constructor, may be nil
|
41
|
+
attr_reader :prng
|
42
|
+
|
43
|
+
# @!attribute [r] min
|
44
|
+
# @return [Integer] minimum possible result from a call to #roll
|
25
45
|
def min
|
26
46
|
1
|
27
47
|
end
|
28
48
|
|
29
|
-
#
|
49
|
+
# @!attribute [r] max
|
50
|
+
# @return [Integer] maximum possible result from a call to #roll
|
30
51
|
def max
|
31
52
|
@sides
|
32
53
|
end
|
33
54
|
|
34
|
-
#
|
55
|
+
# Calculates probability distribution for this die.
|
56
|
+
# @return [GamesDice::Probabilities] probability distribution of the die
|
35
57
|
def probabilities
|
36
58
|
return @probabilities if @probabilities
|
37
59
|
@probabilities = GamesDice::Probabilities.for_fair_die( @sides )
|
38
60
|
end
|
39
61
|
|
40
|
-
#
|
62
|
+
# Simulates rolling the die
|
63
|
+
# @return [Integer] selected value between 1 and #sides inclusive
|
41
64
|
def roll
|
42
65
|
if @prng
|
43
66
|
@result = @prng.rand(@sides) + 1
|
@@ -46,13 +69,17 @@ class GamesDice::Die
|
|
46
69
|
end
|
47
70
|
end
|
48
71
|
|
49
|
-
#
|
72
|
+
# @!attribute [r] rerolls
|
73
|
+
# Rules for when to re-roll this die.
|
74
|
+
# @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
|
50
75
|
def rerolls
|
51
76
|
nil
|
52
77
|
end
|
53
78
|
|
54
|
-
#
|
79
|
+
# @!attribute [r] maps
|
80
|
+
# Rules for when to map return value of this die.
|
81
|
+
# @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
|
55
82
|
def maps
|
56
83
|
nil
|
57
84
|
end
|
58
|
-
end # class Die
|
85
|
+
end # class GamesDice::Die
|
data/lib/games_dice/version.rb
CHANGED
data/spec/die_spec.rb
CHANGED
@@ -46,8 +46,10 @@ describe GamesDice::Die do
|
|
46
46
|
describe "#probabilities" do
|
47
47
|
it "should return the die's probability distribution as a GamesDice::Probabilities object" do
|
48
48
|
die = GamesDice::Die.new(6)
|
49
|
-
die.probabilities.is_a?( GamesDice::Probabilities ).should be_true
|
50
49
|
probs = die.probabilities
|
50
|
+
probs.is_a?( GamesDice::Probabilities ).should be_true
|
51
|
+
|
52
|
+
probs.to_h.should be_valid_distribution
|
51
53
|
|
52
54
|
probs.p_eql(1).should be_within(1e-10).of 1/6.0
|
53
55
|
probs.p_eql(2).should be_within(1e-10).of 1/6.0
|
@@ -55,23 +57,6 @@ describe GamesDice::Die do
|
|
55
57
|
probs.p_eql(4).should be_within(1e-10).of 1/6.0
|
56
58
|
probs.p_eql(5).should be_within(1e-10).of 1/6.0
|
57
59
|
probs.p_eql(6).should be_within(1e-10).of 1/6.0
|
58
|
-
probs.to_h.values.inject(:+).should be_within(1e-9).of 1.0
|
59
|
-
|
60
|
-
probs.p_gt(6).should == 0.0
|
61
|
-
probs.p_gt(-20).should == 1.0
|
62
|
-
probs.p_gt(4).should be_within(1e-10).of 2/6.0
|
63
|
-
|
64
|
-
probs.p_ge(20).should == 0.0
|
65
|
-
probs.p_ge(1).should == 1.0
|
66
|
-
probs.p_ge(4).should be_within(1e-10).of 0.5
|
67
|
-
|
68
|
-
probs.p_le(6).should == 1.0
|
69
|
-
probs.p_le(-3).should == 0.0
|
70
|
-
probs.p_le(5).should be_within(1e-10).of 5/6.0
|
71
|
-
|
72
|
-
probs.p_lt(7).should == 1.0
|
73
|
-
probs.p_lt(1).should == 0.0
|
74
|
-
probs.p_lt(3).should be_within(1e-10).of 2/6.0
|
75
60
|
|
76
61
|
probs.expected.should be_within(1e-10).of 3.5
|
77
62
|
end
|
data/spec/helpers.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# games_dice/spec/helpers.rb
|
2
2
|
|
3
|
+
# TestPRNG tests short predictable series
|
3
4
|
class TestPRNG
|
4
5
|
def initialize
|
5
6
|
@numbers = [0.123,0.234,0.345,0.999,0.876,0.765,0.543,0.111,0.333,0.777]
|
@@ -9,6 +10,20 @@ class TestPRNG
|
|
9
10
|
end
|
10
11
|
end
|
11
12
|
|
13
|
+
# TestPRNGMax checks behaviour of re-rolls
|
14
|
+
class TestPRNGMax
|
15
|
+
def rand(n)
|
16
|
+
Integer( n ) - 1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# TestPRNGMax checks behaviour of re-rolls
|
21
|
+
class TestPRNGMin
|
22
|
+
def rand(n)
|
23
|
+
1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
12
27
|
# A valid distribution is:
|
13
28
|
# A hash
|
14
29
|
# Keys are all Integers
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: games_dice
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-05
|
12
|
+
date: 2013-06-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -43,12 +43,44 @@ dependencies:
|
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: 1.9.1
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: yard
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.8.6
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.8.6
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: redcarpet
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 2.3.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 2.3.0
|
46
78
|
- !ruby/object:Gem::Dependency
|
47
79
|
name: parslet
|
48
80
|
requirement: !ruby/object:Gem::Requirement
|
49
81
|
none: false
|
50
82
|
requirements:
|
51
|
-
- -
|
83
|
+
- - ! '>='
|
52
84
|
- !ruby/object:Gem::Version
|
53
85
|
version: 1.5.0
|
54
86
|
type: :runtime
|
@@ -56,7 +88,7 @@ dependencies:
|
|
56
88
|
version_requirements: !ruby/object:Gem::Requirement
|
57
89
|
none: false
|
58
90
|
requirements:
|
59
|
-
- -
|
91
|
+
- - ! '>='
|
60
92
|
- !ruby/object:Gem::Version
|
61
93
|
version: 1.5.0
|
62
94
|
description: ! "A simulated-dice library, with flexible rules that allow dice systems
|
@@ -112,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
144
|
version: '0'
|
113
145
|
segments:
|
114
146
|
- 0
|
115
|
-
hash:
|
147
|
+
hash: -3596350699515982404
|
116
148
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
149
|
none: false
|
118
150
|
requirements:
|
@@ -121,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
153
|
version: '0'
|
122
154
|
segments:
|
123
155
|
- 0
|
124
|
-
hash:
|
156
|
+
hash: -3596350699515982404
|
125
157
|
requirements: []
|
126
158
|
rubyforge_project:
|
127
159
|
rubygems_version: 1.8.24
|
@@ -141,3 +173,4 @@ test_files:
|
|
141
173
|
- spec/probability_spec.rb
|
142
174
|
- spec/readme_spec.rb
|
143
175
|
- spec/reroll_rule_spec.rb
|
176
|
+
has_rdoc:
|