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.
@@ -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: