games_dice 0.0.1 → 0.0.2
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/lib/games_dice.rb +1 -0
- data/lib/games_dice/die_result.rb +162 -0
- data/lib/games_dice/version.rb +1 -1
- data/spec/die_result_spec.rb +261 -0
- data/spec/die_spec.rb +1 -1
- metadata +5 -2
data/lib/games_dice.rb
CHANGED
@@ -0,0 +1,162 @@
|
|
1
|
+
# returned by any complex die roll (i.e. one that may be subject to re-rolls or adjustments to each die)
|
2
|
+
# dr = GamesDice::DieResult.new
|
3
|
+
# dr.add_roll(5)
|
4
|
+
# dr.add_roll(4,:reroll_add)
|
5
|
+
# dr.value # => 9
|
6
|
+
# dr.rolls # => [5,4]
|
7
|
+
# dr.roll_reasons # => [:basic,:reroll_add]
|
8
|
+
# dr + 5 # => 14
|
9
|
+
# As the last example implies, GamesDice::DieResult objects coerce to the #value attribute
|
10
|
+
class GamesDice::DieResult
|
11
|
+
include Comparable
|
12
|
+
|
13
|
+
# allowed reasons for making a roll, and symbol to use before number in #explain
|
14
|
+
REASONS = {
|
15
|
+
:basic => ',',
|
16
|
+
:reroll_add => '+',
|
17
|
+
:reroll_new_die => '*', # TODO: This needs to be flagged *before* value, and maybe linked to cause
|
18
|
+
:reroll_new_keeper => '*',
|
19
|
+
:reroll_subtract => '-',
|
20
|
+
:reroll_replace => '|',
|
21
|
+
:reroll_use_best => '/',
|
22
|
+
:reroll_use_worst => '\\',
|
23
|
+
}
|
24
|
+
|
25
|
+
# first_roll_result is optional value of first roll of the die
|
26
|
+
def initialize( first_roll_result=nil, first_roll_reason=:basic )
|
27
|
+
unless REASONS.has_key?(first_roll_reason)
|
28
|
+
raise ArgumentError, "Unrecognised reason for roll #{first_roll_reason}"
|
29
|
+
end
|
30
|
+
|
31
|
+
if (first_roll_result)
|
32
|
+
@rolls = [Integer(first_roll_result)]
|
33
|
+
@roll_reasons = [first_roll_reason]
|
34
|
+
@total = @rolls[0]
|
35
|
+
else
|
36
|
+
@rolls = []
|
37
|
+
@roll_reasons = []
|
38
|
+
@total = nil
|
39
|
+
end
|
40
|
+
@mapped = false
|
41
|
+
@value = @total
|
42
|
+
end
|
43
|
+
|
44
|
+
public
|
45
|
+
|
46
|
+
# array of integers
|
47
|
+
attr_reader :rolls
|
48
|
+
|
49
|
+
# array of symbol reasons for making roll
|
50
|
+
attr_reader :roll_reasons
|
51
|
+
|
52
|
+
# combined numeric value of all rolls, nil if nothing calculated yet. Often the same number as
|
53
|
+
# #result, but may be different if there has been a call to #apply_map
|
54
|
+
attr_reader :total
|
55
|
+
|
56
|
+
# overall result of applying all rolls, nil if nothing calculated yet
|
57
|
+
attr_reader :value
|
58
|
+
|
59
|
+
# true if #apply_map has been called, and no more roll results added since
|
60
|
+
attr_reader :mapped
|
61
|
+
|
62
|
+
# stores the value of a simple die roll. roll_result should be an Integer,
|
63
|
+
# roll_reason is an optional symbol description of why the roll was made
|
64
|
+
# #total and #value are calculated based on roll_reason
|
65
|
+
def add_roll( roll_result, roll_reason=:basic )
|
66
|
+
unless REASONS.has_key?(roll_reason)
|
67
|
+
raise ArgumentError, "Unrecognised reason for roll #{roll_reason}"
|
68
|
+
end
|
69
|
+
@rolls << Integer(roll_result)
|
70
|
+
@roll_reasons << roll_reason
|
71
|
+
if @rolls.length == 1
|
72
|
+
@total = 0
|
73
|
+
end
|
74
|
+
|
75
|
+
case roll_reason
|
76
|
+
when :basic
|
77
|
+
@total = roll_result
|
78
|
+
when :reroll_add
|
79
|
+
@total += roll_result
|
80
|
+
when :reroll_subtract
|
81
|
+
@total -= roll_result
|
82
|
+
when :reroll_new_die
|
83
|
+
@total = roll_result
|
84
|
+
when :reroll_new_keeper
|
85
|
+
@total = roll_result
|
86
|
+
when :reroll_replace
|
87
|
+
@total = roll_result
|
88
|
+
when :reroll_use_best
|
89
|
+
@total = [@value,roll_result].max
|
90
|
+
when :reroll_use_worst
|
91
|
+
@total = [@value,roll_result].min
|
92
|
+
end
|
93
|
+
|
94
|
+
@mapped = false
|
95
|
+
@value = @total
|
96
|
+
end
|
97
|
+
|
98
|
+
# sets #value arbitrarily intended for use by GamesDice::MapRule objects
|
99
|
+
def apply_map( to_value, description = '' )
|
100
|
+
@mapped = true
|
101
|
+
@value = to_value
|
102
|
+
@map_description = description
|
103
|
+
end
|
104
|
+
|
105
|
+
# returns a string summary of how #value was obtained, showing all contributing rolls
|
106
|
+
def explain_value
|
107
|
+
text = ''
|
108
|
+
if @rolls.length < 2
|
109
|
+
text = @total.to_s
|
110
|
+
else
|
111
|
+
text = '[' + @rolls[0].to_s
|
112
|
+
text = (1..@rolls.length-1).inject( text ) { |so_far,i| so_far + REASONS[@roll_reasons[i]] + @rolls[i].to_s }
|
113
|
+
text += '] ' + @total.to_s
|
114
|
+
end
|
115
|
+
text += ' ' + @map_description if @mapped && @map_description && @map_description.length > 0
|
116
|
+
return text
|
117
|
+
end
|
118
|
+
|
119
|
+
# returns a string summary of the #total, including effect of any maps that been applied
|
120
|
+
def explain_total
|
121
|
+
text = @total.to_s
|
122
|
+
text += ' ' + @map_description if @mapped && @map_description && @map_description.length > 0
|
123
|
+
return text
|
124
|
+
end
|
125
|
+
|
126
|
+
# all coercions simply use #value (i.e. nil or a Fixnum)
|
127
|
+
def coerce(thing)
|
128
|
+
@value.coerce(thing)
|
129
|
+
end
|
130
|
+
|
131
|
+
# addition uses #value
|
132
|
+
def +(thing)
|
133
|
+
@value + thing
|
134
|
+
end
|
135
|
+
|
136
|
+
# subtraction uses #value
|
137
|
+
def -(thing)
|
138
|
+
@value - thing
|
139
|
+
end
|
140
|
+
|
141
|
+
# multiplication uses #value
|
142
|
+
def *(thing)
|
143
|
+
@value * thing
|
144
|
+
end
|
145
|
+
|
146
|
+
# comparison <=> uses #value
|
147
|
+
def <=>(other)
|
148
|
+
self.value <=> other
|
149
|
+
end
|
150
|
+
|
151
|
+
# implements a deep clone (used for recursive probability calculations)
|
152
|
+
def clone
|
153
|
+
cloned = GamesDice::DieResult.new()
|
154
|
+
cloned.instance_variable_set('@rolls', @rolls.clone)
|
155
|
+
cloned.instance_variable_set('@roll_reasons', @roll_reasons.clone)
|
156
|
+
cloned.instance_variable_set('@total', @total)
|
157
|
+
cloned.instance_variable_set('@value', @value)
|
158
|
+
cloned.instance_variable_set('@mapped', @mapped)
|
159
|
+
cloned.instance_variable_set('@map_description', @map_description)
|
160
|
+
cloned
|
161
|
+
end
|
162
|
+
end
|
data/lib/games_dice/version.rb
CHANGED
@@ -0,0 +1,261 @@
|
|
1
|
+
require 'games_dice'
|
2
|
+
|
3
|
+
describe GamesDice::DieResult do
|
4
|
+
|
5
|
+
describe ".new" do
|
6
|
+
|
7
|
+
it "should work without parameters to represent 'no results yet'" do
|
8
|
+
die_result = GamesDice::DieResult.new()
|
9
|
+
die_result.value.should == nil
|
10
|
+
die_result.rolls.should == []
|
11
|
+
die_result.roll_reasons.should == []
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should work with a single Integer param to represent an initial result" do
|
15
|
+
die_result = GamesDice::DieResult.new(8)
|
16
|
+
die_result.value.should == 8
|
17
|
+
die_result.rolls.should == [8]
|
18
|
+
die_result.roll_reasons.should == [:basic]
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should not accept a param that cannot be coerced to Integer" do
|
22
|
+
lambda { GamesDice::DieResult.new([]) }.should raise_error( TypeError )
|
23
|
+
lambda { GamesDice::DieResult.new('N') }.should raise_error( ArgumentError )
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should not accept unknown reasons for making a roll" do
|
27
|
+
lambda { GamesDice::DieResult.new(8,'wooo') }.should raise_error( ArgumentError )
|
28
|
+
lambda { GamesDice::DieResult.new(8,:frabulous) }.should raise_error( ArgumentError )
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#add_roll" do
|
34
|
+
|
35
|
+
context "starting from 'no results yet'" do
|
36
|
+
let(:die_result) { GamesDice::DieResult.new() }
|
37
|
+
|
38
|
+
it "should create an initial result" do
|
39
|
+
die_result.add_roll(5)
|
40
|
+
die_result.value.should == 5
|
41
|
+
die_result.rolls.should == [5]
|
42
|
+
die_result.roll_reasons.should == [:basic]
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should accept non-basic reasons for the first roll" do
|
46
|
+
die_result.add_roll(4,:reroll_subtract)
|
47
|
+
die_result.value.should == -4
|
48
|
+
die_result.rolls.should == [4]
|
49
|
+
die_result.roll_reasons.should == [:reroll_subtract]
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should not accept a first param that cannot be coerced to Integer" do
|
53
|
+
lambda { die_result.add_roll([]) }.should raise_error( TypeError )
|
54
|
+
lambda { die_result.add_roll('N') }.should raise_error( ArgumentError )
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should not accept an unsupported second param" do
|
58
|
+
lambda { die_result.add_roll(5,[]) }.should raise_error( ArgumentError )
|
59
|
+
lambda { die_result.add_roll(15,:bam) }.should raise_error( ArgumentError )
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
context "starting with an initial result" do
|
65
|
+
let(:die_result) { GamesDice::DieResult.new(7) }
|
66
|
+
|
67
|
+
it "should not accept a first param that cannot be coerced to Integer" do
|
68
|
+
lambda { die_result.add_roll([]) }.should raise_error( TypeError )
|
69
|
+
lambda { die_result.add_roll('N') }.should raise_error( ArgumentError )
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should not accept an unsupported second param" do
|
73
|
+
lambda { die_result.add_roll(5,[]) }.should raise_error( ArgumentError )
|
74
|
+
lambda { die_result.add_roll(15,:bam) }.should raise_error( ArgumentError )
|
75
|
+
end
|
76
|
+
|
77
|
+
context "add another basic roll" do
|
78
|
+
it "should replace an initial result, as if the die were re-rolled" do
|
79
|
+
die_result.add_roll(5)
|
80
|
+
die_result.value.should == 5
|
81
|
+
die_result.rolls.should == [7,5]
|
82
|
+
die_result.roll_reasons.should == [:basic, :basic]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "exploding dice" do
|
87
|
+
it "should add to value when exploding up" do
|
88
|
+
die_result.add_roll( 6, :reroll_add )
|
89
|
+
die_result.value.should == 13
|
90
|
+
die_result.rolls.should == [7,6]
|
91
|
+
die_result.roll_reasons.should == [:basic, :reroll_add]
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should subtract from value when exploding down" do
|
95
|
+
die_result.add_roll( 4, :reroll_subtract )
|
96
|
+
die_result.value.should == 3
|
97
|
+
die_result.rolls.should == [7,4]
|
98
|
+
die_result.roll_reasons.should == [:basic, :reroll_subtract]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "re-roll dice" do
|
103
|
+
it "should optionally replace roll unconditionally" do
|
104
|
+
die_result.add_roll( 2, :reroll_replace )
|
105
|
+
die_result.value.should == 2
|
106
|
+
die_result.rolls.should == [7,2]
|
107
|
+
die_result.roll_reasons.should == [:basic, :reroll_replace]
|
108
|
+
|
109
|
+
die_result.add_roll( 5, :reroll_replace )
|
110
|
+
die_result.value.should == 5
|
111
|
+
die_result.rolls.should == [7,2,5]
|
112
|
+
die_result.roll_reasons.should == [:basic, :reroll_replace, :reroll_replace]
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should optionally use best roll" do
|
116
|
+
die_result.add_roll( 2, :reroll_use_best )
|
117
|
+
die_result.value.should == 7
|
118
|
+
die_result.rolls.should == [7,2]
|
119
|
+
die_result.roll_reasons.should == [:basic, :reroll_use_best]
|
120
|
+
|
121
|
+
die_result.add_roll( 9, :reroll_use_best )
|
122
|
+
die_result.value.should == 9
|
123
|
+
die_result.rolls.should == [7,2,9]
|
124
|
+
die_result.roll_reasons.should == [:basic, :reroll_use_best, :reroll_use_best]
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should optionally use worst roll" do
|
128
|
+
die_result.add_roll( 4, :reroll_use_worst )
|
129
|
+
die_result.value.should == 4
|
130
|
+
die_result.rolls.should == [7,4]
|
131
|
+
die_result.roll_reasons.should == [:basic, :reroll_use_worst]
|
132
|
+
|
133
|
+
die_result.add_roll( 5, :reroll_use_worst )
|
134
|
+
die_result.value.should == 4
|
135
|
+
die_result.rolls.should == [7,4,5]
|
136
|
+
die_result.roll_reasons.should == [:basic, :reroll_use_worst, :reroll_use_worst]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "combinations of reroll reasons" do
|
141
|
+
it "should correctly handle valid reasons for extra rolls in combination" do
|
142
|
+
die_result.add_roll( 10, :reroll_add )
|
143
|
+
die_result.add_roll( 3, :reroll_subtract)
|
144
|
+
die_result.value.should == 14
|
145
|
+
die_result.rolls.should == [7,10,3]
|
146
|
+
die_result.roll_reasons.should == [:basic, :reroll_add, :reroll_subtract]
|
147
|
+
|
148
|
+
die_result.add_roll( 12, :reroll_replace )
|
149
|
+
die_result.value.should == 12
|
150
|
+
die_result.rolls.should == [7,10,3,12]
|
151
|
+
die_result.roll_reasons.should == [:basic, :reroll_add, :reroll_subtract, :reroll_replace]
|
152
|
+
|
153
|
+
die_result.add_roll( 9, :reroll_use_best )
|
154
|
+
die_result.value.should == 12
|
155
|
+
die_result.rolls.should == [7,10,3,12,9]
|
156
|
+
die_result.roll_reasons.should == [:basic, :reroll_add, :reroll_subtract, :reroll_replace, :reroll_use_best]
|
157
|
+
|
158
|
+
die_result.add_roll( 15, :reroll_add)
|
159
|
+
die_result.value.should == 27
|
160
|
+
die_result.rolls.should == [7,10,3,12,9,15]
|
161
|
+
die_result.roll_reasons.should == [:basic, :reroll_add, :reroll_subtract, :reroll_replace, :reroll_use_best, :reroll_add]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "#explain_value" do
|
170
|
+
let(:die_result) { GamesDice::DieResult.new() }
|
171
|
+
|
172
|
+
it "should be empty string for 'no results yet'" do
|
173
|
+
die_result.explain_value.should == ''
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should be a simple stringified number when there is one die roll" do
|
177
|
+
die_result.add_roll(3)
|
178
|
+
die_result.explain_value.should == '3'
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should describe all single rolls made and how they combine" do
|
182
|
+
die_result.add_roll(6)
|
183
|
+
die_result.explain_value.should == '6'
|
184
|
+
|
185
|
+
die_result.add_roll(5,:reroll_add)
|
186
|
+
die_result.explain_value.should == '[6+5] 11'
|
187
|
+
|
188
|
+
die_result.add_roll(2,:reroll_replace)
|
189
|
+
die_result.explain_value.should == '[6+5|2] 2'
|
190
|
+
|
191
|
+
die_result.add_roll(7,:reroll_subtract)
|
192
|
+
die_result.explain_value.should == '[6+5|2-7] -5'
|
193
|
+
|
194
|
+
die_result.add_roll(4,:reroll_use_worst)
|
195
|
+
die_result.explain_value.should == '[6+5|2-7\\4] -5'
|
196
|
+
|
197
|
+
die_result.add_roll(3,:reroll_use_best)
|
198
|
+
die_result.explain_value.should == '[6+5|2-7\\4/3] 3'
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should combine via +,- and * intuitively based on #value" do
|
205
|
+
die_result = GamesDice::DieResult.new(7)
|
206
|
+
(die_result + 3).should == 10
|
207
|
+
(4 + die_result).should == 11
|
208
|
+
(die_result - 2).should == 5
|
209
|
+
(9 - die_result).should == 2
|
210
|
+
|
211
|
+
(die_result + 7.7).should == 14.7
|
212
|
+
(4.1 + die_result).should == 11.1
|
213
|
+
|
214
|
+
(die_result * 2).should == 14
|
215
|
+
(1 * die_result).should == 7
|
216
|
+
|
217
|
+
other_die_result = GamesDice::DieResult.new(6)
|
218
|
+
other_die_result.add_roll(6,:reroll_add)
|
219
|
+
(die_result + other_die_result).should == 19
|
220
|
+
(other_die_result - die_result).should == 5
|
221
|
+
end
|
222
|
+
|
223
|
+
it "should support comparison with >,<,>=,<= as if it were an integer, based on #value" do
|
224
|
+
die_result = GamesDice::DieResult.new(7)
|
225
|
+
|
226
|
+
(die_result > 3).should be_true
|
227
|
+
(14 > die_result).should be_true
|
228
|
+
(die_result >= 7).should be_true
|
229
|
+
(9.5 >= die_result).should be_true
|
230
|
+
(die_result < 3).should be_false
|
231
|
+
(14 < die_result).should be_false
|
232
|
+
(die_result <= 8).should be_true
|
233
|
+
(14 <= die_result).should be_false
|
234
|
+
|
235
|
+
other_die_result = GamesDice::DieResult.new(6)
|
236
|
+
other_die_result.add_roll(6,:reroll_add)
|
237
|
+
(die_result > other_die_result).should be_false
|
238
|
+
(other_die_result > die_result).should be_true
|
239
|
+
(die_result >= other_die_result).should be_false
|
240
|
+
(other_die_result >= die_result).should be_true
|
241
|
+
(die_result < other_die_result).should be_true
|
242
|
+
(other_die_result < die_result).should be_false
|
243
|
+
(die_result <= other_die_result).should be_true
|
244
|
+
(other_die_result <= die_result).should be_false
|
245
|
+
|
246
|
+
end
|
247
|
+
|
248
|
+
it "should sort, based on #value" do
|
249
|
+
die_results = [
|
250
|
+
GamesDice::DieResult.new(7), GamesDice::DieResult.new(5), GamesDice::DieResult.new(8), GamesDice::DieResult.new(3)
|
251
|
+
]
|
252
|
+
|
253
|
+
die_results.sort!
|
254
|
+
|
255
|
+
die_results[0].value.should == 3
|
256
|
+
die_results[1].value.should == 5
|
257
|
+
die_results[2].value.should == 7
|
258
|
+
die_results[3].value.should == 8
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
data/spec/die_spec.rb
CHANGED
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.0.
|
4
|
+
version: 0.0.2
|
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-04-
|
12
|
+
date: 2013-04-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -44,7 +44,9 @@ files:
|
|
44
44
|
- games_dice.gemspec
|
45
45
|
- lib/games_dice.rb
|
46
46
|
- lib/games_dice/die.rb
|
47
|
+
- lib/games_dice/die_result.rb
|
47
48
|
- lib/games_dice/version.rb
|
49
|
+
- spec/die_result_spec.rb
|
48
50
|
- spec/die_spec.rb
|
49
51
|
homepage: ''
|
50
52
|
licenses: []
|
@@ -71,5 +73,6 @@ signing_key:
|
|
71
73
|
specification_version: 3
|
72
74
|
summary: Simulates and explains dice rolls from a variety of game systems.
|
73
75
|
test_files:
|
76
|
+
- spec/die_result_spec.rb
|
74
77
|
- spec/die_spec.rb
|
75
78
|
has_rdoc:
|