Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

[QUIZ] Dice Roller (#61)

25 views
Skip to first unread message

Ruby Quiz

unread,
Jan 6, 2006, 1:56:47 PM1/6/06
to
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!

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

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').


Gregory Seidman

unread,
Jan 6, 2006, 2:00:52 PM1/6/06
to
On Sat, Jan 07, 2006 at 03:56:47AM +0900, Ruby Quiz wrote:
[...]

} [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.]

I would appreciate the full BNF, please.

--Greg

Jacob Fugal

unread,
Jan 6, 2006, 2:13:45 PM1/6/06
to
On 1/6/06, Ruby Quiz <ja...@grayproductions.net> wrote:
> Or, for something more complicated:
>
> > roll.rb "(5d5-4)d(16/d4)+3"
> 31

I assume integer arithmetic? So if, for example, a 3 comes up on your
d4, 16/d4 would be 5?

Jacob Fugal


J. Ryan Sobol

unread,
Jan 6, 2006, 2:29:26 PM1/6/06
to
Please don't take this the wrong way, but I've never played D&D.
Would someone mind explaining the math that went into the command
below to generate it's result?

~ 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...

Austin Ziegler

unread,
Jan 6, 2006, 2:36:23 PM1/6/06
to
On 06/01/06, J. Ryan Sobol <ryan...@gmail.com> wrote:
> Please don't take this the wrong way, but I've never played D&D.
> Would someone mind explaining the math that went into the command
> below to generate it's result?

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


James Edward Gray II

unread,
Jan 6, 2006, 2:37:10 PM1/6/06
to

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

Matthew Moss

unread,
Jan 6, 2006, 2:38:18 PM1/6/06
to
Sticking with typical integer division (ie, round-down) is fine.

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.

Matthew Moss

unread,
Jan 6, 2006, 2:39:57 PM1/6/06
to
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

=)

Will Shattuck

unread,
Jan 6, 2006, 2:46:49 PM1/6/06
to
On 1/6/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
>

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.


Austin Ziegler

unread,
Jan 6, 2006, 2:49:29 PM1/6/06
to
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.

James Edward Gray II

unread,
Jan 6, 2006, 2:53:07 PM1/6/06
to
On Jan 6, 2006, at 1:49 PM, Austin Ziegler wrote:

> 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


Matthew Moss

unread,
Jan 6, 2006, 2:53:35 PM1/6/06
to
On 1/6/06, Will Shattuck <willsh...@gmail.com> wrote:
> On 1/6/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
> >
>
> Actually 3d6 means roll a 6 sided die 3 times so you would have a result of 3-18

Actually, you're right, but actually my post was a half-joke. The
munchkin players seem to roll 18's every time. ;)


J. Ryan Sobol

unread,
Jan 6, 2006, 3:21:25 PM1/6/06
to
I guess that must be a D&D inside half-joke because I'm totally
confused.

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.

James Edward Gray II

unread,
Jan 6, 2006, 3:25:08 PM1/6/06
to
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. :)

James Edward Gray II


Austin Ziegler

unread,
Jan 6, 2006, 3:29:33 PM1/6/06
to

In most versions of D&D/AD&D, this was also limited to Strength attributes only.

This may have changed recently. ;)

James Edward Gray II

unread,
Jan 6, 2006, 3:35:00 PM1/6/06
to
On Jan 6, 2006, at 2:29 PM, Austin Ziegler wrote:

> 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


Matthew Moss

unread,
Jan 6, 2006, 3:37:13 PM1/6/06
to
> 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.

Will Shattuck

unread,
Jan 6, 2006, 3:42:32 PM1/6/06
to
forgive my ignorance... BNF?

w


On 1/6/06, Matthew Moss <matthew.m...@gmail.com> wrote:

Bill Kelly

unread,
Jan 6, 2006, 3:51:23 PM1/6/06
to

Ross Bamford

unread,
Jan 6, 2006, 3:59:57 PM1/6/06
to
On Fri, 06 Jan 2006 18:56:47 -0000, Ruby Quiz <ja...@grayproductions.net>
wrote:

>
> 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

Jim Freeze

unread,
Jan 6, 2006, 4:52:06 PM1/6/06
to
On Jan 6, 2006, at 12:56 PM, Ruby Quiz wrote:
>
> 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
>
Ok, I'm still a little confused. This should have output something like:

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?

