tabletop 0.2.1 → 0.3.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.
- data/.gitignore +2 -1
- data/README.markdown +59 -37
- data/Rakefile +6 -2
- data/lib/fixnum.rb +4 -8
- data/lib/tabletop/condition.rb +20 -0
- data/lib/tabletop/pool.rb +61 -30
- data/lib/tabletop/randomizers.rb +37 -22
- data/lib/tabletop/roll.rb +37 -29
- data/lib/tabletop/token.rb +57 -19
- data/lib/tabletop/version.rb +1 -1
- data/lib/tabletop.rb +1 -0
- data/spec/condition_spec.rb +15 -0
- data/spec/fixnum_spec.rb +4 -0
- data/spec/pool_spec.rb +141 -117
- data/spec/randomizers_spec.rb +79 -59
- data/spec/roll_spec.rb +19 -18
- data/spec/spec_helper.rb +1 -5
- data/spec/token_spec.rb +72 -13
- data/tabletop.gemspec +1 -1
- metadata +7 -4
data/lib/tabletop/token.rb
CHANGED
@@ -1,50 +1,88 @@
|
|
1
1
|
module Tabletop
|
2
2
|
|
3
3
|
class NotEnoughTokensError < ArgumentError
|
4
|
+
def initialize(wanted, available)
|
5
|
+
w_t, a_t = "token", "token"
|
6
|
+
|
7
|
+
w_t << "s" if wanted > 1 or wanted == 0
|
8
|
+
|
9
|
+
a_t << "s" if available > 1 or available == 0
|
10
|
+
|
11
|
+
available = available > 0 ? available : "no"
|
12
|
+
|
13
|
+
super("tried to remove #{wanted} #{w_t} from a stack with #{available} #{a_t}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class ExceedMaxTokensError < ArgumentError
|
4
18
|
end
|
5
19
|
|
6
20
|
class TokenStack
|
7
21
|
|
8
|
-
# The number of tokens in the
|
9
|
-
attr_accessor :count
|
22
|
+
# The number of tokens in the stack, and the maximum number it can have
|
23
|
+
attr_accessor :count, :max
|
10
24
|
include Comparable
|
11
25
|
|
12
|
-
def initialize(num_tokens = 1)
|
26
|
+
def initialize(num_tokens = 1, hash={})
|
13
27
|
@count = num_tokens
|
28
|
+
@max = hash[:max]
|
14
29
|
end
|
15
30
|
|
16
31
|
def <=>(operand)
|
17
32
|
count <=> operand.to_int
|
18
33
|
end
|
19
34
|
|
35
|
+
def count=(new_value)
|
36
|
+
raise_if_over_max(new_value)
|
37
|
+
@count = new_value
|
38
|
+
end
|
39
|
+
|
20
40
|
def add(n = 1)
|
21
|
-
raise ArgumentError unless n.
|
41
|
+
raise ArgumentError unless n.respond_to?(:to_i)
|
42
|
+
raise ArgumentError if n < 0
|
43
|
+
n = n.to_i
|
44
|
+
raise_if_over_max(n + @count)
|
22
45
|
@count += n
|
23
46
|
end
|
24
47
|
|
48
|
+
def raise_if_over_max(value)
|
49
|
+
if @max
|
50
|
+
raise ExceedMaxTokensError if value > @max
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
25
54
|
# Raises NotEnoughTokensError if there aren't enough tokens to remove
|
26
55
|
def remove(n=1)
|
27
|
-
raise ArgumentError unless n.
|
56
|
+
raise ArgumentError unless n.respond_to?(:to_i)
|
57
|
+
n = n.to_i
|
58
|
+
raise ArgumentError if n < 0
|
28
59
|
if n > @count
|
29
|
-
|
30
|
-
|
31
|
-
n_t << "s" if n > 1 or n == 0
|
32
|
-
|
33
|
-
c_t << "s" if @count > 1 or @count == 0
|
34
|
-
|
35
|
-
c = @count > 0 ? @count : "no"
|
36
|
-
errmsg = "tried to remove #{n} #{n_t} from a stack with #{c} #{c_t}"
|
37
|
-
raise NotEnoughTokensError, errmsg
|
60
|
+
raise NotEnoughTokensError.new(n, @count)
|
38
61
|
end
|
39
62
|
@count -= n
|
40
63
|
end
|
41
64
|
|
42
|
-
#
|
43
|
-
#
|
65
|
+
# Usage: stack_a.move(N, :to => stack_b)
|
66
|
+
# Removes N tokens from stack_a, and adds
|
67
|
+
# the same number to stack_b
|
44
68
|
def move(n, opts)
|
45
|
-
|
46
|
-
|
47
|
-
|
69
|
+
begin
|
70
|
+
opts[:to].add(n)
|
71
|
+
rescue NoMethodError
|
72
|
+
raise ArgumentError
|
73
|
+
end
|
74
|
+
|
75
|
+
begin
|
76
|
+
remove(n)
|
77
|
+
rescue NotEnoughTokensError
|
78
|
+
opts[:to].remove(n)
|
79
|
+
raise
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def refresh
|
84
|
+
raise NoMethodError if @max.nil?
|
85
|
+
@count = @max
|
48
86
|
end
|
49
87
|
end
|
50
88
|
end
|
data/lib/tabletop/version.rb
CHANGED
data/lib/tabletop.rb
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Tabletop
|
4
|
+
describe Condition do
|
5
|
+
describe "#met_by?" do
|
6
|
+
it "it evaluates the block passed on initialization" do
|
7
|
+
c = Condition.new do |p|
|
8
|
+
p.sum > 7
|
9
|
+
end
|
10
|
+
c.met_by?(Pool.new("2/6 4/10")).should be_false
|
11
|
+
c.met_by?(Pool.new("4/8 4/12")).should be_true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/spec/fixnum_spec.rb
CHANGED
@@ -9,6 +9,10 @@ module Tabletop
|
|
9
9
|
10.d100.should be_instance_of(Pool)
|
10
10
|
end
|
11
11
|
|
12
|
+
it "raises an exception for invalid method names" do
|
13
|
+
expect {10.dthing}.to raise_error(NoMethodError)
|
14
|
+
end
|
15
|
+
|
12
16
|
it "shows up in respond_to?(:dN)" do
|
13
17
|
1.should respond_to(:d50)
|
14
18
|
10.should_not respond_to(:dthing)
|
data/spec/pool_spec.rb
CHANGED
@@ -2,121 +2,165 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
module Tabletop
|
4
4
|
describe Pool do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
|
6
|
+
let(:d6_set) { Pool.new("2/6 1/6 3/6 4/6 5/6 6/6") }
|
7
|
+
|
8
|
+
describe ".new" do
|
9
|
+
it "can accept a string of d-notation" do
|
10
|
+
p = Pool.new("2d10 d20")
|
11
|
+
p.length.should == 3
|
12
|
+
p[0].sides.should == 10
|
13
|
+
p[1].sides.should == 10
|
14
|
+
p[2].sides.should == 20
|
15
|
+
end
|
16
|
+
it "can accept an array of dice objects" do
|
17
|
+
# mostly used internally
|
18
|
+
p = Pool.new([Die.new(value: 1), Die.new(sides: 4)])
|
19
|
+
p.length.should == 2
|
20
|
+
p[0].sides.should == 6
|
21
|
+
p[0].value.should == 1
|
22
|
+
p[1].sides.should == 4
|
23
|
+
end
|
24
|
+
it "can accept a string describing a specific dice configuration" do
|
25
|
+
pool = Pool.new("1/4 2/6 3/8")
|
26
|
+
pool.length.should == 3
|
27
|
+
pool[0].value.should == 1
|
28
|
+
pool[0].sides.should == 4
|
29
|
+
pool[1].value.should == 2
|
30
|
+
pool[1].sides.should == 6
|
31
|
+
pool[2].value.should == 3
|
32
|
+
pool[2].sides.should == 8
|
33
|
+
end
|
11
34
|
end
|
12
35
|
|
13
36
|
describe "#dice" do
|
14
37
|
it "should return an array of dice notation" do
|
15
|
-
@mixed.dice.should == ["2d10","d20"]
|
16
|
-
@d6.dice.should == ["d6"]
|
17
|
-
@d17s.dice.should == ["5d17"]
|
18
|
-
@fudge.dice.should == ["3dF"]
|
19
38
|
Pool.new("d20 2dF 2d10").dice.should == ["2d10","d20", "2dF"]
|
20
39
|
end
|
21
40
|
end
|
22
41
|
|
23
42
|
describe "[]" do
|
24
|
-
it "should access
|
25
|
-
|
26
|
-
|
43
|
+
it "should access the objects " do
|
44
|
+
d = Pool.new("1/4")[0]
|
45
|
+
d.value.should == 1
|
46
|
+
d.sides.should == 4
|
27
47
|
end
|
28
48
|
end
|
29
49
|
|
30
50
|
describe "+" do
|
31
|
-
|
32
|
-
|
33
|
-
(@d6 + @fudge).should be_instance_of(Pool)
|
34
|
-
end
|
35
|
-
|
36
|
-
it "should persist die types" do
|
37
|
-
(@d6 + @fudge)[1].should be_instance_of(FudgeDie)
|
38
|
-
end
|
39
|
-
|
40
|
-
it "should join pools without rolling them" do
|
41
|
-
merge = @d6 + @d17s
|
42
|
-
merge.values.should == [2, 5, 16, 1, 17, 9]
|
43
|
-
merge.roll
|
44
|
-
merge.values.should == [4, 17, 5, 16, 12, 12]
|
45
|
-
end
|
46
|
-
|
47
|
-
it "creates genuinely new pools" do
|
48
|
-
merge = @d6 + @d17s
|
49
|
-
merge.roll
|
50
|
-
@d6.values.should == [2]
|
51
|
-
@d17s.values.should == [5, 16, 1, 17, 9]
|
51
|
+
before(:each) do
|
52
|
+
@a = 5.d6
|
52
53
|
end
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
@mixed = Pool.new("2d10 d20")
|
58
|
-
(@d6 + @d17s).dice.should == ["d6", "5d17"]
|
59
|
-
(@d17s + @d6).dice.should == ["d6", "5d17"]
|
60
|
-
(@d17s + @mixed).dice.should == ["2d10","5d17","d20"]
|
61
|
-
(@mixed + @fudge).dice.should == ["2d10", "d20", "3dF"]
|
54
|
+
context "adding a number" do
|
55
|
+
it "should return the pool's sum plus the number" do
|
56
|
+
(@a + 5).should == @a.sum + 5
|
57
|
+
end
|
62
58
|
end
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
59
|
+
context "adding a randomizer" do
|
60
|
+
it "adds to the pool" do
|
61
|
+
mixed = @a + Die.new
|
62
|
+
mixed.length.should == 6
|
63
|
+
end
|
64
|
+
it "preserves class" do
|
65
|
+
(@a + FudgeDie.new(value:-1))[-1].value.should == -1
|
66
|
+
(@a + Coin.new)[-1].should respond_to :flip
|
67
|
+
end
|
68
68
|
end
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
69
|
+
context "adding another pool" do
|
70
|
+
before(:each) do
|
71
|
+
@b = 4.d4
|
72
|
+
@merge = @a+@b
|
73
|
+
end
|
74
|
+
it "should make a union of the pools" do
|
75
|
+
@merge.values.should == @a.values + @b.values
|
76
|
+
end
|
77
|
+
it "should make new die objects" do
|
78
|
+
@merge.roll
|
79
|
+
@merge.values.should_not == @a.values + @b.values
|
80
|
+
end
|
81
|
+
it "should persist die types" do
|
82
|
+
(Pool.new("d6")+Pool.new("dF"))[1].should be_instance_of(FudgeDie)
|
83
|
+
(Pool.new("d6")+Pool.new([Coin.new]))[1].should respond_to(:flip)
|
84
|
+
end
|
85
|
+
it "should alter #dice accordingly" do
|
86
|
+
(Pool.new("2d17 d6")+Pool.new("3d17")).dice.should == ["d6", "5d17"]
|
87
|
+
end
|
76
88
|
end
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
89
|
+
context "adding anything else" do
|
90
|
+
it "should raise an exception" do
|
91
|
+
expect {Pool.new("d6") + "foof"}.to raise_error(ArgumentError)
|
92
|
+
expect {Pool.new("d6") + [Die.new, Object.new]}.to raise_error(ArgumentError)
|
93
|
+
end
|
81
94
|
end
|
82
95
|
end
|
83
96
|
|
84
97
|
describe "*" do
|
85
98
|
it "should multiply by the sum of the pool" do
|
86
99
|
(1..10).each do |v|
|
87
|
-
p = Pool.new(
|
100
|
+
p = Pool.new("#{v}/10")
|
88
101
|
(p * 5).should == (v * 5)
|
89
102
|
(5 * p).should == (5 * v)
|
90
103
|
end
|
91
104
|
end
|
92
105
|
end
|
106
|
+
|
107
|
+
describe "-" do
|
108
|
+
context "subtracting a number" do
|
109
|
+
it "should return the pool's sum minus the number" do
|
110
|
+
(d6_set - 1).should == 20
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
93
114
|
|
94
115
|
describe "#values" do
|
95
|
-
it "should be an array of
|
96
|
-
|
97
|
-
|
98
|
-
|
116
|
+
it "should be an array of the values of the dice" do
|
117
|
+
d6_set.values.each_with_index do |v, i|
|
118
|
+
v.should == d6_set[i].value
|
119
|
+
end
|
99
120
|
end
|
100
121
|
end
|
101
122
|
|
102
123
|
describe "#roll" do
|
103
124
|
it "should return the Pool itself" do
|
104
|
-
|
105
|
-
|
125
|
+
actual = d6_set.roll
|
126
|
+
d6_set.length.times do |i|
|
127
|
+
actual[i].value.should == d6_set[i].value
|
128
|
+
actual[i].sides.should == d6_set[i].sides
|
129
|
+
end
|
106
130
|
end
|
107
131
|
|
108
|
-
it "
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
@d17s.values.should == [17, 5, 16, 12, 12]
|
113
|
-
@mixed.roll
|
114
|
-
@mixed.values.should == [2, 9, 5]
|
132
|
+
it "calls roll on its contents" do
|
133
|
+
d = double("a die")
|
134
|
+
d.should_receive(:roll)
|
135
|
+
Pool.new([d]).roll
|
115
136
|
end
|
116
137
|
it "can roll only dice below a certain value"
|
117
138
|
it "can roll only dice above a certain value"
|
118
139
|
it "can roll only dice equal to a certain value"
|
119
140
|
end
|
141
|
+
|
142
|
+
describe "#roll_if" do
|
143
|
+
before :each do
|
144
|
+
@d1 = double("a die")
|
145
|
+
@d2 = double("a die")
|
146
|
+
end
|
147
|
+
it "rolls dice when the block returns true" do
|
148
|
+
@d1.should_receive(:roll)
|
149
|
+
Pool.new([@d1]).roll_if {|die| true}
|
150
|
+
end
|
151
|
+
it "doesn't roll dice when the block returns false" do
|
152
|
+
@d1.should_not_receive(:roll)
|
153
|
+
Pool.new([@d1]).roll_if {|die| false}
|
154
|
+
end
|
155
|
+
it "rolls dice that satisfy the block condition" do
|
156
|
+
@d1.stub(:sides).and_return(3)
|
157
|
+
@d2.stub(:sides).and_return(4)
|
158
|
+
|
159
|
+
@d1.should_not_receive(:roll)
|
160
|
+
@d2.should_receive(:roll)
|
161
|
+
Pool.new([@d1, @d2]).roll_if {|die| die.sides > 3}
|
162
|
+
end
|
163
|
+
end
|
120
164
|
|
121
165
|
describe "#sum" do
|
122
166
|
it "should sum the dice values" do
|
@@ -136,94 +180,74 @@ module Tabletop
|
|
136
180
|
|
137
181
|
describe "<=>" do
|
138
182
|
it "should compare the sums of different pools" do
|
139
|
-
|
140
|
-
|
183
|
+
Pool.new("1/4 1/4").should == Pool.new("2/6")
|
184
|
+
Pool.new("10/10").should == Pool.new("10/50")
|
185
|
+
Pool.new("3/6").should < Pool.new("4/4")
|
141
186
|
end
|
142
187
|
|
143
188
|
it "should compare pools to numbers" do
|
144
|
-
|
145
|
-
|
146
|
-
|
189
|
+
Pool.new("4/8 5/10").should < 10
|
190
|
+
Pool.new("1/6 1/8").should == 2
|
191
|
+
Pool.new("49/50").should <= 49
|
147
192
|
end
|
148
193
|
end
|
149
194
|
|
150
195
|
describe "#sets" do
|
151
|
-
it "should
|
152
|
-
|
153
|
-
ore.sets.should == ["2x9", "2x5", "2x4", "2x2", "1x7", "1x1"]
|
154
|
-
ore.roll
|
155
|
-
ore.sets.should == ["3x10", "2x7", "1x6", "1x5", "1x4", "1x3", "1x2"]
|
156
|
-
ore.roll
|
157
|
-
ore.sets.should == ["3x9", "2x8", "2x7", "1x10", "1x3", "1x1"]
|
196
|
+
it "should group dice in sets, by order of height, then width" do
|
197
|
+
Pool.new("9/10 1/10 5/10 4/10 9/10 5/10 7/10 4/10").sets.should == ["2x9", "2x5", "2x4", "1x7", "1x1"]
|
158
198
|
end
|
159
199
|
end
|
160
200
|
|
161
201
|
describe "#highest" do
|
162
202
|
it "should return a pool of the highest-value die" do
|
163
|
-
|
164
|
-
@d6.highest.values.should == [2]
|
165
|
-
@d17s.highest.values.should == [17]
|
166
|
-
@mixed.highest.values.should == [11]
|
203
|
+
d6_set.highest.values.should == [6]
|
167
204
|
end
|
168
205
|
|
169
206
|
it "should return as many items as are specified" do
|
170
|
-
|
171
|
-
|
172
|
-
@mixed.highest(2).values.should == [11, 10]
|
207
|
+
d6_set.highest(3).values.should == [4,5,6]
|
208
|
+
d6_set.highest(10).values.should == [2,1,3,4,5,6]
|
173
209
|
end
|
174
210
|
end
|
175
211
|
|
176
212
|
describe "#lowest" do
|
177
213
|
it "should return a pool of the lowest-value die." do
|
178
|
-
|
179
|
-
@d17s.lowest.should be_instance_of(Pool)
|
180
|
-
@d17s.lowest.values.should == [1]
|
181
|
-
@mixed.lowest.values.should == [1]
|
214
|
+
d6_set.lowest.values.should == [1]
|
182
215
|
end
|
183
216
|
|
184
217
|
it "should return as many items as are specified" do
|
185
|
-
|
186
|
-
|
187
|
-
@mixed.lowest(2).values.should == [1, 10]
|
218
|
+
d6_set.lowest(3).values.should == [2,1,3]
|
219
|
+
d6_set.lowest(10).values.should == [2,1,3,4,5,6]
|
188
220
|
end
|
189
221
|
end
|
190
222
|
|
191
223
|
describe "#drop_highest" do
|
192
224
|
it "should return a new pool missing the highest result" do
|
193
|
-
|
194
|
-
p.values.should == [5, 16, 1, 9]
|
195
|
-
@d17s.values.should == [5, 16, 1, 17, 9]
|
225
|
+
d6_set.drop_highest.values.should == [2,1,3,4,5]
|
196
226
|
end
|
197
227
|
|
198
228
|
it "should drop as many items as are specified and are possible" do
|
199
|
-
|
200
|
-
|
201
|
-
p = @d6.drop_highest(10)
|
202
|
-
p.values.should == []
|
229
|
+
d6_set.drop_highest(3).values.should == [2,1,3]
|
230
|
+
d6_set.drop_highest(10).values.should == []
|
203
231
|
end
|
204
232
|
end
|
205
233
|
|
206
234
|
describe "#drop_lowest" do
|
207
235
|
it "should return a pool missing the lowest result" do
|
208
|
-
|
209
|
-
p.values.should == [5, 16, 17, 9]
|
210
|
-
@d17s.values.should == [5, 16, 1, 17, 9]
|
236
|
+
d6_set.drop_lowest.values.should == [2, 3, 4, 5, 6]
|
211
237
|
end
|
212
238
|
|
213
|
-
it "should drop as many items as are specified" do
|
214
|
-
|
215
|
-
|
239
|
+
it "should drop as many items as are specified and are possible" do
|
240
|
+
d6_set.drop_lowest(2).values.should == [3,4,5,6]
|
241
|
+
d6_set.drop_lowest(10).values.should == []
|
216
242
|
end
|
217
243
|
end
|
218
244
|
|
219
245
|
describe "#drop" do
|
220
246
|
it "should drop any dice of the specified value" do
|
221
247
|
ore = Pool.new("10d10")
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
at_least_three = ore.drop([1,2])
|
226
|
-
at_least_three.values.should == [4, 5, 7, 9, 9, 5, 4]
|
248
|
+
(10..1).each do |i|
|
249
|
+
ore.drop(i).should_not include(i)
|
250
|
+
end
|
227
251
|
end
|
228
252
|
end
|
229
253
|
|