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 +7 -0
- data/lib/Dice.rb +147 -0
- data/lib/Dice_Set.rb +152 -0
- data/lib/Internal_Utilities/Arbitrary_base_counter.rb +85 -0
- data/lib/Internal_Utilities/Filtered_distribution.rb +70 -0
- data/lib/Internal_Utilities/Math_Utilities.rb +167 -0
- data/lib/Internal_Utilities/probability_cache_db.rb +188 -0
- data/lib/dice_stats.rb +22 -0
- metadata +70 -0
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: []
|