Austin Ziegler

unread,
Jan 6, 2006, 5:31:10 PM1/6/06
to
On 06/01/06, Jim Freeze <j...@freeze.org> wrote:
> On Jan 6, 2006, at 12:56 PM, Ruby Quiz wrote:
>> 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
> Ok, I'm still a little confused. This should have output something
> like:
> rand(16)+3 rand(16)+3 rand(16)+3

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.

Robert Retzbach

unread,
Jan 6, 2006, 5:45:38 PM1/6/06
to
Ruby Quiz schrieb:

>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


Matthew Moss

unread,
Jan 6, 2006, 5:49:47 PM1/6/06
to
> How do you parse 5d6d7?
> As (5d6)d7 or 5d(6d7) since there is no "Assoziativgesetz" like (AdB)dC
> == Ad(BdC).

All binary operators are left associative, so 5d6d7 is (5d6)d7.


Sascha Abel

unread,
Jan 6, 2006, 6:04:07 PM1/6/06
to
Moin,

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/.


James Edward Gray II

unread,
Jan 6, 2006, 6:10:32 PM1/6/06
to
On Jan 6, 2006, at 5:04 PM, Sascha Abel wrote:

> < 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


Austin Ziegler

unread,
Jan 6, 2006, 6:14:25 PM1/6/06
to
On 06/01/06, Sascha Abel <sasch...@ewetel.name> wrote:
> 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?

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.

Dave Lee

unread,
Jan 6, 2006, 6:19:57 PM1/6/06
to
On 1/6/06, Matthew Moss <matthew.m...@gmail.com> wrote:

so 1+2*3 == (1+2)*3 == 9?

Dave


James Edward Gray II

unread,
Jan 6, 2006, 6:23:51 PM1/6/06
to

I misread. Sorry.

James Edward Gray II


Morus Walter

unread,
Jan 6, 2006, 6:25:33 PM1/6/06
to
In article <20060106185354.LPAB61...@localhost.localdomain>,

Ruby Quiz <ja...@grayproductions.net> writes:
> Or, for something more complicated:
>
> > roll.rb "(5d5-4)d(16/d4)+3"
> 31

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

Austin Ziegler

unread,
Jan 6, 2006, 6:28:26 PM1/6/06
to
On 06/01/06, James Edward Gray II <ja...@grayproductions.net> wrote:
> On Jan 6, 2006, at 5:04 PM, Sascha Abel wrote:
> I wrote:
> >> 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.

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.

Jim Freeze

unread,
Jan 6, 2006, 6:35:34 PM1/6/06
to

On Jan 6, 2006, at 4:31 PM, Austin Ziegler wrote:

