polyhedra 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +10 -0
- data/Guardfile +47 -0
- data/README.md +209 -0
- data/Rakefile +1 -0
- data/lib/polyhedra.rb +3 -0
- data/lib/polyhedra/dice.rb +112 -0
- data/lib/polyhedra/version.rb +3 -0
- data/polyhedra.gemspec +24 -0
- data/spec/lib/dice_spec.rb +137 -0
- data/spec/spec_helper.rb +4 -0
- metadata +61 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ruby-1.9.3@polyhedra --create
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :version => 2 do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch(%r{^lib/polyhedra/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
8
|
+
watch('spec/spec_helper.rb') { "spec" }
|
9
|
+
|
10
|
+
# Rails example
|
11
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
12
|
+
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
13
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
14
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
15
|
+
watch('config/routes.rb') { "spec/routing" }
|
16
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
17
|
+
|
18
|
+
# Capybara request specs
|
19
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
20
|
+
|
21
|
+
# Turnip features and steps
|
22
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
23
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
guard 'rspec', :version => 2 do
|
28
|
+
watch(%r{^spec/.+_spec\.rb$})
|
29
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
30
|
+
watch('spec/spec_helper.rb') { "spec" }
|
31
|
+
|
32
|
+
# Rails example
|
33
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
34
|
+
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
35
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
36
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
37
|
+
watch('config/routes.rb') { "spec/routing" }
|
38
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
39
|
+
|
40
|
+
# Capybara request specs
|
41
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
42
|
+
|
43
|
+
# Turnip features and steps
|
44
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
45
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
46
|
+
end
|
47
|
+
|
data/README.md
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
# Overview
|
2
|
+
|
3
|
+
Polyhedra is a tool to help you write programs that manage dice using
|
4
|
+
the kinds of expressions most RPG rulebooks use. Some examples of dice
|
5
|
+
expressions that can be handled:
|
6
|
+
|
7
|
+
* d20
|
8
|
+
* 3d6
|
9
|
+
* 6d6
|
10
|
+
* 1d4-1
|
11
|
+
* 2d6x10
|
12
|
+
* 2d6*10
|
13
|
+
* 4d6t3 # roll 4d6, take highest 3
|
14
|
+
* 3d6r2 # roll 3d6, re-rolling 1's or 2's
|
15
|
+
* 2d6-1x10 # note that +/- modifiers happen before multipliers; that's just how dice math works. 2d6-1x10 == 1-11x10 == 10-110
|
16
|
+
* 1d6 fire # you can assign units to the dice as well
|
17
|
+
|
18
|
+
Polyhedra understands dice math:
|
19
|
+
|
20
|
+
* 3d6 + 2d6 => 5d6
|
21
|
+
* 2d6 + 1d4 => 2d6 + 1d4 # dissimilar bases can't be added
|
22
|
+
* 1d6 fire + 1d4 fire + 1d6 earth => 1d6 + 1d4 fire + 1d6 earth # similar units are grouped before bases
|
23
|
+
|
24
|
+
Note that adding two dissimilar dice results in a "DiceSet", which is
|
25
|
+
a collection of Dice objects. You can still "roll" a DiceSet just like
|
26
|
+
you would a Dice object, but #sides won't work and #units will return
|
27
|
+
an array.
|
28
|
+
|
29
|
+
Polyhedra also understands units, and knows to avoid adding dice of
|
30
|
+
different units:
|
31
|
+
|
32
|
+
* 3d6 fire + 2d6 physical
|
33
|
+
* 1d4 frost + 2
|
34
|
+
|
35
|
+
# Dice Notation
|
36
|
+
|
37
|
+
The simplest RPG Dice notation is given as numbers and letters of the
|
38
|
+
form _NdS_, where N is the number of dice to roll and S is the number
|
39
|
+
of sides per die. When you roll all the dice in a Yahtzee cup, you are
|
40
|
+
rolling 5d6, or five dice, each with six sides. If you are rolling a
|
41
|
+
single die, you can omit the 1 and represent the notation as e.g d6.
|
42
|
+
|
43
|
+
Role-playing games make allowances for other alterations to the roll:
|
44
|
+
you can add or subtract (2d6+3, 1d4-1), multiply or divide (1d4x10,
|
45
|
+
1d6/2). (Note that most game systems round down, so 1d6/2 actually
|
46
|
+
yields a number between 0 and 3.)
|
47
|
+
|
48
|
+
Mathematically, none of the following are equivalent: 1d3, 1d4-1,
|
49
|
+
1d6/2. The first one cannot roll 0's, and although the second two have
|
50
|
+
the same min/max range, 1d4-1 has an even distribution--exactly a 25%
|
51
|
+
chance of rolling each number--while 1d6/2 has an uneven distribution
|
52
|
+
(a 17% chance of rolling a 0, 33% chance of rolling a 1, etc).
|
53
|
+
|
54
|
+
## Notational Affixes
|
55
|
+
|
56
|
+
* rR - Reroll any dice equal or below R. R must be < S. 3d6r3 will
|
57
|
+
roll 12-18 as each die must be 4 or higher to be kept.
|
58
|
+
Mathematically 3d6r3 is identical to 3d3+9 but the semantics are
|
59
|
+
considered to be different so the extra notation is allowed.
|
60
|
+
|
61
|
+
* RR - Reroll any dice equal or above R. R must be > 1 and <= S.
|
62
|
+
|
63
|
+
* kM - Keep highest M dice. 1 < M < N. 4d6k3 means roll 4d6, but only
|
64
|
+
count the highest 3 dice in the final tally.
|
65
|
+
|
66
|
+
* KM - Keep lowest M dice. 1 < M < N.
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
# Order of Operations
|
71
|
+
|
72
|
+
Rerolls are considered first, then keeps. Next--and this is the
|
73
|
+
reverse of typical algebraic precedence--addition or subtraction
|
74
|
+
happens and finally multiplication or division. Thus
|
75
|
+
|
76
|
+
4d6r1k3+1x10
|
77
|
+
|
78
|
+
Would roll dice until 4 dice showed 2 or higher, then the top 3 dice
|
79
|
+
would be kept. The range at this point would be 6-18. 1 would be added
|
80
|
+
to this bumping the range to 7-19. Finally the roll is mulitplied by
|
81
|
+
10 to yield a range of 70-190. Note that because multiplication is
|
82
|
+
performed last the final value will be in steps of the multiplier;
|
83
|
+
e.g. the above dice expression could roll a 70, 80, 90, etc., but
|
84
|
+
could not roll a 75 or an 81.
|
85
|
+
|
86
|
+
# Monkeypatch! Woo! (Fixnum shorthand)
|
87
|
+
|
88
|
+
Polyhedra will add .d* methods to Fixnum and to Kernel (d20 is the same
|
89
|
+
as 1.d20 is the same as Dice.new "1d20"). Since this is a monkeypatch,
|
90
|
+
it's optional. Require polyhedra/shorthand instead of polyhedra to get
|
91
|
+
these methods.
|
92
|
+
|
93
|
+
|
94
|
+
require 'polyhedra/shorthand'
|
95
|
+
dice = 3.d6
|
96
|
+
# => <#Dice: 3d6>
|
97
|
+
|
98
|
+
fire1 = 1.d6("fire")
|
99
|
+
fire2 = 2.d8("fire")
|
100
|
+
fire3 = 3.d6(:fire) # symbols and strings are okay
|
101
|
+
frost = 2.d6("frost")
|
102
|
+
|
103
|
+
fire1 + fire2 + fire3 + frost
|
104
|
+
# => <DiceSet: <Dice: 4d6, fire>, <Dice: 2d8, fire>, <Dice: 2d6 frost>]
|
105
|
+
|
106
|
+
Note that while the shorthand allows for modifiers like -1 or x10, it
|
107
|
+
looks kind of lame.
|
108
|
+
|
109
|
+
1.d6("r1-1")
|
110
|
+
# => <Dice: 1d6r1-1>
|
111
|
+
2.d10("+3x10")
|
112
|
+
# => <Dice: 2d10+3x10>
|
113
|
+
d20
|
114
|
+
# => <Dice: 1d20>
|
115
|
+
|
116
|
+
I am open to changing the interface to cascade like so:
|
117
|
+
|
118
|
+
4.d6.r1.k3("+1x10") but honestly this still looks unnecessarily
|
119
|
+
complicated. Better to just use Dice.new() at this point.
|
120
|
+
|
121
|
+
Then again, since we have to catch method_missing to allow for
|
122
|
+
arbitrary digits, maybe we could steal a page from the Rails playbook
|
123
|
+
and allow longer methods names that encode the entire sequence, such
|
124
|
+
as:
|
125
|
+
|
126
|
+
4.d6r1t3
|
127
|
+
# => <Dice: 4d6r1t3
|
128
|
+
|
129
|
+
The only gotcha is that we can't capture +/- in its proper place
|
130
|
+
(immediately after the dX sequence). Legal ruby would be `4.d6r1t3+3`
|
131
|
+
but proper dice notation would `4.d6+3r1t3` which won't work.
|
132
|
+
|
133
|
+
Hrm, if a Dice.new() or Fixnum#dXX statement returns a Dice object,
|
134
|
+
maybe we should define +,-,*,/ etc to return new Dice objects?
|
135
|
+
|
136
|
+
d6
|
137
|
+
# => <Dice: 1d6>
|
138
|
+
d6 + 1
|
139
|
+
# => <Dice: 1d6+1>
|
140
|
+
|
141
|
+
|
142
|
+
## Complex Notation
|
143
|
+
|
144
|
+
A set of Dice can be collected into a DiceSet and rolled as a unit:
|
145
|
+
|
146
|
+
bag = DiceSet.new
|
147
|
+
bag << Dice.new('3d6')
|
148
|
+
bag << '2d4' # bag#<< accepts Dice or string
|
149
|
+
bag
|
150
|
+
# => <DiceSet: <Dice: 3d6>, <Dice: 2d4>>
|
151
|
+
bag.roll
|
152
|
+
# => 19
|
153
|
+
bag.max
|
154
|
+
# => 26
|
155
|
+
bag.min
|
156
|
+
# => 5
|
157
|
+
|
158
|
+
## Units
|
159
|
+
|
160
|
+
Each Dice object can have its own units, and units are treated as a
|
161
|
+
higher-order grouping than number of sides. Note that #min, #max and
|
162
|
+
#roll will return an array of pairs containing the amount and unit for
|
163
|
+
each type of dice.
|
164
|
+
|
165
|
+
bag = DiceSet.new
|
166
|
+
bag << 1.d6("fire")
|
167
|
+
bag << 1.d6("fire")
|
168
|
+
bag << 1.d4("frost")
|
169
|
+
bag.min
|
170
|
+
# => [[2, :fire], [1, :frost]]
|
171
|
+
bag.min_without_units
|
172
|
+
# => 3
|
173
|
+
bag.max
|
174
|
+
# => [[12, :fire], [4, :frost]]
|
175
|
+
bag.max_without_units
|
176
|
+
# => 16
|
177
|
+
bag.roll
|
178
|
+
# => [[7, :fire], [2, :frost]]
|
179
|
+
bag.roll_without_units
|
180
|
+
# => 11
|
181
|
+
|
182
|
+
You can also pass a specific unit to #min, #max and #roll to isolate
|
183
|
+
part of the roll:
|
184
|
+
|
185
|
+
bag.max(:fire)
|
186
|
+
# => 12
|
187
|
+
bag.min(:frost)
|
188
|
+
# => 1
|
189
|
+
bag.roll(:fire)
|
190
|
+
# => 3
|
191
|
+
|
192
|
+
|
193
|
+
# Contributing
|
194
|
+
|
195
|
+
1. Pull requests are welcome!
|
196
|
+
2. Fork the repo
|
197
|
+
3. Write _good_ specs for your change: specs that fully cover and
|
198
|
+
clearly document the intent of your change
|
199
|
+
4. Send a pull request
|
200
|
+
|
201
|
+
*Note:* If you want to make a wild and crazy change (and have it
|
202
|
+
merged into the gem), I'm open to that--but please get in touch with
|
203
|
+
me and let's chat about the direction you want to go so you don't
|
204
|
+
waste your time (unless you're okay with your changes staying in
|
205
|
+
your fork forever)
|
206
|
+
|
207
|
+
|
208
|
+
|
209
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/polyhedra.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module Polyhedra
|
2
|
+
class Dice
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_accessor :number, :sides, :offset, :multiplier, :divisor, :reroll_under, :take_top
|
6
|
+
attr_writer :rng
|
7
|
+
|
8
|
+
def initialize(dice_expression)
|
9
|
+
dice_expression = dice_expression.gsub(/s+/, '')
|
10
|
+
|
11
|
+
@offset = @reroll_under = 0
|
12
|
+
@multiplier = @divisor = 1
|
13
|
+
|
14
|
+
@take_top = @number = [1, dice_expression.to_i].max
|
15
|
+
dice_expression.sub!(/^\d+/, '')
|
16
|
+
|
17
|
+
while dice_expression.length > 0
|
18
|
+
action, amount, dice_expression = pop_expression(dice_expression)
|
19
|
+
amount = amount.to_i
|
20
|
+
case action
|
21
|
+
when 'd'
|
22
|
+
self.sides = amount
|
23
|
+
when '+'
|
24
|
+
self.offset = amount
|
25
|
+
when '-'
|
26
|
+
self.offset = -amount
|
27
|
+
when '*', 'x'
|
28
|
+
self.multiplier = amount
|
29
|
+
when '/'
|
30
|
+
self.divisor = amount
|
31
|
+
when 'r'
|
32
|
+
self.reroll_under = amount
|
33
|
+
when 't'
|
34
|
+
self.take_top = amount
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
str = ""
|
41
|
+
str << "%dd%d" % [number, sides]
|
42
|
+
str << "%+d" % offset unless offset.zero?
|
43
|
+
str << "x%d" % multiplier unless multiplier == 1
|
44
|
+
str << "/%d" % divisor unless divisor == 1
|
45
|
+
str << "r%d" % reroll_under unless reroll_under.zero?
|
46
|
+
str << "t%d" % take_top unless take_top == number
|
47
|
+
str
|
48
|
+
end
|
49
|
+
|
50
|
+
def roll
|
51
|
+
rolled_dice = []
|
52
|
+
|
53
|
+
while rolled_dice.size < number
|
54
|
+
roll=rand(sides)+1
|
55
|
+
rolled_dice << roll if roll > reroll_under
|
56
|
+
end
|
57
|
+
|
58
|
+
rolled_dice = rolled_dice.sort.reverse.take take_top
|
59
|
+
|
60
|
+
rolled_dice.inject {|a,b| a+b}
|
61
|
+
end
|
62
|
+
|
63
|
+
def min
|
64
|
+
((take_top + offset + (reroll_under*take_top)) * multiplier) / divisor
|
65
|
+
end
|
66
|
+
|
67
|
+
def max
|
68
|
+
((take_top * sides + offset) * multiplier) / divisor
|
69
|
+
end
|
70
|
+
|
71
|
+
def pop_expression(string)
|
72
|
+
%r|^([dr+-x\*/])(\d+)(.*)$|.match(string).to_a.dup.tap(&:shift)
|
73
|
+
end
|
74
|
+
|
75
|
+
# def dice_matrix
|
76
|
+
# # TODO: make this work with take_top--probably a sort/take in the each_slice?
|
77
|
+
# # TODO: make this work with reroll--probably change the 1..sides to reroll+1..sides ?
|
78
|
+
# Array.new(number) { (1..sides).to_a }.inject(:product).flatten.each_slice(number).map {|ray| ray.sort.take take_top }
|
79
|
+
# end
|
80
|
+
|
81
|
+
def rand(num)
|
82
|
+
rng.rand(num)
|
83
|
+
end
|
84
|
+
|
85
|
+
def inverted_divisor
|
86
|
+
1.0 / divisor
|
87
|
+
end
|
88
|
+
|
89
|
+
def <=>(other)
|
90
|
+
a,b = [:sides, :number, :multiplier, :inverted_divisor, :offset].map {|sym| [send(sym), other.send(sym)] }.detect {|a,b| (a <=> b) != 0 }
|
91
|
+
a <=> b
|
92
|
+
# if sides != other.sides
|
93
|
+
# sides <=> other.sides
|
94
|
+
# else
|
95
|
+
# if number != other.number
|
96
|
+
# number <=> other.number
|
97
|
+
# else
|
98
|
+
# if multiplier != other.multiplier
|
99
|
+
# multiplier <=> other.multiplier
|
100
|
+
# else
|
101
|
+
# -divisor <=> -other.divisor
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def rng
|
109
|
+
@rng ||= Random.new
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/polyhedra.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "polyhedra/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "polyhedra"
|
7
|
+
s.version = Polyhedra::VERSION
|
8
|
+
s.authors = ["David Brady"]
|
9
|
+
s.email = ["github@shinybit.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Dice manipulation and rolling gem capable of handling complex dice expressions}
|
12
|
+
s.description = %q{Dice manipulation and rolling gem capable of handling complex RPG dice expressions, from a simple "d6" up to "4d6r1k3-2x10 frost" and "3d6", etc.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "polyhedra"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
|
23
|
+
# s.add_runtime_dependency "treetop"
|
24
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'polyhedra/dice'
|
3
|
+
|
4
|
+
# I roll twenties! rand(n) always returns max possible value
|
5
|
+
class LuckyRng < Random
|
6
|
+
def rand(num)
|
7
|
+
num-1
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns 0, 1, 2, ..., n-1 in order.
|
12
|
+
class GaussianRng < Random
|
13
|
+
def rand(num)
|
14
|
+
@sequence ||= (0..num).cycle
|
15
|
+
@sequence.next
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns... whatever... you tell... it to...
|
20
|
+
class JediMindTrickRng < Random
|
21
|
+
def initialize(sequence)
|
22
|
+
@sequence = sequence.cycle
|
23
|
+
super(0)
|
24
|
+
end
|
25
|
+
|
26
|
+
def rand(num)
|
27
|
+
@sequence.next
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Polyhedra
|
32
|
+
describe Dice do
|
33
|
+
context "simple dice expression" do
|
34
|
+
let(:dice) { Dice.new("3d6") }
|
35
|
+
|
36
|
+
it { dice.offset.should == 0 }
|
37
|
+
it { dice.sides.should == 6 }
|
38
|
+
it { dice.number.should == 3 }
|
39
|
+
it { dice.roll.should >= 3 }
|
40
|
+
it { dice.roll.should <= 18 }
|
41
|
+
it { dice.min.should == 3 }
|
42
|
+
it { dice.max.should == 18 }
|
43
|
+
end
|
44
|
+
|
45
|
+
context "with offset" do
|
46
|
+
it { Dice.new("3d6+2").min.should == 5 }
|
47
|
+
it { Dice.new("3d6+2").max.should == 20 }
|
48
|
+
it { Dice.new("3d6-2").min.should == 1 }
|
49
|
+
it { Dice.new("3d6-2").max.should == 16 }
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with scalar" do
|
53
|
+
it { Dice.new("3d6x10").min.should == 30 }
|
54
|
+
it { Dice.new("3d6x10").max.should == 180 }
|
55
|
+
it { Dice.new("3d6*10").min.should == 30 }
|
56
|
+
it { Dice.new("3d6*10").max.should == 180 }
|
57
|
+
it { Dice.new("1d6/2").min.should == 0 }
|
58
|
+
it { Dice.new("1d6/2").max.should == 3 }
|
59
|
+
end
|
60
|
+
|
61
|
+
context "with reroll_under" do
|
62
|
+
let(:dice) { Dice.new("3d6r1") }
|
63
|
+
before(:each) do
|
64
|
+
dice.rng = JediMindTrickRng.new [0, 1, 0, 2, 0, 3]
|
65
|
+
end
|
66
|
+
it { dice.reroll_under.should == 1 }
|
67
|
+
it { dice.min.should == 6 }
|
68
|
+
it { dice.max.should == 18 }
|
69
|
+
|
70
|
+
it { dice.roll.should == 9 }
|
71
|
+
end
|
72
|
+
|
73
|
+
context "with take_top" do
|
74
|
+
let(:dice) { Dice.new("4d6t3") }
|
75
|
+
it { dice.take_top.should == 3 }
|
76
|
+
it { dice.max.should == 18 }
|
77
|
+
it { dice.min.should == 3 }
|
78
|
+
context "with loaded dice" do
|
79
|
+
before :each do
|
80
|
+
dice.rng = JediMindTrickRng.new [2,0,4,5]
|
81
|
+
end
|
82
|
+
it { dice.roll.should == 14 }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "when I roll twenties" do
|
87
|
+
let(:dice) { Dice.new("4d6") }
|
88
|
+
before :each do
|
89
|
+
dice.rng = LuckyRng.new
|
90
|
+
end
|
91
|
+
it { dice.roll.should == 24 }
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#pop_expression" do
|
95
|
+
describe "returns subexpression, rest_of_string" do
|
96
|
+
it { Dice.new("1d6").pop_expression("r3string").should == ["r", "3", "string"] }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "#to_s" do
|
101
|
+
it { Dice.new("d20").to_s.should == "1d20" }
|
102
|
+
it { Dice.new("1d4*10").to_s.should =="1d4x10" }
|
103
|
+
|
104
|
+
# These expressions should all parse and to_s as themselves
|
105
|
+
[ "3d6",
|
106
|
+
"1d4+1",
|
107
|
+
"3d6r1",
|
108
|
+
"4d6t3",
|
109
|
+
"6d6+3r2t4",
|
110
|
+
"1d10x10",
|
111
|
+
"1d4+2x10",
|
112
|
+
"1d6/2",
|
113
|
+
"9d8+2x5r2t6"
|
114
|
+
].each do |str|
|
115
|
+
it "correctly parses '#{str}'" do
|
116
|
+
Dice.new(str).to_s.should eq str
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "comparison" do
|
122
|
+
it { Dice.new("3d6").should == Dice.new("3d6") }
|
123
|
+
it("favors bases first") { Dice.new("10d4").should < Dice.new("1d5") }
|
124
|
+
it("favors number second") { Dice.new("3d6").should > Dice.new("2d6") }
|
125
|
+
it("favors multipliers third") { Dice.new("3d6x10").should > Dice.new("3d6x8") }
|
126
|
+
it("favors divisors third") { Dice.new("3d6/10").should < Dice.new("3d6/8") }
|
127
|
+
it("favors offset fourth") { Dice.new("3d6+3").should > Dice.new("3d6") }
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "dice math" do
|
131
|
+
describe "addition" do
|
132
|
+
it { Dice.new("3d6")}
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: polyhedra
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Brady
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-17 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Dice manipulation and rolling gem capable of handling complex RPG dice
|
15
|
+
expressions, from a simple "d6" up to "4d6r1k3-2x10 frost" and "3d6", etc.
|
16
|
+
email:
|
17
|
+
- github@shinybit.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .gitignore
|
23
|
+
- .rspec
|
24
|
+
- .rvmrc
|
25
|
+
- Gemfile
|
26
|
+
- Guardfile
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- lib/polyhedra.rb
|
30
|
+
- lib/polyhedra/dice.rb
|
31
|
+
- lib/polyhedra/version.rb
|
32
|
+
- polyhedra.gemspec
|
33
|
+
- spec/lib/dice_spec.rb
|
34
|
+
- spec/spec_helper.rb
|
35
|
+
homepage: ''
|
36
|
+
licenses: []
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubyforge_project: polyhedra
|
55
|
+
rubygems_version: 1.8.23
|
56
|
+
signing_key:
|
57
|
+
specification_version: 3
|
58
|
+
summary: Dice manipulation and rolling gem capable of handling complex dice expressions
|
59
|
+
test_files:
|
60
|
+
- spec/lib/dice_spec.rb
|
61
|
+
- spec/spec_helper.rb
|