fifthed_sim 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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