fifthed_sim 0.1.0 → 0.2.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -1
  3. data/HelpWanted.md +23 -0
  4. data/README.md +144 -5
  5. data/bin/console +2 -8
  6. data/bin/playground +42 -0
  7. data/exe/diceroll +9 -0
  8. data/fifthed_sim.gemspec +4 -0
  9. data/lib/fifthed_sim/actor.rb +80 -0
  10. data/lib/fifthed_sim/attack.rb +86 -0
  11. data/lib/fifthed_sim/calculated_fixnum.rb +57 -0
  12. data/lib/fifthed_sim/compiler/parser.rb +46 -0
  13. data/lib/fifthed_sim/compiler/transform.rb +28 -0
  14. data/lib/fifthed_sim/compiler.rb +51 -0
  15. data/lib/fifthed_sim/damage.rb +54 -0
  16. data/lib/fifthed_sim/damage_types.rb +39 -0
  17. data/lib/fifthed_sim/dice_expression.rb +134 -0
  18. data/lib/fifthed_sim/distribution.rb +219 -20
  19. data/lib/fifthed_sim/nodes/addition_node.rb +75 -0
  20. data/lib/fifthed_sim/nodes/block_node.rb +46 -0
  21. data/lib/fifthed_sim/nodes/division_node.rb +26 -0
  22. data/lib/fifthed_sim/nodes/greater_node.rb +41 -0
  23. data/lib/fifthed_sim/nodes/less_node.rb +46 -0
  24. data/lib/fifthed_sim/nodes/multi_node.rb +135 -0
  25. data/lib/fifthed_sim/nodes/multiplication_node.rb +25 -0
  26. data/lib/fifthed_sim/nodes/number_node.rb +38 -0
  27. data/lib/fifthed_sim/nodes/roll_node.rb +88 -0
  28. data/lib/fifthed_sim/nodes/subtraction_node.rb +24 -0
  29. data/lib/fifthed_sim/roll_repl.rb +117 -0
  30. data/lib/fifthed_sim/spell.rb +74 -0
  31. data/lib/fifthed_sim/stat.rb +49 -0
  32. data/lib/fifthed_sim/stat_block.rb +57 -0
  33. data/lib/fifthed_sim/version.rb +3 -1
  34. data/lib/fifthed_sim.rb +28 -4
  35. metadata +74 -8
  36. data/lib/fifthed_sim/dice_calculation.rb +0 -88
  37. data/lib/fifthed_sim/dice_result.rb +0 -108
  38. data/lib/fifthed_sim/die_roll.rb +0 -66
  39. data/lib/fifthed_sim/helpers/average_comparison.rb +0 -14
