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.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/HelpWanted.md +23 -0
- data/README.md +144 -5
- data/bin/console +2 -8
- data/bin/playground +42 -0
- data/exe/diceroll +9 -0
- data/fifthed_sim.gemspec +4 -0
- data/lib/fifthed_sim/actor.rb +80 -0
- data/lib/fifthed_sim/attack.rb +86 -0
- data/lib/fifthed_sim/calculated_fixnum.rb +57 -0
- data/lib/fifthed_sim/compiler/parser.rb +46 -0
- data/lib/fifthed_sim/compiler/transform.rb +28 -0
- data/lib/fifthed_sim/compiler.rb +51 -0
- data/lib/fifthed_sim/damage.rb +54 -0
- data/lib/fifthed_sim/damage_types.rb +39 -0
- data/lib/fifthed_sim/dice_expression.rb +134 -0
- data/lib/fifthed_sim/distribution.rb +219 -20
- data/lib/fifthed_sim/nodes/addition_node.rb +75 -0
- data/lib/fifthed_sim/nodes/block_node.rb +46 -0
- data/lib/fifthed_sim/nodes/division_node.rb +26 -0
- data/lib/fifthed_sim/nodes/greater_node.rb +41 -0
- data/lib/fifthed_sim/nodes/less_node.rb +46 -0
- data/lib/fifthed_sim/nodes/multi_node.rb +135 -0
- data/lib/fifthed_sim/nodes/multiplication_node.rb +25 -0
- data/lib/fifthed_sim/nodes/number_node.rb +38 -0
- data/lib/fifthed_sim/nodes/roll_node.rb +88 -0
- data/lib/fifthed_sim/nodes/subtraction_node.rb +24 -0
- data/lib/fifthed_sim/roll_repl.rb +117 -0
- data/lib/fifthed_sim/spell.rb +74 -0
- data/lib/fifthed_sim/stat.rb +49 -0
- data/lib/fifthed_sim/stat_block.rb +57 -0
- data/lib/fifthed_sim/version.rb +3 -1
- data/lib/fifthed_sim.rb +28 -4
- metadata +74 -8
- data/lib/fifthed_sim/dice_calculation.rb +0 -88
- data/lib/fifthed_sim/dice_result.rb +0 -108
- data/lib/fifthed_sim/die_roll.rb +0 -66
- data/lib/fifthed_sim/helpers/average_comparison.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0087fa99f564c98c6d462c20828beb2acf4146be
|
4
|
+
data.tar.gz: 26b12a81c991271aa16d25f9479b0859188f33e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1c996014ded703a1caa63f3be5f2a9fdedd5dace5bfb588829248aa92705a90f54c21ffe50f8890cf2be6abc930096fcb15ebeb5954b4350c9d0cbcc2ce0b16
|
7
|
+
data.tar.gz: 4eb2c588b583539b52f3a5122c1e17a95f9b0b394b80806b6cd5bf8c70196ef4fab891395d0044c870739d8791c128c450898a9948de24138022224067d1c38e
|
data/.travis.yml
CHANGED
data/HelpWanted.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Help Wanted
|
2
|
+
|
3
|
+
This file contains a list of improvements we desire for this gem.
|
4
|
+
If you can help out, feel free to make a pull request on the [Github](https://github.com/AnthonySuper/FifthedSim)
|
5
|
+
|
6
|
+
|
7
|
+
## Top and Bottom Nodes
|
8
|
+
Some dice games require that you roll a certain amount of dice, and take the top or bottom rolls.
|
9
|
+
For example, to apply *disadvantage* in D&D, you roll 2d20 and take the lowest D20.
|
10
|
+
|
11
|
+
For the case of choosing between two dice calculations, we use the `.or_less` and `.or_greater` methods.
|
12
|
+
Unfortunately, these are not the only types of top and bottom calculations used in games.
|
13
|
+
Rolling a stat in D&D, for example, is `4d6 (drop lowest)`.
|
14
|
+
|
15
|
+
Finding a generic distribution for this is, as it turns out, hard to do efficiently.
|
16
|
+
The simple way is to take all possible combinations of rolls, drop the lowest, count the remaining values, and create a distribution from this information.
|
17
|
+
Unfortunately, the number of combinations of rolls is `dice_type^dice_number`, which is an exponential calculation.
|
18
|
+
|
19
|
+
If you know of a better formula, we'd love to hear about it.
|
20
|
+
You can either [open an issue](https://github.com/AnthonySuper/FifthedSim/issues/new) describing it, or create a pull request yourself.
|
21
|
+
|
22
|
+
If there does not exist a better formula, we'd also love to hear about it.
|
23
|
+
You can [open an issue](https://github.com/AnthonySuper/FifthedSim/issues/new) with a link to a proof that this operation cannot be done any quicker, and we will implement the brute-force solution.
|
data/README.md
CHANGED
@@ -1,8 +1,151 @@
|
|
1
1
|
# FifthedSim
|
2
|
+
[](https://travis-ci.org/AnthonySuper/FifthedSim)
|
3
|
+
|
2
4
|
|
3
5
|
This is a gem to simulate a game that you play with dice on a d20 system.
|
4
6
|
It is unfinished, but intends to enable a user to run simulations, or to see the overall probability of things which happen.
|
5
7
|
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
### Executable
|
11
|
+
|
12
|
+
If you just want to roll dice, simply install the gem and type "diceroll".
|
13
|
+
This will bring up a Dice REPL, which has some nice functionality.
|
14
|
+
Just type a dice expression, and you will get a result:
|
15
|
+
|
16
|
+
```
|
17
|
+
> d20 + 3d6
|
18
|
+
=> 18 + (3 2 6)
|
19
|
+
= 29
|
20
|
+
```
|
21
|
+
You can also get some info about a roll by typing `info`.
|
22
|
+
Type `help` to see all the commands.
|
23
|
+
|
24
|
+
### Dice Expressions
|
25
|
+
|
26
|
+
This gem generalizes the use of dice into *DiceExpressions*, which is an expression representing a calculation done on dice.
|
27
|
+
This expression is *lazily evaluated*, which means that it does not turn into an actual numerical value until you call `.to_i` or `.value` on it.
|
28
|
+
|
29
|
+
DiceExpressions are more powerful than simply rolling dice and doing math on the results, as we can both reuse them and do statistics on them.
|
30
|
+
This allows us to figure out a variety of useful information.
|
31
|
+
|
32
|
+
Dice expressions are almost always constructed via the `Fixnum#d` method:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
1.d(20)
|
36
|
+
```
|
37
|
+
|
38
|
+
From here, mathematical operations work as normal:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
result = (1.d(20) + 3 + 2.d(6)) / (1.d(4) * 2 * 1.d(2))
|
42
|
+
```
|
43
|
+
|
44
|
+
As mentioned earlier, we can get a numeric value from this expression:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
result.value # -> 2
|
48
|
+
```
|
49
|
+
|
50
|
+
We can also reroll this expression, to get another DiceExpression with a new value:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
result.reroll.value # -> 6
|
54
|
+
```
|
55
|
+
|
56
|
+
More interestingly, we can do statistics on this value.
|
57
|
+
We can obtain a `Distribution` of possible results for a given dice expression easily:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
distribution = result.distribution
|
61
|
+
```
|
62
|
+
|
63
|
+
Now, let's see how likely our value of 2 was.
|
64
|
+
Let's also see what percentile our value is in.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
distribution.percent_exactly(2) # -> 0.199305...
|
68
|
+
distribution.percentile_of(2) # -> 0.475694...
|
69
|
+
```
|
70
|
+
|
71
|
+
So our roll wasn't terrible, but it wasn't great as well.
|
72
|
+
|
73
|
+
#### Combinations
|
74
|
+
`DiceExpressions` are a powerful construct, because they allow combinations with arbitrary functions, while still being a `DiceExpression`.
|
75
|
+
To use an example, let's try to model the damage of a kobold's dagger attack against a player character with 12 AC.
|
76
|
+
|
77
|
+
The kobold rolls `1d20 + 4` to hit.
|
78
|
+
If he gets a 12 or higher, he does `1d4 + 2` damage to this player.
|
79
|
+
Let's model this attack:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
attack = (1.d(20) + 4).test_then do |result|
|
83
|
+
if result < 12
|
84
|
+
0.to_dice_expression
|
85
|
+
else
|
86
|
+
1.d(4) + 2
|
87
|
+
end
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
This is just a `DiceExpression`, so we can get its value, and reroll it:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
attack.value # => 0, the poor guy missed
|
95
|
+
attack.reroll.value # => 3, he hit... and critfailed damage.
|
96
|
+
```
|
97
|
+
|
98
|
+
Even more interestingly, however, we can do *statistics* on it.
|
99
|
+
Let's see the chance of him doing at least one damage:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
attack.distribution.percent_greater(0) # => 0.65, 65% of doing damage
|
103
|
+
```
|
104
|
+
What about the average damage per attack?
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
attack.average # => 2.925
|
108
|
+
```
|
109
|
+
|
110
|
+
As long as the block passed to `test_then` is *pure* (IE, the same input maps to the same output, regardless of anything else that happens in the program), then we can do any kind of calculation we want inside of it.
|
111
|
+
This is useful in a variety of situations.
|
112
|
+
|
113
|
+
### Simulation
|
114
|
+
FifthedSim was originally designed to simulate D&D 5e games.
|
115
|
+
Doing this is still under construction, but it's coming along nicely.
|
116
|
+
|
117
|
+
Simulation is based on *Actors*.
|
118
|
+
An Actor represents a character or NPC in the game.
|
119
|
+
Actors are defined with a nice DSL:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
actor = FifthedSim.define_actor("Bobby") do
|
123
|
+
base_ac 10
|
124
|
+
stats do
|
125
|
+
str 10
|
126
|
+
dex do
|
127
|
+
value 18
|
128
|
+
save_mod_bonus 5
|
129
|
+
end
|
130
|
+
wis 8
|
131
|
+
cha 16
|
132
|
+
con 14
|
133
|
+
int 12
|
134
|
+
end
|
135
|
+
attack "rapier" do
|
136
|
+
to_hit 5
|
137
|
+
damage do
|
138
|
+
piercing 1.d(6)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
Construction based on YAML or JSON is still a work in progress.
|
145
|
+
Actors are intended to be used to simulate battles.
|
146
|
+
This is currently a work in progress, although simulating individual attacks and such *does* currently work.
|
147
|
+
|
148
|
+
|
6
149
|
## Installation
|
7
150
|
|
8
151
|
Add this line to your application's Gemfile:
|
@@ -19,10 +162,6 @@ Or install it yourself as:
|
|
19
162
|
|
20
163
|
$ gem install fifthed_sim
|
21
164
|
|
22
|
-
## Usage
|
23
|
-
|
24
|
-
See the rdoc for now.
|
25
|
-
|
26
165
|
## Development
|
27
166
|
|
28
167
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -32,7 +171,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
171
|
## Contributing
|
33
172
|
|
34
173
|
Bug reports and pull requests are welcome on GitHub at https://github.com/anthonysuper/fifthedsim.
|
35
|
-
|
174
|
+
Please see the [HelpWanted.md](HelpWanted.md) file for specific patches we are looking for.
|
36
175
|
|
37
176
|
## License
|
38
177
|
|
data/bin/console
CHANGED
@@ -3,12 +3,6 @@
|
|
3
3
|
require "bundler/setup"
|
4
4
|
require "fifthed_sim"
|
5
5
|
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start
|
7
|
+
require "pry"
|
8
|
+
Pry.start
|
data/bin/playground
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "fifthed_sim"
|
5
|
+
|
6
|
+
|
7
|
+
require "pry"
|
8
|
+
require 'ostruct'
|
9
|
+
require 'csv'
|
10
|
+
|
11
|
+
iron_will = FifthedSim::Attack.define "Sword of Iron Will" do
|
12
|
+
to_hit 9
|
13
|
+
damage do
|
14
|
+
slashing 2.d(6) + 5
|
15
|
+
end
|
16
|
+
crit_threshold 19
|
17
|
+
crit_damage do
|
18
|
+
slashing 7.d(6) + 5
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class FakeMonster
|
23
|
+
def initialize(ac = 18); @ac = ac; end;
|
24
|
+
def resistant_to?(_); false; end;
|
25
|
+
def immune_to?(_); false; end;
|
26
|
+
attr_accessor :ac
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def write_csv(d, filename= "data.csv")
|
31
|
+
m = d.map
|
32
|
+
keys = m.keys.sort
|
33
|
+
CSV.open(filename, "wb") do |csv|
|
34
|
+
csv << ["Outcome", *keys]
|
35
|
+
csv << ["Chance", *keys.map{|k| m[k]}]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
hit = iron_will.against(FakeMonster.new)
|
41
|
+
|
42
|
+
binding.pry
|
data/exe/diceroll
ADDED
data/fifthed_sim.gemspec
CHANGED
@@ -17,9 +17,13 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
18
|
spec.bindir = "exe"
|
19
19
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.executables << 'diceroll'
|
20
21
|
spec.require_paths = ["lib"]
|
21
22
|
|
23
|
+
spec.add_dependency "parslet", "~> 1.7.1"
|
24
|
+
spec.add_dependency "rainbow", "~> 2.0.0"
|
22
25
|
spec.add_development_dependency "bundler", "~> 1.12"
|
23
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
24
27
|
spec.add_development_dependency "rspec", "~> 3.0"
|
28
|
+
spec.add_development_dependency "pry", "~> 0.10.4"
|
25
29
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module FifthedSim
|
2
|
+
class Actor
|
3
|
+
class DefinitionProxy
|
4
|
+
def initialize(name, &block)
|
5
|
+
@attrs = {
|
6
|
+
name: name,
|
7
|
+
attacks: {},
|
8
|
+
spells: {},
|
9
|
+
base_ac: 10
|
10
|
+
}
|
11
|
+
instance_eval(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :attrs
|
15
|
+
|
16
|
+
def attack(name, &block)
|
17
|
+
if block_given? && name.is_a?(String)
|
18
|
+
@attrs[:attacks][name] = Attack.define(name, &block)
|
19
|
+
elsif name.is_a?(Attack)
|
20
|
+
@attrs[:attacks][name.name] << name
|
21
|
+
else
|
22
|
+
raise ArgumentError, "must be an attack"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def spell(name, &block)
|
27
|
+
if block_given? && name.is_a?(String)
|
28
|
+
@attrs[:spells][name] = Spell.define(name, &block)
|
29
|
+
elsif name.is_a?(Spell)
|
30
|
+
@attrs[:spells][name.name] << name
|
31
|
+
else
|
32
|
+
raise ArgumentError, "must be a spell"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def stats(n = nil, &block)
|
37
|
+
if n && n.is_a?(StatBlock)
|
38
|
+
@attrs[:stats] = n
|
39
|
+
elsif block
|
40
|
+
@attrs[:stats] = StatBlock.define(&block)
|
41
|
+
else
|
42
|
+
raise ArgumentError, "Must be a statblock"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def base_ac(num)
|
47
|
+
@attrs[:base_ac] = num
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.define(name, &block)
|
52
|
+
d = DefinitionProxy.new(name, &block)
|
53
|
+
return self.new(d.attrs)
|
54
|
+
end
|
55
|
+
|
56
|
+
ASSIGNABLE_ATTRS = [:base_ac,
|
57
|
+
:name,
|
58
|
+
:attacks,
|
59
|
+
:spells]
|
60
|
+
def initialize(attrs)
|
61
|
+
attrs.to_a.keep_if{ |(k, v)| ASSIGNABLE_ATTRS.include?(v) }
|
62
|
+
.each { |(k, v)| self.instance_variable_set("@#{k}", v) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def random_attack
|
66
|
+
@attacks.keys.sample
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# TODO: Implement armor
|
71
|
+
def ac
|
72
|
+
base_ac
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader :base_ac,
|
76
|
+
:name,
|
77
|
+
:attacks,
|
78
|
+
:spells
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require_relative './damage'
|
2
|
+
module FifthedSim
|
3
|
+
##
|
4
|
+
# Model an attack vs AC
|
5
|
+
class Attack
|
6
|
+
class DefinitionProxy
|
7
|
+
def initialize(name, &block)
|
8
|
+
@attrs = {
|
9
|
+
name: name,
|
10
|
+
to_hit: 0,
|
11
|
+
damage: nil,
|
12
|
+
crit_threshold: 20,
|
13
|
+
crit_damage: nil
|
14
|
+
}
|
15
|
+
instance_eval(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :attrs
|
19
|
+
|
20
|
+
def to_hit(num)
|
21
|
+
@attrs[:to_hit] = num
|
22
|
+
end
|
23
|
+
|
24
|
+
def damage(arg = nil, &block)
|
25
|
+
if block_given?
|
26
|
+
@attrs[:damage] = Damage.define(&block)
|
27
|
+
else
|
28
|
+
damage_check(arg)
|
29
|
+
@attrs[:damage] = arg
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def crit_damage(arg = nil, &block)
|
34
|
+
if block_given?
|
35
|
+
@attrs[:crit_damage] = Damage.define(&block)
|
36
|
+
else
|
37
|
+
damage_check(arg)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def crit_threshold(thr)
|
42
|
+
@attrs[:crit_threshold] = thr
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
def damage_check(arg)
|
47
|
+
unless arg.is_a? Damage
|
48
|
+
raise TypeError, "#{arg.inspect} is not Damage"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.define(name, &block)
|
54
|
+
d = DefinitionProxy.new(name, &block)
|
55
|
+
self.new(d.attrs)
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize(attrs)
|
59
|
+
@name = attrs[:name]
|
60
|
+
@to_hit = attrs[:to_hit]
|
61
|
+
@damage = attrs[:damage]
|
62
|
+
@crit_threshold = attrs[:crit_threshold]
|
63
|
+
@crit_damage = attrs[:crit_damage]
|
64
|
+
end
|
65
|
+
|
66
|
+
def hit_roll
|
67
|
+
1.d(20) + @to_hit
|
68
|
+
end
|
69
|
+
|
70
|
+
def against(other)
|
71
|
+
hit_roll.test_then do |res|
|
72
|
+
if res < other.ac
|
73
|
+
0.to_dice_expression
|
74
|
+
elsif res > other.ac && res < (@crit_threshold + @to_hit)
|
75
|
+
@damage.to(other)
|
76
|
+
else
|
77
|
+
@crit_damage.to(other)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def raw_damage
|
83
|
+
@damage.raw
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module FifthedSim
|
2
|
+
module CalculatedFixnum
|
3
|
+
refine Fixnum do
|
4
|
+
def value
|
5
|
+
self
|
6
|
+
end
|
7
|
+
|
8
|
+
def average
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def has_critfail?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
def has_crit?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def distance_from_average
|
21
|
+
0
|
22
|
+
end
|
23
|
+
|
24
|
+
def distribution
|
25
|
+
FifthedSim::Distribution.for_number(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
def average?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def below_average?
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def above_average?
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
def difference_from_average
|
41
|
+
0
|
42
|
+
end
|
43
|
+
|
44
|
+
def reroll
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def value_equation(terminal: false)
|
49
|
+
self.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
def expression_equation
|
53
|
+
self.to_s
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
class FifthedSim::Compiler
|
4
|
+
class Parser < Parslet::Parser
|
5
|
+
root :addition
|
6
|
+
rule(:space) { match('\s').repeat(1) }
|
7
|
+
rule(:space?) { space.maybe }
|
8
|
+
rule(:lparen) { str("(") >> space? }
|
9
|
+
rule(:rparen) { str(")") >> space? }
|
10
|
+
rule(:comma) { str(",") >> space? }
|
11
|
+
rule(:number) { match('[0-9]').repeat(1).as(:number) }
|
12
|
+
rule(:number?) { number.maybe }
|
13
|
+
rule(:dice) do
|
14
|
+
(number?.as(:die_count) >> str("d") >> number.as(:die_type) >> space?)
|
15
|
+
.as(:dice)
|
16
|
+
end
|
17
|
+
rule(:base_term) { (dice | number) >> space? }
|
18
|
+
rule(:primary) { lparen >> addition >> rparen | base_term }
|
19
|
+
|
20
|
+
rule(:add_op) { match('\+|-').as(:op) >> space? }
|
21
|
+
rule(:addition) do
|
22
|
+
(multiplication.as(:lhs) >> add_op >> addition.as(:rhs)) |
|
23
|
+
multiplication
|
24
|
+
end
|
25
|
+
|
26
|
+
rule(:mult_op) { match('\*|/').as(:op) >> space? }
|
27
|
+
|
28
|
+
rule(:multiplication) do
|
29
|
+
(mult_term.as(:lhs) >> (mult_op >> addition.as(:rhs))) |
|
30
|
+
mult_term
|
31
|
+
end
|
32
|
+
|
33
|
+
rule(:mult_term) do
|
34
|
+
primary | funcall
|
35
|
+
end
|
36
|
+
|
37
|
+
rule(:ident) { match('[a-zA-z]').repeat(1) }
|
38
|
+
|
39
|
+
rule(:arglist) do
|
40
|
+
addition >> (comma >> addition).repeat
|
41
|
+
end
|
42
|
+
|
43
|
+
rule(:funcall) { ident.as(:ident) >> lparen >> arglist.as(:args) >> rparen }
|
44
|
+
rule(:expression) { funcall | multiplication | addition | base_term }
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
class FifthedSim::Compiler
|
4
|
+
class Transform < Parslet::Transform
|
5
|
+
rule(:number => simple(:x)) { Integer(x) }
|
6
|
+
rule(dice: {die_count: simple(:c), die_type: simple(:t)}) do
|
7
|
+
if c
|
8
|
+
c.d(t)
|
9
|
+
else
|
10
|
+
FifthedSim::RollNode.roll(t)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
rule(op: simple(:op), lhs: simple(:lhs), rhs: simple(:rhs)) do
|
14
|
+
lhs.to_dice_expression.public_send(op, rhs.to_dice_expression)
|
15
|
+
end
|
16
|
+
FUNC_MAP = {
|
17
|
+
"max" => :or_greater,
|
18
|
+
"min" => :or_least
|
19
|
+
}
|
20
|
+
FUNC_MAP.each do |k, v|
|
21
|
+
rule(ident: k, args: sequence(:args)) do |d|
|
22
|
+
d[:args].inject do |mem, arg|
|
23
|
+
mem.to_dice_expression.public_send(v, arg)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module FifthedSim
|
2
|
+
class Compiler
|
3
|
+
def self.parse(str)
|
4
|
+
begin
|
5
|
+
Parser.new.parse(str)
|
6
|
+
rescue Parslet::ParseFailed => e
|
7
|
+
msg = e.message
|
8
|
+
raise CompileError.new(e)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.compile(str)
|
13
|
+
tree = self.parse(str)
|
14
|
+
transformed = Transform.new.apply(tree)
|
15
|
+
if transformed.is_a? DiceExpression
|
16
|
+
transformed
|
17
|
+
else
|
18
|
+
raise TransformError.new(transformed)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class CompileError < StandardError
|
23
|
+
def initialize(err)
|
24
|
+
msg = err.message
|
25
|
+
super(msg)
|
26
|
+
@line = msg.match(/line (\d+)/)[1].to_i
|
27
|
+
@char = msg.match(/char (\d+)/)[1].to_i
|
28
|
+
@tree_cause = err.cause.ascii_tree
|
29
|
+
end
|
30
|
+
attr_reader :line
|
31
|
+
attr_reader :char
|
32
|
+
attr_reader :tree_cause
|
33
|
+
end
|
34
|
+
|
35
|
+
class TransformError < CompileError
|
36
|
+
def initialize(hash)
|
37
|
+
@message = "Could not transform"
|
38
|
+
if hash.is_a? Hash
|
39
|
+
@source_hash = hash
|
40
|
+
@line, @char = hash.values.keep_if{|x| x.is_a? Parslet::Slice}
|
41
|
+
.first.line_and_column
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :source_hash, :message
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
require_relative './compiler/parser'
|
51
|
+
require_relative './compiler/transform'
|