[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


James Edward Gray II

unread,
Jan 6, 2006, 6:41:39 PM1/6/06
to
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.

James Edward Gray II


Jim Freeze

unread,
Jan 6, 2006, 6:45:41 PM1/6/06
to

On Jan 6, 2006, at 5:41 PM, James Edward Gray II 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.
>

That doesn't jive with what was said earlier. There should be no zero
on the tens dice. Only 1..10.

Jim


Jacob Fugal

unread,
Jan 6, 2006, 7:17:31 PM1/6/06
to
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.

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


hitesh...@gmail.com

unread,
Jan 6, 2006, 7:24:04 PM1/6/06
to
> 1+2*3 == (1+2)*3 == 9?

You may want to review the precedence order again.

* All binary operators are left-associative.
* Operator precedence:
( ) highest
d
* /
+ - lowest

Jim Freeze

unread,
Jan 6, 2006, 7:35:35 PM1/6/06
to

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.

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

Matthew D Moss

unread,
Jan 6, 2006, 8:06:09 PM1/6/06
to
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

Austin Ziegler

unread,
Jan 6, 2006, 8:36:37 PM1/6/06
to
On 06/01/06, Jim Freeze <j...@freeze.org> wrote:
> 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?

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.

James Edward Gray II

unread,
Jan 6, 2006, 8:37:15 PM1/6/06
to
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.

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


James Edward Gray II

unread,
Jan 6, 2006, 8:44:26 PM1/6/06
to
On Jan 6, 2006, at 6:35 PM, Jim Freeze wrote:

>
> 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

Pierre Barbier de Reuille

unread,
Jan 6, 2006, 8:46:48 PM1/6/06
to
Austin Ziegler a écrit :

> On 06/01/06, Sascha Abel <sasch...@ewetel.name> wrote:
>
>>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?
>
>
> 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.

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

Pierre Barbier de Reuille

unread,
Jan 6, 2006, 8:51:34 PM1/6/06
to
Morus Walter a écrit :

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

Bill Kelly

unread,
Jan 6, 2006, 9:00:58 PM1/6/06
to
Hehe, sorry, it's too early. I'm just happy because I finally
finished a ruby quiz.

Cya in 40 hours or so... (and hope I'm really computing non-fubar'd
results... :)


Regards,

Bill


Matthew Moss

unread,
Jan 6, 2006, 9:14:16 PM1/6/06
to
> If so, why is a 0 never possible?
> And why does d10 have 0 -> 10 while a d6 has 1 -> 6?

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.


Ross Bamford

unread,
Jan 6, 2006, 10:09:20 PM1/6/06
to
On Sat, 07 Jan 2006 01:44:26 -0000, James Edward Gray II
<ja...@grayproductions.net> wrote:

[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

J. Ryan Sobol

unread,
Jan 6, 2006, 10:26:36 PM1/6/06
to
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 ?

~ ryan ~


Ross Bamford

unread,
Jan 6, 2006, 11:11:19 PM1/6/06
to
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 = {
'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.

James Edward Gray II

unread,
Jan 6, 2006, 11:43:38 PM1/6/06
to
On Jan 6, 2006, at 9:26 PM, J. Ryan Sobol wrote:

> 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


Matt Lawrence

unread,
Jan 7, 2006, 12:44:56 AM1/7/06
to
On Sat, 7 Jan 2006, Pierre Barbier de Reuille wrote:

> 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_

Matthew D Moss

unread,
Jan 6, 2006, 2:43:16 PM1/6/06
to
I'm not sure whether this made it to the list first time I sent it, or
is just delayed. Here it is again in case folks missed it...

> 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.

Matthew D Moss

unread,
Jan 6, 2006, 7:58:36 PM1/6/06
to
Resolve precedence before associativity.

So 1+2-3 == (1+2)-3 because + and - have the same precedence.

But 1+2*3 == 1+(2*3) because * has precedence over +.

Matthew D Moss

unread,
Jan 6, 2006, 5:07:24 PM1/6/06
to
The quiz shows incorrect output for 3d6. The line reading "72 64 113
33 78 82" was mistakenly copied from a different set of dice.

"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).

Matthew D Moss

unread,
Jan 6, 2006, 5:15:38 PM1/6/06
to
> 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.

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".

Henrik Martensson

unread,
Jan 7, 2006, 4:31:31 AM1/7/06
to
On Fri, 2006-01-06 at 23:45, Robert Retzbach wrote:

> >
> 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

Christian Neukirchen

unread,
Jan 7, 2006, 7:03:33 AM1/7/06
to

So what are the maximum and minimum values of this?

--
Christian Neukirchen <chneuk...@gmail.com> http://chneukirchen.org


Christian Neukirchen

unread,
Jan 7, 2006, 7:27:05 AM1/7/06
to
"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.

> Ross Bamford - ro...@roscopeco.remove.co.uk

Ross Bamford

unread,
Jan 7, 2006, 7:59:54 AM1/7/06
to

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?

Robert Retzbach

unread,
Jan 7, 2006, 8:51:23 AM1/7/06
to

>
> 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)d(16/d4)+3

(5d5-4) is 25 at max
(16/d4) is 16 at max
25d16+3 is 339 at max

qed


Christian Neukirchen

unread,
Jan 7, 2006, 8:51:51 AM1/7/06
to
"Ross Bamford" <ro...@roscopeco.remove.co.uk> writes:

Yeah. I got your list wrong then, I thought the number means the
maximum reachable, not what to throw with loaded dice. Sorry.

Robert Retzbach

unread,
Jan 7, 2006, 9:00:03 AM1/7/06
to
Robert Retzbach schrieb:

Ignore me for lifetime please.


Ross Bamford

unread,
Jan 7, 2006, 9:06:54 AM1/7/06
to
On Sat, 07 Jan 2006 13:51:51 -0000, Christian Neukirchen
<chneuk...@gmail.com> wrote:

> "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.

Reinder Verlinde

unread,
Jan 7, 2006, 11:53:29 AM1/7/06
to
In article <m2fyo0g...@lilith.local>,
Christian Neukirchen <chneuk...@gmail.com> wrote:

> "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

Gavin Kistner

unread,
Jan 7, 2006, 11:55:36 AM1/7/06
to
On Jan 7, 2006, at 5:03 AM, Christian Neukirchen wrote:
> "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?

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.


Gavin Kistner

unread,
Jan 7, 2006, 12:23:02 PM1/7/06
to
On Jan 7, 2006, at 9:55 AM, Gavin Kistner wrote:
> Min: (5*1-4)*(1)+3 = 1*1+3 = 5

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

Gavin Kistner

unread,
Jan 7, 2006, 12:25:35 PM1/7/06
to
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

Gavin Kistner

unread,
Jan 7, 2006, 12:26:25 PM1/7/06
to
On Jan 7, 2006, at 9:58 AM, Reinder Verlinde wrote:
> ([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)

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]

Christian Neukirchen

unread,
Jan 7, 2006, 1:32:22 PM1/7/06
to
Gavin Kistner <ga...@refinery.com> writes:

I claim 4 and 339.

Bill Kelly

unread,
Jan 7, 2006, 1:36:14 PM1/7/06
to
From: "Gavin Kistner" <ga...@refinery.com>

>
> Min: (5*1-4)*(1)+3 = 1*1+3 = 4
> Max: (5*5-4)*(16/4)+3 = 21*4+3 = 87

For max, should be (16/1) not (16/4), no?


Christer Nilsson

unread,
Jan 7, 2006, 1:56:06 PM1/7/06
to

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/.


Gavin Kistner

unread,
Jan 7, 2006, 1:58:12 PM1/7/06
to

You're all too damned smart. :)

Brilliant!


Matthew Smillie

unread,
Jan 7, 2006, 2:27:31 PM1/7/06
to
On Jan 7, 2006, at 18:32, Christian Neukirchen wrote:

> 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.


Matthew Moss

unread,
Jan 7, 2006, 4:57:34 PM1/7/06
to
> 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.

Ahhh.... this must be a game system you don't know. =)


Matthew Moss

unread,
Jan 7, 2006, 5:03:13 PM1/7/06
to
Wow... You guys are just having too much fun with
"(5d5-4)d(16/d4)+3", I think. Heck, if y'all know Ruby so well (more
than me, cause I'm still such a n00b), you'd be able to swap in loaded
dice for random dice and get your min's and max's. =)

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.


Matthew Moss

unread,
Jan 7, 2006, 6:15:30 PM1/7/06
to
On 1/7/06, Christer Nilsson <janchrist...@gmail.com> wrote:
>
> 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?

Off the top of my head, that seems right.


Ron M

unread,
Jan 7, 2006, 8:15:53 PM1/7/06
to
Austin Ziegler wrote:
> 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,

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.

Pierre Barbier de Reuille

unread,
Jan 8, 2006, 8:45:41 AM1/8/06
to
Matthew Moss a écrit :

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

Robert Retzbach

unread,
Jan 8, 2006, 9:15:54 AM1/8/06
to
Pierre Barbier de Reuille schrieb:

Gregory Seidman

unread,
Jan 8, 2006, 9:24:49 AM1/8/06
to
On Sun, Jan 08, 2006 at 10:48:01PM +0900, Pierre Barbier de Reuille wrote:
} Matthew Moss a ?crit :

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