@@ -0,0 +1,46 @@
1
+ require_relative '../dice_expression'
2
+ require_relative '../calculated_fixnum'
3
+
4
+ module FifthedSim
5
+ class LessNode < DiceExpression
6
+ using CalculatedFixnum
7
+
8
+ def initialize(lhs, rhs)
9
+ @lhs, @rhs = lhs, rhs
10
+ end
11
+
12
+ def value
13
+ [@lhs.value, @rhs.value].min
14
+ end
15
+
16
+ def reroll
17
+ self.class.new(@lhs.reroll, @rhs.reroll)
18
+ end
19
+
20
+ def max
21
+ [@lhs.max, @rhs.max].min
22
+ end
23
+
24
+ def min
25
+ [@lhs.min, @rhs.min].min
26
+ end
27
+
28
+ def distribution
29
+ @lhs.distribution.convolve_least(@rhs.distribution)
30
+ end
31
+
32
+ def equation_representation
33
+ "min(#{@lhs.equation_representation}, #{@rhs.equation_representation})"
34
+ end
35
+
36
+ def value_equation(terminal: false)
37
+ lhs = @lhs.value_equation(terminal: terminal)
38
+ rhs = @rhs.value_equation(terminal: terminal)
39
+ "min(#{lhs}, #{rhs})"
40
+ end
41
+
42
+ def expression_equation
43
+ "min(#{@lhs.expression_equation}, #{@rhs.expression_equation})"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,135 @@
1
+ require_relative '../distribution'
2
+ require_relative '../dice_expression'
3
+
4
+ ##
5
+ # We sneakily monkey-patch Fixnum here, to allow us to use a nice syntax
6
+ # for factorials.
7
+ #
8
+ # We only do this if somebody hasn't done it already, in case ruby adds this to the standard one day.
9
+ class Fixnum
10
+ unless self.instance_methods(:false).include?(:factorial)
11
+ ##
12
+ # Mathematical factorial
13
+ def factorial
14
+ (1..self).inject(:*) || 1
15
+ end
16
+ end
17
+ end
18
+
19
+
20
+ ##
21
+ # Mathemetical combination
22
+ def combination(n, r)
23
+ n.factorial / (r.factorial * (n - r).factorial)
24
+ end
25
+
26
+ module FifthedSim
27
+ ##
28
+ # This class models the result of a roll of multiple dice.
29
+ # It is filled with the actual result of randomly-rolled dice, but contains
30
+ # methods to enable the calculation of average values.
31
+ class MultiNode < DiceExpression
32
+ def self.d(num, type)
33
+ self.new(num.times.map{RollNode.roll(type)})
34
+ end
35
+
36
+ ##
37
+ # Generally, don't calculate this yourself
38
+ def initialize(array)
39
+ unless array.is_a?(Array) && ! array.empty?
40
+ raise ArgumentError, "Not a valid array"
41
+ end
42
+ unless array.all?{|elem| elem.is_a?(RollNode) }
43
+ raise ArgumentError, "Not all die rolls"
44
+ end
45
+ @array = array
46
+ end
47
+
48
+ def reroll
49
+ self.class.new(@array.map(&:reroll))
50
+ end
51
+
52
+ ##
53
+ # Did any of our dice crit?
54
+ def has_crit?
55
+ @array.any?(&:crit?)
56
+ end
57
+
58
+ ##
59
+ # Did any of our dice critically fail?
60
+ def has_critfail?
61
+ @array.any?(&:critfail?)
62
+ end
63
+
64
+ ##
65
+ # What is the theoretical average value when we roll this many dice?
66
+ def average
67
+ @array.map(&:average).inject(:+)
68
+ end
69
+
70
+ ##
71
+ # Calculate the value of these dice
72
+ def value
73
+ @array.map(&:value).inject(:+)
74
+ end
75
+
76
+ ##
77
+ # How many dice did we roll?
78
+ def roll_count
79
+ @array.count
80
+ end
81
+
82
+ ##
83
+ # What kind of dice did we roll?
84
+ def dice_type
85
+ @array.first.type
86
+ end
87
+
88
+ ##
89
+ # The minimum value we could have rolled
90
+ def min_value
91
+ roll_count
92
+ end
93
+
94
+ ##
95
+ # The maximum value we could have rolled
96
+ def max_value
97
+ dice_type * roll_count
98
+ end
99
+
100
+ ##
101
+ # Obtain a probability distribution for when we roll this many dice.
102
+ # This is an instnace of the Distribution class.
103
+ def distribution
104
+ total_possible = (dice_type ** roll_count)
105
+ mapped = min_value.upto(max_value).map do |k|
106
+ [k, (occurences(k) / total_possible.to_f)]
107
+ end
108
+ Distribution.new(Hash[mapped])
109
+ end
110
+
111
+ def value_equation(terminal: false)
112
+ "(" + @array.map do |a|
113
+ a.value_equation(terminal: terminal)
114
+ end.join(", ") + ")"
115
+ end
116
+
117
+ def expression_equation
118
+ "(" + @array.map do |a|
119
+ a.expression_equation
120
+ end.join(", ") + ")"
121
+ end
122
+
123
+ private
124
+
125
+ def occurences(num)
126
+ dice_type = @array.first.type
127
+ num_dice = @array.size
128
+ 0.upto((num - num_dice) / dice_type).map do |k|
129
+ ((-1) ** k) *
130
+ combination(num_dice, k) *
131
+ combination(num - (dice_type*k) - 1, num_dice- 1)
132
+ end.inject(:+)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,25 @@
1
+ require_relative '../dice_expression'
2
+
3
+ module FifthedSim
4
+ class MultiplicationNode < DiceExpression
5
+ using CalculatedFixnum
6
+ def initialize(lhs, rhs)
7
+ @lhs = lhs
8
+ @rhs = rhs
9
+ end
10
+
11
+ def value
12
+ @lhs.value * @rhs.value
13
+ end
14
+
15
+ def distribution
16
+ @lhs.distribution.convolve_multiply(@rhs.distribution)
17
+ end
18
+
19
+ def reroll
20
+ self.class.new(@lhs.reroll, @rhs.reroll)
21
+ end
22
+
23
+ define_binary_op_equations "*"
24
+ end
25
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '../dice_expression'
2
+
3
+ module FifthedSim
4
+ ##
5
+ # Normally we handle numbers by use of a refinement on Fixnum
6
+ # However, in some cases, we may have the fixnum as the *start* of an expression.
7
+ # In this case, we have a problem, because Fixnum#+ is not overloaded to return a DiceNode.
8
+ # In this case, we must use this, a NumberNode.
9
+ # NumberNodes wrap a number.
10
+ class NumberNode < DiceExpression
11
+ def initialize(arg)
12
+ unless arg.is_a? Fixnum
13
+ raise ArgumentError, "#{arg.inspect} is not a fixnum"
14
+ end
15
+ @value = arg
16
+ end
17
+
18
+ def distribution
19
+ Distribution.for(@value)
20
+ end
21
+
22
+ def value
23
+ @value
24
+ end
25
+
26
+ def reroll
27
+ self.class.new(@value)
28
+ end
29
+
30
+ def value_equation(terminal: false)
31
+ @value.to_s
32
+ end
33
+
34
+ def expression_equation
35
+ @value.to_s
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,88 @@
1
+ require_relative '../dice_expression.rb'
2
+
3
+ module FifthedSim
4
+ ##
5
+ # Model a single roll of the dice.
6
+ # Users of the library will rarely interact with this class, and will instead manpiulate values based on the DiceResult type.
7
+ #
8
+ class RollNode < DiceExpression
9
+
10
+ ##
11
+ # Create a diceresult by rolling a certain type.
12
+ def self.roll(type)
13
+ raise ArgumentError, "Must be an Integer" unless type.is_a? Fixnum
14
+ self.new(SecureRandom.random_number(type) + 1, type)
15
+ end
16
+
17
+ ##
18
+ # Obtain a DieRoll filled with the average result of this die type
19
+ # This will round down.
20
+ def self.average(type)
21
+ self.new((type + 1) / 2, type)
22
+ end
23
+
24
+ ##
25
+ # Obtain an average value for this die type, as a float
26
+ # We're extremely lazy here.
27
+ def self.average_value(type)
28
+ self.new(1, type).average
29
+ end
30
+
31
+ def initialize(val, type)
32
+ unless val.is_a?(Fixnum) && type.is_a?(Fixnum)
33
+ raise ArgumentError, "Type invald"
34
+ end
35
+ @value = val
36
+ @type = type
37
+ end
38
+
39
+ def reroll
40
+ self.class.roll(@type)
41
+ end
42
+
43
+ attr_reader :value, :type
44
+
45
+ ##
46
+ # The average roll for a die of this type
47
+ def average
48
+ (@type + 1) / 2.0
49
+ end
50
+
51
+ ##
52
+ # How far away this roll is from the average roll
53
+ def difference_from_average
54
+ @value - average
55
+ end
56
+
57
+ ##
58
+ # Is this roll a critical failure? (AKA, is it a 1?)
59
+ def critfail?
60
+ @value == 1
61
+ end
62
+
63
+ ##
64
+ # Is this roll a critical? (AKA, is it the max value of the dice?)
65
+ def crit?
66
+ @value == @type
67
+ end
68
+
69
+ def distribution
70
+ Distribution.for((1..@type))
71
+ end
72
+
73
+ def value_equation(terminal: false)
74
+ return value.to_s unless terminal
75
+ if critfail?
76
+ Rainbow(value.to_s).color(:red).bright.to_s
77
+ elsif crit?
78
+ Rainbow(value.to_s).color(:yellow).bright.to_s
79
+ else
80
+ value.to_s
81
+ end
82
+ end
83
+
84
+ def expression_equation
85
+ "d#{@type}"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,24 @@
1
+ require_relative '../dice_expression'
2
+ module FifthedSim
3
+ class SubtractionNode < DiceExpression
4
+ using CalculatedFixnum
5
+ def initialize(lhs, rhs)
6
+ @lhs = lhs
7
+ @rhs = rhs
8
+ end
9
+
10
+ def value
11
+ @lhs - @rhs
12
+ end
13
+
14
+ def reroll
15
+ self.class.new(@lhs.reroll, @rhs.reroll)
16
+ end
17
+
18
+ def distribution
19
+ @lhs.distribution.convolve_subtract(@rhs.distribution)
20
+ end
21
+
22
+ define_binary_op_equations "-"
23
+ end
24
+ end
@@ -0,0 +1,117 @@
1
+ module FifthedSim
2
+ class RollRepl
3
+ def initialize(inspect = true, errors = false)
4
+ @inspect = inspect
5
+ @errors = errors
6
+ end
7
+
8
+ def run(kill_on_interrupt = false)
9
+ begin
10
+ while buf = Readline.readline("> ", true)
11
+ run_cmd(buf)
12
+ kill_on_interrupt = false
13
+ end
14
+ rescue Interrupt
15
+ self.exit if kill_on_interrupt
16
+ puts "Control-C again or 'Exit' to quit"
17
+ run(true)
18
+ end
19
+ end
20
+
21
+ def info(cmd)
22
+ s = cmd.gsub(/info/, "").chomp
23
+ if s.length > 0
24
+ run_command(s)
25
+ end
26
+ return error_msg("Have nothing to get info of") unless @last_roll
27
+ lb = ->(x){Rainbow(x).color(:yellow).bright.to_s + ": "}
28
+ puts (%i(max min percentile).map do |p|
29
+ lb[p] + @last_roll.public_send(p).to_s
30
+ end.inject{|m, x| m + ", " + x})
31
+ end
32
+
33
+ def reroll
34
+ return error_msg("Nothing to reroll") unless @last_roll
35
+ display_roll(@last_roll.reroll)
36
+ end
37
+
38
+ def run_cmd(cmd)
39
+ case cmd
40
+ when /(quit|exit|stop)/
41
+ self.exit
42
+ when "rr", "reroll"
43
+ self.reroll
44
+ when /info/
45
+
46
+ self.info(cmd)
47
+ when "help"
48
+ self.help
49
+ when "inspect"
50
+ toggle_inspect
51
+ when "errors"
52
+ toggle_errors
53
+ else
54
+ self.roll(cmd)
55
+ end
56
+ end
57
+
58
+ def roll(cmd)
59
+ r = DiceExpression(cmd)
60
+ @last_roll = r
61
+ display_roll(r)
62
+ rescue FifthedSim::Compiler::CompileError => e
63
+ if e.char
64
+ display_compile_error(e, cmd)
65
+ else
66
+ error_msg("Could not parse expression!")
67
+ end
68
+ end
69
+
70
+ def display_roll(r)
71
+ if @inspect
72
+ puts " = " + r.value_equation(terminal: true)
73
+ puts " => " + Rainbow(r.value.to_s).underline.bright.to_s
74
+ else
75
+ puts r.value.to_s
76
+ end
77
+ end
78
+
79
+ def error_msg(msg)
80
+ puts Rainbow(msg).color(:red).bright
81
+ end
82
+
83
+ def display_compile_error(err, cmd)
84
+ if @errors
85
+ error_msg("Could not read line: #{err.tree_cause}")
86
+ else
87
+ error_msg("Could not read line: #{err.message}")
88
+ end
89
+ end
90
+
91
+ [:inspect, :errors].each do |m|
92
+ send(:define_method, "toggle_#{m}") do
93
+ t = instance_variable_get("@#{m}")
94
+ puts "#{m}: #{t.inspect} => #{(!t).inspect}"
95
+ instance_variable_set("@#{m}", !t)
96
+ end
97
+ end
98
+
99
+ def exit
100
+ puts "Goodbye."
101
+ exit!
102
+ end
103
+
104
+ def help
105
+ cmd = ->(a){ Rainbow(a.to_s).bright.underline + ":" }
106
+ puts %Q{COMMANDS:
107
+ #{cmd[:help]} display this message
108
+ #{cmd[:inspect]} toggle equation inspection
109
+ #{cmd[:quit]} exit the roller
110
+ #{cmd[:errors]} toggle in-depth compile errors for dice expressions
111
+ #{cmd["arrow keys"]} navigate like GNU readline
112
+ #{cmd[:info]} get info about the previous roll, or a roll on this line.
113
+ #{cmd["rr, reroll"]} reroll the previous dice
114
+ }
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,74 @@
1
+ module FifthedSim
2
+ ##
3
+ # Spells model save-or-take-damage stuff.
4
+ # At some point in the future I hope to modify them so they work as other stu
5
+ class Spell
6
+ class DefinitionProxy
7
+ ##
8
+ def initialize(name, &block)
9
+ @hash = {
10
+ name: name
11
+ }
12
+ instance_eval(&block)
13
+ end
14
+
15
+ %i(damage save_damage).each do |m|
16
+ self.send(:define_method, m) do |damage = nil, &block|
17
+ if block
18
+ @hash[m] = Damage.define(&block)
19
+ elsif damage.is_a?(Damage)
20
+ @hash[m] = damage
21
+ else
22
+ raise ArgumentError, "#{damage} is not damage!"
23
+ end
24
+ end
25
+ end
26
+
27
+ def save_dc(n)
28
+ @hash[:save_dc] = n
29
+ end
30
+
31
+ def save_type(n)
32
+ @hash[:save_type] = n
33
+ end
34
+
35
+ def attrs
36
+ @hash
37
+ end
38
+ end
39
+
40
+ def self.define(name, &block)
41
+ h = DefinitionProxy.new(name, &block).attrs
42
+ self.new(h)
43
+ end
44
+
45
+ def initialize(hash)
46
+ @name = hash[:name]
47
+ @damage = hash[:damage]
48
+ @save_damage = hash[:save_damage]
49
+ @save_type = hash[:save_type]
50
+ @save_dc = hash[:save_dc]
51
+ end
52
+
53
+ attr_reader :save_dc,
54
+ :save_type
55
+
56
+ def against(other)
57
+ other.saving_throw(@save_type).test_then do |res|
58
+ if res >= @save_dc
59
+ @save_damage.to(other)
60
+ else
61
+ @damage.to(other)
62
+ end
63
+ end
64
+ end
65
+
66
+ def raw_damage
67
+ @damage.raw
68
+ end
69
+
70
+ def raw_save_damage
71
+ @save_damage.raw
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,49 @@
1
+ module FifthedSim
2
+ class Stat
3
+ class DefinitionProxy
4
+ def initialize(&block)
5
+ @hash = {
6
+ mod_bonus: 0,
7
+ save_mod_bonus: 0
8
+ }
9
+ instance_eval(&block)
10
+ end
11
+
12
+ %i(value mod_bonus save_mod_bonus).each do |e|
13
+ self.send(:define_method, e) do |x|
14
+ @hash[e] = x
15
+ end
16
+ end
17
+
18
+ attr_reader :hash
19
+ end
20
+
21
+ def self.define(&block)
22
+ h = DefinitionProxy.new(&block).hash
23
+ self.new(h)
24
+ end
25
+
26
+ def self.from_value(h)
27
+ raise ArgumentError, "#{h} not fixnum" unless h.is_a?(Fixnum)
28
+ self.new({value: h, save_mod: 0, mod_bonus: 0})
29
+ end
30
+
31
+ def initialize(hash)
32
+ @value = hash[:value]
33
+ @mod_bonus = (hash[:mod_bonus] || 0)
34
+ @save_mod_bonus = (hash[:save_mod_bonus] || 0)
35
+ end
36
+
37
+ attr_reader :value,
38
+ :save_mod_bonus,
39
+ :mod_bonus
40
+
41
+ def mod
42
+ ((@value - 10) / 2) + @mod_bonus
43
+ end
44
+
45
+ def saving_throw
46
+ 1.d(20) + mod + save_mod_bonus
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,57 @@
1
+ require_relative './stat'
2
+
3
+ module FifthedSim
4
+ STAT_TYPES = [:str,
5
+ :dex,
6
+ :wis,
7
+ :cha,
8
+ :con,
9
+ :int]
10
+ class StatBlock
11
+
12
+
13
+ class DefinitionProxy
14
+ def initialize(&block)
15
+ @hash = {}
16
+ instance_eval(&block)
17
+ end
18
+
19
+ STAT_TYPES.each do |type|
20
+ self.send(:define_method, type) do |x = nil, &block|
21
+ if block
22
+ @hash[type] = Stat.define(&block)
23
+ elsif x.is_a?(Stat)
24
+ @hash[type] = x
25
+ elsif x.is_a?(Fixnum)
26
+ @hash[type] = Stat.from_value(x)
27
+ else
28
+ raise ArgumentError, "not a stat"
29
+ end
30
+ end
31
+ end
32
+
33
+ attr_accessor :hash
34
+ end
35
+
36
+ def self.define(&block)
37
+ h = DefinitionProxy.new(&block)
38
+ self.new(h.hash)
39
+ end
40
+
41
+ def initialize(hash)
42
+ @hash = Hash[hash.map do |k, v|
43
+ if v.is_a?(Stat)
44
+ [k, v]
45
+ else
46
+ [k, Stat.new(v)]
47
+ end
48
+ end]
49
+ end
50
+
51
+ STAT_TYPES.each do |st|
52
+ self.send(:define_method, st) do
53
+ @hash[st]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,3 +1,5 @@
1
1
  module FifthedSim
