pool_of_entropy 0.0.1

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