Pierre Barbier de Reuille

unread,
Jan 8, 2006, 9:43:13 AM1/8/06
to
Gregory Seidman a écrit :

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
>
>
>

Luke Blanshard

unread,
Jan 8, 2006, 2:05:54 PM1/8/06
to
Attached is my submission. It looks pretty cool to me, but then this is
only my second-ever Ruby program.

Meta-comment: if [QUIZ] opens the quiz, then surely [/QUIZ] should close it.

Luke Blanshard

roll.rb

Bill Kelly

unread,
Jan 8, 2006, 2:11:54 PM1/8/06
to
Hi,

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)

61_dice_roller.rb

Matthew Smillie

unread,
Jan 8, 2006, 2:17:01 PM1/8/06
to
> Leexer/parsers? We ain't got no Leexer/parsers. We don't need no
> Leexer/parsers. I don't have to show you any steenking Leexer/parser.
> Just call eval and use Ruby's fine lexer/parser (apologies to Mel
> Brooks, John Huston and Banditos Mexicanos everywhere).
>
> This approach uses regex substitutions to first munge the input
> expression to deal with the default cases (like d6 to 1d6 and 1% to
> 1d100), then it substitutes ** for d and hands it over to the
> evaluator and prints the result.
>

> 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


Paul Novak

unread,
Jan 8, 2006, 2:28:45 PM1/8/06
to
Good catch. I felt uncomfortable building this without unit testing.
It should be possible to write good repeatable tests using srand in
place of rand...