2
- VERSION = "0.1.0"
2
+ ##
3
+ # Constant version string
4
+ VERSION = "0.2.0"
3
5
  end
data/lib/fifthed_sim.rb CHANGED
@@ -1,18 +1,42 @@
1
1
  require "fifthed_sim/version"
2
- require "fifthed_sim/dice_result"
3
- require "fifthed_sim/die_roll"
4
- require "fifthed_sim/dice_calculation"
2
+ require "fifthed_sim/dice_expression"
5
3
  require "fifthed_sim/distribution"
4
+ require "fifthed_sim/attack"
5
+ require "fifthed_sim/actor"
6
+ require "fifthed_sim/stat_block"
7
+ require "fifthed_sim/damage_types"
8
+ require "fifthed_sim/damage"
9
+ require "fifthed_sim/spell"
10
+ require "fifthed_sim/compiler"
11
+ require "fifthed_sim/roll_repl"
6
12
  require "securerandom"
7
13
 
8
14
  module FifthedSim
15
+
16
+ ##
17
+ # Roll a dice.
18
+ # Normally, you access this through the monkey-patch on Fixnum.
9
19
  def self.d(*args)
10
- DiceResult.d(*args)
20
+ MultiNode.d(*args)
21
+ end
22
+
23
+ def self.make_roll(val, type)
24
+ RollNode.new(val, type)
25
+ end
26
+
27
+ def self.define_actor(name, &block)
28
+ Actor.define(name, &block)
11
29
  end
12
30
  end
13
31
 
14
32
  class Fixnum
33
+ ##
34
+ # Enable you to create dice rolls via `3.d(6)` syntax.
35
+ # This returns a DiceResult, meaning that you can add them together
36
+ # to form a calculation.
15
37
  def d(o)
16
38
  FifthedSim.d(self, o)
17
39
  end
40
+
41
+
18
42
  end