pool_of_entropy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,161 @@
1
+ require 'digest/sha2'
2
+ require 'securerandom'
3
+
4
+ # This class implements a random number generator based on SHA-512
5
+ #
6
+ # An object of the class has internal state that is modified on each call to
7
+ # #update or any #read_... method (internally the #read_... methods call #update).
8
+ # The #read_... methods generate a pseudo-random number from the current
9
+ # state pool using SHA-512 and use it in the return value. It is not feasible to
10
+ # determine the internal state from the pseudo-random data received, and not
11
+ # possible to manipulate results from the PRNG in a predictable manner without
12
+ # knowing the internal state.
13
+ #
14
+ # @example Using default internal state, initialised using SecureRandom.random_bytes
15
+ # prng = PoolOfEntropy::CorePRNG.new
16
+ # prng.read_bytes
17
+ # # E.g. => "]\x12\x9E\xF5\x17\xF3\xC2\x1A\x15\xDFu]\x95\nd\x12"
18
+ # prng.read_hex
19
+ # # E.g. => "a0e00d2848242ec49e0a15ef411ba647"
20
+ # prng.read_bignum
21
+ # # E.g. => 33857278877368906880463811096418580004
22
+ # prng.read_float
23
+ # # E.g. => 0.6619838265836278
24
+ # prng.generate_integer( 20 )
25
+ # # E.g. => 7
26
+ #
27
+
28
+ class PoolOfEntropy::CorePRNG
29
+
30
+ # Creates a new random number source. All parameters are optional.
31
+ # @param [Integer] size Number of 64-byte blocks, between 1 and 256 (16KB)
32
+ # @param [String] initial_state Sets contents of state pool (default uses SecureRandom)
33
+ # @param [Integer] mix_block_id
34
+ # @return [PoolOfEntropy::CorePRNG]
35
+ def initialize size = 1, initial_state = SecureRandom.random_bytes( 64 * Integer(size) ), mix_block_id = 0
36
+ @size = Integer( size )
37
+ if @size < 1 || @size > 256
38
+ raise ArgumentError, "Size of pool must be in Range 1..256, got #{@size}"
39
+ end
40
+ unless initial_state.is_a? String
41
+ raise TypeError, "Initial state must be a String, got #{initial_state.inspect}"
42
+ end
43
+ @state = initial_state.clone
44
+ @state.force_encoding( 'BINARY' )
45
+
46
+ if @state.size != size * 64
47
+ raise ArgumentError, "Initial state bad size - expected #{size * 64} bytes, got #{@state.size} bytes"
48
+ end
49
+
50
+ @mix_block_id = Integer( mix_block_id ) % @size
51
+ end
52
+
53
+ # The number of 64-byte blocks used in the internal state pool.
54
+ # @return [Integer]
55
+ attr_reader :size
56
+
57
+ # Identifies the next 64-byte block in the pool that will be altered
58
+ # by a read or update process.
59
+ # @return [Integer]
60
+ attr_reader :mix_block_id
61
+
62
+ # A clone of the internal state pool. In combination with #mix_block_id, describes
63
+ # the whole PRNG. If this value is supplied to an end user, then they can easily
64
+ # predict future values of the PRNG.
65
+ # @return [PoolOfEntropy::CorePRNG]
66
+ def state
67
+ @state.clone
68
+ end
69
+
70
+ # The clone of a PoolOfEntropy::CorePRNG object includes separate copy of internal state
71
+ # @return [PoolOfEntropy::CorePRNG]
72
+ def clone
73
+ PoolOfEntropy::CorePRNG.new( self.size, self.state, self.mix_block_id )
74
+ end
75
+
76
+ # Mixes supplied data into the curent state. This is called
77
+ # internally by #read_... methods as well.
78
+ # @param [String] data Data to be mixed. Note empty string '' and nil are equivalent and *do* change the state.
79
+ # @return [nil]
80
+ def update data
81
+ new_block = Digest::SHA512.digest( @state + data.to_s )
82
+ @state[64*@mix_block_id,64] = new_block
83
+ @mix_block_id = (@mix_block_id + 1) % @size
84
+ nil
85
+ end
86
+
87
+ # Statistically flat distribution of 128 bits (16 bytes)
88
+ # @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
89
+ # @return [String] 16 characters in ASCII-8BIT encoding
90
+ def read_bytes *adjustments
91
+ raw_digest = Digest::SHA512.digest( @state )
92
+ self.update( raw_digest )
93
+ adjustments.compact.each do |adjust|
94
+ raw_digest = Digest::SHA512.digest( raw_digest + adjust )
95
+ end
96
+ fold_bits( fold_bits( raw_digest ) )
97
+ end
98
+
99
+ # Statistically flat distribution of 32 hex digits
100
+ # @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
101
+ # @return [String] 32 hex digits
102
+ def read_hex *adjustments
103
+ read_bytes( *adjustments ).unpack('H*').first
104
+ end
105
+
106
+ # Statistically flat distribution from range 0...2**128
107
+ # @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
108
+ # @return [Bignum,Fixnum] between 0 and 0xffffffffffffffffffffffffffffffff
109
+ def read_bignum *adjustments
110
+ nums = read_bytes( *adjustments ).unpack('Q>*')
111
+ nums.inject(0) { |sum,v| (sum << 64) + v }
112
+ end
113
+
114
+ # Statistically flat distribution from interval 0.0...1.0, with 53-bit precision
115
+ # @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
116
+ # @return [Float] between 0.0 and 0.9999999999999999
117
+ def read_float *adjustments
118
+ num = read_bytes( *adjustments ).unpack('Q>*').first >> 11
119
+ num.to_f / 2 ** 53
120
+ end
121
+
122
+ # Statistically flat distribution from range (0...top).
123
+ # If necessary, it will read more data to ensure absolute fairness. This method
124
+ # can generate an unbiased distribution of Bignums up to roughly half the maximum bit size
125
+ # allowed by Ruby (i.e. much larger than 2**128 generated in a single read)
126
+ # @param [Fixnum,Bignum] top upper bound of distribution, not inclusive
127
+ # @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
128
+ # @return [Fixnum,Bignum] between 0 and top-1 inclusive
129
+ def generate_integer top, *adjustments
130
+ power = 1
131
+ sum = 0
132
+ lower_bound = 0
133
+ words = []
134
+
135
+ loop do
136
+ words = read_bytes( *adjustments ).unpack('L>*') if words.empty?
137
+ sum = 2**32 * sum + words.shift
138
+ power *= 2**32
139
+ lower_bound = sum * top / power
140
+ upper_bound = ( (sum + 1) * top ) / power
141
+ break if lower_bound == upper_bound
142
+ end
143
+
144
+ lower_bound
145
+ end
146
+
147
+ private
148
+
149
+ # Xors first half of a message with second half. Only works for messages
150
+ # which are multiples of 8 bytes long.
151
+ # @param [String] msg bytes to fold
152
+ # @return [String] folded message, half the length of original
153
+ def fold_bits msg
154
+ l = msg.length/2
155
+ folded_32bits = msg[0,l].unpack('L>*').zip( msg[l,l].unpack('L>*') ).map do |x,y|
156
+ x ^ y
157
+ end
158
+ folded_32bits.pack('L>*')
159
+ end
160
+
161
+ end
@@ -0,0 +1,3 @@
1
+ class PoolOfEntropy
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pool_of_entropy/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pool_of_entropy"
8
+ spec.version = PoolOfEntropy::VERSION
9
+ spec.authors = ["Neil Slater"]
10
+ spec.email = ["slobo777@gmail.com"]
11
+ spec.summary = %q{Random number generator with extra features for gamers.}
12
+ spec.description = %q{PoolOfEntropy is a PRNG based on cryptographic secure PRNGs, intended to bring back the feeling of 'personal luck' that some gamers feel when rolling their own dice.}
13
+ spec.homepage = "https://github.com/neilslater/pool_of_entropy"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "yard", ">= 0.8.7.2"
22
+ spec.add_development_dependency "bundler", ">= 1.3"
23
+ spec.add_development_dependency "rspec", ">= 2.13.0"
24
+ spec.add_development_dependency "rake", ">= 1.9.1"
25
+ spec.add_development_dependency "coveralls", ">= 0.6.7"
26
+ end
@@ -0,0 +1,541 @@
1
+ require 'spec_helper'
2
+
3
+ describe PoolOfEntropy::CorePRNG do
4
+ describe "class methods" do
5
+
6
+ describe "#new" do
7
+ it "should instantiate a default object" do
8
+ prng = PoolOfEntropy::CorePRNG.new
9
+ prng.should be_a PoolOfEntropy::CorePRNG
10
+ prng.size.should == 1
11
+ end
12
+
13
+ it "should allow setting number of blocks in pool" do
14
+ prng = PoolOfEntropy::CorePRNG.new( 10 )
15
+ prng.should be_a PoolOfEntropy::CorePRNG
16
+ prng.size.should == 10
17
+ end
18
+
19
+ it "should fail with incorrect block number" do
20
+ expect { PoolOfEntropy::CorePRNG.new( -43 ) }.to raise_error ArgumentError
21
+ expect { PoolOfEntropy::CorePRNG.new( -1 ) }.to raise_error ArgumentError
22
+ expect { PoolOfEntropy::CorePRNG.new( 0 ) }.to raise_error ArgumentError
23
+ expect { PoolOfEntropy::CorePRNG.new( 257 ) }.to raise_error ArgumentError
24
+ expect { PoolOfEntropy::CorePRNG.new( 1000) }.to raise_error ArgumentError
25
+ expect { PoolOfEntropy::CorePRNG.new( nil ) }.to raise_error TypeError
26
+ expect { PoolOfEntropy::CorePRNG.new( '' ) }.to raise_error ArgumentError
27
+ expect { PoolOfEntropy::CorePRNG.new( :foo => 2 ) }.to raise_error TypeError
28
+ end
29
+
30
+ it "should allow setting internal state" do
31
+ prng = PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
32
+ prng.should be_a PoolOfEntropy::CorePRNG
33
+ prng.size.should == 1
34
+ prng.state.should == "\x0" * 64
35
+ end
36
+
37
+ it "should fail with bad state data" do
38
+ expect { PoolOfEntropy::CorePRNG.new( 1, '' ) }.to raise_error ArgumentError
39
+ expect { PoolOfEntropy::CorePRNG.new( 1, "\x0" * 63 ) }.to raise_error ArgumentError
40
+ expect { PoolOfEntropy::CorePRNG.new( 1, "\x0" * 200 ) }.to raise_error ArgumentError
41
+ expect { PoolOfEntropy::CorePRNG.new( 2, "\x0" * 64 ) }.to raise_error ArgumentError
42
+ end
43
+
44
+ it "should allow setting mix_block_id" do
45
+ prng = PoolOfEntropy::CorePRNG.new( 3, "\x12" * 192, 1 )
46
+ prng.should be_a PoolOfEntropy::CorePRNG
47
+ prng.size.should == 3
48
+ prng.state.should == "\x12" * 192
49
+ prng.mix_block_id.should == 1
50
+ end
51
+ end
52
+ end
53
+
54
+ describe "instance methods" do
55
+
56
+ describe "#clone" do
57
+ it "should copy all attributes" do
58
+ prng_orig = PoolOfEntropy::CorePRNG.new
59
+ prng_copy = prng_orig.clone
60
+
61
+ prng_copy.size.should == prng_orig.size
62
+ prng_copy.state.should == prng_orig.state
63
+ prng_copy.mix_block_id.should == prng_orig.mix_block_id
64
+ end
65
+
66
+ it "should deep clone the internal state string" do
67
+ prng_orig = PoolOfEntropy::CorePRNG.new
68
+ prng_copy = prng_orig.clone
69
+ prng_copy.state.should_not be prng_orig.state
70
+ end
71
+ end
72
+
73
+ describe "#update" do
74
+ it "should change the internal state" do
75
+ prng = PoolOfEntropy::CorePRNG.new
76
+ init_state = prng.state.clone
77
+ prng.update( 'boo' )
78
+ prng.state.should_not == init_state
79
+ end
80
+
81
+ it "should not change the length of the internal state" do
82
+ prng = PoolOfEntropy::CorePRNG.new
83
+ prng.update( 'boo' )
84
+ prng.state.length.should == 64
85
+ prng.update( 'boowgkjwrhqgioueqrhgiue2hguirhqwiughreuioghreuifhqwoifhr3iufghfwrgrwgetdfwd' )
86
+ prng.state.length.should == 64
87
+
88
+ prng = PoolOfEntropy::CorePRNG.new( 5 )
89
+ prng.update( 'boo' )
90
+ prng.state.length.should == 5 * 64
91
+ prng.update( 'getdfwd' * 1000 )
92
+ prng.state.length.should == 5 * 64
93
+ prng.update( 'boefewfweo' )
94
+ prng.state.length.should == 5 * 64
95
+ prng.update( 'geefewftdfwd' * 1000 )
96
+ prng.state.length.should == 5 * 64
97
+ end
98
+
99
+ it "should only change 64 bytes of state at a time" do
100
+ prng = PoolOfEntropy::CorePRNG.new( 5 )
101
+ init_state = prng.state.clone
102
+
103
+ prng.update( 'boo' )
104
+ prng.state[64,4*64].should == init_state[64,4*64]
105
+ next_state = prng.state.clone
106
+
107
+ prng.update( 'getdfwd' * 1000 )
108
+ prng.state[128,3*64].should == init_state[128,3*64]
109
+ prng.state[0,1*64].should == next_state[0,1*64]
110
+ next_state = prng.state.clone
111
+
112
+ prng.update( 'boefewfweo' )
113
+ prng.state[192,2*64].should == init_state[192,2*64]
114
+ prng.state[0,2*64].should == next_state[0,2*64]
115
+ next_state = prng.state.clone
116
+
117
+ prng.update( 'geefewftdfwd' * 1000 )
118
+ prng.state[256,1*64].should == init_state[256,1*64]
119
+ prng.state[0,3*64].should == next_state[0,3*64]
120
+ end
121
+ end
122
+
123
+ instance01 = PoolOfEntropy::CorePRNG.new
124
+ instance01.update( 'QWertyuiopp' )
125
+ instance01.update( 'Asdfghjkjl' )
126
+ instance01.update( 'Zxcvbnm' )
127
+
128
+ pool_types = [
129
+ [
130
+ 'default instance',
131
+ PoolOfEntropy::CorePRNG.new
132
+ ],
133
+ [
134
+ 'instance with 2KB pool size',
135
+ PoolOfEntropy::CorePRNG.new( 32 )
136
+ ],
137
+ [
138
+ 'instance with initial state all 0',
139
+ PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
140
+ ],
141
+ [
142
+ 'instance with fixed initial state',
143
+ PoolOfEntropy::CorePRNG.new( 5, "fiver" * 64, 3 )
144
+ ],
145
+ [
146
+ 'instance cloned from 2KB instance',
147
+ PoolOfEntropy::CorePRNG.new( 32 ).clone
148
+ ],
149
+ [
150
+ 'instance that has been updated with user data',
151
+ instance01
152
+ ],
153
+ ]
154
+
155
+ # NB "probability" and "randomness" tests in the following block are very light, just
156
+ # intended to capture high-level failures in logic. See DIEHARDER_TEST.md for thorough
157
+ # checks on statistical randomness of PoolOfEntropy::CorePRNG
158
+ pool_types.each do |prng_name, prng|
159
+
160
+ context "using #{prng_name}" do
161
+
162
+ describe "#read_bytes" do
163
+ it "always returns a 16 byte string" do
164
+ 100.times { prng.read_bytes.length.should == 16 }
165
+ end
166
+
167
+ it "has a high probability of returning a different string each time" do
168
+ Set[ *(1..100).map {prng.read_bytes} ].size.should == 100
169
+ end
170
+
171
+ describe "with adjustments" do
172
+ it "always returns a 16 byte string" do
173
+ 100.times { prng.read_bytes('654321').length.should == 16 }
174
+ end
175
+
176
+ it "has a high probability of returning a different string each time" do
177
+ Set[ *(1..100).map {prng.read_bytes('654321')} ].size.should == 100
178
+ end
179
+
180
+ it "changes output, but does not include adjustments in changes to state" do
181
+ prng_copy = prng.clone
182
+ 10.times do
183
+ prng.read_bytes('Hello!').should == prng_copy.read_bytes('Hello!')
184
+ prng.state.should == prng_copy.state
185
+ prng.read_bytes('Hello!').should_not == prng_copy.read_bytes('Goodbye!')
186
+ prng.state.should == prng_copy.state
187
+ prng.read_bytes.should_not == prng_copy.read_bytes('Goodbye!')
188
+ prng.state.should == prng_copy.state
189
+ prng.read_bytes('Hello!').should_not == prng_copy.read_bytes
190
+ prng.state.should == prng_copy.state
191
+ prng.read_bytes('Hello','Goodbye').should_not == prng_copy.read_bytes
192
+ prng.state.should == prng_copy.state
193
+ # Verify that output remains same for next rolls
194
+ prng.read_bytes('Foobar','Wibble').should == prng_copy.read_bytes('Foobar','Wibble')
195
+ prng.state.should == prng_copy.state
196
+ prng.read_bytes.should == prng_copy.read_bytes
197
+ prng.state.should == prng_copy.state
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "#read_hex" do
204
+ it "always returns a 32 digit hex string" do
205
+ 100.times do
206
+ hex = prng.read_hex
207
+ hex.length.should == 32
208
+ hex.should match /\A[0-9a-f]{32}\z/
209
+ end
210
+ end
211
+
212
+ it "has a high probability of returning a different string each time" do
213
+ Set[ *(1..100).map {prng.read_hex} ].size.should == 100
214
+ end
215
+
216
+ describe "with adjustments" do
217
+ it "always returns a 32 digit hex string" do
218
+ 100.times do
219
+ hex = prng.read_hex('QWertyeu')
220
+ hex.length.should == 32
221
+ hex.should match /\A[0-9a-f]{32}\z/
222
+ end
223
+ end
224
+
225
+ it "has a high probability of returning a different string each time" do
226
+ Set[ *(1..100).map {prng.read_hex('654321')} ].size.should == 100
227
+ end
228
+
229
+ it "changes output, but does not include adjustments in changes to state" do
230
+ prng_copy = prng.clone
231
+ 10.times do
232
+ prng.read_hex('Hello!').should == prng_copy.read_hex('Hello!')
233
+ prng.state.should == prng_copy.state
234
+ prng.read_hex('Hello!').should_not == prng_copy.read_hex('Goodbye!')
235
+ prng.state.should == prng_copy.state
236
+ prng.read_hex.should_not == prng_copy.read_hex('Goodbye!')
237
+ prng.state.should == prng_copy.state
238
+ prng.read_hex('Hello!').should_not == prng_copy.read_hex
239
+ prng.state.should == prng_copy.state
240
+ prng.read_hex('Hello','Goodbye').should_not == prng_copy.read_hex
241
+ prng.state.should == prng_copy.state
242
+ # Verify that output remains same for next rolls
243
+ prng.read_hex('Foobar','Wibble').should == prng_copy.read_hex('Foobar','Wibble')
244
+ prng.state.should == prng_copy.state
245
+ prng.read_hex.should == prng_copy.read_hex
246
+ prng.state.should == prng_copy.state
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ describe "#read_bignum" do
253
+ it "always returns a 128-bit unsigned integer" do
254
+ 100.times do
255
+ num = prng.read_bignum
256
+ num.should >= 0
257
+ num.should < 2**128
258
+ end
259
+ end
260
+
261
+ it "has a high probability of returning a different number each time" do
262
+ Set[ *(1..100).map {prng.read_bignum} ].size.should == 100
263
+ end
264
+
265
+ describe "with adjustments" do
266
+ it "always returns a 128-bit unsigned integer" do
267
+ 100.times do
268
+ num = prng.read_bignum( 'Biggest' )
269
+ num.should >= 0
270
+ num.should < 2**128
271
+ end
272
+ end
273
+
274
+ it "has a high probability of returning a different number each time" do
275
+ Set[ *(1..100).map {prng.read_bignum('654321')} ].size.should == 100
276
+ end
277
+
278
+ it "changes output, but does not include adjustments in changes to state" do
279
+ prng_copy = prng.clone
280
+ 10.times do
281
+ prng.read_bignum('Hello!').should == prng_copy.read_bignum('Hello!')
282
+ prng.state.should == prng_copy.state
283
+ prng.read_bignum('Hello!').should_not == prng_copy.read_bignum('Goodbye!')
284
+ prng.state.should == prng_copy.state
285
+ prng.read_bignum.should_not == prng_copy.read_bignum('Goodbye!')
286
+ prng.state.should == prng_copy.state
287
+ prng.read_bignum('Hello!').should_not == prng_copy.read_bignum
288
+ prng.state.should == prng_copy.state
289
+ prng.read_bignum('Hello','Goodbye').should_not == prng_copy.read_bignum
290
+ prng.state.should == prng_copy.state
291
+ # Verify that output remains same for next rolls
292
+ prng.read_bignum('Foobar','Wibble').should == prng_copy.read_bignum('Foobar','Wibble')
293
+ prng.state.should == prng_copy.state
294
+ prng.read_bignum.should == prng_copy.read_bignum
295
+ prng.state.should == prng_copy.state
296
+ end
297
+ end
298
+ end
299
+ end
300
+
301
+ describe "#read_float" do
302
+ it "always returns a Float between 0.0 (inclusive) and 1.0 (exclusive)" do
303
+ 100.times do
304
+ num = prng.read_float
305
+ num.should be_a Float
306
+ num.should >= 0.0
307
+ num.should < 1.0
308
+ end
309
+ end
310
+
311
+ it "has a high probability of returning a different Float each time" do
312
+ Set[ *(1..100).map {prng.read_float} ].size.should == 100
313
+ end
314
+
315
+ describe "with adjustments" do
316
+ it "always returns a Float between 0.0 (inclusive) and 1.0 (exclusive)" do
317
+ 100.times do
318
+ num = prng.read_float('Boom')
319
+ num.should be_a Float
320
+ num.should >= 0.0
321
+ num.should < 1.0
322
+ end
323
+ end
324
+
325
+ it "has a high probability of returning a different Float each time" do
326
+ Set[ *(1..100).map {prng.read_float('654321')} ].size.should == 100
327
+ end
328
+
329
+ it "changes output, but does not include adjustments in changes to state" do
330
+ prng_copy = prng.clone
331
+ 10.times do
332
+ prng.read_float('Hello!').should == prng_copy.read_float('Hello!')
333
+ prng.state.should == prng_copy.state
334
+ prng.read_float('Hello!').should_not == prng_copy.read_float('Goodbye!')
335
+ prng.state.should == prng_copy.state
336
+ prng.read_float.should_not == prng_copy.read_float('Goodbye!')
337
+ prng.state.should == prng_copy.state
338
+ prng.read_float('Hello!').should_not == prng_copy.read_float
339
+ prng.state.should == prng_copy.state
340
+ prng.read_float('Hello','Goodbye').should_not == prng_copy.read_float
341
+ prng.state.should == prng_copy.state
342
+ # Verify that output remains same for next rolls
343
+ prng.read_float('Foobar','Wibble').should == prng_copy.read_float('Foobar','Wibble')
344
+ prng.state.should == prng_copy.state
345
+ prng.read_float.should == prng_copy.read_float
346
+ prng.state.should == prng_copy.state
347
+ end
348
+ end
349
+ end
350
+ end
351
+
352
+ describe "#generate_integer" do
353
+ it "always returns an integer between 0 (inclusive) and supplied top (exclusive)" do
354
+ 100.times do
355
+ num = prng.generate_integer( 10 )
356
+ num.should be_a Fixnum
357
+ num.should >= 0
358
+ num.should < 10
359
+ end
360
+
361
+ 100.times do
362
+ num = prng.generate_integer( 100 )
363
+ num.should be_a Fixnum
364
+ num.should >= 0
365
+ num.should < 100
366
+ end
367
+
368
+ 100.times do
369
+ num = prng.generate_integer( 1000 )
370
+ num.should be_a Fixnum
371
+ num.should >= 0
372
+ num.should < 1000
373
+ end
374
+
375
+ 100.times do
376
+ num = prng.generate_integer( 647218456 )
377
+ num.should be_a Fixnum
378
+ num.should >= 0
379
+ num.should < 647218456
380
+ end
381
+ end
382
+
383
+ it "can generate integers larger than 2**128" do
384
+ results = (0..100).map do
385
+ num = prng.generate_integer( 2 ** 1024 )
386
+ num.should >= 0
387
+ num.should < 2 ** 1024
388
+ num
389
+ end
390
+ results.select { |n| n > 2 ** 1020 }.count.should > 50
391
+ end
392
+
393
+ it "with a large enough value for top, has a high probability of returning a different integer each time" do
394
+ Set[ *(1..100).map {prng.generate_integer( 2**75 - 7 )} ].size.should == 100
395
+ end
396
+
397
+ it "covers a distribution 0...top" do
398
+ Set[ *(1..100).map {prng.generate_integer(10)} ].size.should == 10
399
+ end
400
+
401
+ describe "with adjustments" do
402
+ it "always returns an integer between 0 (inclusive) and supplied top (exclusive)" do
403
+ 100.times do
404
+ num = prng.generate_integer( 10, 'jkffwe' )
405
+ num.should be_a Fixnum
406
+ num.should >= 0
407
+ num.should < 10
408
+ end
409
+
410
+ 100.times do
411
+ num = prng.generate_integer( 100, 'jkffweefewg' )
412
+ num.should be_a Fixnum
413
+ num.should >= 0
414
+ num.should < 100
415
+ end
416
+
417
+ 100.times do
418
+ num = prng.generate_integer( 1000, 'jkffweefewg', 'efhwjkfgw' )
419
+ num.should be_a Fixnum
420
+ num.should >= 0
421
+ num.should < 1000
422
+ end
423
+
424
+ 100.times do
425
+ num = prng.generate_integer( 647218456, 'j*****g', 'efhwjkfgw' )
426
+ num.should be_a Fixnum
427
+ num.should >= 0
428
+ num.should < 647218456
429
+ end
430
+ end
431
+
432
+ it "with a large enough value for top, has a high probability of returning a different integer each time" do
433
+ Set[ *(1..100).map {prng.generate_integer( 2**80 - 5, '654321')} ].size.should == 100
434
+ end
435
+
436
+ it "changes output, but does not include adjustments in changes to state" do
437
+ prng_copy = prng.clone
438
+ big = 2 ** 64 - 3
439
+ small = 20
440
+ 10.times do
441
+ prng.generate_integer( small, 'Hello!').should == prng_copy.generate_integer( small, 'Hello!')
442
+ prng.state.should == prng_copy.state
443
+ prng.generate_integer( big, 'Hello!').should_not == prng_copy.generate_integer( big, 'Goodbye!')
444
+ prng.state.should == prng_copy.state
445
+ prng.generate_integer( big ).should_not == prng_copy.generate_integer( big, 'Goodbye!')
446
+ prng.state.should == prng_copy.state
447
+ prng.generate_integer( big, 'Hello!').should_not == prng_copy.generate_integer( big )
448
+ prng.state.should == prng_copy.state
449
+ prng.generate_integer( big, 'Hello','Goodbye').should_not == prng_copy.generate_integer( big )
450
+ prng.state.should == prng_copy.state
451
+ # Verify that output remains same for next rolls
452
+ prng.generate_integer( small, 'Foobar','Wibble').should == prng_copy.generate_integer( small, 'Foobar','Wibble')
453
+ prng.state.should == prng_copy.state
454
+ prng.generate_integer( big ).should == prng_copy.generate_integer( big )
455
+ prng.state.should == prng_copy.state
456
+ end
457
+ end
458
+ end
459
+ end
460
+
461
+
462
+ describe "all read methods" do
463
+
464
+ it "generate higher and lower values in sync" do
465
+ prngs = (0..4).map { prng.clone }
466
+ results = (1..20).map do
467
+ Hash[
468
+ :bytes => prngs[0].read_bytes,
469
+ :hex => prngs[1].read_hex,
470
+ :float => prngs[2].read_float,
471
+ :num => prngs[3].read_bignum,
472
+ :int => prngs[4].generate_integer( 1_000_000_000 ),
473
+ ]
474
+ end
475
+ results.sort_by { |h| h[:int] }.should == results.sort_by { |h| h[:hex] }
476
+ results.sort_by { |h| h[:float] }.should == results.sort_by { |h| h[:hex] }
477
+ results.sort_by { |h| h[:num] }.should == results.sort_by { |h| h[:bytes] }
478
+ results.sort_by { |h| h[:float] }.should == results.sort_by { |h| h[:int] }
479
+ end
480
+ end
481
+
482
+ end
483
+ end
484
+
485
+ describe "using predictable sequences" do
486
+ it "generates expected hex strings from simple zeroed pool" do
487
+ prng = PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
488
+ (0..24).map { prng.read_hex }.should == [ "da0cd77eb1c84458ddcc91e36b9dcb35",
489
+ "498ec24d1126440047eed396d836b5e1", "0b90df55c5e7c1513b072367eae4a4ce",
490
+ "f12b5b54b5594e785bbb9a4ac50ccec8", "d506bd4e201a00dc30499bd8e59a30d8",
491
+ "2557893cf995fe43bd00721fce6ab16a", "41ee50244cdd02334bafc3e9d8f564d9",
492
+ "8156c7c7bcfd856cb9d1012243cfc662", "1116f840e2aee924bd2c7d722c602635",
493
+ "96cf31967465b83f5ef3476c60afe20a", "f041f6df72aa2eab7394f08e83d52a0c",
494
+ "5ccbb30077f2433bd765ddc86840a880", "6a4339fd5d445024048ea8f91a3e02fd",
495
+ "707e9499b9f0e9e906a0c0ddd0530803", "553704d95703284df323f0aa4244cf81",
496
+ "cb1522ce0bdf2504fdd62df7416be73e", "05c4932cb9d3a7c0675b0228826e661a",
497
+ "59606f23e34e726a7912dacfde533d97", "188cf17dde6947264ce05f8274874ffa",
498
+ "24c184b56453891657953f557635b742", "a5f969e50228b2ee0f38cc37c5033541",
499
+ "f1de0e5d149cc5a7f0e38c0ee501d7bc", "5ca3a6beb0b810568be83ab2179eb550",
500
+ "756fb9c58277eb8c6092142224caecf4", "0ed9505eadb3def60ec42051f0bf15ef" ]
501
+ end
502
+
503
+ it "generates expected hex strings from simple zeroed pool and an adjustment" do
504
+ prng = PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
505
+ (0..24).map { prng.read_hex( 'bananas' ) }.should == [
506
+ "fed9de9a612f4157ebb49582ca557a50", "a7adc2374a4df2ed67846ac09a3b6645",
507
+ "cbdf8bbbe6145751fe14004719915160", "8cd84be95376f72918c305bdea43e36d",
508
+ "9e08e80ff40c942f0cf4d479b5378fa0", "54d80c5330873f9733a0adef0197220f",
509
+ "2abe07bd85d2066a624dd3630e59730a", "88b6a697fe74aeb8ec83845e103b7b63",
510
+ "9c6c9d613855f6535adb419cc564fd10", "23f9b778b254035a0e4219423a52da77",
511
+ "4cd50c14ee17fa29c8b1f209432a36b3", "2b5646b164d863de716f67adef653859",
512
+ "fd81d0bbbd2828ecdfcba0b486ef786c", "08e6594fc277ff7fafbf37475ecadbf6",
513
+ "5e2333ecc800eb9a06e347924ed42e94", "8a14e48013028b2c9f174a07ddd5ef49",
514
+ "e9a5c331f54b155f570c2cf2bcd37209", "1ce84279195a5b23ffeb3063edbfab21",
515
+ "7422e19e58f2fcf20805f601266bf676", "8df0226465b74d830360ea8609d181bf",
516
+ "3e5d5e8cf8ad032fb4005826bdf7bb94", "65e3f9ab28c26bead6647c21bcc6245e",
517
+ "8ed5d8892d464ebeaa8dcbcef0968936", "2f000bc1ab14d914196cc1d5055db189",
518
+ "8e9c4b8152f14e47ac31f25a80765ebf" ]
519
+ end
520
+
521
+ it "generates expected hex strings from simple zeroed pool and alternating adjustments" do
522
+ prng = PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
523
+ (0..24).map { |x| x.odd? ? prng.read_hex( 'bananas', x.to_s ) : prng.read_hex }.should == [
524
+ "da0cd77eb1c84458ddcc91e36b9dcb35", "de4b20a0560263090c6fe11ebba6256e",
525
+ "0b90df55c5e7c1513b072367eae4a4ce", "0c92d534160cb268cb3282a9dd3f1efe",
526
+ "d506bd4e201a00dc30499bd8e59a30d8", "089f799236a9cf64f22f1deafbe28214",
527
+ "41ee50244cdd02334bafc3e9d8f564d9", "84477b9a3d51bb72868ccb85bea71cad",
528
+ "1116f840e2aee924bd2c7d722c602635", "469e4bb2f38719c7f85d79c8900a8fa8",
529
+ "f041f6df72aa2eab7394f08e83d52a0c", "e21bf2508b8e1755ae70c74c0d36ec2e",
530
+ "6a4339fd5d445024048ea8f91a3e02fd", "a94a80c80c9edd95808a67775c2b42d4",
531
+ "553704d95703284df323f0aa4244cf81", "edd6aea5bda7b2d9c494067c5cdbe410",
532
+ "05c4932cb9d3a7c0675b0228826e661a", "cf81635ca2fa910fd11cf45508dc658b",
533
+ "188cf17dde6947264ce05f8274874ffa", "bb4aaeb98e13400c1216c674a872ba93",
534
+ "a5f969e50228b2ee0f38cc37c5033541", "1a795355f979575247b3fa9c92d97917",
535
+ "5ca3a6beb0b810568be83ab2179eb550", "1a98419cca1dd1b370ce8a4a2ab721b7",
536
+ "0ed9505eadb3def60ec42051f0bf15ef" ]
537
+ end
538
+ end
539
+ end
540
+
541
+ end