Andrew McGuinness

unread,
Jan 8, 2006, 2:31:07 PM1/8/06
to
Ruby Quiz wrote:
> 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!
>
> -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
>
> 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
>

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/

Ross Bamford

unread,
Jan 8, 2006, 2:33:18 PM1/8/06
to
Hi,

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
=====================================

Ross Bamford

unread,
Jan 8, 2006, 2:38:03 PM1/8/06
to
On Sun, 08 Jan 2006 19:33:18 -0000, Ross Bamford
<ro...@roscopeco.remove.co.uk> wrote:

> This is implemented in the DiceRoller.parse method, which returns the
> string.

Sorry, I changed that. It gives a block now.

Christer Nilsson

unread,
Jan 8, 2006, 3:01:27 PM1/8/06
to
Very interesting, and different solutions, this time!

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

Pierre Barbier de Reuille

unread,
Jan 8, 2006, 3:01:56 PM1/8/06
to
Well, here is my first solution to a quizz ^^
I tried to use racc for that ... so you need to generate the ruby script
using :

$ 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

roll.y

Christian Neukirchen

unread,
Jan 8, 2006, 3:12:05 PM1/8/06
to

# There it goes, using eval for simplicity, but at least compiling the
# dice into a Proc:

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__

Austin Ziegler

unread,
Jan 8, 2006, 3:29:15 PM1/8/06
to
Here is my submission. Yes, this is my first completed Ruby Quiz ;)
Thanks to Eric Mahurin's syntax.rb for making this work. I've attached
it as well, because it's not easily accessible otherwise ;)

-austin

roll.rb
syntax.rb

Pablo Hoch

unread,
Jan 8, 2006, 4:42:06 PM1/8/06
to
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). My solution is not very strict, so it allows '%' as an alias for
100 anywhere in the expression (not just after a 'd'), but I think that
should not be a big problem. It also ignores other characters, so whitespace
is allowed anywhere.


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

Dennis Ranke

unread,
Jan 8, 2006, 4:44:10 PM1/8/06
to
Hi,

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

Dennis Ranke

unread,
Jan 8, 2006, 4:51:44 PM1/8/06
to
Hi,

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

John Earles

unread,
Jan 8, 2006, 4:59:41 PM1/8/06
to
Hi!  This is my first entry to a RUBY-QUIZ.  Sure I could have opted to use
'eval', but I figured I might as well learn a little about expression
parsing.  Fun stuff!
 
My entry is focused around The Shunting Yard Algorithm, allowing me to
parse, transform and evaluate in the same step.  I do not keep the 'postfix'
transform, so it is slightly inefficient to perform multiple 'rolls'.  That
is an optimization for another day.
 
I also got to try out some new regexp stuff.  Of particular note is the use
of (?=d) in the expression used to search for d's that need an implicit 1
lvalue.  I was having problems with 5dddd7 type commands until I discovered
this allowed the target d NOT to be consumed by the regexp.
 
As a final note I am a Ruby Newbie, and a Java developer by day, so any tips
or comments on my coding would be appreciated.  Thanks!
 
- John
 
-------------------
 
$DEBUG = false
 
# Dice Roller entry point
def roll_dice( dice_command, roll_count )
  begin
    puts "Executing #{roll_count} roll(s) of #{dice_command}"
    results, total = Dice.new( dice_command ).roll( roll_count )
    puts "Result: [#{results.join(', ')}] => #{total}" 
  rescue Exception => e
    puts "Roll error: #{e}"
  end
end
 
