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 CHANGED
@@ -3,8 +3,8 @@ rvm:
3
3
  - "1.8.7"
4
4
  - "1.9.3"
5
5
  - "2.0.0"
6
- - jruby-18mode # JRuby in 1.8 mode
7
- - jruby-19mode # JRuby in 1.9 mode
8
6
  - rbx-18mode
9
7
  - rbx-19mode
8
+ - ruby-head
9
+ - ree
10
10
 
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", "~> 1.5.0"
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
@@ -11,6 +11,7 @@ require "games_dice/dice"
11
11
  require "games_dice/parser"
12
12
 
13
13
  module GamesDice
14
+ # @!visibility private
14
15
  @@parser = GamesDice::Parser.new
15
16
 
16
17
  def self.create dice_description, prng = nil
@@ -1,59 +1,82 @@
1
- # complex die that rolls 1..N, and may re-roll and adjust final value based on
2
- # parameters it is instantiated with
3
- # d = GamesDice::DieExploding.new( 6, :explode_up => true ) # 'exploding' die
4
- # d.roll # => GamesDice::DieResult of rolling die
5
- # d.result # => same GamesDice::DieResult as returned by d.roll
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
- # arbitrary limit to simplify calculations and stay in Integer range for convenience. It should
9
- # be much larger than anything seen in real-world tabletop games.
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
- # sides is e.g. 6 for traditional cubic die, or 20 for icosahedron.
13
- # It can take non-traditional values, such as 7, but must be at least 1.
14
- #
15
- # options_hash may contain keys setting following attributes
16
- # :rerolls => an array of rules that cause the die to roll again, see #rerolls
17
- # :maps => an array of rules to convert a value into a final result for the die, see #maps
18
- # :prng => any object that has a rand(x) method, which will be used instead of internal rand()
19
- def initialize(sides, options_hash = {})
20
- @basic_die = GamesDice::Die.new(sides, options_hash[:prng])
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( options_hash[:rerolls] )
23
- @maps = construct_maps( options_hash[: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
- # underlying GamesDice::Die object, used to generate all individual rolls
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
- # may be nil, in which case no re-rolls are triggered, or an array of GamesDice::RerollRule objects
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
- # may be nil, in which case no mappings apply, or an array of GamesDice::MapRule objects
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
- # result of last call to #roll, nil if no call made yet
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
- # true if probability calculation did not hit any limitations, so has covered all possible scenarios
42
- # false if calculation was cut short and probabilities are an approximation
43
- # nil if probabilities have not been calculated yet
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
- # number of sides, same as #basic_die.sides
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
- # string explanation of roll, including any re-rolls etc, same as #result.explain_value
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
- # minimum possible value
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
- # maximum possible value. A ComplexDie with open-ended additive re-rolls will calculate roughly 1001 times the
67
- # maximum of #basic_die.max (although the true value is infinite)
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
- # returns a hash of value (Integer) => probability (Float) pairs. For efficiency with re-rolls, the calculation may cut
78
- # short based on depth of recursion or closeness to total 1.0 probability. Therefore low probabilities
79
- # (less than one in a billion) in open-ended re-rolls are not always represented in the hash.
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
- # generates Integer between #min and #max, using rand()
110
- # first roll reason can be over-ridden, required for re-roll types that spawn new dice
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 )
@@ -1,12 +1,28 @@
1
- # basic die that rolls 1..N, typically with equal weighting for each value
2
- # d = Die.new(6)
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
- # sides is e.g. 6 for traditional cubic die, or 20 for icosahedron.
7
- # It can take non-traditional values, such as 7, but must be at least 1.
8
- # prng is an object that has a rand(x) method. If provided, it will be called as
9
- # prng.rand(sides), and is expected to return an integer in range 0...sides
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 as set by #new
34
+ # @return [Integer] number of sides on simulated die
19
35
  attr_reader :sides
20
36
 
21
- # integer result of last call to #roll, nil if no call made yet
37
+ # @return [Integer] result of last call to #roll, nil if no call made yet
22
38
  attr_reader :result
23
39
 
24
- # minimum possible value
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
- # maximum possible value
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
- # returns a GamesDice::Probabilities object that models distribution of the die
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
- # generates Integer between #min and #max, using rand()
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
- # always nil, available for compatibility with ComplexDie
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
- # always nil, available for compatibility with ComplexDie
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
@@ -1,3 +1,3 @@
1
1
  module GamesDice
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
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.0
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-30 00:00:00.000000000 Z
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: 4046591063539756555
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: 4046591063539756555
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: