dice_stats 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b7086bffbd719c8eee013b15b67695a448dbd6d2
4
+ data.tar.gz: d4eb3dbf7782a16674b9b71fb4eae0da8df263f0
5
+ SHA512:
6
+ metadata.gz: 425c4bb5208588c105c22003dce0fe6d845a4c8ae80f00009623c73b35e1fd5bc4ce9fcbab0eb98b81e25bda47282f517bdfb0a80527cb96f22a63764e0f70be
7
+ data.tar.gz: 9bf601977f33c8ad2430732ca58b396b16655d8287d4446e142437069e76b5811634ab60aa6cce96567eae4e98e8c40774073447fd0faf22289121143eaeac55
data/lib/Dice.rb ADDED
@@ -0,0 +1,147 @@
1
+ require 'bigdecimal'
2
+ require 'Internal_Utilities/Math_Utilities'
3
+ require 'Internal_Utilities/probability_cache_db'
4
+
5
+ module Dice_Stats
6
+
7
+ ##
8
+ # This class repsents the roll statistics for a single type of dice. i.e. all d6s, or all d8s.
9
+ # The probability distribution is generated via the generating function found on line (10) of http://mathworld.wolfram.com/Dice.html
10
+ class Dice
11
+
12
+ ##
13
+ # The number of dice, and how many sides they have.
14
+ attr_accessor :count, :sides
15
+
16
+ ##
17
+ # The probability distribution of the dice.
18
+ attr_reader :probability_distribution
19
+
20
+ ##
21
+ # Creates a new Dice instance of a number of dice (+dice_count+) with +dice_sides+ faces.
22
+ # All dice are assumed to go from 1 to +dice_sides+.
23
+ def initialize(dice_count, dice_sides)
24
+ @count = dice_count
25
+ @sides = dice_sides
26
+ @probability_distribution = {}
27
+
28
+ if (@count < 0 || @sides < 0)
29
+ #error
30
+ else
31
+ t1 = Time.now
32
+ if Cache.checkDice(@count.to_s + "d" + @sides.to_s)
33
+ @probability_distribution = Cache.getDice(@count.to_s + "d" + @sides.to_s)
34
+ else
35
+ @probability_distribution = calculate_probability_distribution
36
+ Cache.addDice(@count.to_s + "d" + @sides.to_s, @probability_distribution, (Time.now - t1).round(5))
37
+ end
38
+ #t2 = Time.now
39
+ #puts "Probabilities determined in #{(t2-t1).round(5)}"
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Returns the highest possible roll
45
+ def max
46
+ @count*@sides
47
+ end
48
+
49
+ ##
50
+ # Returns the lowest possible roll
51
+ def min
52
+ @count
53
+ end
54
+
55
+ ##
56
+ # Returns the average roll result
57
+ def expected
58
+ BigDecimal(@count) * ((@sides + 1.0) / 2.0)
59
+ end
60
+
61
+ ##
62
+ # Returns the variance of the roll
63
+ def variance
64
+ var = BigDecimal.new(0)
65
+ (1..@sides).each { |i|
66
+ e = BigDecimal.new(@sides+1) / BigDecimal.new(2)
67
+ var += (BigDecimal.new(i - e)**2) / BigDecimal.new(@sides)
68
+ }
69
+ var * BigDecimal.new(@count)
70
+ end
71
+
72
+ ##
73
+ # Returns the standard deviation of the roll
74
+ def standard_deviation
75
+ BigDecimal.new(variance).sqrt(5)
76
+ end
77
+
78
+ ##
79
+ # Prints some basic stats about this roll
80
+ def print_stats
81
+ puts "Min: #{min}"
82
+ puts "Max: #{max}"
83
+ puts "Expected: #{expected}"
84
+ puts "Std Dev: #{standard_deviation}"
85
+ puts "Variance: #{variance}"
86
+
87
+ @probability_distribution.each { |k,v|
88
+ puts "P(#{k}) => #{v}"
89
+ }
90
+ end
91
+
92
+ ##
93
+ # Rolls the dice and returns the result
94
+ def roll
95
+ sum = 0
96
+ @count.times do |i|
97
+ sum += (1 + rand(@sides))
98
+ end
99
+ return sum
100
+ end
101
+
102
+ ##
103
+ # For internal use only.
104
+ # Caclulates the probability distribution on initialization
105
+ def calculate_probability_distribution
106
+ number_of_possible_combinations = (@sides**@count)
107
+ #puts "Number of possible combinations: #{number_of_possible_combinations}"
108
+ result = {}
109
+ # weep softly: http://mathworld.wolfram.com/Dice.html
110
+ (min..max).each { |p|
111
+ if p > (max + min) / 2
112
+ result[p] = result[max - p + min]
113
+ else
114
+ thing = (BigDecimal.new(p - @count) / BigDecimal.new(@sides)).floor
115
+
116
+ c = BigDecimal.new(0)
117
+ ((0..thing).each { |k|
118
+ n1 = ((-1)**k)
119
+ n2 = BigDecimal.new(Internal_Utilities::Math_Utilities.Choose(@count, k))
120
+ n3 = BigDecimal.new(Internal_Utilities::Math_Utilities.Choose(p - (@sides * k) - 1, @count - 1))
121
+ t = BigDecimal.new(n1 * n2 * n3)
122
+
123
+ c += t
124
+ })
125
+
126
+ #result = result.abs
127
+
128
+ result[p] = BigDecimal.new(c) / BigDecimal.new(number_of_possible_combinations)
129
+ end
130
+
131
+ #puts "\tProbability of #{p}: #{@probability_distribution[p].add(0, 5).to_s('F')}"
132
+ }
133
+ @probability_distribution = result
134
+ #puts "Sum of probability_distribution: " + (@probability_distribution.inject(BigDecimal.new(0)) {|total, (k,v)| BigDecimal.new(total + v) }).add(0, 5).to_s('F')
135
+ end
136
+
137
+ ##
138
+ # Returns the probability of a specific result (+val+). *Not* to be confused with Dice_Set#p.
139
+ def p(val)
140
+ if (@probability_distribution.key?(val))
141
+ return @probability_distribution[val]
142
+ else
143
+ return 0
144
+ end
145
+ end
146
+ end
147
+ end
data/lib/Dice_Set.rb ADDED
@@ -0,0 +1,152 @@
1
+ require 'Dice'
2
+ require 'Internal_Utilities/Math_Utilities'
3
+ require 'Internal_Utilities/Filtered_distribution'
4
+ require 'Internal_Utilities/probability_cache_db'
5
+
6
+ module Dice_Stats
7
+
8
+ ##
9
+ # This class represents the roll statistics for a combination of dice.
10
+ # The probability distribution is based off the constituent dice distributions.
11
+ class Dice_Set
12
+ ##
13
+ # The raw probability distribution of the dice set.
14
+ # Can be queried more interactively through Dice_Set#p.
15
+ attr_reader :probability_distribution
16
+
17
+ ##
18
+ # The constituent separate dice
19
+ attr_accessor :dice
20
+
21
+ ##
22
+ # Instantiates a new Dice_Set with the specified +dice_string+ pattern.
23
+ # Examples:
24
+ # "2d6 + 1d3"
25
+ # "2d6 + 5"
26
+ # "1d8"
27
+ # "5d4 + 3d10"
28
+ def initialize(dice_string)
29
+ @dice = []
30
+ @constant = 0
31
+ @input_string = dice_string
32
+ @aborted_probability_distribution = false
33
+
34
+ split_string = dice_string.split('+')
35
+
36
+ split_string.map!{|i| i.strip }
37
+
38
+ split_string.count.times { |i|
39
+ if /\d+[dD]\d+/.match(split_string[i])
40
+ sub_string_split = split_string[i].downcase.split('d')
41
+ @dice << Dice.new(sub_string_split[0].to_i, sub_string_split[1].to_i)
42
+ elsif (split_string[i].to_i > 0)
43
+ @constant += split_string[i].to_i
44
+ else
45
+ puts "Unexpected paramter: #{split_string[0]}"
46
+ end
47
+ }
48
+
49
+ if @dice.inject(1) { |memo,d| memo * d.probability_distribution.length } > 10_000_000
50
+ # if the n-ary cartesian product has to process more than 10,000,000 combinations it can take quite a while to finish...
51
+ @aborted_probability_distribution = true
52
+ else
53
+ @dice.sort! { |d1,d2| d2.sides <=> d1.sides }
54
+
55
+ t1 = Time.now
56
+ if Cache.checkDice(self.clean_string(false))
57
+ @probability_distribution = Cache.getDice(self.clean_string(false))
58
+ else
59
+ @probability_distribution = combine_probability_distributions
60
+ Cache.addDice(self.clean_string(false), @probability_distribution, (Time.now - t1).round(5))
61
+ end
62
+ #t2 = Time.now
63
+ #puts "Probabilities determined in #{(t2-t1).round(5)}"
64
+
65
+ if (@probability_distribution.inject(0) { |memo,(k,v)| memo + v }.round(3).to_f != 1.0)
66
+ #puts "Error in probability distrubtion."
67
+ end
68
+ end
69
+ end
70
+
71
+ ##
72
+ # Returns the highest possible roll
73
+ def max
74
+ @dice.inject(0) { |memo, d| memo + d.max }.to_i + @constant
75
+ end
76
+
77
+ ##
78
+ # Returns the lowest possible roll
79
+ def min
80
+ @dice.inject(0) { |memo, d| memo + d.min }.to_i + @constant
81
+ end
82
+
83
+ ##
84
+ # Returns the average roll result
85
+ def expected
86
+ @dice.inject(0) { |memo, d| memo + d.expected }.to_f + @constant
87
+ end
88
+
89
+ ##
90
+ # Returns the variance of the roll
91
+ def variance
92
+ @probability_distribution.inject(0) { |memo, (key,val)| memo + ((key - (expected - @constant))**2 * val) }.round(10).to_f
93
+ end
94
+
95
+ ##
96
+ # Returns the standard deviation of the roll
97
+ def standard_deviation
98
+ BigDecimal.new(variance, 10).sqrt(5).round(10).to_f
99
+ end
100
+
101
+ ##
102
+ # For internal use only.
103
+ # Takes the cartesian product of the individual dice and combines them.
104
+ def combine_probability_distributions
105
+ separate_distributions = @dice.map { |d| d.probability_distribution }
106
+ Internal_Utilities::Math_Utilities.Cartesian_Product_For_Probabilities(separate_distributions)
107
+ end
108
+
109
+ ##
110
+ # Returns the dice string used to generate the pattern sorted by dice face, descending.
111
+ # For example "2d3 + 2 + 1d6" would become "1d6 + 2d3 + 2"
112
+ # If +with_constant+ is set to false, the constant "+ 2" will be ommitted.
113
+ def clean_string(with_constant=true)
114
+ formatted_string = ""
115
+ @dice.each { |d|
116
+ formatted_string += d.count.to_s + "d" + d.sides.to_s + " + "
117
+ }
118
+ if with_constant && @constant > 0
119
+ formatted_string + @constant.to_s
120
+ else
121
+ formatted_string[0..formatted_string.length-4]
122
+ end
123
+ end
124
+
125
+ ##
126
+ # Displays the probability distribution.
127
+ # Can take up quite a lot of screen space for the mroe complicated rolls.
128
+ def print_probability
129
+ @probability_distribution.each { |k,v| puts "p(#{k}) => #{v.round(8).to_f}"}
130
+ end
131
+
132
+ ##
133
+ # Simulates a roll of the dice
134
+ def roll
135
+ @dice.inject(@constant || 0) { |memo,d| memo + d.roll }
136
+ end
137
+
138
+ ##
139
+ # Instantiates and returns a Filtered_distribution. See the documentation for Filtered_distribution.rb.
140
+ def p
141
+ weighted_prob_dist = @probability_distribution.inject(Hash.new) { |m,(k,v)| m[k+@constant] = v; m }
142
+ filtered_distribution = Internal_Utilities::Filtered_distribution.new(weighted_prob_dist)
143
+ return filtered_distribution
144
+ end
145
+
146
+ ##
147
+ # If the probability distribution was determined to be too complex to compute this will return true.
148
+ def too_complex?
149
+ @aborted_probability_distribution
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,85 @@
1
+ module Dice_Stats
2
+
3
+ end
4
+
5
+ module Dice_Stats::Internal_Utilities
6
+
7
+ ##
8
+ # This class defines a counter where each "digit" has a different base.
9
+ # For example, a counter of two digits, the first with base 3 and the second with base 2 may go like this:
10
+ # 0 => [0, 0]
11
+ # 1 => [0, 1]
12
+ # 2 => [1, 0]
13
+ # 3 => [1, 1]
14
+ # 4 => [2, 0]
15
+ # 5 => [2, 1]
16
+ # 5 would be the maximum number the counter could hold.
17
+ #
18
+ # TODO:
19
+ # * Add a "decrement" method
20
+ # * Add a "value" method to return the count in base 10
21
+
22
+ class Arbitrary_base_counter
23
+ ##
24
+ # A boolean value representing if the result has overflown.
25
+ # Will be false initially, will be set to true if the counter ends up back at [0, 0, ..., 0]
26
+ attr_reader :overflow
27
+
28
+ ##
29
+ # Define a new counter.
30
+ # +maximums+ is an array of integers, each specifying the base of its respective digit.
31
+ # For example, to create a counter of 3 base 2 digits, supply [2,2,2]
32
+ def initialize(maximums)
33
+ @overflow = false
34
+ @index = maximums.map { |i| {:val => 0, :max => i} }
35
+ end
36
+
37
+ ##
38
+ # Increase the "value" of the counter by one
39
+ def increment
40
+ #start at the end of the array (i.e. the "lowest" significant digit)
41
+ i = @index.length - 1
42
+
43
+ loop do
44
+ #increment the last value
45
+ @index[i][:val] += 1
46
+
47
+ #check if it has "overflown" that digits base
48
+ if @index[i][:val] >= @index[i][:max]
49
+ #set it to 0
50
+ @index[i][:val] = 0
51
+
52
+ if (i == 0)
53
+ @overflow = true
54
+ end
55
+
56
+ #move to the next digit to the "left"
57
+ i -= 1
58
+
59
+ else
60
+ #done
61
+ break
62
+ end
63
+ end
64
+ end
65
+
66
+ ##
67
+ # Return an integer representing how many digits the counter holds
68
+ def length
69
+ @index.length
70
+ end
71
+
72
+
73
+ ##
74
+ # Overloaded index operator, used to retrieve the number stored in the +i+th digit place
75
+ def [](i)
76
+ @index[i][:val]
77
+ end
78
+
79
+ ##
80
+ # Puts the array of digits.
81
+ def print
82
+ puts @index
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,70 @@
1
+ module Dice_Stats
2
+
3
+ end
4
+
5
+ module Dice_Stats::Internal_Utilities
6
+
7
+ ##
8
+ # This class is used as a way to build clauses into a query of a probability distribution
9
+ # The +probability_distribution+ start out complete and each query removes selections of it.
10
+
11
+ class Filtered_distribution
12
+ ##
13
+ # For internal use only. The Dice_Set#p method instantiates this with a fresh distribution for use in the query.
14
+ def initialize(probability_distribution)
15
+ @pd = probability_distribution
16
+ end
17
+
18
+ ##
19
+ # Use Filtered_distribution#get to return the sum of the remaining probabilities in the distribution
20
+ def get
21
+ @pd.inject(BigDecimal.new(0)) { |m, (k,v)| m + v }
22
+ end
23
+
24
+ ##
25
+ # The "less than" operator.
26
+ def lt(val)
27
+ @pd.select! { |k,v| k < val }
28
+ return self
29
+ end
30
+
31
+ ##
32
+ # The "less than or equal to" operator.
33
+ def lte(val)
34
+ @pd.select! { |k,v| k <= val }
35
+ return self
36
+ end
37
+
38
+ ##
39
+ # The "greater than" operator.
40
+ def gt(val)
41
+ @pd.select! { |k,v| k > val }
42
+ return self
43
+ end
44
+
45
+ ##
46
+ # The "greater than or equal to" operator.
47
+ def gte(val)
48
+ @pd.select! { |k,v| k >= val }
49
+ return self
50
+ end
51
+
52
+ ##
53
+ # The "equal to" operator.
54
+ # *Note:* This removes all options except the one specified. This is not useful in conjunction with any other operators.
55
+ # The result of "p.eq(x).eq(y).get" will always be 0 if x and y are different.
56
+ def eq(val)
57
+ @pd.select! { |k,v| k == val }
58
+ return self
59
+ end
60
+
61
+ ##
62
+ # The "not equal to" operator.
63
+ # This allows for arbitrailty selecting out option that don't fit a ranged criteria.
64
+ # For example "p.ne(2).ne(4).ne(6).get" would give the odds of rolling an odd number on a d6.
65
+ def ne(val)
66
+ @pd.select! { |k,v| k != val }
67
+ return self
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,167 @@
1
+ require_relative 'Arbitrary_base_counter'
2
+
3
+ module Dice_Stats
4
+
5
+ end
6
+
7
+ module Dice_Stats::Internal_Utilities
8
+
9
+ ##
10
+ # This class simple contains some methods to aid the calculating of probability distributions
11
+ class Math_Utilities
12
+
13
+ ##
14
+ # The "Choose" operator. Sometimes noted as "(5 3)" or "5 c 3".
15
+ def self.Choose(a, b)
16
+ if (a < 0 || b < 0 || (a < b))
17
+ 1
18
+ elsif (a == b || b == 0)
19
+ 1
20
+ else
21
+ (a-b+1..a).inject(1, &:*) / (2..b).inject(1, &:*)
22
+ end
23
+ end
24
+
25
+ ##
26
+ # The "Factorial" function
27
+ def self.Factorial(a)
28
+ (1..a).inject(:*) || 0
29
+ end
30
+
31
+ ##
32
+ # Note that this method is not actually used. It was a proof of concept for templating a less "clever" way to do a
33
+ # Cartesian product of arbitrary objects in such a way that additional processing can be done on the elements.
34
+ def self.Cartesian_Product(arrays) #arrays is an array of array to cartesian product
35
+
36
+ # FROM https://gist.github.com/sepastian/6904643
37
+ # I found these examples later, after creating Option 3.
38
+ # TODO: See if using these can be used to simplify the process.
39
+ #Option 1
40
+ arrays[0].product(*arrays[1..-1])
41
+ #Option 2
42
+ arrays[1..-1].inject(arrays[0]) { |m,v| m.product(v).map(&:flatten) }
43
+
44
+ #Option 3
45
+ # however, we need to actually perform additional logic on the specific combinations,
46
+ # not just aggregate them into one giant array
47
+ result = []
48
+ if (arrays.class != [].class)
49
+ puts "Not an array"
50
+ elsif (arrays.length == 1)
51
+ arrays[0]
52
+ elsif (arrays.length == 0)
53
+ puts "No input."
54
+ elsif (arrays[0].class != [].class)
55
+ puts "Not an array of arrays"
56
+ else
57
+ counter = Arbitrary_base_counter.new([*0..arrays.length-1].map { |i| arrays[i].length })
58
+
59
+ while !counter.overflow do
60
+ sub_result = []
61
+ (0..counter.length-1).each { |i|
62
+ sub_result << arrays[i][counter[i]]
63
+ }
64
+ result << sub_result
65
+
66
+ counter.increment
67
+ end
68
+ end
69
+
70
+ result
71
+ end
72
+
73
+ ##
74
+ # This method combines an array of hashes (i.e. an array of probabilities) into an aggregate probability distribution
75
+ def self.Cartesian_Product_For_Probabilities(hashes) #hashes is a hash of hashes to cartesian product
76
+ result = {}
77
+
78
+ if (hashes.class != Array)
79
+ puts "Not an array"
80
+ elsif (hashes.length == 1)
81
+ puts "Returning first result"
82
+ result = hashes.first
83
+ elsif (hashes.length == 0)
84
+ puts "Returning new hash"
85
+ result = { 0 => 1 }
86
+ elsif (hashes[0].class != Hash)
87
+ puts "Not a Hash of Hashes"
88
+ else
89
+ counter = Arbitrary_base_counter.new([*0..hashes.length-1].map { |i| hashes[i].length })
90
+ hashes.map! { |h| h.to_a }
91
+
92
+ while !counter.overflow do
93
+ value = 0
94
+ probability = 1
95
+ sub_result = {}
96
+
97
+ (0..counter.length-1).each { |i|
98
+ value += hashes[i][counter[i]][0]
99
+ probability *= hashes[i][counter[i]][1]
100
+ }
101
+ if (result.key?(value))
102
+ result[value] += probability
103
+ else
104
+ result[value] = probability
105
+ end
106
+
107
+ counter.increment
108
+ end
109
+ end
110
+
111
+ result
112
+ end
113
+
114
+ ##
115
+ # Deprecated / incomplete.
116
+ # This method runs some quick tests on the basic math helper functions
117
+ # to make sure that the Factorial and Choose methods are working as expected.
118
+ def self.Test_Suite
119
+ #Choose
120
+ puts "Testing Choose edge cases..."
121
+ Test_Choose(6, 10, 1)
122
+ Test_Choose(6, 6, 1)
123
+ Test_Choose(5, 0, 1)
124
+ Test_Choose(-1, 5, 1)
125
+ Test_Choose(5, -1, 1)
126
+ puts
127
+ puts "Testing Choose basic math..."
128
+ Test_Choose(5, 3, 10)
129
+ Test_Choose(6, 2, 15)
130
+ Test_Choose(14, 7, 3432)
131
+ Test_Choose(7, 2, 21)
132
+ Test_Choose(7, 5, 21)
133
+ Test_Choose(7, 5, 21)
134
+ Test_Choose(6, 3, 20)
135
+
136
+ #Factorial
137
+ puts
138
+ puts "Testing Factorial edge cases..."
139
+ Test_Factorial(0, 0)
140
+ Test_Factorial(-2, 0)
141
+ puts
142
+ puts "Testing Factorial basic math..."
143
+ Test_Factorial(6, 720)
144
+ Test_Factorial(5, 120)
145
+ Test_Factorial(4, 24)
146
+ Test_Factorial(3, 6)
147
+ Test_Factorial(2, 2)
148
+ end
149
+
150
+ ##
151
+ # Helper function for testing the Choose method.
152
+ def self.Test_Choose(a, b, expected)
153
+ puts "(#{a} #{b}) => #{expected} (Actual: " + self.Choose(a, b).to_s + ")"
154
+ if (self.Choose(a, b) != expected)
155
+ puts "`--> ERROR. FAILING CASE."
156
+ end
157
+ end
158
+
159
+ ## Helper function for testing the Factorial method.
160
+ def self.Test_Factorial(a, expected)
161
+ puts "#{a}! => #{expected} (Actual: " + self.Factorial(a).to_s + ")"
162
+ if (self.Factorial(a) != expected)
163
+ puts "`--> ERROR. FAILING CASE."
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,188 @@
1
+ require 'sqlite3'
2
+ require 'bigdecimal'
3
+
4
+ module Dice_Stats
5
+
6
+ end
7
+
8
+ module Dice_Stats::Internal_Utilities
9
+
10
+ ##
11
+ # This class represents the cache. The cache is used for storing previously calculated results.
12
+ # Some dice sets can take quite a while to calculate.
13
+ # Storing them drastically increases performance when a cache hit is found.
14
+ class DB_cache_connection
15
+ ##
16
+ # The version of the gem. If this is updated, the DB_cache_connection#initialize method will drop and recreate the tables
17
+ @@Version = [0, 1, 0]
18
+
19
+ ##
20
+ # The path of the sqlite3 db file.
21
+ @@Path = '/srv/Dice_Stats/'
22
+
23
+ ##
24
+ # The name of the sqlite3 db file.
25
+ @@DB_name = 'probability_cache.db'
26
+
27
+ ##
28
+ # For internal use only. Checks the database version and schema on startup.
29
+ def initialize
30
+ checkSchema
31
+ end
32
+
33
+ ##
34
+ # Checks the database version and schema and creates the table structures.
35
+ def checkSchema
36
+ begin
37
+ db = SQLite3::Database.open(@@Path + @@DB_name)
38
+ db.execute "CREATE TABLE IF NOT EXISTS DiceConfig(Key TEXT PRIMARY KEY, Val TEXT);"
39
+ statement = db.prepare "SELECT Val FROM DiceConfig WHERE Key = 'Version';"
40
+ row = statement.execute.next
41
+
42
+ if !row
43
+ db.execute "INSERT INTO DiceConfig (Key, Val) VALUES ('Version', '#{@@Version.join('.')}');"
44
+ createSchema(db, true)
45
+ else
46
+ version = row[0]
47
+ version = version.split('.')
48
+ version.map! { |i| i.to_i }
49
+ if (@@Version[0] != version[0] || @@Version[1] != version[1] || @@Version[2] != version[2])
50
+ # Version of database is different, drop and recreate!
51
+ createSchema(db, true)
52
+ db.execute "UPDATE DiceConfig SET Version = '#{@@Version.join('.')}' WHERE Key = 'Version'"
53
+ end
54
+
55
+ end
56
+ rescue SQLite3::Exception => e
57
+ puts e
58
+ ensure
59
+ statement.close if statement
60
+ db.close if db
61
+ end
62
+
63
+ end
64
+
65
+ ##
66
+ # Creates the tables if they don't already exist.
67
+ # If +drop+ is set to true, the tables will be dropped first.
68
+ def createSchema(db, drop=false)
69
+ db.execute "DROP TABLE IF EXISTS DiceSet;" if drop
70
+ db.execute "CREATE TABLE IF NOT EXISTS DiceSet (Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT UNIQUE, TimeElapsed DECIMAL)"
71
+
72
+ db.execute "DROP TABLE IF EXISTS RollProbability;" if drop
73
+ db.execute "CREATE TABLE IF NOT EXISTS RollProbability (Id INTEGER PRIMARY KEY AUTOINCREMENT, DiceSetId INT, Value INTEGER, Probability DECIMAL)"
74
+ end
75
+
76
+ ##
77
+ # Drops and recreates the schema for the purpose of clearing the cache.
78
+ def purge
79
+ begin
80
+ db = SQLite3::Database.open(@@Path + @@DB_name)
81
+ puts "Purging cache..."
82
+ createSchema(db, true)
83
+ rescue SQLite3::Exception => e
84
+ puts e
85
+ ensure
86
+ db.close if db
87
+ end
88
+ end
89
+
90
+ ##
91
+ # Checks the cache to see if the pattern has been previously calculated.
92
+ def checkDice(dice_pattern)
93
+ begin
94
+ db = SQLite3::Database.open(@@Path + @@DB_name)
95
+
96
+ statement = db.prepare "SELECT Id FROM DiceSet WHERE Name = '#{dice_pattern}'"
97
+
98
+ return statement.execute.count >= 1
99
+
100
+ rescue SQLite3::Exception => e
101
+ puts e
102
+ ensure
103
+ statement.close if statement
104
+ db.close if db
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Caches a dice pattern for future retrieval.
110
+ def addDice(dice_pattern, probability_distribution, timeElapsed=0.0)
111
+ begin
112
+ db = SQLite3::Database.open(@@Path + @@DB_name)
113
+ db.execute "INSERT INTO DiceSet (Name) VALUES ('#{dice_pattern}')"
114
+ diceset_id = db.last_insert_row_id
115
+
116
+ values = []
117
+ probability_distribution.each { |k,v|
118
+ values << "(#{diceset_id}, #{k}, #{v})"
119
+ }
120
+ #puts "Values:"
121
+ #puts values
122
+
123
+ insert = "INSERT INTO RollProbability (DiceSetId, Value, Probability) VALUES " + values.join(", ")
124
+
125
+ db.execute insert
126
+ rescue SQLite3::Exception => e
127
+ puts e
128
+ ensure
129
+ db.close if db
130
+ end
131
+ end
132
+
133
+ ##
134
+ # Retrieves the probability distribution for a dice pattern from the cache.
135
+ def getDice(dice_pattern)
136
+ begin
137
+ db = SQLite3::Database.open(@@Path + @@DB_name)
138
+
139
+ statement1 = db.prepare "SELECT Id FROM DiceSet WHERE Name = '#{dice_pattern}'"
140
+ diceset_id = statement1.execute.first[0]
141
+
142
+ statement2 = db.prepare "SELECT Value, Probability FROM RollProbability WHERE DiceSetId = #{diceset_id}"
143
+
144
+ rs = statement2.execute
145
+ result = {}
146
+ rs.each { |row|
147
+ result[row[0]] = BigDecimal.new(row[1], 15)
148
+ }
149
+
150
+ return result
151
+
152
+ rescue SQLite3::Exception => e
153
+ puts e
154
+ ensure
155
+ statement1.close if statement1
156
+ statement2.close if statement2
157
+ db.close if db
158
+ end
159
+ end
160
+
161
+ ##
162
+ # Retrieves how long a cached result originally took to calculate.
163
+ def getElapsed(dice_pattern)
164
+ begin
165
+ db = SQLite3::Database.open(@@Path + @@DB_name)
166
+ statement = db.prepare "SELECT TimeElapsed FROM DiceSet WHERE Name = '#{dice_pattern}'"
167
+
168
+ rs = statement.execute
169
+ result = nil
170
+ rs.each { |row|
171
+ result = row[0]
172
+ }
173
+
174
+ if (result == nil)
175
+ return 0.0
176
+ else
177
+ return result
178
+ end
179
+
180
+ rescue SQLite3::Exception => e
181
+ puts e
182
+ ensure
183
+ statement.close if statement
184
+ db.close if db
185
+ end
186
+ end
187
+ end
188
+ end
data/lib/dice_stats.rb ADDED
@@ -0,0 +1,22 @@
1
+ #The "main" function
2
+
3
+ ##
4
+ # Dice_Stats is the main namespace for the gem.
5
+ # Dice Stats is a ruby gem for handling dice related statistics.
6
+ # see the README.md for more information.
7
+ module Dice_Stats
8
+ end
9
+
10
+ ##
11
+ # The Internal_Utilities module is a namespace for some basic helper functions and classes used by Dice_Stats.
12
+ # Things in it are simply encapsulated this way to avoid polluting the lexical scope of the main module.
13
+ module Dice_Stats::Internal_Utilities
14
+ end
15
+
16
+ require 'Dice_Set'
17
+
18
+ ##
19
+ # Instantiates the cache.
20
+ # The cache is an instance of a DB_cache_connection in the Internal_Utilities module.
21
+ # It is used to cache past results.
22
+ Dice_Stats::Cache = Dice_Stats::Internal_Utilities::DB_cache_connection.new
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dice_stats
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Foy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-11-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.5'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 3.5.0
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '3.5'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 3.5.0
33
+ description: Provides utilities for calculating dice statistics
34
+ email: mattfoy91@gmail.com
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - lib/Dice.rb
40
+ - lib/Dice_Set.rb
41
+ - lib/Internal_Utilities/Arbitrary_base_counter.rb
42
+ - lib/Internal_Utilities/Filtered_distribution.rb
43
+ - lib/Internal_Utilities/Math_Utilities.rb
44
+ - lib/Internal_Utilities/probability_cache_db.rb
45
+ - lib/dice_stats.rb
46
+ homepage: http://matthewfoy.ca
47
+ licenses:
48
+ - MIT
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 2.5.1
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Dice statistics utility.
70
+ test_files: []