1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.
2. Support Ruby Quiz by submitting ideas as often as you can:
3. Enjoy!
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
by Matthew D Moss
Time to release your inner nerd.
The task for this Ruby Quiz is to write a dice roller. You should write a
program that takes two arguments: a dice expression followed by the number of
times to roll it (being optional, with a default of 1). So to calculate those
stats for your AD&D character, you would do this:
> roll.rb "3d6" 6
72 64 113 33 78 82
Or, for something more complicated:
> roll.rb "(5d5-4)d(16/d4)+3"
31
[NOTE: You'll usually want quotes around the dice expression to hide parenthesis
from the shell, but the quotes are not part of the expression.]
The main code of roll.rb should look something like this:
d = Dice.new(ARGV[0])
(ARGV[1] || 1).to_i.times { print "#{d.roll} " }
The meat of this quiz is going to be parsing the dice expression (i.e.,
implementing Dice.new). Let's first go over the grammar, which I present in a
simplified BNF notation with some notes:
<expr> := <expr> + <expr>
| <expr> - <expr>
| <expr> * <expr>
| <expr> / <expr>
| ( <expr> )
| [<expr>] d <expr>
| integer
* Integers are positive; never zero, never negative.
* The "d" (dice) expression XdY rolls a Y-sided die (numbered
from 1 to Y) X times, accumulating the results. X is optional
and defaults to 1.
* All binary operators are left-associative.
* Operator precedence:
( ) highest
d
* /
+ - lowest
[NOTE: The BNF above is simplified here for clarity and space. If requested, I
will make available the full BNF description I've used in my own solution, which
incorporates the association and precedence rules.]
A few more things... Feel free to either craft this by hand or an available
lexing/parsing library. Handling whitespace between integers and operators is
nice. Some game systems use d100 quite often, and may abbreviate it as "d%"
(but note that '%' is only allowed immediately after a 'd').
I would appreciate the full BNF, please.
--Greg
I assume integer arithmetic? So if, for example, a 3 comes up on your
d4, 16/d4 would be 5?
Jacob Fugal
~ ryan ~
On Jan 6, 2006, at 1:56 PM, Ruby Quiz wrote:
> Time to release your inner nerd.
>
> The task for this Ruby Quiz is to write a dice roller...
I suspect user error.
The correct answer will always be between 3 and 18 for 3d6.
-austin
--
Austin Ziegler * halos...@gmail.com
* Alternate: aus...@halostatue.ca
Hmm, that example looks wrong now that you mention it. It should be
6 numbers between 3 and 18 (the roll of 3 six-sided dice).
James Edward Gray II
If you wanted to extend the syntax to support round-up division (using '\'
perhaps) or other options, feel free. Extra credit.
A lot of extra credit if you add syntax to support some RPGs/home rules
where you might want 3d6, but you'll actually roll 4d6 and toss the lowest.
That should look like this:
> roll.rb "3d6" 6
18 18 18 18 18 18
=)
Actually 3d6 means roll a 6 sided die 3 times so you would have a result of 3-18
so this:
> roll.rb "3d6" 6
Would actully be: (RND = Random)
RND(3-18) RND(3-18) RND(3-18) RND(3-18) RND(3-18) RND(3-18)
Below is 3d6 from the DnD Dice Roller on Wizards.com. The +0 would be
a modifier from depending if it was an attack roll or a defense roll.
For our purposes you would remove the +0
Roll(3d6)+0:
1,6,6,+0
Total:13
DnD Dice Roller:
http://www.wizards.com/default.asp?x=dnd/dnd/20040517a
Will
--
Will Shattuck ( willshattuck.at.gmail.com )
Home Page: http://www.thewholeclan.com/will
When you get to your wit's end, you'll find God lives there.
Just don't tell me that the first one is 18/00.
> On 06/01/06, Matthew Moss <matthew.m...@gmail.com> wrote:
>> Ha ha... Must have copied the wrong line when writing up the quiz
>> description.
>>
>> That should look like this:
>>
>>> roll.rb "3d6" 6
>> 18 18 18 18 18 18
>
> Just don't tell me that the first one is 18/00.
<dies laughing> They all were, of course.
James Edward Gray II
Actually, you're right, but actually my post was a half-joke. The
munchkin players seem to roll 18's every time. ;)
Don't worry about explaining it as I just needed to know what that
command, roll.rb "3d6" 6, did.
~ ryan ~
On Jan 6, 2006, at 2:39 PM, Matthew Moss wrote:
>> roll.rb "3d6" 6
> 18 18 18 18 18 18
On Jan 6, 2006, at 2:49 PM, Austin Ziegler wrote:
> Just don't tell me that the first one is 18/00.
On Jan 6, 2006, at 2:53 PM, James Edward Gray II wrote:
> <dies laughing> They all were, of course.
> I guess that must be a D&D inside half-joke because I'm totally
> confused.
18 was the best stat a starting character could have. If you got
one, they let you roll d% and put it after the slash (the higher the
better). 00 == 100. So characters with 18/00 had some damn lucky
die rolls. :)
James Edward Gray II
In most versions of D&D/AD&D, this was also limited to Strength attributes only.
This may have changed recently. ;)
> On 06/01/06, James Edward Gray II <ja...@grayproductions.net> wrote:
>> On Jan 6, 2006, at 2:21 PM, J. Ryan Sobol wrote:
>>> I guess that must be a D&D inside half-joke because I'm totally
>>> confused.
>> 18 was the best stat a starting character could have. If you got
>> one, they let you roll d% and put it after the slash (the higher the
>> better). 00 == 100. So characters with 18/00 had some damn lucky
>> die rolls. :)
>
> In most versions of D&D/AD&D, this was also limited to Strength
> attributes only.
>
> This may have changed recently. ;)
Ah, yeah, you're right. It's been too long.
Actually, I believe 3rd Edition and up did away with the extra
percentile roll altogether.
James Edward Gray II
Okay, this is what I've done in my current version that takes care of basic
precedence and associativity.
INTEGER = /[1-9][0-9]*/
expr: fact
| expr '+' fact
| expr '-' fact
fact: term
| fact '*' term
| fact '/' term
term: unit
| [term] 'd' dice
dice: '%'
| unit
unit: '(' expr ')'
| INTEGER
Actually, this is slightly different than my current version, which after
reexamining to extract this BNF, I found a minor error (in handling of the
term rules and handling of the optional arg). My own code has a morphed
version of this BNF in order to code up a recursive descent parser, but this
BNF shows one way to handle the precedence/association rules.
http://www.garshol.priv.no/download/text/bnf.html
http://en.wikipedia.org/wiki/Backus-Naur_form
HTH,
Bill
>
> Time to release your inner nerd.
>
> The task for this Ruby Quiz is to write a dice roller.
Well, I'm no D&Der, but I think I'm gonna hand in my solution for this one
as my first Ruby Quiz entry :)
Cheers,
--
Ross Bamford - ro...@roscopeco.remove.co.uk
rand(16)+3 rand(16)+3 rand(16)+3
> Or, for something more complicated:
>
> > roll.rb "(5d5-4)d(16/d4)+3"
> 31
What is the -4 and the /d4 do?
Does the +3 apply to (5d5-4)d(16/d4) or to (16/d4) only, assuming it
matters
since I don't know what this stuff does.
>
> A few more things... Feel free to either craft this by hand or an
> available
> lexing/parsing library. Handling whitespace between integers and
> operators is
> nice. Some game systems use d100 quite often, and may abbreviate
> it as "d%"
> (but note that '%' is only allowed immediately after a 'd').
So d100 == d% == d00
and
100 == 00
correct?
Okay, that output is bogus. However, it is not rand(16) at all. It's:
(1..3).inject(0) { |sum, ii| sum + (rand(6) + 1) }
The fact that it is three 6-sided dice rolled is important (and is
perhaps more important in a PRNG) because the weighting is a little
different. With rand(16) + 3 you're just as likely to get 3 as you are
18. With three rand(6) + 1 values, you're going to get much closer to a
bell curve than a straight probability line. This is a good thing,
because in D&D, 10 is described as absolutely average and 12 is the
high-end for most people. Adventurers, of course, can go to 18, but even
16 is good. Gandalf would be an 18 INT; Sam might be an 11 INT (INT ==
"intelligence").
>> Or, for something more complicated:
>>> roll.rb "(5d5-4)d(16/d4)+3"
>> 31
> What is the -4 and the /d4 do?
(5d5-4) => Roll a 5-sided dice 5 times and take the sum, subtract 4.
=> Result will be between 1 and 21.
(16 / d4) => Roll a 4-sided dice and divide 16 by the result.
=> Result will be 4, 5, 8, or 16.
d => Roll a [4, 5, 8, or 16]-sided dice 1-21 times and total.
=> The total result will be between 1 and 336.
+3 => Add three to the result.
=> The final result will be between 4 and 339.
> Does the +3 apply to (5d5-4)d(16/d4) or to (16/d4) only, assuming it
> matters since I don't know what this stuff does.
d binds tighter than addition.
>> A few more things... Feel free to either craft this by hand or an
>> available lexing/parsing library. Handling whitespace between
>> integers and operators is nice. Some game systems use d100 quite
>> often, and may abbreviate it as "d%" (but note that '%' is only
>> allowed immediately after a 'd').
> So d100 == d% == d00
Yes.
> and
> 100 == 00
No. d00/d%/d100 all refer to values from 1 to 100. It should be
considered impossible to get a value of 0 from dice. Strictly speaking,
d100 should be a special case simulated where you are rolling two d10
values and treating one of them as the 10s and one of them as the 1s.
Again, it results in a slightly different curve than a pure d100 result
would be. One gaming system developed by Gary Gygax after he was ousted
from TSR in the mid-80s used what he termed d10x, which was d10*d10,
resulting in values from 1 - 100 with a radically different probability
curve than a normal d100.
The "natural" dice created are:
d4, d6, d8, d10, d12, d20
Novelty dice created in the past include:
d30, d100
The latter is quite unwieldy.
Strictly speaking, it is not possible to make a die (polyhedron) with an
odd number of faces, but d5 can be simulated by doing a rounded d10/2 or
d20/4.
>The three rules of Ruby Quiz:
>
>1. Please do not post any solutions or spoiler discussion for this quiz until
>48 hours have passed from the time on this message.
>
>2. Support Ruby Quiz by submitting ideas as often as you can:
>
>http://www.rubyquiz.com/
>
>3. Enjoy!
>
>
Huhu.
How do you parse 5d6d7?
As (5d6)d7 or 5d(6d7) since there is no "Assoziativgesetz" like (AdB)dC
== Ad(BdC).
-
aTdHvAaNnKcSe
All binary operators are left associative, so 5d6d7 is (5d6)d7.
Austin Ziegler wrote:
> No. d00/d%/d100 all refer to values from 1 to 100. It should be
> considered impossible to get a value of 0 from dice. Strictly speaking,
> d100 should be a special case simulated where you are rolling two d10
> values and treating one of them as the 10s and one of them as the 1s.
> Again, it results in a slightly different curve than a pure d100 result
> would be.
How exactly would those d10s differ from a d100?
< One gaming system developed by Gary Gygax after he was ousted
> from TSR in the mid-80s used what he termed d10x, which was d10*d10,
> resulting in values from 1 - 100 with a radically different probability
> curve than a normal d100.
Not only a different curve, but also some values would be impossible to
get (as 13 and 51)
*Sascha
--
Posted via http://www.ruby-forum.com/.
> < One gaming system developed by Gary Gygax after he was ousted
>> from TSR in the mid-80s used what he termed d10x, which was d10*d10,
>> resulting in values from 1 - 100 with a radically different
>> probability
>> curve than a normal d100.
>
> Not only a different curve, but also some values would be
> impossible to
> get (as 13 and 51)
Na, if you get a 1 on the tens die and a 3 on the ones die, you have
rolled a 13.
James Edward Gray II
In the same way that 3d6 is different than rand(16)+3. It's not
necessarily as dramatic a difference, but IME, the incidences of the
very lows (01-19) and very highs (81-00) are not as common as those in
the middle.
>> One gaming system developed by Gary Gygax after he was ousted from
>> TSR in the mid-80s used what he termed d10x, which was d10*d10,
>> resulting in values from 1 - 100 with a radically different
>> probability curve than a normal d100.
> Not only a different curve, but also some values would be impossible
> to get (as 13 and 51)
Yes.
I misread. Sorry.
James Edward Gray II
What's the execution order in this case?
Do 5d5-4 rolls with 5d5-4 probably different dices having 16/d4 sides
(number of sides calculated for each roll individually) or should one
choose the number of sides once for all rolls?
I guess it doesn't make much difference but it should be specified...
Morus
That's for d%; I was referring to "Cyborg Commando" which had a d10x,
which is (d10)*(d10), making a 1,3 combination 3 always. You'd never get
a prime number larger than 7 under the d10x system.
combo = Hash.new(0)
1.upto(10) { |i|
1.upto(10) { |j|
combo[i * j] += 1
}
}
There are 42 possible values here, and 9 values (6, 8, 10, 12, 18, 20,
24, 30, 40) appear four times each. Four values (4, 9, 16, 36) appear
three times each, 23 values twice each, and 6 values once.
It was a truly fucked up system. I think it's because he was mad to be
ousted.
[great explanation snipped]
> No. d00/d%/d100 all refer to values from 1 to 100. It should be
> considered impossible to get a value of 0 from dice. Strictly
> speaking,
> d100 should be a special case simulated where you are rolling two d10
> values and treating one of them as the 10s and one of them as the 1s.
> Again, it results in a slightly different curve than a pure d100
> result
> would be. One gaming system developed by Gary Gygax after he was
> ousted
> from TSR in the mid-80s used what he termed d10x, which was d10*d10,
> resulting in values from 1 - 100 with a radically different
> probability
> curve than a normal d100.
If the 10's dice is 3 and the 1's dice is 1, you get 31.
What do you need to roll to get a 0 and 100?
I could see this working if the dice were 0..9 and you add one to the
final result,
but you said that dice should be 1..x. So do you subtract one from each
digit, then add one to the final result?
Example:
10's 1's
1 1 => (1-1)(1-1) => (00)+1 => 1
4 1 => (4-1)(1-1) => (30)+1 => 31
10 10 => (10-1)(10-1) => (99)+1 => 100
Jim
> What do you need to roll to get a 0 and 100?
A zero on the tens dice is 10. On the one's dice, it's zero. 00 is
100.
James Edward Gray II
> On Jan 6, 2006, at 5:35 PM, Jim Freeze wrote:
>
>> What do you need to roll to get a 0 and 100?
>
> A zero on the tens dice is 10. On the one's dice, it's zero. 00
> is 100.
>
That doesn't jive with what was said earlier. There should be no zero
on the tens dice. Only 1..10.
Jim
Clarification: presented in short, long and practical. :)
Short clarification:
Actually, when rolled together, both dice are zero-based. The
double-nought is the only special combination of 00 -> 100. When
rolled singly, a d10 has 0 -> 10. Rolling a 0 is never possible.
Long clarification:
Normally, a d% is rolled as a combination of a "d100" and a d10.
"d100" is in quotes, because it's actually just a special d10 -- 10
sided die, that is -- except the numbers on the "d100" are 00, 10,
20... 90. The numbers on the d10 are 0, 1, 2... 9. Rolling the two
together and adding you have a range from 0..99. However, since the
tables that require a d% roll are normally 1-based (1..100), the
'double-nought' -- a 00 on the "d100" and 0 on the d10 -- is
considered 100, everything else is face value. Some examples:
00 / 5 -> 5
10 / 5 -> 15
20 / 0 -> 20
00 / 0 -> 100
Similarly, when asked to roll a d10, the face numbers are 0..9, but
are interpreted as 1..10 by making the 0 a 10 and leaving the other
faces at face value.
All other dice (in my experience) are always interpreted as face value
(the sides being 1-based).
Regarding the probability curve of a d% versus a true d100 (100-sided
die), they are the same. Consider the d100: there are 100 faces, each
with a 1% probability. With a d% roll ("d100" + d10), each integer
between 1 and 100 (again, double-nought counting as 100) is produced
exactly once, and with the same probability. 53 (produced only by 50 +
3) is no more likely than 99 (90 + 9) or 1 (00 + 1). So for all
intents and purposes, a d% is equivalent to a d100.
Practical clarification:
As mentioned above, rolling two ten-sided dice versus rolling a
100-sided dice produce the same distribution (given the method of
combination). Rolling a ten-sided zero-based die then converting 0 to
10 versus rolling a ten-sided one-based die produce the same
distribution. So if you see dM then rand(M) + 1 will produce the
correct distribution. d% counts as d100.
Now if you'll excuse me, I'm late for an appointment with *my* dice.
Your die-rolling lesson for the day was brought to you by the numbers
3, 5 and the letter D. :)
Jacob Fugal
You may want to review the precedence order again.
* All binary operators are left-associative.
* Operator precedence:
( ) highest
d
* /
+ - lowest
> On 1/6/06, James Edward Gray II <ja...@grayproductions.net> wrote:
>> On Jan 6, 2006, at 5:35 PM, Jim Freeze wrote:
>>
>>> What do you need to roll to get a 0 and 100?
>>
>> A zero on the tens dice is 10. On the one's dice, it's zero. 00 is
>> 100.
>
> Clarification: presented in short, long and practical. :)
>
> Short clarification:
>
> Actually, when rolled together, both dice are zero-based. The
> double-nought is the only special combination of 00 -> 100. When
> rolled singly, a d10 has 0 -> 10. Rolling a 0 is never possible.
No wonder I don't play D&D. I don't think I am smart enough.
What does 0 -> 10 mean. Does it mean a dice can have the
values 0,1,2,3...10?
If so, why is a 0 never possible?
And why does d10 have 0 -> 10 while a d6 has 1 -> 6?
Jim
Parse tree for: (5d5-4)d(16/d4)+3
_______________ + _______________
| |
_______ d _______ 3
| |
___ - ___ ___ / ___
| | | |
___ d ___ 4 16 ___ d ___
5 5 1 4
d10 has size 0..9, generally because of size (the print on them is
typically only large enough to have one decimal digit). 0 is rarely a
useful number in gaming, so it is treated as a 10 result. Therefore
rand(10) + 1 is sufficient to represent d10. When used as d100, you'll
get the values 00 .. 99, but again, 00 is not a useful value so it is
treated as 100. So rand(100) + 1 is sufficient to represent d100.
> On 1/6/06, James Edward Gray II <ja...@grayproductions.net> wrote:
>> On Jan 6, 2006, at 5:35 PM, Jim Freeze wrote:
>>
>>> What do you need to roll to get a 0 and 100?
>>
>> A zero on the tens dice is 10. On the one's dice, it's zero. 00 is
>> 100.
>
> Clarification: presented in short, long and practical. :)
>
> Short clarification:
>
> Actually, when rolled together, both dice are zero-based. The
> double-nought is the only special combination of 00 -> 100. When
> rolled singly, a d10 has 0 -> 10. Rolling a 0 is never possible.
Egad, I have been extra dumb today, haven't I? I'm very sorry to
keep leading everyone astray. Jacob has it right here, not me.
James Edward Gray II
>
> On Jan 6, 2006, at 6:17 PM, Jacob Fugal wrote:
>
>> On 1/6/06, James Edward Gray II <ja...@grayproductions.net> wrote:
>>> On Jan 6, 2006, at 5:35 PM, Jim Freeze wrote:
>>>
>>>> What do you need to roll to get a 0 and 100?
>>>
>>> A zero on the tens dice is 10. On the one's dice, it's zero. 00 is
>>> 100.
>>
>> Clarification: presented in short, long and practical. :)
>>
>> Short clarification:
>>
>> Actually, when rolled together, both dice are zero-based. The
>> double-nought is the only special combination of 00 -> 100. When
>> rolled singly, a d10 has 0 -> 10. Rolling a 0 is never possible.
>
> No wonder I don't play D&D. I don't think I am smart enough.
It's really my fault. I keep leading you astray.
> What does 0 -> 10 mean. Does it mean a dice can have the
> values 0,1,2,3...10?
On a ten sided die are printed the numbers 0, 1, 2, 3, 4, 5, 6, 7, 8,
9. I have no idea why it's zero based. In most rolls for most games
though, 0 is considered 10.
1d10 will be a number between 1 and 10 for this quiz.
When two tens are rolled together for d100, the first is taken as the
tens digit (0-9) and the second as the ones digit (0-9). The special
case is that 00 is considered 100.
Honestly though, I won't think less of you if you generate a random
number between 1 and 100. :)
James Edward Gray II
Not in that case ! Very simple : you have 100 possible values, ranging
from 1 to 100 ... each value correspond to a single dice configuration
(it you rool 2 and 5 you get 25 and you have no other way to get 25).
Thus the probability of each value is 1/100 ... and all values are
equiprobable !
And of course you can generalize the result ^^
You want a d1000 ? take 3 d10
You want a d36 ? take 2 d6 and calculate : 6*(d6-1) + d6
You want a d144 ? take 2 d12 : 12*(d12-1) + d12
IMO, all the parenthesis must be resolved before going further.
Thus in the example you rool :
5d5
1d4
and the last one with the result of the two computations.
Otherwise, it would be impraticle to do that with real dices (mmmmhh ...)
Pierre
Cya in 40 hours or so... (and hope I'm really computing non-fubar'd
results... :)
Regards,
Bill
For the purposes of this quiz, I propose that dice are 1-based. Which
means a d6 has six sides numbered 1, 2, 3, 4, 5 and 6 (ie, 1->6). A
d10 should be 1->10, not 0->10.
There is a bunch of discussion above talking about d10 variants, but
for simplicity, a N-sided die generates values from 1 to N inclusive.
That is, rand(N)+1.
[snip involved dice discussion]
> Honestly though, I won't think less of you if you generate a random
> number between 1 and 100. :)
>
Phew :D
--
Ross Bamford - ro...@roscopeco.remove.co.uk
~ ryan ~
Just to make sure I have the precedence and so on right, I used a loaded
dice that always rolls it's number of sides to write some tests. Since
there's been some discussion over the precedence rules, I'll post them to
maybe compare with others and see if I'm on the right track. Hope that's
within the rules? I've left out broken input ones, since at the moment
mine just 'does it's best' but I might tighten that up yet...
@asserts = {
'1' => 1,
'1+2' => 3,
'1+3*4' => 13,
'1*2+4/8-1' => 1,
'd1' => 1,
'1d1' => 1,
'd10' => 10,
'1d10' => 10,
'10d10' => 100,
'd3*2' => 6,
'5d6d7' => 210, # left assoc
'2d3+8' => 14, # not 22
'(2d(3+8))' => 22, # not 14
'd3+d3' => 6,
'd2*2d4' => 16,
'd(2*2)+d4' => 8,
'd%' => 100,
'2d%' => 200,
'14+3*10d2' => 74,
'(5d5-4)d(16/d4)+3' => 87, #25d4 + 3
}
Cheers.
> Thank you all for pitching in your explanation of dice rollers to
> non-D&D players like my self. However, there's a considerable
> amount of noise for this post (already) and I'm not 100% confident
> I could parse the dice syntax in English let alone ruby. Would it
> be possible to summarize this discussion and post it as an addendum
> at http://www.rubyquiz.com/quiz61.html ?
Feel free to summarize here, but I generally don't add the discussion
to the quizzes themselves. I like to keep them pretty basic an we
can always go to the archives as needed, I figure.
I did correct the error on the site though. :)
James Edward Gray II
> Not in that case ! Very simple : you have 100 possible values, ranging
> from 1 to 100 ... each value correspond to a single dice configuration
> (it you rool 2 and 5 you get 25 and you have no other way to get 25).
> Thus the probability of each value is 1/100 ... and all values are
> equiprobable !
Wimps! REAL gamers roll 100 sided dice (aka Zoccihedron)
-- Matt
Nothing great was ever accomplished without _passion_
> I would appreciate the full BNF, please.
Okay, this is what I've done in my current version that takes care of
basic precedence and associativity.
INTEGER = /[1-9][0-9]*/
expr: fact
| expr '+' fact
| expr '-' fact
fact: term
| fact '*' term
| fact '/' term
term: unit
| [term] 'd' dice
dice: '%'
| unit
unit: '(' expr ')'
| INTEGER
Actually, this is slightly different than my current version, which
after reexamining to extract this BNF, I found a minor error (in
handling of the term rules and handling of the optional arg). My own
code has a morphed version of this BNF in order to code up a recursive
descent parser, but this BNF shows one way to handle the
precedence/association rules.
So 1+2-3 == (1+2)-3 because + and - have the same precedence.
But 1+2*3 == 1+(2*3) because * has precedence over +.
"3d6" means: (rand(6)+1) + (rand(6)+1) + (rand(6)+1)
(The +1 are because rand is zero-based, but dice are one-based.)
- is subtraction, so -4 means subtract 4.
/ is division, so /d4 means roll a d4 and divide that into the
expression left of the /
d% == d100
d00 is not valid; there is no such thing as a zero-sided die (although
if you want to make 00 an extension to imply d100 in your own
implementation, that's fine).
This dice roller is, for the most part, a simple integer calculator
with addition, subtraction, multiplication, division, and grouping via
parentheses. In order to turn it into a dice calculator, we add the
'd' (dice) binary operator. The required right argument to 'd' is the
number of sides on the die, while the option left argument (defaulting
to 1) is how many to roll and sum.
So 16 / d4 means "roll one 4-sided die and divide the result into 16".
> >
> Huhu.
> How do you parse 5d6d7?
> As (5d6)d7 or 5d(6d7) since there is no "Assoziativgesetz" like (AdB)dC
> == Ad(BdC).
> -
> aTdHvAaNnKcSe
With the game systems I know, and I admit I haven't played for a couple
of years, 5d6d7 would not be a legal expression, and would raise an
exception.
/Henrik
--
http://www.henrikmartensson.org/ - Reflections on software development
So what are the maximum and minimum values of this?
--
Christian Neukirchen <chneuk...@gmail.com> http://chneukirchen.org
> Hi,
>
> Just to make sure I have the precedence and so on right, I used a
> loaded dice that always rolls it's number of sides to write some
> tests. Since there's been some discussion over the precedence rules,
> I'll post them to maybe compare with others and see if I'm on the
> right track. Hope that's within the rules? I've left out broken input
> ones, since at the moment mine just 'does it's best' but I might
> tighten that up yet...
>
> @asserts = {
..
> '(5d5-4)d(16/d4)+3' => 87, #25d4 + 3
> }
This is wrong, the maximum is 339: (25-4)d(16/1)+3.
> Ross Bamford - ro...@roscopeco.remove.co.uk
I don't understand that. I get:
(5d5-4)d(16/d4)+3 = 87
(5d5-4)d(16/d1)+3 = 339
I read the first as 21 rolls (25 - 4) of a four sided (16 / 4) dice plus
3, while the second is 21 rolls (25 - 4) of a 16 sided (16 / 1) dice, plus
3.
Right?
(5d5-4) is 25 at max
(16/d4) is 16 at max
25d16+3 is 339 at max
qed
Yeah. I got your list wrong then, I thought the number means the
maximum reachable, not what to throw with loaded dice. Sorry.
Ignore me for lifetime please.
> "Ross Bamford" <ro...@roscopeco.remove.co.uk> writes:
>
>> On Sat, 07 Jan 2006 12:27:05 -0000, Christian Neukirchen
>> <chneuk...@gmail.com> wrote:
>>
>>> "Ross Bamford" <ro...@roscopeco.remove.co.uk> writes:
>>>
>>>> Hi,
>>>>
>>>> Just to make sure I have the precedence and so on right, I used a
>>>> loaded dice that always rolls it's number of sides to write some
>>>> tests. Since there's been some discussion over the precedence rules,
>>>> I'll post them to maybe compare with others and see if I'm on the
>>>> right track. Hope that's within the rules? I've left out broken input
>>>> ones, since at the moment mine just 'does it's best' but I might
>>>> tighten that up yet...
>>>>
>>>> @asserts = {
>>> ..
>>>> '(5d5-4)d(16/d4)+3' => 87, #25d4 + 3
>>>> }
>>>
>>> This is wrong, the maximum is 339: (25-4)d(16/1)+3.
>>>
>>
>> I don't understand that. I get:
>>
>> (5d5-4)d(16/d4)+3 = 87
>> (5d5-4)d(16/d1)+3 = 339
>>
>> I read the first as 21 rolls (25 - 4) of a four sided (16 / 4) dice
>> plus 3, while the second is 21 rolls (25 - 4) of a 16 sided (16 / 1)
>> dice, plus 3.
>>
>> Right?
>
> Yeah. I got your list wrong then, I thought the number means the
> maximum reachable, not what to throw with loaded dice. Sorry.
>
Oh, I see. I guess it's a standard thing to do to find the maximum? As I
say I'm a rank amateur when it comes to dice so I apologise if I've gone
against the normal way to do things.
I just wanted to be able to predict the result of the expressions, so I
could calculate the expected result to test the operator precedence rules.
With the loaded dice, 5d5 is effectively a (higher-precedence) 5*5. Hope
it doesn't cause confusion.
> "Matthew D Moss" <matthew.m...@gmail.com> writes:
>
> > Hopefully this ASCII art comes through clean:
> >
> > Parse tree for: (5d5-4)d(16/d4)+3
> >
> > _______________ + _______________
> > | |
> > _______ d _______ 3
> > | |
> > ___ - ___ ___ / ___
> > | | | |
> > ___ d ___ 4 16 ___ d ___
> > 5 5 1 4
>
> So what are the maximum and minimum values of this?
Notation: [x,y] is any distribution with minimum x and maximum y. I then
get:
(5d5-4)d(16/d4)+3 = (adb = [a,a*b], so 5d5 = [5,25])
([5,25]-4)d(16/d4)+3 = ([x,y] - c = [x-c,y-c])
([1,21])d(16/d4)+3 = (adb = [a,a*b], so d4 = 1d4 = [1,4])
([1,21])d(16/[1,4])+3 = (c/[x,y] = [c/y,c/x] if x and y > 0)
([1,21])d([4,16])+3 = ([a,b]d[c,d] = [a*c,b*d] if a,b,c, and d > 0)
[4,336]+3 = ([x,y] + c = [x+c,y+c])
[7,339]
Reinder
Min: (5*1-4)*(1)+3 = 1*1+3 = 5
Max: (5*5+4)*(16/4)+3 = 29*4+3 = 119
The min for ndm is always n, because all dice start at 1.
The max for ndm is n*m.
Wow. That's some brilliant math.
1+3, of course, equals 4. No matter how many 1's are multiplied to
result in 1. :p
Man, *and* I flipped the sign on that -4. *shakes head sadly*
One more shot.
Min: (5*1-4)*(1)+3 = 1*1+3 = 4
Max: (5*5-4)*(16/4)+3 = 21*4+3 = 87
I'm agree with you on that top line, but I'm not sure how you got to
the second line from there.
I think you meant:
([1,21])d([1,4])+3 = ...
which results in:
[1,84]+3
[4,87]
For max, should be (16/1) not (16/4), no?
d(16/[1,4]) must be [16/4,16/1] => [4,16]
[1,21] d [4,16] must be [1*4, 16*21] => [4, 336]
I would like to introduce distribution. Using one normal dice we have an
even distribution
distr("d6") = [0,1,1,1,1,1,1] Sum=6
Using two normal dices we have the following distribution
distr("2d6")=[0,0,1,2,3,4,5,6,5,4,3,2,1] Sum=36 (min:max) =
(2:12)
P("2d6",12) = 1/36 = 2.8%
I have a question regarding the distribution for "(d2)d6".
In words, I'm first throwing a coin, to decide how many times I will
throw a the dice.
[1,2] d [1,2,3,4,5,6]
My guess:
distr("d6") = [0,1,1,1,1,1,1] Sum=6
distr("2d6") = [0,0,1,2,3,4,5,6,5,4,3,2,1] Sum=36
Probability merge
distr("d6") [0,6,6,6,6, 6, 6] Sum=36
distr("2d6") [0,0,1,2,3, 4, 5,6,5,4,3,2,1] Sum=36
distr("(d2)d6") [0,6,7,8,9,10,11,6,5,4,3,2,1] Sum=72
The probability of having one point is P("(d2)d6",1) = 6/72 = 8.3%
Can somebody agree or disagree on this?
Christer
--
Posted via http://www.ruby-forum.com/.
You're all too damned smart. :)
Brilliant!
> Gavin Kistner <ga...@refinery.com> writes:
>
>> On Jan 7, 2006, at 9:55 AM, Gavin Kistner wrote:
>>> On Jan 7, 2006, at 5:03 AM, Christian Neukirchen wrote:
>>>> "Matthew D Moss" <matthew.m...@gmail.com> writes:
>>>>> Parse tree for: (5d5-4)d(16/d4)+3
>>>>
>>>> So what are the maximum and minimum values of this?
>>>
>>> Max: (5*5+4)*(16/4)+3 = 29*4+3 = 119
>>
>> Man, *and* I flipped the sign on that -4. *shakes head sadly*
>>
>> One more shot.
>>
>> Min: (5*1-4)*(1)+3 = 1*1+3 = 4
>> Max: (5*5-4)*(16/4)+3 = 21*4+3 = 87
>
> I claim 4 and 339.
Both (4, 87) and (4, 339) are correct depending on your definition of
'max'. You get the former if you assume maximum rolls for each die,
and the latter if you want the maximum possible result of the
expression.
In any case, the extremal results aren't particularly useful from a
testing point of view, as different distributions can have the same
extremal points, and you could potentially erroneously pass an
incorrect case. For example: 2d2d6
left-associative:
(2d2)d6 -> 4d6 -> 24 max
right-associative (oops!):
2d(2d6) -> 2d12 -> 24 max
But the distributions are different in each case (the latter result
could include 2 and 3, for instance).
matthew smillie.
Ahhh.... this must be a game system you don't know. =)
Anyway, I just wanted to add a couple of notes. It was made aware to
me that the simplified BNF (in the original post) is slightly in
error, in that it allows expressions like this:
3dddd6
which is invalid.
A couple possible fixes:
1. Use the expanded BNF I posted, which doesn't have this fault.
2. Implement your dice parser using right-associativity for 'd'
operators (but maintain left-assoc for the other binary operators).
Feel free to do what you like.
Off the top of my head, that seems right.
Uh, of course you can make such a polyhedron. Consider the
Egyptian and Mayan pyramids as examples of 5-sided polyhedron
(four triangles on the sides and a square on the bottom).
Adjusting the steepness of the sides can make it as fair
or unfair as you'd want.
Sure, they're not regular polyhedra, but neither is the d30 you spoke of.
Well, why do you say it's invalid ? Given the simplified BNF it must be
read as :
3d(d(d(d6)))
and it is perfectly valid as there is no other way to understand that ...
Pierre
That is only true if the d operator is right-associative. According to the
original spec, it is left-associative and, therefore, 3dddd6 is a syntax
error. Mind you, the second option Matthew gave is to make it
right-associative, which is what you have done. I chose to treat it as a
syntax error.
} Pierre
--Greg
Well, I disagree ...
As I see the things, there are two "d" operators :
- one left-associative infix operator (i.e. 3d6)
- one prefix operator (i.e. d6)
The first "d" in your example is the infix one, thus left-associative,
while the other ones are prefix ... and there is nothing to tell about
association as they get only one argument ...
If I'm not clear enough, I hope it will be when we will be allowed to
disclose our solutions ;)
Pierre
> } Pierre
> --Greg
>
>
>
I finally finished a Ruby Quiz! Albeit by means of a goofy
method_missing hack. <grin> But it was fun.
Here 'tis:
------------------------------------------------------------------
#!/usr/bin/env ruby
expr = ARGV[0] || abort('Please specify expression, such as "(5d5-4)d(16/d4)+3"')
expr = expr.dup # unfreeze
class Object
def method_missing(name, *args)
# Intercept dieroll-method calls, like :_5d5, and compute
# their value:
if name.to_s =~ /^_(\d*)d(\d+)$/
rolls = [1, $1.to_i].max
nsides = $2.to_i
(1..rolls).inject(0) {|sum,n| sum + (rand(nsides) + 1)}
else
raise NameError, [name, *args].inspect
end
end
end
class String
def die_to_meth
# Prepend underscore to die specs, like (5d5-4) -> (_5d5-4)
# making them grist for our method_missing mill:
self.gsub(/\b([0-9]*d[0-9]*)\b/, '_\1')
end
end
expr.gsub!(/d%/,"d100") # d% support
# inner->outer reduce
true while expr.gsub!(/\(([^()]*)\)/) {eval($1.die_to_meth)}
p eval(expr.die_to_meth)
> Conveniently, ** has the desired
> precedence relative to the other operators, plus it is binary and
> left-associative. This feels so evil. Seduced by the Dark Side I am.
>
I used the same approach, but found that ** is right-associative (as
it's generally defined outside of Ruby). To confirm the
associativity for yourself, try this: 2**3**4. If it's left
associative, it should equal 8**4 (4096), right-associativity gives
2**81 (a lot). I ended up doing a lot more redefining and mucking
about:
Dice Ruby
d *
* +
/ -
+ <<
- >>
Interestingly, the difference between a left-associating and a right-
associating 'd' operator isn't particularly visible from the 'loaded-
dice' testing common on the list. For example, 2d2d6 gives a maximum
of 24 whichever associativity is used, but the distributions of the
two solutions are vastly different; the left-associative result has a
minimum value of 4, the right-associative result has a minimum of 2.
Here's my solution, which maintains correct associativity for 'd'
according to the initial quiz, but does a lot more mucking about with
Fixnum:
matthew smillie.
#!/usr/local/bin/ruby
class Fixnum
alias old_mult *
alias old_div /
alias old_plus +
alias old_minus -
def >>(arg) old_minus(arg) end
def <<(arg) old_plus(arg) end
def -(arg) old_div(arg) end
def +(arg) old_mult(arg) end
def *(arg)
sum = 0
self.times do
sum = sum.old_plus(rand(arg).old_plus(1))
end
sum
end
end
class Dice
def initialize(str)
# make assumed '1's explicit - do it twice to cover cases
# like '3ddd6' which would otherwise miss one match.
@dice = str.gsub(/([+\-*\/d])(d)/) { |s| "#{$1}1#{$2}" }
@dice = @dice.gsub(/([+\-*\/d])(d)/) { |s| "#{$1}1#{$2}" }
# sub all the operators.
@dice = @dice.gsub(/\+/, "<<")
@dice = @dice.gsub(/-/, ">>")
@dice = @dice.gsub(/\*/, "+")
@dice = @dice.gsub(/\//, "-")
@dice = @dice.gsub(/d/, "*")
end
def roll
eval(@dice)
end
end
d = Dice.new(ARGV[0])
(ARGV[1] || 1).to_i.times { print "#{d.roll} " }
----
Matthew Smillie <M.B.S...@sms.ed.ac.uk>
Institute for Communicating and Collaborative Systems
University of Edinburgh
My submission isn't going to win points for brevity - at 600+ lines it's
maybe a bit long to post here.
It's got a few extras in there, though:
$ ./dice.rb "(5d5-4)d(16/d4)+3"
45
$ ./dice.rb "3d6" 6
11 7 10 13 9 14
$ ./dice.rb -dist "2d5 + 1dd12"
Distribution:
3 0.0103440355940356
4 0.0276987734487735
5 0.0503975468975469
6 0.0773292448292448
7 0.107660533910534
8 0.120036676286676
9 0.120568783068783
10 0.112113997113997
11 0.096477873977874
12 0.07495670995671
13 0.0588945406445407
14 0.0457661135161135
15 0.0345793650793651
16 0.0250565175565176
17 0.0171049783549784
18 0.0107247474747475
19 0.00596632996632997
20 0.00290909090909091
21 0.00113636363636364
22 0.000277777777777778
Check total: 1.0
Mean 9.75 std. dev 3.37782803325187
$ ./dice.rb -cheat "2d5 + 1dd12" 19
19 : D5=2 D5=5 D12=12 D12=12 p=0.000277777777777778
$ ./dice.rb -cheat "2d5 + 1dd12" 25
Cannot get 25
I've shoved it on http://homepage.ntlworld.com/a.mcguinness/files/dice.rb
--
Andrew McGuinness
http://anomalyuk.blogspot.com/
This is my quiz entry for Ruby Quiz 61 (Dice Roller). It's actually the
second idea I had, after starting out with Antlr (I still finished that
one, because I wanted to get to grips with Antlr anyway - I happened to
be playing with it when this quiz came out :)). I've bundled both this
entry and that one at:
http://roscopeco.co.uk/code/ruby-quiz-entries/quiz61-dice-roller.tar.gz
Anyway, back to my real entry. I guess I took the short-cut route to
the dice-roller, and instead of parsing out the expressions I instead
decided to 'coerce' them to Ruby code, by just implementing the 'd'
operator with a 'rolls' method on Fixnum, and using gsub to convert
the input expression.
d3*2 => 1.rolls(3)*2
(5d5-4)d(16/d4)+3 => (5.rolls(5)-4).rolls(16/1.rolls(4))+3
d%*7 => 1.rolls(100)*7
This is implemented in the DiceRoller.parse method, which returns the
string. You can just 'eval' this of course, or use the 'roll' method
(also provided as a more convenient class method that wraps the whole
thing up for you) to do it. Ruby runs the expression, and gives back
the result. I almost feel like I cheated...?
As well as the main 'roll.rb' I also included a separate utility that
uses loaded dice to find min/max achievable. All three files can be
executed, and if you enable --verbose mode on Ruby you'll see the
dice rolls and parsed expressions.
----------[MAIN (roll.rb)]-----------
#!/usr/local/bin/ruby
#
# Ruby Quiz 61, the quick way
# by Ross Bamford
# Just a debugging helper
module Kernel
def dbg(*s)
puts(*s) if $VERBOSE|| @dice_debug
end
attr_writer :dice_debug
def dice_debug?; @dice_debug; end
end
# Need to implement the 'rolls' method. Wish it didn't have to
# be on Fixnum but for this it makes the parsing *lots* easier.
class Fixnum
def self.roll_proc=(blk)
@roll_proc = blk
end
def self.roll_proc
@roll_proc ||= method(:rand).to_proc
end
def rolls(sides)
(1..self).inject(0) { |s,v| s + Fixnum.roll_proc[sides] }
end
end
# Here's the roller.
class DiceRoller
class << self
# Completely wrap up a roll
def roll(expr, count = 1, debug = false)
new(expr,debug).roll(count)
end
# The main 'parse' method. Just really coerces the code to Ruby
# and then compiles to a block that returns the result.
def parse(expr)
# very general check here. Will pass lots of invalid syntax,
# but hopefully that won't compile later. This removes the
# possibility of using variables and the like, but that wasn't
# required anyway. The regexps would be a bit more difficult
# if we wanted to do that.
raise SyntaxError, "'#{expr}' is not a valid dice expression", [] if
expr =~ /[^d\d\(\)\+\-\*\/\%]|[^d]%|d-|\*\*/
# Rubify!
s = expr.gsub( /([^\d\)])d|^d/, '\11d') # fix e.g. 'd5'
and '33+d3' to '1.d5' and '33+1d3'
s.gsub!( /d%/, 'd(100)' ) # fix e.g. 'd%'
to 'd(100)'
s.gsub!( /d([\+\-]?\d+)/, '.rolls(\1)') # fix e.g. '3d8'
to '3.rolls(8) (*)
s.gsub!( /d\(/, '.rolls(') # fix e.g.
'2d(5+5)' to '2.rolls(5+5)'
# (*) This line treats + or - straight after 'd' as a unary sign,
# so you can have '3d-8*7' => '3.rolls(+8)-7'
# This would throw a runtime error from rolls, though.
# Make a block. Doing it this way gets Ruby to compile it now
# so we'll reliably get fail fast on bad syntax.
dbg "PARS: #{expr} => #{s}"
begin
eval("lambda { #{s} }")
rescue Exception => ex
raise SyntaxError, "#{expr} is not a valid dice expression", []
end
end
end
# Create a new roller that rolls the specified dice expression
def initialize(expr, debug = false)
dbg "NEW : #{to_s}: #{expr} => #{expr_code}"
@expr_code, @expr, @debug = expr, DiceRoller.parse(expr), debug
end
# Get hold of the original expression and compiled block, respectively
attr_reader :expr_code, :expr
# Roll this roller count times
def roll(count = 1)
dbg " ROLL: #{to_s}: #{count} times"
r = (1..count).inject([]) do |totals,v|
this_r = begin
expr.call
rescue Exception => ex
raise RuntimeError, "'#{expr_code}' raised: #{ex}", []
end
dbg " r#{v}: rolled #{this_r}"
totals << this_r
end
r.length < 2 ? r[0] : r
end
end
# Library usage:
#
# require 'roll'
#
# # is the default:
# # Fixnum.roll_proc = lambda { |sides| rand(sides) + 1 }
#
# DiceRoller.roll('1+2*d6')
#
# d = DiceRoller.new('((3d%)+8*(d(5*5)))')
# d.roll(5)
#
# d = DiceRoller.new('45*10d3') # debug
#
# # ... or
# one_roll = d.expr.call
#
# command-line usage
if $0 == __FILE__
unless expr = ARGV[0]
puts "Usage: ruby [--verbose] roll.rb expr [count]"
else
(ARGV[1] || 1).to_i.times { print "#{DiceRoller.roll(expr)} " }
print "\n"
end
end
=====================================
-----------[UTIL: minmax.rb]----------
#!/usr/local/bin/ruby
require 'roll'
LOW_DICE = lambda { |sides| 1 }
HIGH_DICE = lambda { |sides| sides }
# Adds a 'minmax' method that uses loaded dice to find
# min/max achievable for a given expression.
#
# Obviously not thread safe, but then neither is the
# whole thing ;D
class DiceRoller
def self.minmax(expr)
old_proc = Fixnum.roll_proc
Fixnum.roll_proc = LOW_DICE
low = DiceRoller.roll(expr)
Fixnum.roll_proc = HIGH_DICE
high = DiceRoller.roll(expr)
Fixnum.roll_proc = old_proc
[low,high]
end
end
if $0 == __FILE__
if expr = ARGV[0]
min, max = DiceRoller.minmax(expr)
puts "Expression: #{expr} ; min / max = #{min} / #{max}"
else
puts "Usage: minmax.rb <expr>"
end
end
=====================================
-----------[TEST: test.rb]----------
#!/usr/local/bin/ruby
#
# Ruby Quiz, number 61 - Dice roller
# This entry by Ross Bamford (rosco<at>roscopeco.co.uk)
require 'test/unit'
require 'roll'
ASSERTS = {
'1' => 1,
'1+2' => 3,
'1+3*4' => 13,
'1*2+4/8-1' => 1,
'd1' => 1,
'1d1' => 1,
'd10' => 10,
'1d10' => 10,
'10d10' => 100,
'd3*2' => 6,
'5d6d7' => 210, # left assoc
'2d3+8' => 14, # not 22
'(2d(3+8))' => 22, # not 14
'd3+d3' => 6,
'33+d3+10' => 46,
'd2*2d4' => 16,
'd(2*2)+d4' => 8,
'd%' => 100,
'2d%' => 200,
'd%*7' => 700,
'14+3*10d2' => 74,
'(5d5-4)d(16/d4)+3' => 87, #25d4 + 3
'3d+8/8' => 3 #3d(+8)/8
}
ERRORS = {
# Bad input, all should raise exception
'd' => SyntaxError,
'3d' => SyntaxError,
'3d-8' => SyntaxError, # - # of sides
'3ddd6' => SyntaxError,
'3%2' => SyntaxError,
'%d' => SyntaxError,
'+' => SyntaxError,
'4**3' => SyntaxError
}
# bit messy, but can't get class methods on Fixnum
Fixnum.roll_proc = lambda { |sides| sides }
class TestDiceRoller < Test::Unit::TestCase
def initialize(*args)
super
end
ASSERTS.each do |expr, expect|
eval <<-EOC
def test_good_#{expr.hash.abs}
expr, expect = #{expr.inspect}, #{expect.inspect}
puts "\n-----------------------\n\#{expr} => \#{expect}" if
$VERBOSE
res = DiceRoller.roll(expr)
puts "Returned \#{res}\n-----------------------" if $VERBOSE
assert_equal expect, res
end
EOC
end
ERRORS.each do |expr, expect|
eval <<-EOC
def test_error_#{expr.hash.abs}
expr, expect = #{expr.inspect}, #{expect.inspect}
assert_raise(#{expect}) do
puts "\n-----------------------\n\#{expr} => \#{expect}" if
$VERBOSE
res = DiceRoller.roll(expr)
puts "Returned \#{res}\n-----------------------" if $VERBOSE
end
end
EOC
end
end
=====================================
> This is implemented in the DiceRoller.parse method, which returns the
> string.
Sorry, I changed that. It gives a block now.
Here's my recursive descent solution with histogram:
=begin
Ruby Quiz #61
by Matthew D Moss
Solution by Christer Nilsson
"3d6" gives 3..18 randomly
"(5d5-4)d(16/d4)+3"
Backus Naur Form:
expr: term ['+' expr | '-' expr]
term: fact ['*' term | '/' term]
fact: [unit] 'd' dice
unit: '(' expr ')' | integer
dice: '%' | term
integer: digit [integer]
digit: /[0-9]/
* Integers are positive
* The "d" (dice) expression XdY rolls a Y-sided die (numbered
from 1 to Y) X times, accumulating the results. X is optional
and defaults to 1.
* All binary operators are left-associative.
* Operator precedence:
( ) highest
d
* /
+ - lowest
Some game systems use d100 quite often, and may abbreviate it as "d%"
(but note that '%' is only allowed immediately after a 'd').
=end
class String
def behead
return ['',''] if self == ''
[self[0..0], self[1...self.size]]
end
end
class Array
def sum
inject(0) {|sum,e| sum += e}
end
def histogram(header="")
width = 100
each_index {|i| self[i]=0 if self[i].nil?}
sum = self.sum
max = self.max if max.nil?
s = " " + header + "\n"
each_with_index do |x,i|
label = " " + format("%2.1f",100.0*x/sum)+"%"
s += format("%2d",i) + " " + "*" * ((x-min) * width / (max-min)) +
label + "\n"
end
s += "\n"
end
end
class Dice
def statistics(expr, n=1000)
prob = []
n.times do
value = evaluate(expr)
prob[value]=0 if prob[value].nil?
prob[value] += 1
end
prob
end
def evaluate s
@sym, @s = s.behead
@stack = []
expr
pop
end
def drop (pattern)
raise 'syntax error: expected ' + pattern unless pattern === @sym
@sym, @s = @s.behead
end
def push(x) @stack.push x end
def top2() @stack[-2] end
def top() @stack[-1] end
def pop() @stack.pop end
def calc value
pop
push value
end
def try symbol
return nil unless @sym == symbol
drop symbol
case symbol
when '+' then expr; calc top2 + pop
when '-' then expr; calc top2 - pop
when '*' then term; calc top2 * pop
when '/' then term; calc top2 / pop
when '%' then push 100
when '(' then expr; drop ')'
#when 'd' then dice; calc top2 * pop # debug mode
when 'd' # release mode
dice
sum = 0
sides = pop
count = pop
count.times {sum += rand(sides) + 1}
push sum
end
end
def expr
term
try('+') or try('-')
end
def term
fact
try('*') or try('/')
end
def fact
@sym == 'd' ? push(1) : unit # implicit 1
try('d')
end
def dice
#unit unless try('%')# if 5d6d7 is not accepted
term unless try('%') # if 5d6d7 is accepted
end
def unit
integer @sym.to_i unless try('(')
end
def integer(i)
return if @sym == ''
digit = /[0-9]/
drop(digit)
digit === @sym ? integer( 10 * i + @sym.to_i ) : push(i)
end
end
require 'test/unit'
class TestDice < Test::Unit::TestCase
def t (actual, expect)
assert_equal expect, actual
end
def test_all
t(/[0-9]/==="0", true)
t(/[0-9]/==="a", false)
t "abc".behead, ["a","bc"]
t "a".behead, ["a",""]
t "".behead, ["",""]
dice = Dice.new()
print dice.statistics("d6").histogram("d6")
print dice.statistics("2d6").histogram("2d6")
print dice.statistics("(d6)d6",10000).histogram("(d6)d6")
#t dice.evaluate("(6)"), 6
#t dice.evaluate("12+34"), 46
#t dice.evaluate("3*4+2"), 14
#t dice.evaluate("5+6+7"), 18
#t dice.evaluate("5+6-7"), 4
#t dice.evaluate("(5+6)+7"), 18
#t dice.evaluate("5"), 5
#t dice.evaluate("5+(6+7)"), 18
#t dice.evaluate("(5+6+7)"), 18
#t dice.evaluate("5*6*7"), 210
#t dice.evaluate("2+3*4"), 14
#t dice.evaluate("12+13*14"), 194
#t dice.evaluate("(2+3)*4"), 20
#t dice.evaluate("(5d5-4)d(16/1d4)+3"), 45
#t dice.evaluate("(5d5-4)d(400/1d%)+3"), 87
#t dice.evaluate("1"), 1
#t dice.evaluate("1+2"),3
#t dice.evaluate("1+3*4"),13
#t dice.evaluate("1*2+4/8-1"), 1
#t dice.evaluate("d1"),1
#t dice.evaluate("1d1"),1
#t dice.evaluate("1d10"), 10
#t dice.evaluate("10d10"),100
#t dice.evaluate("d3*2"), 6
#t dice.evaluate("2d3+8"), 14
#t dice.evaluate("(2*(3+8))"),22
#t dice.evaluate("d3+d3"),6
#t dice.evaluate("d2*2d4"),16
#t dice.evaluate("2d%"),200
#t dice.evaluate("14+3*10d2"), 74
#t dice.evaluate("(5d5-4)d(16/d4)+3"),87
#t dice.evaluate("d10"), 10
#t dice.evaluate("d%"),100
#t dice.evaluate("d(2*2)+d4"),8
#t dice.evaluate("(5d6)d7"), 210
#t dice.evaluate("5d(6d7)"), 210
#t dice.evaluate("5d6d7)"), 210
#t dice.evaluate("12d13d14)"), 2184
#t dice.evaluate("12*d13)"), 156
#t dice.evaluate("12+d13)"), 25
end
end
$ racc roll.y -o roll.rb
Otherwise, it is pretty simple ...
A small explanation is included within the file. If needed, I will post
the generated file.
Pierre
class Integer
def d(n) # evil }:-)
(1..self).inject(0) { |a,e| a + rand(n) + 1 }
end
end
class Dice
def initialize(dice)
@src = dice.gsub(/d(%|00)(\D|$)/, 'd100\2').
gsub(/d(\d+)/, 'd(\1)').
gsub(/(\d+|\))d/, '\1.d').
gsub(/\d+/) { $&.gsub(/^0+/, '') }
raise ArgumentError, "invalid dice: `#{dice}'" if @src =~ /[^-+\/*()d0-9. ]/
begin
@dice = eval "lambda{ #@src }"
roll # try the dice
rescue
raise ArgumentError, "invalid dice: `#{dice}'"
end
end
def d(n)
1.d(n)
end
def roll
@dice.call
end
end
unless $DEBUG
d = Dice.new(ARGV[0] || "d6")
puts Array.new((ARGV[1] || 1).to_i) { d.roll }.join(" ")
else
$DEBUG = false # only makes test/unit verbose now
warn "This is a heuristic test-suite. Please re-run (or increase N) on failure."
require 'test/unit'
N = 100000
class TestDice < Test::Unit::TestCase
def test_00_invalid_dice
assert_raises(ArgumentError) { Dice.new("234%21") }
assert_raises(ArgumentError) { Dice.new("%d5") }
assert_raises(ArgumentError) { Dice.new("d5%") }
assert_raises(ArgumentError) { Dice.new("d%5") }
end
def test_10_fixed_expr
dice_min_max({
'1' => [1, 1],
'1+2' => [3, 3],
'1+3*4' => [13, 13],
'1*2+4/8-1' => [1, 1],
'd1' => [1, 1],
'1d1' => [1, 1],
'066d1' => [66, 66]
}, 10)
end
def test_20_small_dice
dice_min_max({
'd10' => [1, 10],
'1d10' => [1, 10],
'd3*2' => [2, 6],
'2d3+8' => [10, 14], # not 22
'(2d(3+8))' => [2, 22], # not 14
'd3+d3' => [2, 6],
'd2*2d4' => [2, 16],
'd(2*2)+d4' => [2, 8]
})
end
def test_30_percent_dice
dice_min_max({
'd%' => [1, 100],
'2d%' => [2, 200]
}, 100_000)
end
def test_40_complicated_dice
dice_min_max({
'10d10' => [10, 100],
'5d6d7' => [5, 210], # left assoc
'14+3*10d2' => [44, 74],
'(5d5-4)d(16/d4)+3' => [4, 339],
}, 1_000_000)
end
def dice_min_max(asserts, n=10_000)
asserts.each { |k, v|
dice = Dice.new k
v2 = (1..n).inject([1.0/0.0, 0]) { |(min, max), e|
r = dice.roll
[[min, r].min, [max, r].max]
}
assert_equal v, v2, k
}
end
end
end
__END__
Pablo
---
#!/usr/bin/ruby
class Fixnum
def d(b)
(1..self).inject(0) {|s,x| s + rand(b) + 1}
end
end
class Dice
def initialize(exp)
@expr = to_rpn(exp)
end
def roll
stack = []
@expr.each do |token|
case token
when /\d+/
stack << token.to_i
when /[-+*\/d]/
b = stack.pop
a = stack.pop
stack << a.send(token.to_sym, b)
end
end
stack.pop
end
private
def to_rpn(infix)
stack, rpn, last = [], [], nil
infix.scan(/\d+|[-+*\/()d%]/) do |token|
case token
when /\d+/
rpn << token
when '%'
rpn << "100"
when /[-+*\/d]/
while stack.any? && stronger(stack.last, token)
rpn << stack.pop
end
rpn << "1" unless last =~ /\d+|\)|%/
stack << token
when '('
stack << token
when ')'
while (op = stack.pop) && (op != '(')
rpn << op
end
end
last = token
end
while op = stack.pop
rpn << op
end
rpn
end
def stronger(op1, op2)
(op1 == 'd' && op2 != 'd') || (op1 =~ /[*\/]/ && op2 =~ /[-+]/)
end
end
if $0 == __FILE__
d = Dice.new(ARGV[0])
(ARGV[1] || 1).to_i.times { print "#{d.roll} " }
end
Here is my solution #1 for this nice quiz. Hacky and short, without (!)
using eval... ;)
module Dice
def self.roll(expr)
expr = expr.gsub(/\s/, '')
while
expr.sub!(/\(([^()]+)\)/) { roll($1) } ||
expr.sub!(/(\A|[^\d])\-\-(\d+)/, '\\1\\2') ||
expr.sub!(/d%/, 'd100') ||
expr.sub!(/(\d+)d(\d+)/) { (1..$1.to_i).inject(0) {|a, b| a +
rand($2.to_i) + 1} } ||
expr.sub!(/d(\d+)/, '1d\\1') ||
expr.sub!(/(\d+)\/(\-?\d+)/) { $1.to_i / $2.to_i } ||
expr.sub!(/(\d+)\*(\-?\d+)/) { $1.to_i * $2.to_i } ||
expr.sub!(/(\-?\d+)\-(\-?\d+)/) { $1.to_i - $2.to_i } ||
expr.sub!(/(\-?\d+)\+(\-?\d+)/) { $1.to_i + $2.to_i }
end
return $1.to_i if /\A(\-?\d+)\Z/ =~ expr
raise "Error evaluating dice expression, stuck at '#{expr}'"
end
end
(ARGV[1] || 1).to_i.times { print "#{Dice.roll(ARGV[0])} " }
puts
here is my second solution. Quite a bit longer, but a lot nicer.
For this I implemented a simple recursive descent parser class that
allows the tokens and the grammar to be defined in a very clean ruby
syntax. I think I'd really like to see a production quality
parser(generator) using something like this grammar format.
class RDParser
attr_accessor :pos
attr_reader :rules
def initialize(&block)
@lex_tokens = []
@rules = {}
@start = nil
instance_eval(&block)
end
def parse(string)
@tokens = []
until string.empty?
raise "unable to lex '#{string}" unless @lex_tokens.any? do |tok|
match = tok.pattern.match(string)
if match
@tokens << tok.block.call(match.to_s) if tok.block
string = match.post_match
true
else
false
end
end
end
@pos = 0
@max_pos = 0
@expected = []
result = @start.parse
if @pos != @tokens.size
raise "Parse error. expected: '#{@expected.join(', ')}', found
'#{@tokens[@max_pos]}'"
end
return result
end
def next_token
@pos += 1
return @tokens[@pos - 1]
end
def expect(tok)
t = next_token
if @pos - 1 > @max_pos
@max_pos = @pos - 1
@expected = []
end
return t if tok === t
@expected << tok if @max_pos == @pos - 1 && !@expected.include?(tok)
return nil
end
private
LexToken = Struct.new(:pattern, :block)
def token(pattern, &block)
@lex_tokens << LexToken.new(Regexp.new('\\A' + pattern.source), block)
end
def start(name, &block)
rule(name, &block)
@start = @rules[name]
end
def rule(name)
@current_rule = Rule.new(name, self)
@rules[name] = @current_rule
yield
@current_rule = nil
end
def match(*pattern, &block)
@current_rule.add_match(pattern, block)
end
class Rule
Match = Struct.new :pattern, :block
def initialize(name, parser)
@name = name
@parser = parser
@matches = []
@lrmatches = []
end
def add_match(pattern, block)
match = Match.new(pattern, block)
if pattern[0] == @name
pattern.shift
@lrmatches << match
else
@matches << match
end
end
def parse
match_result = try_matches(@matches)
return nil unless match_result
loop do
result = try_matches(@lrmatches, match_result)
return match_result unless result
match_result = result
end
end
private
def try_matches(matches, pre_result = nil)
match_result = nil
start = @parser.pos
matches.each do |match|
r = pre_result ? [pre_result] : []
match.pattern.each do |token|
if @parser.rules[token]
r << @parser.rules[token].parse
unless r.last
r = nil
break
end
else
nt = @parser.expect(token)
if nt
r << nt
else
r = nil
break
end
end
end
if r
if match.block
match_result = match.block.call(*r)
else
match_result = r[0]
end
break
else
@parser.pos = start
end
end
return match_result
end
end
end
parser = RDParser.new do
token(/\s+/)
token(/\d+/) {|m| m.to_i }
token(/./) {|m| m }
start :expr do
match(:expr, '+', :term) {|a, _, b| a + b }
match(:expr, '-', :term) {|a, _, b| a - b }
match(:term)
end
rule :term do
match(:term, '*', :dice) {|a, _, b| a * b }
match(:term, '/', :dice) {|a, _, b| a / b }
match(:dice)
end
def roll(times, sides)
(1..times).inject(0) {|a, b| a + rand(sides) + 1 }
end
rule :dice do
match(:atom, 'd', :sides) {|a, _, b| roll(a, b) }
match('d', :sides) {|_, b| roll(1, b) }
match(:atom)
end
rule :sides do
match('%') { 100 }
match(:atom)
end
rule :atom do
match(Integer)
match('(', :expr, ')') {|_, a, _| a }
end
end
(ARGV[1] || 1).to_i.times { print "#{parser.parse(ARGV[0])} " }
puts
--
No virus found in this outgoing message.
Checked by AVG Free Edition.
Version: 7.1.371 / Virus Database: 267.14.15/223 - Release Date: 06/01/2006
> Here is my solution. I convert the expression into RPN (using the
> algorithm
> described in the Wikipedia article) and then calculate it (I have
> added a
> 'd' method to Fixnum so that I can use it like the standard arithmetic
> operators).
Wow. That is very cool. Thanks for sharing!
James Edward Gray II
> For this I implemented a simple recursive descent parser class that
> allows the tokens and the grammar to be defined in a very clean
> ruby syntax.
Awesome!
> I think I'd really like to see a production quality parser
> (generator) using something like this grammar format.
I agree. This is fantastic.
So what do we have to do to get you to add the polish and make it
available? :)
James Edward Gray II
On Sun, 08 Jan 2006 19:38:03 -0000, I wrote:
> On Sun, 08 Jan 2006 19:33:18 -0000, I also wrote:
>
>> This is implemented in the DiceRoller.parse method, which returns the
>> string.
>
> Sorry, I changed that. It gives a block now.
>
There were a few other inaccuracies in the comments, where I'd obviously
not been merciless enough when refactoring. Specifically, a (*) comment
about one of the parse regexps (no longer applies) and a comment in the
tests about Fixnum and class methods (from before I realised my error).
Oh, and a debug message used still referenced an attr that was no longer
set up at that point.
I updated the archive at the link I posted with those removed, and also
took the chance to slip in a tiny fix for whitespace (just remove it all
at the start of the parse) but I guess it doesn't 'count' for the quiz :)
Anyway, thanks all concerned for the fun quiz - this is the first one I've
done so take it easy on my solution :)
(Oh, and I missed [SOLUTION] before and since a lot of people seem to be
doing that I felt just [QUIZ] might get missed).
The original solution post was this one:
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/174815
It's a recursive descent parser, that parses the full BNF posted by
Matthew Moss. It doesn't "compile" the expresion into nodes or something
similar, instead it evaluates the expression while parsing (so it has to
be reparsed for every dice rolling). It uses StringScanner, which was
quite handy for this task. (And it also uses eval() ;-)
Dominik
require "strscan"
class Dice
def initialize(expr)
@expr = expr.gsub(/\s+/, "")
end
def roll
s = StringScanner.new(@expr)
res = expr(s)
raise "garbage after end of expression" unless s.eos?
res
end
private
def split_expr(s, sub_expr, sep)
expr = []
loop do
expr << send(sub_expr, s)
break unless s.scan(sep)
expr << s[1] if s[1]
end
expr
end
def expr(s)
eval(split_expr(s, :fact, /([+\-])/).join)
end
def fact(s)
eval(split_expr(s, :term, /([*\/])/).join)
end
def term(s)
first_rolls = s.match?(/d/) ? 1 : unit(s)
dices = s.scan(/d/) ? split_expr(s, :dice, /d/) : []
dices.inject(first_rolls) do |rolls, dice|
raise "invalid dice (#{dice})" unless dice > 0
(1..rolls).inject(0) { |sum, _| sum + rand(dice) + 1 }
end
end
def dice(s)
s.scan(/%/) ? 100 : unit(s)
end
def unit(s)
if s.scan(/(\d+)/)
s[1].to_i
else
unless s.scan(/\(/) && (res = expr(s)) && s.scan(/\)/)
raise "error in expression"
end
res
end
end
end
if $0 == __FILE__
begin
d = Dice.new(ARGV[0])
puts (1..(ARGV[1] || 1).to_i).map { d.roll }.join(" ")
rescue => e
puts e
end
end
$searches = [
[/\(\d*\)/, lambda{|m| m[1..-2]}],
[/^d/, lambda{|m| "1d"}],
[/d%/, lambda{|m| "d100"}],
[/(\+|-|\*|\/|\()d\d+/, lambda{|m| m[0..0]+'1'+m[1..-1]}],
[/\d+d\d+/, lambda{|m| dice(*m.split('d').map {|i|i.to_i}) }],
[/\d+(\*|\/)\d+/, lambda{|m| eval m}],
[/\d+(\+|-)\d+/, lambda{|m| eval m}]
]
def parse(to_parse)
s = to_parse
while(s =~ /d|\+|-|\*|\/|\(|\)/)
$searches.each do |search|
if(s =~ search[0]) then
s = s.sub(search[0], &search[1])
break
end
end
end
s
end
def dice(times, sides)
Array.new(times){rand(sides)+1}.inject(0) {|s,i|s+i}
end
srand
string = ARGV[0]
(puts "usage: #{$0} <string> [<iterations>]"; exit) if !string
(ARGV[1] || 1).to_i.times { print parse(string), ' ' }
-----Horndude77