class Dice 
  # operator => [precendence, associativity]
  @@operators = { "d" => [3, :right],
                  "*" => [2, :left] , "/" => [2, :left],
                  "+" => [1, :left] , "-" => [1, :left]
                }
 
  # Initialize the Stacks and load the dice instructions
  def initialize( dice_command )   
    if $DEBUG
      alias :d :dnd_roll_loaded
    else
      alias :d :dnd_roll_random
    end
 
    @operator_stack, @value_stack = [], []
    prepare_instructions( dice_command )
  end
 
  def roll( roll_count )
    results = (1..roll_count).collect { execute }
    [results, results.inject {|sum, item| sum + item } || 0]
  end 
 
  private
 
  # The infix command is parsed into tokens and then executed using
  # The Shunting Yard Algorithm. Evaluation is done "on-the-fly" as
  # items are placed on the value stack (acting as the post-fix "output").
  def execute
    @operator_stack.clear
    @value_stack.clear
 
    # Process the tokens in L -> R order
    # Look for non-digit characters and numbers
    @instructions.scan(/\D|\d+/) do | token |
      case token
        when "("
          @operator_stack.push token
 
        when /\d+/ # any number
          @value_stack.push token.to_i
         
        when /[-\+*\/d]/ # the operators
          finished = false
          until finished or @operator_stack.empty?
            if higher_operator(token)
              finished = true
            else
              resolve_expression
            end
          end
          @operator_stack.push token
 
        when ")"
          resolve_expression while @operator_stack.last != "("
          @operator_stack.pop
         
        else
          raise "Invalid token found: #{token}"
      end
    end
 
    resolve_expression while !@operator_stack.empty?
 
    raise "Unexpected problem. #{@value_stack.size} values remain after
execution." \
      unless @value_stack.size == 1
    @value_stack.pop
  end   
 
  def resolve_expression
    opr, rhv, lhv = @operator_stack.pop, @value_stack.pop, @value_stack.pop
    raise "No more values left for #{opr} to consume!" unless rhv && lhv
   
    value = (opr == "d") ? value = d( lhv, rhv ) : lhv.send( opr, rhv )
    @value_stack.push value.to_i
  end
 
  def dnd_roll_random( roll_count, die_value )
    (1..roll_count).inject(0) { |value, item| value + ( rand(die_value) + 1
) }
  end
 
  def dnd_roll_loaded( roll_count, die_value )
    roll_count * die_value
  end
 
  def higher_operator(opr)
    if associativity(opr) == :left
      precedence(opr) > precedence(@operator_stack.last)
    else
      precedence(opr) >= precedence(@operator_stack.last)   
    end
  end
 
  def precedence(opr)
    @@operators[opr] ? @@operators[opr][0] : 0
  end
 
  def associativity(opr)
    @@operators[opr] ? @@operators[opr][1] : :left
  end
 
  def prepare_instructions( dice_command )
    # 1) Eliminate all whitespace.
    # 2) Substitute d100 for d%
    # 3) Insert the implied 1 if a d is the first character
    #    or is preceded by an operator other than ')'
    @instructions = dice_command.gsub(/\s+/, '')   
    @instructions.gsub!(/d%/, 'd100')
    @instructions.gsub!(/([-\+*\/(d]|\A)(?=d)/, '\11')  
    puts "Normalized instructions: #@instructions" if $DEBUG
   
    raise "Unmatched left / right parenthesis" unless \
      @instructions.scan(/\(/).size == @instructions.scan(/\)/).size
  end 
end
 
# Argument parsing
if $0 == __FILE__
  raise "DiceRoller dice_command [roll_count=1]" unless (1..2).include?(
ARGV.length )
  roll_dice(ARGV[0], ARGV[1] ? ARGV[1].to_i : 1)
end
 

--
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

James Edward Gray II

unread,
Jan 8, 2006, 5:23:59 PM1/8/06
to
On Jan 8, 2006, at 3:42 PM, Pablo Hoch wrote:

> 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


James Edward Gray II

unread,
Jan 8, 2006, 5:29:34 PM1/8/06
to
On Jan 8, 2006, at 3:53 PM, Dennis Ranke wrote:

> 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

Ross Bamford

unread,
Jan 8, 2006, 5:29:24 PM1/8/06
to
Sorry for the noise.

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

Dominik Bathon

unread,
Jan 8, 2006, 7:46:01 PM1/8/06
to
Here is my solution.

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


hornd...@gmail.com

unread,
Jan 8, 2006, 9:25:56 PM1/8/06
to
I didn't try anything fancy for this. I did try to get eval to do all
the work, but ran into too many problems. Here's my solution:

$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

It is loading more messages.
0 new messages