Compare commits
No commits in common. "master" and "documentation-add" have entirely different histories.
master
...
documentat
0
.gitignore
vendored
Executable file → Normal file
0
.gitignore
vendored
Executable file → Normal file
0
.travis.yml
Executable file → Normal file
0
.travis.yml
Executable file → Normal file
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,15 +0,0 @@
|
||||||
# v0.1
|
|
||||||
|
|
||||||
## v0.1.3
|
|
||||||
- Minor improvements
|
|
||||||
|
|
||||||
## v0.1.2
|
|
||||||
- Block DOS attack in the parser
|
|
||||||
|
|
||||||
## v0.1.1
|
|
||||||
- Dice can be ordered (<, <=, >, >=, <=>), Roll.compact, Roll.order
|
|
||||||
|
|
||||||
## v0.1.0
|
|
||||||
- Roll.parse("...") to create a rollable set of dices
|
|
||||||
- Die (one die), Dice (count of Die), Roll (list of Dice)
|
|
||||||
- Specs to cover most of the code
|
|
16
Makefile
16
Makefile
|
@ -1,16 +0,0 @@
|
||||||
NAME=`ls -d src/*/ | cut -f2 -d'/'`
|
|
||||||
|
|
||||||
all: deps_opt test
|
|
||||||
|
|
||||||
test:
|
|
||||||
crystal spec
|
|
||||||
deps:
|
|
||||||
shards install
|
|
||||||
deps_update:
|
|
||||||
shards update
|
|
||||||
deps_opt:
|
|
||||||
@[ -d lib/ ] || make deps
|
|
||||||
doc:
|
|
||||||
crystal docs
|
|
||||||
|
|
||||||
.PHONY: all run build release test deps deps_update doc
|
|
15
README.md
15
README.md
|
@ -1,20 +1,16 @@
|
||||||
# rollable
|
# rollable
|
||||||
|
|
||||||
Roll and parse dices
|
TODO: Write a description here
|
||||||
|
|
||||||
Works with crystal v1.0.0
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
[![travis](https://travis-ci.org/Nephos/crystal_rollable.svg)](https://travis-ci.org/Nephos/crystal_rollable)
|
|
||||||
|
|
||||||
Add this to your application's `shard.yml`:
|
Add this to your application's `shard.yml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
dependencies:
|
dependencies:
|
||||||
rollable:
|
rollable:
|
||||||
git: https://git.sceptique.eu/Sceptique/rollable
|
github: Nephos/crystal_rollable
|
||||||
branch: master
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,8 +19,7 @@ dependencies:
|
||||||
|
|
||||||
```crystal
|
```crystal
|
||||||
require "rollable"
|
require "rollable"
|
||||||
Rollable::Roll.parse("2d6+4").test # => Roll 2 dices and add 4 to the sum
|
Rollable::Roll.parse("2d6+4").test # => roll 2 dices and add 4 to the sum
|
||||||
Rollable::Roll.parse("!1d20 + !1d8").test # => Exploding dices
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,7 +29,7 @@ TODO: Write development instructions here
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
1. Fork it ( https://git.sceptique.eu/Sceptique/rollable/fork )
|
1. Fork it ( https://github.com/Nephos/crystal_rollable/fork )
|
||||||
2. Create your feature branch (git checkout -b my-new-feature)
|
2. Create your feature branch (git checkout -b my-new-feature)
|
||||||
3. Commit your changes (git commit -am 'Add some feature')
|
3. Commit your changes (git commit -am 'Add some feature')
|
||||||
4. Push to the branch (git push origin my-new-feature)
|
4. Push to the branch (git push origin my-new-feature)
|
||||||
|
@ -42,4 +37,4 @@ TODO: Write development instructions here
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
- [Sceptique](https://git.sceptique.eu/Sceptique) Arthur Poulet - creator, maintainer
|
- [Nephos](https://github.com/Nephos) Arthur Poulet - creator, maintainer
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
name: rollable
|
name: rollable
|
||||||
version: 1.0.0
|
version: 0.1.0
|
||||||
|
|
||||||
crystal: 1.0.0
|
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Arthur Poulet <arthur.poulet@sceptique.eu>
|
- Arthur Poulet <arthur.poulet@mailoo.org>
|
||||||
|
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
describe Rollable do
|
|
||||||
it "consts" do
|
|
||||||
Rollable::D100.max.should eq 100
|
|
||||||
Rollable::D20.max.should eq 20
|
|
||||||
Rollable::D12.max.should eq 12
|
|
||||||
Rollable::D10.max.should eq 10
|
|
||||||
Rollable::D8.max.should eq 8
|
|
||||||
Rollable::D6.max.should eq 6
|
|
||||||
Rollable::D4.max.should eq 4
|
|
||||||
Rollable::D3.max.should eq 3
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -5,7 +5,6 @@ describe Rollable::Dice do
|
||||||
d.min.should eq 2
|
d.min.should eq 2
|
||||||
d.max.should eq 40
|
d.max.should eq 40
|
||||||
d.average.should eq 21
|
d.average.should eq 21
|
||||||
expect_raises(Exception) { Rollable::Dice.new 1001, 20 }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "details" do
|
it "details" do
|
||||||
|
@ -20,13 +19,9 @@ describe Rollable::Dice do
|
||||||
|
|
||||||
it "reverse" do
|
it "reverse" do
|
||||||
d = Rollable::Dice.new(2, 20).reverse
|
d = Rollable::Dice.new(2, 20).reverse
|
||||||
d.count.should eq 2
|
|
||||||
d.die.min.should eq -20
|
|
||||||
d.min.should eq -40
|
d.min.should eq -40
|
||||||
d.max.should eq -2
|
d.max.should eq -2
|
||||||
d = Rollable::Dice.new(1, 1).reverse
|
d = Rollable::Dice.new(1, 1).reverse
|
||||||
d.count.should eq 1
|
|
||||||
d.die.min.should eq -1
|
|
||||||
d.min.should eq -1
|
d.min.should eq -1
|
||||||
d.max.should eq -1
|
d.max.should eq -1
|
||||||
end
|
end
|
||||||
|
@ -38,18 +33,11 @@ describe Rollable::Dice do
|
||||||
Rollable::Dice.parse("4d6").min.should eq 4
|
Rollable::Dice.parse("4d6").min.should eq 4
|
||||||
Rollable::Dice.parse("4d6").max.should eq 24
|
Rollable::Dice.parse("4d6").max.should eq 24
|
||||||
Rollable::Dice.parse("1d6+1", false).min.should eq 1
|
Rollable::Dice.parse("1d6+1", false).min.should eq 1
|
||||||
Rollable::Dice.parse("-1d6").min.should eq -6
|
|
||||||
Rollable::Dice.parse("-1d6").max.should eq -1
|
|
||||||
Rollable::Dice.parse("-1d6").count.should eq 1
|
|
||||||
Rollable::Dice.parse("!1d6").count.should eq 1
|
|
||||||
Rollable::Dice.parse("!1d6").min.should eq 1
|
|
||||||
Rollable::Dice.parse("!1d6").max.should eq Rollable::Die.new(1..6, true).max
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "parse (error)" do
|
it "parse (error)" do
|
||||||
expect_raises(Exception) { Rollable::Dice.parse("yolo") }
|
expect_raises { Rollable::Dice.parse("yolo") }
|
||||||
expect_raises(Exception) { Rollable::Dice.parse("1d6+1", true) }
|
expect_raises { Rollable::Dice.parse("1d6+1", true) }
|
||||||
expect_raises(Exception) { Rollable::Dice.parse("--1d4") }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "consume" do
|
it "consume" do
|
||||||
|
@ -63,23 +51,6 @@ describe Rollable::Dice do
|
||||||
|
|
||||||
it "to_s" do
|
it "to_s" do
|
||||||
Rollable::Dice.parse("2d6").to_s.should eq("2D6")
|
Rollable::Dice.parse("2d6").to_s.should eq("2D6")
|
||||||
Rollable::Dice.parse("-2d6").to_s.should eq("-2D6")
|
|
||||||
Rollable::Dice.parse("2").to_s.should eq("2")
|
Rollable::Dice.parse("2").to_s.should eq("2")
|
||||||
Rollable::Dice.new(1, Rollable::Die.new(2..2)).to_s.should eq("2")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "cmp" do
|
|
||||||
# same test in Die
|
|
||||||
(Rollable::Dice.parse("2d6") == Rollable::Dice.parse("2d6")).should eq true
|
|
||||||
(Rollable::Dice.parse("2d6") != Rollable::Dice.parse("2d8")).should eq true
|
|
||||||
(Rollable::Dice.parse("2d8") > Rollable::Dice.parse("2d6")).should eq true
|
|
||||||
(Rollable::Dice.parse("2d8") >= Rollable::Dice.parse("2d6")).should eq true
|
|
||||||
(Rollable::Dice.parse("2d4") < Rollable::Dice.parse("2d6")).should eq true
|
|
||||||
(Rollable::Dice.parse("2d4") <= Rollable::Dice.parse("2d6")).should eq true
|
|
||||||
(Rollable::Dice.parse("2d4") <=> Rollable::Dice.parse("2d6") < 0).should eq true
|
|
||||||
((Rollable::Dice.parse("2d6") <=> Rollable::Dice.parse("2d6")) == 0)
|
|
||||||
((Rollable::Dice.parse("3d6") <=> Rollable::Dice.parse("2d6")) > 0)
|
|
||||||
((Rollable::Dice.parse("1d6") <=> Rollable::Dice.parse("2d6")) < 0)
|
|
||||||
(Rollable::Dice.parse("2d6") <=> Rollable::Dice.parse("2d4") > 0).should eq true
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,6 @@ describe Rollable::Die do
|
||||||
Rollable::Die.new(1..20).should be_a(Rollable::Die)
|
Rollable::Die.new(1..20).should be_a(Rollable::Die)
|
||||||
Rollable::Die.new(10..20).should be_a(Rollable::Die)
|
Rollable::Die.new(10..20).should be_a(Rollable::Die)
|
||||||
Rollable::Die.new(20).should be_a(Rollable::Die)
|
Rollable::Die.new(20).should be_a(Rollable::Die)
|
||||||
Rollable::Die.new(20, true).should be_a(Rollable::Die)
|
|
||||||
expect_raises(Exception) { Rollable::Die.new(1001) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "min, max, average" do
|
it "min, max, average" do
|
||||||
|
@ -14,9 +12,6 @@ describe Rollable::Die do
|
||||||
Rollable::Die.new(1..20).min.should eq 1
|
Rollable::Die.new(1..20).min.should eq 1
|
||||||
Rollable::Die.new(1..20).max.should eq 20
|
Rollable::Die.new(1..20).max.should eq 20
|
||||||
Rollable::Die.new(1..20).average.should eq 10.5
|
Rollable::Die.new(1..20).average.should eq 10.5
|
||||||
Rollable::Die.new(1..20, true).min.should eq 1
|
|
||||||
Rollable::Die.new(1..20, true).max.should eq 80
|
|
||||||
Rollable::Die.new(1..20, true).average.should eq (10.5*0.05**0 + 10.5*0.05**1 + 10.5*0.05**2 + 10.5*0.05**3).round(3)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "test" do
|
it "test" do
|
||||||
|
@ -24,84 +19,19 @@ describe Rollable::Die do
|
||||||
min = rand 1..10
|
min = rand 1..10
|
||||||
max = rand min..20
|
max = rand min..20
|
||||||
((min..max).includes? Rollable::Die.new(min..max).test).should eq(true)
|
((min..max).includes? Rollable::Die.new(min..max).test).should eq(true)
|
||||||
((min..max*Rollable::Die::EXPLODING_ITERATIONS).includes? Rollable::Die.new(min..max, true).test).should eq(true)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "like?" do
|
|
||||||
Rollable::Die.new(1..20).like?(Rollable::Die.new(1..20)).should eq true
|
|
||||||
Rollable::Die.new(1..20).like?(Rollable::Die.new(-20..-1)).should eq true
|
|
||||||
Rollable::Die.new(1..20).like?(Rollable::Die.new(1..10)).should eq false
|
|
||||||
Rollable::Die.new(1..20, true).like?(Rollable::Die.new(1..20, false)).should eq true
|
|
||||||
end
|
|
||||||
|
|
||||||
it "negative?" do
|
|
||||||
Rollable::Die.new(1..20).negative?.should eq false
|
|
||||||
Rollable::Die.new(-1..20).negative?.should eq false
|
|
||||||
Rollable::Die.new(1..-20).negative?.should eq false
|
|
||||||
Rollable::Die.new(-1..-20).negative?.should eq true
|
|
||||||
Rollable::Die.new(1..20, true).negative?.should eq false
|
|
||||||
Rollable::Die.new(-1..-20, true).negative?.should eq true
|
|
||||||
end
|
|
||||||
|
|
||||||
it "fixed?" do
|
|
||||||
Rollable::Die.new(1..20).fixed?.should eq false
|
|
||||||
Rollable::Die.new(1..1).fixed?.should eq true
|
|
||||||
Rollable::Die.new(20..20).fixed?.should eq true
|
|
||||||
Rollable::Die.new(1).fixed?.should eq true
|
|
||||||
Rollable::Die.new(1..20, true).fixed?.should eq false
|
|
||||||
Rollable::Die.new(1, true).fixed?.should eq true
|
|
||||||
end
|
|
||||||
|
|
||||||
it "reverse" do
|
it "reverse" do
|
||||||
Rollable::Die.new(1..20).reverse.min.should eq -20
|
Rollable::Die.new(1..20).reverse.min.should eq -20
|
||||||
Rollable::Die.new(1..20).reverse.max.should eq -1
|
Rollable::Die.new(1..20).reverse.max.should eq -1
|
||||||
Rollable::Die.new(1..1).reverse.min.should eq -1
|
Rollable::Die.new(1..1).reverse.min.should eq -1
|
||||||
Rollable::Die.new(1..1).reverse.max.should eq -1
|
Rollable::Die.new(1..1).reverse.max.should eq -1
|
||||||
Rollable::Die.new(1..20, true).reverse.min.should eq -20
|
|
||||||
Rollable::Die.new(1..20, true).reverse.max.should eq -4
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "to_s" do
|
it "to_s" do
|
||||||
Rollable::Die.new(1..20).to_s.should eq "D20"
|
Rollable::Die.new(1..20).to_s.should eq "D20"
|
||||||
Rollable::Die.new(2..2).to_s.should eq "2"
|
Rollable::Die.new(2..2).to_s.should eq "2"
|
||||||
Rollable::Die.new(2..4).to_s.should eq "D(2,4)"
|
Rollable::Die.new(2..4).to_s.should eq "D(2,4)"
|
||||||
Rollable::Die.new(1..20, true).to_s.should eq "!D20"
|
|
||||||
Rollable::Die.new(2..4, true).to_s.should eq "!D(2,4)"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "cmp" do
|
|
||||||
(Rollable::Die.new(1..20) == Rollable::Die.new(1..20)).should eq true
|
|
||||||
(Rollable::Die.new(1..20) == Rollable::Die.new(1..10)).should eq false
|
|
||||||
(Rollable::Die.new(1..10) == Rollable::Die.new(1..20)).should eq false
|
|
||||||
|
|
||||||
(Rollable::Die.new(1..20) != Rollable::Die.new(1..20)).should eq false
|
|
||||||
(Rollable::Die.new(1..20) != Rollable::Die.new(1..10)).should eq true
|
|
||||||
(Rollable::Die.new(1..10) != Rollable::Die.new(1..20)).should eq true
|
|
||||||
|
|
||||||
(Rollable::Die.new(1..20) > Rollable::Die.new(1..20)).should eq false
|
|
||||||
(Rollable::Die.new(1..20) > Rollable::Die.new(1..10)).should eq true
|
|
||||||
(Rollable::Die.new(1..10) > Rollable::Die.new(1..20)).should eq false
|
|
||||||
|
|
||||||
(Rollable::Die.new(1..20) >= Rollable::Die.new(1..20)).should eq true
|
|
||||||
(Rollable::Die.new(1..20) >= Rollable::Die.new(1..10)).should eq true
|
|
||||||
(Rollable::Die.new(1..10) >= Rollable::Die.new(1..20)).should eq false
|
|
||||||
|
|
||||||
(Rollable::Die.new(1..20) < Rollable::Die.new(1..20)).should eq false
|
|
||||||
(Rollable::Die.new(1..20) < Rollable::Die.new(1..10)).should eq false
|
|
||||||
(Rollable::Die.new(1..10) < Rollable::Die.new(1..20)).should eq true
|
|
||||||
|
|
||||||
(Rollable::Die.new(1..20) <= Rollable::Die.new(1..20)).should eq true
|
|
||||||
(Rollable::Die.new(1..20) <= Rollable::Die.new(1..10)).should eq false
|
|
||||||
(Rollable::Die.new(1..10) <= Rollable::Die.new(1..20)).should eq true
|
|
||||||
|
|
||||||
# (Rollable::Die.new(2..6) <=> Rollable::Die.new(4..4) == 0).should eq true
|
|
||||||
((Rollable::Die.new(2..6) <=> Rollable::Die.new(4..4)) == 0).should eq false
|
|
||||||
((Rollable::Die.new(1..4) <=> Rollable::Die.new(1..4)) == 0).should eq true
|
|
||||||
((Rollable::Die.new(2..3) <=> Rollable::Die.new(1..4)) < 0).should eq true
|
|
||||||
(Rollable::Die.new(2..6) == Rollable::Die.new(4..4)).should eq false
|
|
||||||
|
|
||||||
# TODO: (Rollable::Die.new(1..20, true) == Rollable::Die.new(1..20, false)).should eq false
|
|
||||||
# TODO: (Rollable::Die.new(1..20, true) > Rollable::Die.new(1..20, false)).should eq true
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
describe Rollable::FixedValue do
|
describe Rollable::FixedValue do
|
||||||
it "new_die" do
|
it "new" do
|
||||||
10.times do |i|
|
10.times do |i|
|
||||||
n = i - 5
|
n = i - 5
|
||||||
f = Rollable::FixedValue.new_die n
|
f = Rollable::FixedValue.new n
|
||||||
f.should be_a Rollable::Die
|
f.should be_a Rollable::Die
|
||||||
f.min.should eq n
|
f.min.should eq n
|
||||||
f.max.should eq n
|
f.max.should eq n
|
||||||
|
@ -10,13 +10,4 @@ describe Rollable::FixedValue do
|
||||||
f.test.should eq n
|
f.test.should eq n
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "new_dice" do
|
|
||||||
a = Rollable::FixedValue.new_dice 5
|
|
||||||
b = Rollable::Dice.new(1, Rollable::FixedValue.new_die(5))
|
|
||||||
a.should eq b
|
|
||||||
a = Rollable::FixedValue.new_dice -5
|
|
||||||
b = Rollable::Dice.new(1, Rollable::FixedValue.new_die(-5))
|
|
||||||
a.should eq b
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,37 +2,23 @@ describe Rollable::Roll do
|
||||||
it "initialize" do
|
it "initialize" do
|
||||||
r = Rollable::Roll.new [
|
r = Rollable::Roll.new [
|
||||||
Rollable::Dice.new(1, 20),
|
Rollable::Dice.new(1, 20),
|
||||||
Rollable::FixedValue.new_dice(4),
|
Rollable::Dice.new(1, Rollable::FixedValue.new 4),
|
||||||
]
|
]
|
||||||
r.should be_a Rollable::Roll
|
r.should be_a Rollable::Roll
|
||||||
r.min.should eq 5
|
r.min.should eq 5
|
||||||
r.max.should eq 24
|
r.max.should eq 24
|
||||||
r.average.should eq 14.5
|
r.average.should eq 14.5
|
||||||
100.times do
|
10.times do
|
||||||
(5..24).includes?(r.test).should eq true
|
(5..24).includes?(r.test).should eq true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "initialize exploding" do
|
|
||||||
r = Rollable::Roll.new [
|
|
||||||
Rollable::Dice.new(1, 6, true),
|
|
||||||
]
|
|
||||||
r.should be_a Rollable::Roll
|
|
||||||
min = 1
|
|
||||||
max = 6*Rollable::Die::EXPLODING_ITERATIONS
|
|
||||||
r.min.should eq min
|
|
||||||
r.max.should eq max
|
|
||||||
100.times do
|
|
||||||
(min..max).includes?(r.test).should eq true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it "test (details)" do
|
it "test (details)" do
|
||||||
r = Rollable::Roll.new [Rollable::Dice.new(2, 6), Rollable::Dice.new(1, 4)]
|
r = Rollable::Roll.new [Rollable::Dice.new(2, 6), Rollable::Dice.new(1, 4)]
|
||||||
r.min_details.should eq([1, 1, 1])
|
r.min_details.should eq([1, 1, 1])
|
||||||
r.max_details.should eq([6, 6, 4])
|
r.max_details.should eq([6, 6, 4])
|
||||||
r.average_details.should eq([3.5, 3.5, 2.5])
|
r.average_details.should eq([3.5, 3.5, 2.5])
|
||||||
100.times do
|
10.times do
|
||||||
t = r.test_details
|
t = r.test_details
|
||||||
(1..6).includes?(t[0]).should eq true
|
(1..6).includes?(t[0]).should eq true
|
||||||
(1..6).includes?(t[1]).should eq true
|
(1..6).includes?(t[1]).should eq true
|
||||||
|
@ -54,12 +40,10 @@ describe Rollable::Roll do
|
||||||
r1.should be_a(Rollable::Roll)
|
r1.should be_a(Rollable::Roll)
|
||||||
r1.min.should eq 6
|
r1.min.should eq 6
|
||||||
r1.max.should eq 16
|
r1.max.should eq 16
|
||||||
(Rollable::Roll.parse("!2d6+4").average > Rollable::Roll.parse("2d6+4").average).should be_true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "parse (error)" do
|
it "parse (error)" do
|
||||||
expect_raises(Exception) { Rollable::Roll.parse("yolo") }
|
expect_raises { Rollable::Roll.parse("yolo") }
|
||||||
Rollable::Roll.parse("yolo") { |_| true } rescue fail("must be catch in block")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "parse (more)" do
|
it "parse (more)" do
|
||||||
|
@ -70,50 +54,6 @@ describe Rollable::Roll do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "to_s" do
|
it "to_s" do
|
||||||
Rollable::Roll.parse("1d6").to_s.should eq("1D6")
|
|
||||||
Rollable::Roll.parse("-1d6").to_s.should eq("-1D6")
|
|
||||||
Rollable::Roll.parse("4").to_s.should eq("4")
|
|
||||||
Rollable::Roll.parse("-4").to_s.should eq("-4")
|
|
||||||
Rollable::Roll.parse("-4 + 2D6").to_s.should eq("-4 + 2D6")
|
|
||||||
Rollable::Roll.parse(" 1d6 - 1 + 2 - 1d6 ").to_s.should eq("1D6 - 1 + 2 - 1D6")
|
Rollable::Roll.parse(" 1d6 - 1 + 2 - 1d6 ").to_s.should eq("1D6 - 1 + 2 - 1D6")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "cmp" do
|
|
||||||
r1 = Rollable::Roll.parse("2D6 + 1")
|
|
||||||
r2 = Rollable::Roll.parse("2D6 + 2")
|
|
||||||
r3 = Rollable::Roll.parse("2D8")
|
|
||||||
# same tests than Dice and Die
|
|
||||||
(r1 == r1).should eq true
|
|
||||||
(r1 == r2).should eq false
|
|
||||||
(r1 == r3).should eq false
|
|
||||||
|
|
||||||
(r2 == r3).should eq false
|
|
||||||
(r2 >= r3).should eq false
|
|
||||||
(r2 <= r3).should eq true
|
|
||||||
((r2 <=> r3) < 0).should eq true
|
|
||||||
end
|
|
||||||
|
|
||||||
it "compact" do
|
|
||||||
r = Rollable::Roll.parse("1D6 + 1D6")
|
|
||||||
r.to_s.should eq("1D6 + 1D6")
|
|
||||||
r.compact!
|
|
||||||
r.to_s.should eq("2D6")
|
|
||||||
Rollable::Roll.parse("1D6 + 1D6 + 1D4 + 2D6 + 1D4").compact!.to_s.should eq("4D6 + 2D4")
|
|
||||||
Rollable::Roll.parse("1 + 1").compact!.to_s.should eq("2")
|
|
||||||
Rollable::Roll.parse("1 + 1 + 2").compact!.to_s.should eq("4")
|
|
||||||
Rollable::Roll.parse("1 + 1d6").compact!.to_s.should eq("1D6 + 1")
|
|
||||||
Rollable::Roll.parse("1 + 2d6 + 3").compact!.to_s.should eq("2D6 + 4")
|
|
||||||
Rollable::Roll.parse("2d8 + 1d6 + 1d20 + 5 + 2d8 + 1 + 2 + 1d6 + 1 + 1d6").compact!.to_s.should eq "4D8 + 3D6 + 1D20 + 9"
|
|
||||||
Rollable::Roll.parse("2d8 + 1d6 + 1d20 + 5 + 2d8 + 1 + 2 - 1d6 - 1 + 1d6").compact!.to_s.should eq "4D8 + 1D6 + 1D20 + 7"
|
|
||||||
Rollable::Roll.parse("1D6 + 1D6 - 2D6").compact!.size.should eq 0
|
|
||||||
end
|
|
||||||
|
|
||||||
it "order" do
|
|
||||||
r = Rollable::Roll.parse("1D4 + 1D6")
|
|
||||||
r.to_s.should eq("1D4 + 1D6")
|
|
||||||
r.order.to_s.should eq("1D6 + 1D4")
|
|
||||||
r.to_s.should eq("1D4 + 1D6")
|
|
||||||
r.order!
|
|
||||||
r.to_s.should eq("1D6 + 1D4")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
require "./rollable/*"
|
||||||
|
|
||||||
module Rollable
|
module Rollable
|
||||||
class ParsingError < Exception
|
class ParsingError < Exception
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require "./rollable/*"
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
require "./roll"
|
|
||||||
|
|
||||||
module Rollable
|
|
||||||
D100 = Roll.parse "1d100"
|
|
||||||
D20 = Roll.parse "1d20"
|
|
||||||
D12 = Roll.parse "1d12"
|
|
||||||
D10 = Roll.parse "1d10"
|
|
||||||
D8 = Roll.parse "1d8"
|
|
||||||
D6 = Roll.parse "1d6"
|
|
||||||
D4 = Roll.parse "1d4"
|
|
||||||
D3 = Roll.parse "1d3"
|
|
||||||
end
|
|
|
@ -3,61 +3,34 @@ require "./is_rollable"
|
||||||
require "./die"
|
require "./die"
|
||||||
require "./fixed_value"
|
require "./fixed_value"
|
||||||
|
|
||||||
# Not a front class. It is used to represent a tuple of die type and die amount
|
module Rollable
|
||||||
#
|
# A `Dice` is a amount of `Die`.
|
||||||
# A `Dice` is a amount of `Die`.
|
# It is rollable exactly like a classic `Die`
|
||||||
# It is rollable exactly like a classic `Die`
|
#
|
||||||
#
|
# It is also possible to get the details of a roll, using the methods
|
||||||
# It is also possible to get the details of a roll, using the methods
|
# `.min_details`, `.max_details`, `.average_details`, `.test_details`
|
||||||
# `.min_details`, `.max_details`, `.average_details`, `.test_details`
|
#
|
||||||
#
|
# Example:
|
||||||
# Example:
|
# ```
|
||||||
# ```
|
# d = Dice.parse "2d6"
|
||||||
# d = Dice.parse "2d6"
|
# d.min # => 2
|
||||||
# d.min # => 2
|
# d.max # => 12
|
||||||
# d.max # => 12
|
# d.average # => 7
|
||||||
# d.average # => 7
|
# d.min_details # => [1, 1]
|
||||||
# d.min_details # => [1, 1]
|
# d.test # => the sum of 2 random values between 1..6
|
||||||
# d.test # => the sum of 2 random values between 1..6
|
# ```
|
||||||
# ```
|
class Dice < IsRollable
|
||||||
class Rollable::Dice < Rollable::IsRollable
|
|
||||||
MAX = 1000
|
|
||||||
@count : Int32
|
@count : Int32
|
||||||
@die : Die
|
@die : Rollable::Die
|
||||||
|
|
||||||
getter count, die
|
|
||||||
|
|
||||||
def initialize(@count, @die)
|
def initialize(@count, @die)
|
||||||
check_count!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Create a `Dice` with "die_type" faces.
|
# Create a `Dice` with "die_type" faces.
|
||||||
def initialize(@count, die_type : Int32, exploding : Bool = false)
|
def initialize(@count, die_type : Int32)
|
||||||
@die = Die.new(1..die_type, exploding)
|
@die = Rollable::Die.new(1..die_type)
|
||||||
check_count!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private def check_count!
|
|
||||||
raise ParsingError.new "Cannot more than #{MAX} dice (#{@count})" if @count > MAX
|
|
||||||
if @count < 0
|
|
||||||
@count = -@count
|
|
||||||
@die.reverse!
|
|
||||||
end
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def count=(count : Int32)
|
|
||||||
@count = count
|
|
||||||
check_count!
|
|
||||||
end
|
|
||||||
|
|
||||||
def clone
|
|
||||||
Dice.new(@count, @die.clone)
|
|
||||||
end
|
|
||||||
|
|
||||||
delegate "fixed?", to: die
|
|
||||||
delegate "negative?", to: die
|
|
||||||
|
|
||||||
# Reverse the `Die` of the `Dice`.
|
# Reverse the `Die` of the `Dice`.
|
||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
|
@ -65,12 +38,64 @@ class Rollable::Dice < Rollable::IsRollable
|
||||||
# Dice.parse("1d6").reverse # => -1d6
|
# Dice.parse("1d6").reverse # => -1d6
|
||||||
# ```
|
# ```
|
||||||
def reverse : Dice
|
def reverse : Dice
|
||||||
Dice.new -@count, @die
|
Dice.new @count, @die.reverse
|
||||||
end
|
end
|
||||||
|
|
||||||
def reverse!
|
# Returns the `Dice` and the string parsed from `str`, in a `NamedTuple`
|
||||||
@die.reverse!
|
# with "str" and "dice" keys.
|
||||||
self
|
#
|
||||||
|
# - If "strict" is true, then the string must end following the regex
|
||||||
|
# `\A\d+(d\d+)?\Z/i`
|
||||||
|
#
|
||||||
|
# - If "strict" is false, then the string doesn't have to finish following
|
||||||
|
# the regexp.
|
||||||
|
private def self.parse_string(str : String, strict = true) : NamedTuple(str: String, dice: Rollable::Dice)
|
||||||
|
match = str.match(/\A(\d+)(?:(?:d)(\d+))?#{strict ? "\\Z" : ""}/i)
|
||||||
|
raise ParsingError.new("Parsing Error: dice, near to '#{str}'") if match.nil?
|
||||||
|
count = match[1]
|
||||||
|
die = match[2]?
|
||||||
|
if die.nil?
|
||||||
|
return {str: match[0], dice: Rollable::Dice.new(1, Rollable::FixedValue.new(count.to_i))}
|
||||||
|
else
|
||||||
|
return {str: match[0], dice: Rollable::Dice.new(count.to_i, die.to_i)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return a valid string parsed from `str`. (see `#parse_string`)
|
||||||
|
#
|
||||||
|
# Yields the `Dice` parsed from `str`.
|
||||||
|
#
|
||||||
|
# Then, it returns the string read.
|
||||||
|
# If strict is false, only the valid string is returned.
|
||||||
|
def self.parse(str : String, strict = true) : String
|
||||||
|
data = parse_string(str, strict)
|
||||||
|
yield data[:dice]
|
||||||
|
return data[:str]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the `Dice` parsed. (see `#parse_string`)
|
||||||
|
def self.parse(str : String, strict = true) : Rollable::Dice
|
||||||
|
data = parse_string(str, strict)
|
||||||
|
return data[:dice]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the unconsumed string.
|
||||||
|
#
|
||||||
|
# Parse `str`, and yield a `Dice` parsed.
|
||||||
|
# It does not requires to be a full valid string
|
||||||
|
# (see #parse when strict is false).
|
||||||
|
# ```
|
||||||
|
# rest = Dice.consume("1d6+2") do |dice|
|
||||||
|
# # dice = Dice.new(1, Die.new(1..6))
|
||||||
|
# end
|
||||||
|
# # rest = "+2"
|
||||||
|
# ```
|
||||||
|
def self.consume(str : String) : String?
|
||||||
|
str = str.strip
|
||||||
|
consumed = parse(str, false) do |dice|
|
||||||
|
yield dice
|
||||||
|
end
|
||||||
|
return consumed.size >= str.size ? nil : str[consumed.size..-1]
|
||||||
end
|
end
|
||||||
|
|
||||||
{% for ft in ["min", "max"] %}
|
{% for ft in ["min", "max"] %}
|
||||||
|
@ -101,29 +126,16 @@ class Rollable::Dice < Rollable::IsRollable
|
||||||
@count.times.to_a.map { @die.average }
|
@count.times.to_a.map { @die.average }
|
||||||
end
|
end
|
||||||
|
|
||||||
def ==(right : Dice)
|
# Return a string which represents the `Dice`
|
||||||
@count == right.count && @die == right.die
|
#
|
||||||
end
|
# - If the value is fixed ```(n..n)```, then it return the @count * value
|
||||||
|
# - Else, it just add the count before the `Dice` like "{count}{dice.to_s}"
|
||||||
{% for op in [">", "<", ">=", "<="] %}
|
def to_s : String
|
||||||
def {{ op.id }}(right : Dice)
|
if @die.size == 1
|
||||||
average != right.average ?
|
(@count * @die.min).to_s
|
||||||
average {{ op.id }} right.average :
|
|
||||||
max != right.max ?
|
|
||||||
max {{ op.id }} right.max :
|
|
||||||
min {{ op.id }} right.min
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
def <=>(right : Dice) : Int32
|
|
||||||
if average != right.average
|
|
||||||
average - right.average > 0 ? 1 : -1
|
|
||||||
elsif max != right.max
|
|
||||||
max - right.max <=> 0
|
|
||||||
else
|
else
|
||||||
min - right.min <=> 0
|
"#{@count}#{@die.to_s}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require "./dice/*"
|
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
class Rollable::Dice
|
|
||||||
# Returns the `Dice` and the string parsed from `str`, in a `NamedTuple`
|
|
||||||
# with "str" and "dice" keys.
|
|
||||||
#
|
|
||||||
# - If "strict" is true, then the string must end following the regex
|
|
||||||
# `\A(!)?\d+(d\d+)?\Z/i`
|
|
||||||
#
|
|
||||||
# - If "strict" is false, then the string doesn't have to finish following
|
|
||||||
# the regexp.
|
|
||||||
private def self.parse_string(str : String, strict = true) : NamedTuple(str: String, dice: Dice)
|
|
||||||
match = str.match(/\A(?<sign>-|\+)? *(?<exploding>!)?(?<count>\d+)(?:(?:d)(?<die>\d+))?#{strict ? "\\Z" : ""}/i)
|
|
||||||
raise ParsingError.new("Parsing Error: dice, near to '#{str}'") if match.nil?
|
|
||||||
sign = (match["sign"]? || "+") == "+" ? 1 : -1
|
|
||||||
count = match["count"]
|
|
||||||
die = match["die"]?
|
|
||||||
exploding = match["exploding"]? ? true : false
|
|
||||||
if die.nil?
|
|
||||||
{str: match[0], dice: FixedValue.new_dice(sign * count.to_i)}
|
|
||||||
else
|
|
||||||
{str: match[0], dice: Dice.new(sign * count.to_i, die.to_i, exploding)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return a valid string parsed from `str`. (see `#parse_string`)
|
|
||||||
#
|
|
||||||
# Yields the `Dice` parsed from `str`.
|
|
||||||
# Then, it returns the string read.
|
|
||||||
# If strict is false, only the valid string is returned.
|
|
||||||
# ```
|
|
||||||
# Dice.parse("1d6") {|dice| dice.roll } => "1d6"
|
|
||||||
# ```
|
|
||||||
def self.parse(str : String, strict = true) : String
|
|
||||||
data = parse_string(str, strict)
|
|
||||||
yield data[:dice]
|
|
||||||
return data[:str]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the `Dice` parsed. (see `#parse_string`)
|
|
||||||
def self.parse(str : String, strict = true) : Dice
|
|
||||||
data = parse_string(str, strict)
|
|
||||||
return data[:dice]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the unconsumed string.
|
|
||||||
#
|
|
||||||
# Parse `str`, and yield a `Dice` parsed.
|
|
||||||
# It does not requires to be a full valid string
|
|
||||||
# (see #parse when strict is false).
|
|
||||||
# ```
|
|
||||||
# rest = Dice.consume("1d6+2") do |dice|
|
|
||||||
# # dice = Dice.new(1, Die.new(1..6))
|
|
||||||
# end
|
|
||||||
# # rest = "+2"
|
|
||||||
# ```
|
|
||||||
def self.consume(str : String) : String?
|
|
||||||
str = str.strip
|
|
||||||
consumed = parse(str, false) do |dice|
|
|
||||||
yield dice
|
|
||||||
end
|
|
||||||
return consumed.size >= str.size ? nil : str[consumed.size..-1]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return a string which represents the `Dice`
|
|
||||||
#
|
|
||||||
# - If the value is fixed ```(n..n)```, then it return the @count * value
|
|
||||||
# - Else, it just add the count before the `Dice` like "{count}{dice.to_s}"
|
|
||||||
def to_s : String
|
|
||||||
if fixed?
|
|
||||||
(negative? ? "-" : "") + (@count * @die.min).abs.to_s
|
|
||||||
else
|
|
||||||
(negative? ? "-" : "") + "#{@count}" + (negative? ? @die.reverse : @die).to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,62 +1,25 @@
|
||||||
require "./is_rollable"
|
require "./is_rollable"
|
||||||
|
|
||||||
# Not a front class. It is used to represent a type of dice with faces
|
module Rollable
|
||||||
#
|
# A `Die` is a range of Integer values.
|
||||||
# A `Die` is a range of Integer values.
|
# It is rollable.
|
||||||
# It is rollable.
|
#
|
||||||
#
|
# Example:
|
||||||
# Example:
|
# ```
|
||||||
# ```
|
# d = Die.new(1..6)
|
||||||
# d = Die.new(1..6)
|
# d.min # => 1
|
||||||
# d.min # => 1
|
# d.max # => 6
|
||||||
# d.max # => 6
|
# d.average # => 3.5
|
||||||
# d.average # => 3.5
|
# d.test # => a random value included in 1..6
|
||||||
# d.test # => a random value included in 1..6
|
# ```
|
||||||
# ```
|
class Die < IsRollable
|
||||||
# TODO: make it a Struct ?
|
|
||||||
class Rollable::Die < Rollable::IsRollable
|
|
||||||
MAX = 1000
|
|
||||||
EXPLODING_ITERATIONS = 4
|
|
||||||
|
|
||||||
@faces : Range(Int32, Int32)
|
@faces : Range(Int32, Int32)
|
||||||
getter exploding : Bool
|
|
||||||
|
|
||||||
getter faces
|
def initialize(@faces)
|
||||||
|
|
||||||
def initialize(@faces, @exploding = false)
|
|
||||||
raise ParsingError.new "Cannot die with more than #{MAX} faces (#{@faces})" if @faces.size > MAX
|
|
||||||
if @faces.end < @faces.begin
|
|
||||||
@faces = @faces.end..@faces.begin
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def clone
|
def initialize(nb_faces : Int32)
|
||||||
Die.new(@faces, @exploding)
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(nb_faces : Int32, @exploding = false)
|
|
||||||
raise ParsingError.new "Cannot die with more than #{MAX} faces (#{nb_faces})" if nb_faces > MAX
|
|
||||||
@faces = 1..nb_faces
|
@faces = 1..nb_faces
|
||||||
if @faces.end < @faces.begin
|
|
||||||
@faces = @faces.end..@faces.begin
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Number of faces of the `Die`
|
|
||||||
def size
|
|
||||||
@faces.size
|
|
||||||
end
|
|
||||||
|
|
||||||
def fixed?
|
|
||||||
size == 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def negative?
|
|
||||||
min < 0 && max < 0
|
|
||||||
end
|
|
||||||
|
|
||||||
def like?(other : Die)
|
|
||||||
@faces == other.faces || @faces == other.reverse.faces
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reverse the values
|
# Reverse the values
|
||||||
|
@ -66,56 +29,32 @@ class Rollable::Die < Rollable::IsRollable
|
||||||
# Die.new(1..6).reverse # => Die.new -6..-1
|
# Die.new(1..6).reverse # => Die.new -6..-1
|
||||||
# ```
|
# ```
|
||||||
def reverse : Die
|
def reverse : Die
|
||||||
Die.new -@faces.end..-@faces.begin, @exploding
|
Die.new -max..-min
|
||||||
end
|
|
||||||
|
|
||||||
def reverse!
|
|
||||||
@faces = -@faces.end..-@faces.begin
|
|
||||||
self
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def max : Int32
|
def max : Int32
|
||||||
if @exploding
|
|
||||||
@faces.end * EXPLODING_ITERATIONS
|
|
||||||
else
|
|
||||||
@faces.end
|
@faces.end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def min : Int32
|
def min : Int32
|
||||||
@faces.begin
|
@faces.begin
|
||||||
end
|
end
|
||||||
|
|
||||||
private def explode(&block)
|
|
||||||
EXPLODING_ITERATIONS.times do |_|
|
|
||||||
value = @faces.to_a.sample
|
|
||||||
yield value
|
|
||||||
break if value != @faces.end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return a random value in the range of the dice
|
# Return a random value in the range of the dice
|
||||||
def test : Int32
|
def test : Int32
|
||||||
if @exploding
|
|
||||||
sum = 0
|
|
||||||
explode { |value| sum += value }
|
|
||||||
sum
|
|
||||||
else
|
|
||||||
@faces.to_a.sample
|
@faces.to_a.sample
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# Mathematical expectation.
|
# Mathematical expectation.
|
||||||
#
|
#
|
||||||
# A d6 will have a expected value of 3.5
|
# A d6 will have a expected value of 3.5
|
||||||
def average : Float64
|
def average : Float64
|
||||||
proba = @faces.size.to_f64
|
@faces.reduce { |r, l| r + l }.to_f64 / @faces.size
|
||||||
non_exploding_average = @faces.reduce { |r, l| r + l }.to_f64 / proba
|
|
||||||
if @exploding
|
|
||||||
EXPLODING_ITERATIONS.times.reduce(0.0) {|base, i| base + non_exploding_average / proba ** i }.round(3)
|
|
||||||
else
|
|
||||||
non_exploding_average
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Number of faces of the `Die`
|
||||||
|
def size
|
||||||
|
@faces.size
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return a string.
|
# Return a string.
|
||||||
|
@ -123,40 +62,13 @@ class Rollable::Die < Rollable::IsRollable
|
||||||
# - It may be a dice ```(1..n) => "D#{n}"```
|
# - It may be a dice ```(1..n) => "D#{n}"```
|
||||||
# - Else, ```(a..b) => "D(#{a},#{b})"```
|
# - Else, ```(a..b) => "D(#{a},#{b})"```
|
||||||
def to_s : String
|
def to_s : String
|
||||||
string = if self.size == 1
|
if self.size == 1
|
||||||
min.to_s
|
min.to_s
|
||||||
elsif self.min == 1
|
elsif self.min == 1
|
||||||
"D#{@faces.end}"
|
"D#{self.max}"
|
||||||
else
|
else
|
||||||
"D(#{@faces.begin},#{@faces.end})"
|
"D(#{self.min},#{self.max})"
|
||||||
end
|
end
|
||||||
string = "!#{string}" if @exploding
|
|
||||||
return string
|
|
||||||
end
|
|
||||||
|
|
||||||
def ==(right : Die)
|
|
||||||
@faces == right.faces && @exploding == right.exploding
|
|
||||||
end
|
|
||||||
|
|
||||||
{% for op in [">", "<", ">=", "<="] %}
|
|
||||||
def {{ op.id }}(right : Die)
|
|
||||||
return false if @exploding != right.exploding
|
|
||||||
average != right.average ?
|
|
||||||
average {{ op.id }} right.average :
|
|
||||||
max != right.max ?
|
|
||||||
max {{ op.id }} right.max :
|
|
||||||
min {{ op.id }} right.min
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
def <=>(right : Die) : Int32
|
|
||||||
if average != right.average
|
|
||||||
average - right.average > 0 ? 1 : -1
|
|
||||||
elsif max != right.max
|
|
||||||
max - right.max <=> 0
|
|
||||||
else
|
|
||||||
min - right.min <=> 0
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
#
|
|
||||||
|
|
|
@ -1,22 +1,18 @@
|
||||||
require "./die"
|
require "./die"
|
||||||
|
|
||||||
# Allows to create a die with a fixed value.
|
module Rollable
|
||||||
# The die will only gives this value everytime.
|
# Allow to create a die with a fixed value.
|
||||||
# (`.min`, `.max`, `.test`, `.average`)
|
# The die will only gives this value everytime.
|
||||||
#
|
# (`.min`, `.max`, `.test`, `.average`)
|
||||||
# This is equivalent to
|
#
|
||||||
# ```
|
# This is equivalent to
|
||||||
# Die.new(n..n) # => FixedValue.new_die n
|
# ```
|
||||||
# Dice.new(1, Die.new(n..n)) # => FixedValue.new_dice n
|
# Dice.new(n..n) # => FixedValue.new n
|
||||||
# ```
|
# ```
|
||||||
module Rollable::FixedValue
|
module FixedValue
|
||||||
# Return a `Die` with only one face.
|
# Return a `Die` with only one face.
|
||||||
def self.new_die(value : Int32)
|
def self.new(value : Int32)
|
||||||
Die.new value..value
|
Rollable::Die.new(value..value)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return a `Dice` with only one face.
|
|
||||||
def self.new_dice(fixed : Int32)
|
|
||||||
Dice.new 1, FixedValue.new_die(fixed)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
abstract class Rollable::IsRollable
|
module Rollable
|
||||||
|
abstract class IsRollable
|
||||||
abstract def min : Int32
|
abstract def min : Int32
|
||||||
abstract def max : Int32
|
abstract def max : Int32
|
||||||
abstract def average : Float64
|
abstract def average : Int32
|
||||||
abstract def test : Int32
|
abstract def test : Int32
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,34 +3,27 @@ require "./die"
|
||||||
require "./fixed_value"
|
require "./fixed_value"
|
||||||
require "./dice"
|
require "./dice"
|
||||||
|
|
||||||
# `Roll` is a list of `Dice`.
|
module Rollable
|
||||||
#
|
# `Roll` is a list of `Dice`.
|
||||||
# It is rollable, making the sum of each `Dice` values.
|
#
|
||||||
# It is also possible to get the details of a roll, using the methods
|
# It is rollable, making the sum of each `Dice` values.
|
||||||
# `.min_details`, `.max_details`, `.average_details`, `.test_details`
|
# It is also possible to get the details of a roll, using the methods
|
||||||
#
|
# `.min_details`, `.max_details`, `.average_details`, `.test_details`
|
||||||
# Example:
|
#
|
||||||
# ```
|
# Example:
|
||||||
# r = Rollable.parse "1d6+2" # note: it also support "1d6 + 2"
|
# ```
|
||||||
# r.min # => 3
|
# r = Rollable.parse "1d6+2" # note: it also support "1d6 + 2"
|
||||||
# r.max # => 9
|
# r.min # => 3
|
||||||
# r.average # => 5.5
|
# r.max # => 9
|
||||||
# r.test # => the sum of a random value in 1..6 and 2
|
# r.average # => 5.5
|
||||||
# ```
|
# r.test # => the sum of a random value in 1..6 and 2
|
||||||
class Rollable::Roll < Rollable::IsRollable
|
# ```
|
||||||
|
class Roll < IsRollable
|
||||||
@dice : Array(Dice)
|
@dice : Array(Dice)
|
||||||
|
|
||||||
getter dice
|
|
||||||
|
|
||||||
def initialize(@dice)
|
def initialize(@dice)
|
||||||
end
|
end
|
||||||
|
|
||||||
def clone
|
|
||||||
Roll.new(@dice.clone)
|
|
||||||
end
|
|
||||||
|
|
||||||
delegate size, to: @dice
|
|
||||||
|
|
||||||
# Reverse the values of the `Roll`.
|
# Reverse the values of the `Roll`.
|
||||||
def reverse! : Roll
|
def reverse! : Roll
|
||||||
@dice.each { |die| die.reverse! }
|
@dice.each { |die| die.reverse! }
|
||||||
|
@ -47,6 +40,39 @@ class Rollable::Roll < Rollable::IsRollable
|
||||||
Roll.new @dice.map { |die| die.reverse }
|
Roll.new @dice.map { |die| die.reverse }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Parse the string and return an array of `Dice`
|
||||||
|
#
|
||||||
|
# see `Dice.consume`
|
||||||
|
#
|
||||||
|
# The string passed as parameter is consumed, part by part, to create an
|
||||||
|
# Array of `Dice`. The string must follow grammar below (case insensitive):
|
||||||
|
# ```text
|
||||||
|
# - dice = [\d+][d][\d+]
|
||||||
|
# - sign = ['+', '-']
|
||||||
|
# - sdice = [sign]?[dice]
|
||||||
|
# - roll = [sign][dice][sdice]*
|
||||||
|
# ```
|
||||||
|
def self.parse_str(str : String?, list : Array(Dice) = Array(Dice).new) : Array(Dice)
|
||||||
|
return list if str.nil?
|
||||||
|
str = str.strip
|
||||||
|
sign = str[0]
|
||||||
|
if sign != '+' && sign != '-' && !list.empty?
|
||||||
|
raise ParsingError.new("Parsing Error: roll, near to '#{str}'")
|
||||||
|
end
|
||||||
|
str = str[1..-1] if sign == '-' || sign == '+'
|
||||||
|
rest = Dice.consume(str) do |dice|
|
||||||
|
list << (sign == '-' ? dice.reverse : dice)
|
||||||
|
end
|
||||||
|
parse_str(rest, list)
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parse the string "str" and returns a new `Roll` object
|
||||||
|
# see `#parse_str`
|
||||||
|
def self.parse(str : String) : Roll
|
||||||
|
return Roll.new(parse_str(str))
|
||||||
|
end
|
||||||
|
|
||||||
{% for ft in ["min", "max", "test"] %}
|
{% for ft in ["min", "max", "test"] %}
|
||||||
def {{ ft.id }} : Int32
|
def {{ ft.id }} : Int32
|
||||||
@dice.reduce(0) { |r, l| r + l.{{ ft.id }} }
|
@dice.reduce(0) { |r, l| r + l.{{ ft.id }} }
|
||||||
|
@ -65,92 +91,18 @@ class Rollable::Roll < Rollable::IsRollable
|
||||||
@dice.map { |dice| dice.average_details }.flatten
|
@dice.map { |dice| dice.average_details }.flatten
|
||||||
end
|
end
|
||||||
|
|
||||||
def ==(right : Roll)
|
def to_s : String
|
||||||
@dice.size == right.dice.size && @dice.map_with_index { |e, i| right.dice[i] == e }.all? { |e| e == true }
|
@dice.reduce(nil) do |l, r|
|
||||||
end
|
if l
|
||||||
|
if r.min < 0 && r.max < 0
|
||||||
{% for op in [">", "<", ">=", "<="] %}
|
l + " - " + r.reverse.to_s
|
||||||
def {{ op.id }}(right : Roll)
|
|
||||||
average != right.average ?
|
|
||||||
average {{ op.id }} right.average :
|
|
||||||
max != right.max ?
|
|
||||||
max {{ op.id }} right.max :
|
|
||||||
min {{ op.id }} right.min
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
def <=>(right : Roll) : Int32
|
|
||||||
if average != right.average
|
|
||||||
average - right.average > 0 ? 1 : -1
|
|
||||||
elsif max != right.max
|
|
||||||
max - right.max <=> 0
|
|
||||||
else
|
else
|
||||||
min - right.min <=> 0
|
l + " + " + r.to_s
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def order!
|
|
||||||
@dice.sort! { |a, b| b <=> a }
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def order
|
|
||||||
clone.order!
|
|
||||||
end
|
|
||||||
|
|
||||||
# let a [1d6, 1d4, 1d6, 2, 2d6]
|
|
||||||
# first, we copy it
|
|
||||||
# for 1d6 we check evey d6 in the copy, fetch and delete them
|
|
||||||
def compact!
|
|
||||||
i = 0
|
|
||||||
until i >= @dice.size
|
|
||||||
# fetch the current dice
|
|
||||||
dice_current = @dice[i]
|
|
||||||
dice_type = dice_current.die
|
|
||||||
dice_count = dice_current.count
|
|
||||||
# fetch all dice with the same type
|
|
||||||
j = @dice.size
|
|
||||||
until i >= j - 1
|
|
||||||
j = j - 1
|
|
||||||
if @dice[j].die == dice_type
|
|
||||||
@dice[i].count += @dice[j].count
|
|
||||||
elsif @dice[j].die == dice_type.reverse
|
|
||||||
@dice[i].count -= @dice[j].count
|
|
||||||
else
|
else
|
||||||
next
|
r.to_s
|
||||||
end
|
end
|
||||||
@dice.delete_at j
|
end.to_s
|
||||||
end
|
end
|
||||||
i = i + 1
|
|
||||||
end
|
|
||||||
compact_fixed!
|
|
||||||
compact_empty!
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
private def compact_fixed!
|
|
||||||
fixed = @dice.map_with_index { |d, idx| {d, idx} }
|
|
||||||
fixed.select! { |t| t[0].die.fixed? }
|
|
||||||
idx = 0
|
|
||||||
fixed_dice = fixed.map do |t|
|
|
||||||
@dice.delete_at(t[1] - idx)
|
|
||||||
idx = idx + 1
|
|
||||||
t[0].max
|
|
||||||
end.sum
|
|
||||||
@dice << FixedValue.new_dice(fixed_dice) if fixed_dice != 0
|
|
||||||
end
|
|
||||||
|
|
||||||
private def compact_empty!
|
|
||||||
i = @dice.size - 1
|
|
||||||
until i < 0
|
|
||||||
@dice.delete_at(i) if @dice[i].count == 0
|
|
||||||
i = i - 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def compact
|
|
||||||
clone.compact!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require "./roll/*"
|
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
class Rollable::Roll
|
|
||||||
# Parse the string and return an array of `Dice`
|
|
||||||
#
|
|
||||||
# see `Dice.consume`
|
|
||||||
#
|
|
||||||
# The string passed as parameter is consumed, part by part, to create an
|
|
||||||
# Array of `Dice`. The string must follow grammar below (case insensitive):
|
|
||||||
# ```text
|
|
||||||
# - dice = [\d+][d][\d+]
|
|
||||||
# - sign = ['+', '-']
|
|
||||||
# - sdice = [sign]?[dice]
|
|
||||||
# - roll = [sign][dice][sdice]*
|
|
||||||
# ```
|
|
||||||
private def self.parse_str(str : String?, list : Array(Dice) = Array(Dice).new) : Array(Dice)
|
|
||||||
return list if str.nil?
|
|
||||||
str = str.strip
|
|
||||||
sign = str[0]
|
|
||||||
if sign != '+' && sign != '-' && sign != '!' && !list.empty?
|
|
||||||
raise ParsingError.new("Parsing Error: roll, near to '#{str}'")
|
|
||||||
end
|
|
||||||
str = str[1..-1] if sign == '-' || sign == '+'
|
|
||||||
rest = Dice.consume(str) do |dice|
|
|
||||||
list << (sign == '-' ? dice.reverse : dice)
|
|
||||||
end
|
|
||||||
parse_str(rest, list)
|
|
||||||
return list
|
|
||||||
end
|
|
||||||
|
|
||||||
# Parse the string "str" and returns a new `Roll` object
|
|
||||||
#
|
|
||||||
# see `#parse_str`
|
|
||||||
def self.parse(str : String) : Roll
|
|
||||||
return Roll.new(parse_str(str))
|
|
||||||
end
|
|
||||||
|
|
||||||
# Parse the string "str" and returns a new `Roll` object,
|
|
||||||
# and execute the "block" if an error occured
|
|
||||||
def self.parse(str : String) : Roll?
|
|
||||||
begin
|
|
||||||
return self.parse(str)
|
|
||||||
rescue err
|
|
||||||
yield err
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s : String
|
|
||||||
@dice.reduce(nil) do |l, r|
|
|
||||||
# puts "l:#{l.to_s}, r:#{r.to_s}"
|
|
||||||
if l
|
|
||||||
if r.negative?
|
|
||||||
l + " - " + r.reverse.to_s
|
|
||||||
else
|
|
||||||
l + " + " + r.to_s
|
|
||||||
end
|
|
||||||
else
|
|
||||||
r.to_s
|
|
||||||
end
|
|
||||||
end.to_s
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,3 +1,3 @@
|
||||||
module Rollable
|
module Rollable
|
||||||
VERSION = "0.1.4"
|
VERSION = "0.1.0"
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user