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.
- 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
|
+
[![Build Status](https://travis-ci.org/AnthonySuper/FifthedSim.svg?branch=master)](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'
|