games_dice 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,6 @@
1
1
  require "games_dice/version"
2
2
  require "games_dice/die"
3
+ require "games_dice/die_result"
3
4
 
4
5
  module GamesDice
5
6
  # TODO: Factory methods for various dice schemes
@@ -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
@@ -1,3 +1,3 @@
1
1
  module GamesDice
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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
@@ -1,4 +1,4 @@
1
- require 'games_dice/die'
1
+ require 'games_dice'
2
2
 
3
3
  # Test helper class, a stub of a PRNG
4
4
  class TestPRNG
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.1
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-08 00:00:00.000000000 Z
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: