Forum: Ruby Dice Roller (#61)

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
James G. (Guest)
on 2006-01-06 20:57
(Received via mailing list)
The three rules of Ruby Q.:

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 Q. 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 Q. 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 S. (Guest)
on 2006-01-06 21:03
(Received via mailing list)
On Sat, Jan 07, 2006 at 03:56:47AM +0900, Ruby Q. 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 F. (Guest)
on 2006-01-06 21:15
(Received via mailing list)
On 1/6/06, Ruby Q. <removed_email_address@domain.invalid> 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 F.
J. Ryan S. (Guest)
on 2006-01-06 21:30
(Received via mailing list)
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 ~
Austin Z. (Guest)
on 2006-01-06 21:36
(Received via mailing list)
On 06/01/06, J. Ryan S. <removed_email_address@domain.invalid> 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
James G. (Guest)
on 2006-01-06 21:39
(Received via mailing list)
On Jan 6, 2006, at 1:29 PM, J. Ryan S. wrote:

>>
>> The task for this Ruby Q. is to write a dice roller...
>>
>> > roll.rb "3d6" 6
>> 72  64  113  33  78  82

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 G. II
Matthew M. (Guest)
on 2006-01-06 21:39
(Received via mailing list)
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 M. (Guest)
on 2006-01-06 21:42
(Received via mailing list)
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 S. (Guest)
on 2006-01-06 21:48
(Received via mailing list)
On 1/6/06, Matthew M. <removed_email_address@domain.invalid> 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 S.  ( 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 Z. (Guest)
on 2006-01-06 21:51
(Received via mailing list)
On 06/01/06, Matthew M. <removed_email_address@domain.invalid> 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.

-austin
James G. (Guest)
on 2006-01-06 21:54
(Received via mailing list)
On Jan 6, 2006, at 1:49 PM, Austin Z. wrote:

> On 06/01/06, Matthew M. <removed_email_address@domain.invalid> 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 G. II
Matthew M. (Guest)
on 2006-01-06 21:54
(Received via mailing list)
On 1/6/06, Will S. <removed_email_address@domain.invalid> wrote:
> 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 S. (Guest)
on 2006-01-06 22:22
(Received via mailing list)
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 M. wrote:

>> roll.rb "3d6" 6
> 18 18 18 18 18 18

On Jan 6, 2006, at 2:49 PM, Austin Z. wrote:
> Just don't tell me that the first one is 18/00.

On Jan 6, 2006, at 2:53 PM, James Edward G. II wrote:
> <dies laughing>  They all were, of course.
James G. (Guest)
on 2006-01-06 22:28
(Received via mailing list)
On Jan 6, 2006, at 2:21 PM, J. Ryan S. 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 G. II
Austin Z. (Guest)
on 2006-01-06 22:31
(Received via mailing list)
On 06/01/06, James Edward G. II <removed_email_address@domain.invalid> wrote:
> On Jan 6, 2006, at 2:21 PM, J. Ryan S. 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. ;)

-austin
James G. (Guest)
on 2006-01-06 22:37
(Received via mailing list)
On Jan 6, 2006, at 2:29 PM, Austin Z. wrote:

> 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 G. II
Matthew M. (Guest)
on 2006-01-06 22:40
(Received via mailing list)
> 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 S. (Guest)
on 2006-01-06 22:43
(Received via mailing list)
forgive my ignorance... BNF?

w


On 1/6/06, Matthew M. <removed_email_address@domain.invalid> wrote:
>
> 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 S.  ( 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.
Bill K. (Guest)
on 2006-01-06 22:52
(Received via mailing list)
Ross B. (Guest)
on 2006-01-06 23:10
(Received via mailing list)
On Fri, 06 Jan 2006 18:56:47 -0000, Ruby Q.
<removed_email_address@domain.invalid>
wrote:

>
> Time to release your inner nerd.
>
> The task for this Ruby Q. 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 Q. entry :)

Cheers,
Jim F. (Guest)
on 2006-01-06 23:55
(Received via mailing list)
On Jan 6, 2006, at 12:56 PM, Ruby Q. wrote:
> 	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 Z. (Guest)
on 2006-01-07 00:31
(Received via mailing list)
On 06/01/06, Jim F. <removed_email_address@domain.invalid> wrote:
> 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.

-austin
Robert R. (Guest)
on 2006-01-07 00:46
(Received via mailing list)
Ruby Q. schrieb:

>
>
Huhu.
How do you parse 5d6d7?
As (5d6)d7 or 5d(6d7) since there is no "Assoziativgesetz" like (AdB)dC
== Ad(BdC).
-
aTdHvAaNnKcSe
Matthew M. (Guest)
on 2006-01-07 00:52
(Received via mailing list)
> 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 A. (Guest)
on 2006-01-07 01:03
Moin,

Austin Z. 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
James G. (Guest)
on 2006-01-07 01:13
(Received via mailing list)
On Jan 6, 2006, at 5:04 PM, Sascha A. 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 G. II
Austin Z. (Guest)
on 2006-01-07 01:16
(Received via mailing list)
On 06/01/06, Sascha A. <removed_email_address@domain.invalid> wrote:
> Austin Z. 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.

-austin
Dave L. (Guest)
on 2006-01-07 01:22
(Received via mailing list)
On 1/6/06, Matthew M. <removed_email_address@domain.invalid> wrote:
> > 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.

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

Dave
James G. (Guest)
on 2006-01-07 01:25
(Received via mailing list)
On Jan 6, 2006, at 5:10 PM, James Edward G. II wrote:

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

I misread.  Sorry.

James Edward G. II
unknown (Guest)
on 2006-01-07 01:28
(Received via mailing list)
In article
<removed_email_address@domain.invalid>,
	Ruby Q. <removed_email_address@domain.invalid> 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 Z. (Guest)
on 2006-01-07 01:31
(Received via mailing list)
On 06/01/06, James Edward G. II <removed_email_address@domain.invalid> wrote:
> On Jan 6, 2006, at 5:04 PM, Sascha A. 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.

-austin
Jim F. (Guest)
on 2006-01-07 01:37
(Received via mailing list)
On Jan 6, 2006, at 4:31 PM, Austin Z. wrote:

[great explanation snipped]

> 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 G. (Guest)
on 2006-01-07 01:43
(Received via mailing list)
On Jan 6, 2006, at 5:35 PM, Jim F. 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 G. II
Jim F. (Guest)
on 2006-01-07 01:46
(Received via mailing list)
On Jan 6, 2006, at 5:41 PM, James Edward G. II wrote:

> On Jan 6, 2006, at 5:35 PM, Jim F. 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 F. (Guest)
on 2006-01-07 02:19
(Received via mailing list)
On 1/6/06, James Edward G. II <removed_email_address@domain.invalid> wrote:
> On Jan 6, 2006, at 5:35 PM, Jim F. 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 F.
(Guest)
on 2006-01-07 02:28
(Received via mailing list)
> 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 F. (Guest)
on 2006-01-07 02:37
(Received via mailing list)
On Jan 6, 2006, at 6:17 PM, Jacob F. wrote:

> 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 (Guest)
on 2006-01-07 03:10
(Received via mailing list)
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 Z. (Guest)
on 2006-01-07 03:37
(Received via mailing list)
On 06/01/06, Jim F. <removed_email_address@domain.invalid> 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.

-austin
James G. (Guest)
on 2006-01-07 03:37
(Received via mailing list)
On Jan 6, 2006, at 6:17 PM, Jacob F. wrote:

> 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 G. II
James G. (Guest)
on 2006-01-07 03:46
(Received via mailing list)
On Jan 6, 2006, at 6:35 PM, Jim F. wrote:

>>
>> 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 G. II
Pierre Barbier de Reuille (Guest)
on 2006-01-07 03:49
(Received via mailing list)
Austin Z. a écrit :
>>
>>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 (Guest)
on 2006-01-07 03:55
(Received via mailing list)
Morus W. a écrit :
> 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

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 K. (Guest)
on 2006-01-07 04:01
(Received via mailing list)
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 M. (Guest)
on 2006-01-07 04:16
(Received via mailing list)
> 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 B. (Guest)
on 2006-01-07 05:19
(Received via mailing list)
On Sat, 07 Jan 2006 01:44:26 -0000, James Edward G. II
<removed_email_address@domain.invalid> 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
J. Ryan S. (Guest)
on 2006-01-07 05:28
(Received via mailing list)
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 B. (Guest)
on 2006-01-07 06:20
(Received via mailing list)
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 G. (Guest)
on 2006-01-07 06:44
(Received via mailing list)
On Jan 6, 2006, at 9:26 PM, J. Ryan S. 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 G. II
Matt L. (Guest)
on 2006-01-07 07:47
(Received via mailing list)
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 (Guest)
on 2006-01-07 10:14
(Received via mailing list)
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 (Guest)
on 2006-01-07 10:14
(Received via mailing list)
> 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".
Matthew D Moss (Guest)
on 2006-01-07 10:14
(Received via mailing list)
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 +.
Henrik M. (Guest)
on 2006-01-07 11:32
(Received via mailing list)
On Fri, 2006-01-06 at 23:45, Robert R. 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 N. (Guest)
on 2006-01-07 14:06
(Received via mailing list)
"Matthew D Moss" <removed_email_address@domain.invalid> writes:

> ___ d ___   4       16  ___ d ___
> 5       5               1       4

So what are the maximum and minimum values of this?
Matthew D Moss (Guest)
on 2006-01-07 14:09
(Received via mailing list)
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.
Christian N. (Guest)
on 2006-01-07 14:27
(Received via mailing list)
"Ross B." <removed_email_address@domain.invalid> writes:

>      @asserts = {
...
>        '(5d5-4)d(16/d4)+3'   => 87,    #25d4 + 3
>      }

This is wrong, the maximum is 339: (25-4)d(16/1)+3.
Ross B. (Guest)
on 2006-01-07 15:14
(Received via mailing list)
On Sat, 07 Jan 2006 12:27:05 -0000, Christian N.
<removed_email_address@domain.invalid> wrote:

>> 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 B. - removed_email_address@domain.invalid

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 R. (Guest)
on 2006-01-07 15:53
(Received via mailing list)
> 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 N. (Guest)
on 2006-01-07 15:53
(Received via mailing list)
"Ross B." <removed_email_address@domain.invalid> writes:

>>> I'll post them to  maybe compare with others and see if I'm on the
>>
>
> Right?

Yeah.  I got your list wrong then, I thought the number means the
maximum reachable, not what to throw with loaded dice.  Sorry.
Robert R. (Guest)
on 2006-01-07 16:02
(Received via mailing list)
Robert R. schrieb:

>> 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
>
>
Ignore me for lifetime please.
Ross B. (Guest)
on 2006-01-07 16:14
(Received via mailing list)
On Sat, 07 Jan 2006 13:51:51 -0000, Christian N.
<removed_email_address@domain.invalid> wrote:

>>>> loaded  dice that always rolls it's number of sides to write some
>>>
>> 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.
Gavin K. (Guest)
on 2006-01-07 18:57
(Received via mailing list)
On Jan 7, 2006, at 5:03 AM, Christian N. wrote:
>>     ___ - ___       ___ / ___
>>     |       |       |       |
>> ___ 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.
Reinder V. (Guest)
on 2006-01-07 19:00
(Received via mailing list)
In article <removed_email_address@domain.invalid>,
 Christian N. <removed_email_address@domain.invalid> wrote:

> >     ___ - ___       ___ / ___
> >     |       |       |       |
> > ___ 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 K. (Guest)
on 2006-01-07 19:24
(Received via mailing list)
On Jan 7, 2006, at 9:55 AM, Gavin K. 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 K. (Guest)
on 2006-01-07 19:27
(Received via mailing list)
On Jan 7, 2006, at 9:55 AM, Gavin K. wrote:
> On Jan 7, 2006, at 5:03 AM, Christian N. wrote:
>> "Matthew D Moss" <removed_email_address@domain.invalid> 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 K. (Guest)
on 2006-01-07 19:27
(Received via mailing list)
On Jan 7, 2006, at 9:58 AM, Reinder V. 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 N. (Guest)
on 2006-01-07 20:33
(Received via mailing list)
Gavin K. <removed_email_address@domain.invalid> writes:

>
> 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.
Bill K. (Guest)
on 2006-01-07 20:36
(Received via mailing list)
From: "Gavin K." <removed_email_address@domain.invalid>
>
> 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 N. (Guest)
on 2006-01-07 20:55
Gavin K. wrote:
> On Jan 7, 2006, at 9:58 AM, Reinder V. 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]

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
Gavin K. (Guest)
on 2006-01-07 21:00
(Received via mailing list)
On Jan 7, 2006, at 11:36 AM, Bill K. wrote:
> From: "Gavin K." <removed_email_address@domain.invalid>
>> 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?

You're all too damned smart. :)

Brilliant!
Matthew S. (Guest)
on 2006-01-07 21:28
(Received via mailing list)
On Jan 7, 2006, at 18:32, Christian N. wrote:

>>
>> 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 M. (Guest)
on 2006-01-07 23:58
(Received via mailing list)
> 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 M. (Guest)
on 2006-01-08 00:04
(Received via mailing list)
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 M. (Guest)
on 2006-01-08 01:17
(Received via mailing list)
On 1/7/06, Christer N. <removed_email_address@domain.invalid> wrote:
>   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 (Guest)
on 2006-01-08 03:17
(Received via mailing list)
Austin Z. 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 (Guest)
on 2006-01-08 15:50
(Received via mailing list)
Matthew M. 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 R. (Guest)
on 2006-01-08 16:17
(Received via mailing list)
Pierre Barbier de Reuille schrieb:

>>error, in that it allows expressions like this:
>>
>
>and it is perfectly valid as there is no other way to understand that ...
>
>Pierre
>
>
>
>
Hmm so that expr has 18 as maximum result with fully loaded dice?

3d(d(d(d6)))
3d(d(d6))
3d(d6)
3d6

=> 18

Is that correct?
Gregory S. (Guest)
on 2006-01-08 16:26
(Received via mailing list)
On Sun, Jan 08, 2006 at 10:48:01PM +0900, Pierre Barbier de Reuille
wrote:
} Matthew M. a ?crit :
} > 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.
} >
} >
}
} 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
...

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 (Guest)
on 2006-01-08 16:51
(Received via mailing list)
Gregory S. a écrit :
> } >
> } > Feel free to do what you like.
> 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.
>

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
Luke B. (Guest)
on 2006-01-08 21:07
(Received via mailing list)
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 B.
Bill K. (Guest)
on 2006-01-08 21:13
(Received via mailing list)
Hi,

I finally finished a Ruby Q.!  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)
Matthew S. (Guest)
on 2006-01-08 21:17
(Received via mailing list)
> 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 S.            <removed_email_address@domain.invalid>
Institute for Communicating and Collaborative Systems
University of Edinburgh
Paul N. (Guest)
on 2006-01-08 21:29
(Received via mailing list)
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 (Guest)
on 2006-01-08 21:35
(Received via mailing list)
Ruby Q. wrote:
>
>
> 	> 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
Ross B. (Guest)
on 2006-01-08 21:44
(Received via mailing list)
Hi,

This is my quiz entry for Ruby Q. 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/quiz...

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 Q. 61, the quick way
# by Ross B.

# 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 Q., number 61 - Dice roller
# This entry by Ross B. (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 B. (Guest)
on 2006-01-08 21:50
(Received via mailing list)
On Sun, 08 Jan 2006 19:33:18 -0000, Ross B.
<removed_email_address@domain.invalid> wrote:

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

Sorry, I changed that. It gives a block now.
Christer N. (Guest)
on 2006-01-08 22:01
Very interesting, and different solutions, this time!

Here's my recursive descent solution with histogram:

=begin
Ruby Q. #61
by Matthew D Moss

Solution by Christer N.

"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 (Guest)
on 2006-01-08 22:05
(Received via mailing list)
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
Christian N. (Guest)
on 2006-01-08 22:14
(Received via mailing list)
# 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 Z. (Guest)
on 2006-01-08 22:29
(Received via mailing list)
Here is my submission. Yes, this is my first completed Ruby Q. ;)
Thanks to Eric M.'s syntax.rb for making this work. I've attached
it as well, because it's not easily accessible otherwise ;)

-austin
Pablo Hoch (Guest)
on 2006-01-08 23:45
(Received via mailing list)
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 (Guest)
on 2006-01-08 23:51
(Received via mailing list)
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 (Guest)
on 2006-01-08 23:54
(Received via mailing list)
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
James G. (Guest)
on 2006-01-09 00:24
(Received via mailing list)
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 G. II
James G. (Guest)
on 2006-01-09 00:30
(Received via mailing list)
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 G. II
Ross B. (Guest)
on 2006-01-09 00:39
(Received via mailing list)
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/...
Dominik B. (Guest)
on 2006-01-09 02:46
(Received via mailing list)
Here is my solution.

It's a recursive descent parser, that parses the full BNF posted by
Matthew M.. 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
Jay A. (Guest)
on 2006-01-09 04:28
(Received via mailing list)
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
Andrew McGuinness (Guest)
on 2006-01-09 09:03
(Received via mailing list)
I've decided to bite the bullet and post my overlong solution.

It's got a few extras in there:

$ ./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'm getting to grips with block-passing as a routine technique - the
evaluate() method "yield"s its result so that it can provide multiple
results - tens of thousands if you ask it to try every possible roll
involving lots of dice.  The roll_dice method had to be written
recursively for that to work:

   def roll_dice( numdice, sides )
     if ( numdice == 0 )
       yield null_value
     else
       roll_one(sides) do |first|
	roll_dice( numdice-1, sides ) do |rest|
	  yield( first + rest )
	end
       end
     end
   end

Depending on how roll_one has been overridden, "first" and "rest" can be
integers, frequency distributions, or objects representing the history
of every roll to get to this state.  In the last case, roll_one will
yield "sides" times, to give you every possible roll


I'm not quite comfortable with things like redefining Integer#+
If I had done that, I could have avoided a lot of kind_of? calls in
stuff like this:

   def eval_binop( force_same_type = true )
     subtree(:left).evaluate do |l|
       subtree(:right).evaluate do |r|
	if force_same_type
	  if r.kind_of?( Roll_stat ) and ! l.kind_of?( Roll_stat )
	    l = Roll_stat.new(l)
	  end
	end
	yield(l,r)
       end
     end
   end


The whole thing can generate the probability distributions reasonably
quickly - I had ideas of approximating large numbers of dice (100d6
and so on) with the appropriate normal distribution ("clipped" by max
and min values).

The exhaustive output is very large, though.  It would be worth
optimising that by taking out permutations.

I've shoved it on
http://homepage.ntlworld.com/a.mcguinness/files/dice.rb and attached it.
Gustav M. (Guest)
on 2006-01-09 12:28
(Received via mailing list)
this is my first ruby quiz, and here comes my solution.

as a couple of other solutions, it uses ruby to do the dirty work, and
implements the diceroll as a method on integer.

!g
Jacob F. (Guest)
on 2006-01-09 17:45
(Received via mailing list)
On 1/6/06, Jim F. <removed_email_address@domain.invalid> wrote:
>
> > On 1/6/06, James Edward G. II <removed_email_address@domain.invalid> wrote:
> > 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.

Heh, I wouldn't say that. But I will admit that most of my attraction
to the game is the mental challenge of managing (and taking advantage
of) the complex rule system. Call me munchkin. Shrug.

> What does 0 -> 10 mean. Does it mean a dice can have the
> values 0,1,2,3...10?

No, that was a typo on my part. Should have been 0 -> 9 (e.g. rand(10)).

> If so, why is a 0 never possible?

Zero is possible, but is interpreted as a 10. So the effective range
is 1 -> 19 (rand(10) + 1). In this respect, the outcome of a d10
follows the same rules of the outcome from a d6, d8 or d20 (rand(N) +
1). The presentation on the dice is the only difference. As noted by
Austin, this is primarily due to space limitations, but also for
convenience when using two d10 to simulate a d100.

Jacob F.
Jacob F. (Guest)
on 2006-01-09 17:54
(Received via mailing list)
On 1/6/06, Jacob F. <removed_email_address@domain.invalid> wrote:
> 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.

On 1/6/06, Jim F. <removed_email_address@domain.invalid> wrote:
> What does 0 -> 10 mean. Does it mean a dice can have the
> values 0,1,2,3...10?

On 1/9/06, Jacob F. <removed_email_address@domain.invalid> wrote:
> No, that was a typo on my part. Should have been 0 -> 9 (e.g. rand(10)).

Ok, I must fix this previous apology to Jim F.. 0 -> 10 was *not*
a typo. He just misinterpreted my notation. And seeing as I followed
his misinterpretation before second glance, it's perfectly justified.
In the paragraph above the -> represented "is interpreted as". So when
rolling two ten sided dice for a d100, double-nought ('00') is
interpreted as 100. When rolling a single d10, 0 is interpreted as 10.

Jacob F.
Jacob F. (Guest)
on 2006-01-09 18:00
(Received via mailing list)
On 1/7/06, Reinder V. <removed_email_address@domain.invalid> wrote:
>
> ([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]

As pointed out by others, the result should actually by [4,339]. The
error above is in the second to last step. [a,b]d[c,d] != [a*c,b*d],
it should be [a,b]d[c,d] = [a,b*d]. The minimum effective value on a
die roll is always one, regardless of the number of sides (ie. whether
using a c-sided die or d-sided die). The minimum number of rolls being
a, the minimum roll total would then be a*1 = a. So we have:

  [1,21]d[4,16]+3 =
  [1,21*16]+3 =
  [1+3,336+3] =
  [4,339]

Jacob F.
Jacob F. (Guest)
on 2006-01-09 18:06
(Received via mailing list)
On 1/7/06, Ron M <removed_email_address@domain.invalid> wrote:
> 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.

In fact, I've seen a variation on this for the infamous "d3" somtimes
used in D&D. The standard approach is to roll a d6 then divide the
result by two (rounding up). However, one dice seller instead offers a
non-regular tetrahedron where one vertex is stretched out away from
the fourth side. This makes the three stretched side much more likely
than the fourth side, and given the dynamics of the dice, it is very
unlikely for it to land standing on that fourth side. So you have, in
effect, a three sided die. If the fourth side does ever come up, you
just reroll.

Jacob F.
Robert R. (Guest)
on 2006-01-09 18:18
(Received via mailing list)
I really love this mailing list.
It's simple and with threaded view it's perfect.
But sometimes a forum with an edit function is handy too :>
Adam S. (Guest)
on 2006-01-09 19:22
(Received via mailing list)
Here's my solution.

I spent a few hours writing a BNF parser which was supposed to let me do
this:

-- begin buggy code --
CENT = BnfTerm.new(/(%)/ ) { '100' }
INTEGER = /([1-9][0-9]*)/
DICE = BnfTerm.new(CENT,:|,INTEGER)
term = BnfTerm.new()
ROLL = BnfTerm.new(term, /d/, DICE) {|a,b|
(1..a.to_i).inject(0){|s,i|s+rand(b.to_i)+1} }
term.define(DICE, :|,ROLL) {|m| m}
#...
class Dice
  @@rule = DIEROLL
  def initialize expr
    @expr = expr
  end
  def roll
   @@rule.parse(@expr)
  end
end
-- end --
but it was too brittle, and it would go into endless recursion on a
lot of valid inputs.

So I switched to a quick,short simple solution: add a #d method to
integer and let eval do the work:

--- dice.rb --
class Integer
  def d n
    (0...self).inject(0){|s,i| s+rand(n)+1}
  end
end

class Dice
  def initialize str
    @rule= str.gsub(/%/,'100').gsub(/([^\d)]|^)d/,'\1 1d')  # %->100
and bare d ->1d
    while @rule.gsub!(/([^.])d(\d+|\(.*\))/,'\1.d(\2)')          #
'dX' ->  '.d(X)'
    end
    #repeat to deal with nesting
  end
  def roll
    eval(@rule)
  end
end

d = Dice.new(ARGV[0]||'d6')
(ARGV[1] || 1).to_i.times { print "#{d.roll}  " }
puts

---
-Adam
Gregory S. (Guest)
on 2006-01-09 19:34
(Received via mailing list)
On Sat, Jan 07, 2006 at 04:00:52AM +0900, Gregory S. wrote:
} On Sat, Jan 07, 2006 at 03:56:47AM +0900, Ruby Q. 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.

Okay, so I said I wanted the full BNF, and I thought it would be useful
if
I could find a convenient Ruby lex/yacc. Well, I couldn't. I now have
two
solutions, both using my own parsing. Both use eval. One adds a method
to
Fixnum to let eval do even more work. The more complicated version with
syntax trees, which came first, took roughly two hours to work out. The
second, simpler version with the Fixnum method took about 20 minutes to
build from the first version. Note that I maintained left associativity
with the d operator in both methods without having the 3dddd6 problem
Matthew M. mentioned.

test61.rb runs the code on commandline arguments
61.rb is the complicated syntax tree version
61alt.rb is the simpler Fixnum method version

##### test61.rb
################################################################

#require '61'
require '61alt'

d = Dice.new(ARGV[0])
(ARGV[1] || 1).to_i.times { print "#{d.roll} " }
puts ''

##### 61.rb
####################################################################

module DiceRoller

  class ArithOperator
    def initialize(left, op, right)
      @left = left
      @op = op
      @right = right
    end

    def to_i
      return (eval "#{@left.to_i}#{@op}#{@right.to_i}")
    end
  end

  class DieOperator
    #op is a dummy
    def initialize(left, op, right)
      @left = left
      @right = right
    end

    def to_i
      count = @left.to_i
      fail "Die count must be nonnegative: '#{count}'" if count < 0
      die = @right.to_i
      fail "Die size must be positive: '#{die}'" if die < 1
      return (1..count).inject(0) { |sum, waste| sum + (rand(die)+1) }
    end
  end

  OpClass = { '+' => ArithOperator,
              '-' => ArithOperator,
              '*' => ArithOperator,
              '/' => ArithOperator,
              'd' => DieOperator }

  def lex(str)
    tokens = str.scan(/(00)|([-*\/()+d%0])|([1-9][0-9]*)|(.+)/)
    tokens.each_index { |i|
      tokens[i] = tokens[i].compact[0]
      if not /^(00)|([-*\/()+d%0])|([1-9][0-9]*)$/ =~ tokens[i]
        if /^\s+$/ =~ tokens[i]
          tokens[i] = nil
        else
          fail "Found garbage in expression: '#{tokens[i]}'"
        end
      end
    }
    return tokens.compact
  end

  def validate_and_cook(tokens)
    oper = /[-*\/+d]/
    num = /(\d+)|%/
    last_was_op = true
    paren_depth = 0
    prev = ''
    working = []
    tokens.each_index { |i|
      tok = tokens[i]
      if num =~ tok
        fail 'A number cannot follow an expression!' if not last_was_op
        fail 'Found spurious zero or number starting with zero!' if tok
== '0'
        if ( tok == '00' || tok == '%' )
          fail 'Can only use % or 00 after d!' if prev != 'd'
          tokens[i] = 100
          working << 100
        else
          working << tok.to_i
        end
        last_was_op = false
      elsif oper =~ tok
        if last_was_op
          #handle case of dX meaning 1dX
          if tok == 'd'
            fail 'A d cannot follow a d!' if prev == RollMethod
            working << 1
          else
            fail 'An operator cannot follow a operator!'
          end
        end
        working << tok
        last_was_op = true
      elsif tok == "("
        fail 'An expression cannot follow an expression!' if not
last_was_op
        paren_depth += 1
        working << :p_open
      elsif tok == ")"
        fail 'Incomplete expression at close paren!' if last_was_op
        fail 'Too many close parens!' if paren_depth < 1
        paren_depth -= 1
        last_was_op = false
        working << :p_close
      else #what did I miss?
        fail "What kind of token is this? '#{tok}'"
      end
      prev = tok
    }
    fail 'Missing close parens!' if paren_depth != 0
    return working
  end

  def parse_parens(tokens)
    working = []
    i = 0
    while i < tokens.length
      if tokens[i] == :p_open
        i += 1
        paren_depth = 0
        paren_tokens = []
        while (tokens[i] != :p_close) || (paren_depth > 0)
          if tokens[i] == :p_open
            paren_depth += 1
          elsif tokens[i] == :p_close
            paren_depth -= 1
          end
          paren_tokens << tokens[i]
          i += 1
        end
        working << parse(paren_tokens)
      else
        working << tokens[i]
      end
      i += 1
    end
    return working
  end

  def parse_ops(tokens, regex)
    fail "Something broke: len = #{tokens.length}" if tokens.length < 3
||  (tokens.length % 2) == 0
    i = 1
    working = [ tokens[0] ]
    while i < tokens.length
      if regex =~ tokens[i].to_s
        op = OpClass[tokens[i]]
        lindex = working.length-1
        working[lindex] = op.new(working[lindex], tokens[i],
tokens[i+1])
      else
        working << tokens[i]
        working << tokens[i+1]
      end
      i += 2
    end
    return working
  end

  #scan for parens, then d, then */, then +-
  def parse(tokens)
    working = parse_parens(tokens)
    fail "Something broke: len = #{working.length}" if (working.length %
2) == 0
    working = parse_ops(working, /^d$/) if working.length > 1
    fail "Something broke: len = #{working.length}" if (working.length %
2) == 0
    working = parse_ops(working, /^[*\/]$/) if working.length > 1
    fail "Something broke: len = #{working.length}" if (working.length %
2) == 0
    working = parse_ops(working, /^[+-]$/) if working.length > 1
    fail "Something broke: len = #{working.length}" if working.length !=
1
    return working[0]
  end

  def parse_dice(str)
    tokens = lex(str)
    return parse(validate_and_cook(tokens))
  end

end

class Dice

  def initialize(expression)
    @expression = parse_dice(expression)
  end

  def roll
    return @expression.to_i
  end

  private

  include DiceRoller

end

##### 61alt.rb
#################################################################

module DiceRoller

  RollMethod = '.roll'

  def lex(str)
    tokens = str.scan(/(00)|([-*\/()+d%0])|([1-9][0-9]*)|(.+)/)
    tokens.each_index { |i|
      tokens[i] = tokens[i].compact[0]
      if not /^(00)|([-*\/()+d%0])|([1-9][0-9]*)$/ =~ tokens[i]
        if /^\s+$/ =~ tokens[i]
          tokens[i] = nil
        else
          fail "Found garbage in expression: '#{tokens[i]}'"
        end
      end
    }
    return tokens.compact
  end

  def validate_and_cook(tokens)
    oper = /[-*\/+d]/
    num = /(\d+)|%/
    last_was_op = true
    paren_depth = 0
    prev = ''
    working = []
    tokens.each_index { |i|
      tok = tokens[i]
      if num =~ tok
        fail 'A number cannot follow an expression!' if not last_was_op
        fail 'Found spurious zero or number starting with zero!' if tok
== '0'
        if ( tok == '00' || tok == '%' )
          fail 'Can only use % or 00 after d!' if prev != RollMethod
          tokens[i] = 100
          tok = 100
        else
          tok = tok.to_i
        end
        if prev == RollMethod
          working << "(#{tok})"
        else
          working << tok
        end
        last_was_op = false
      elsif oper =~ tok
        tok = RollMethod if tok == 'd'
        if last_was_op
          #handle case of dX meaning 1dX
          if tok == RollMethod
            fail 'A d cannot follow a d!' if prev == RollMethod
            working << 1
          else
            fail 'An operator cannot follow a operator!'
          end
        end
        working << tok
        last_was_op = true
      elsif tok == "("
        fail 'An expression cannot follow an expression!' if not
last_was_op
        paren_depth += 1
        working << tok
      elsif tok == ")"
        fail 'Incomplete expression at close paren!' if last_was_op
        fail 'Too many close parens!' if paren_depth < 1
        paren_depth -= 1
        last_was_op = false
        working << tok
      else #what did I miss?
        fail "What kind of token is this? '#{tok}'"
      end
      prev = tok
    }
    fail 'Missing close parens!' if paren_depth != 0
    return working
  end

  def parse_dice(str)
    tokens = lex(str)
    return validate_and_cook(tokens).to_s
  end

end

class Fixnum
  def roll(die)
    fail "Die count must be nonnegative: '#{self}'" if self < 0
    fail "Die size must be positive: '#{die}'" if die < 1
    return (1..self).inject(0) { |sum, waste| sum + (rand(die)+1) }
  end
end

class Dice

  def initialize(expression)
    @expression = parse_dice(expression)
  end

  def roll
    return (eval @expression)
  end

  private

  include DiceRoller

end
Ross B. (Guest)
on 2006-01-09 21:59
(Received via mailing list)
On Sun, 08 Jan 2006 19:33:18 -0000, a complete idiot (i.e. me) wrote:

> As well as the main 'roll.rb' I also included a separate utility that
> uses loaded dice to find min/max achievable.

Yeah, well, the penny dropped. That'll teach me to try and understand
maths - I did so well at avoiding it in the main thing, then decided to
try that...

Now I've succeeded in making a *complete* fool of myself I'll just get
me
coat...
Dennis Ranke (Guest)
on 2006-01-10 15:50
(Received via mailing list)
James Edward G. II wrote:
>> I think I'd really like to see a production quality parser(generator)
>> using something like this grammar format.
>
> I agree.  This is fantastic.

Thanks :)

> So what do we have to do to get you to add the polish and make it
> available?  :)

I have put it on my mental to-do list, but that doesn't necessarily mean
that I actually get around to doing it. ;)
Right now the grammar is subject to quite some restrictions that I would
like to remove, but I'll need to learn more about parser generators to
do this.

Dennis
unknown (Guest)
on 2006-01-10 21:19
(Received via mailing list)
Another solution:

#! /usr/bin/ruby

# change this to some fixed value for reproducable results
def random(i)
#  i
  # FIXME: check rand's usabilty for throwing dices...
  rand(i)+1
end

class DiceExpr

  def initialize(rolls, sides)
    @rolls, @sides = rolls, sides
  end

  def to_i
    sides = @sides.to_i
    (removed_email_address@domain.invalid_i).inject(0) { | sum, i | sum += 
random(sides) }
  end

  def to_s
    "(#{@rolls}d#{@sides})"
  end

end

class Expr

  def initialize(lhs, rhs, op)
    @lhs, @rhs, @op = lhs, rhs, op
  end

  def to_i
    @lhs.to_i.send(@op, @rhs.to_i)
  end

  def to_s
    "(#{@lhs}#{@op}#{@rhs})"
  end

end

class Dice

  def initialize(expr)
    @expr_org = @expr_str = expr
    next_token
    @expr = addend()
    if @token
      raise "parser error: tokens left: >#{@fulltoken}#{@expr_str}<"
    end
  end

  # "lexer"
  @@regex = Regexp.compile(/^\s*([()+\-*\/]|[1-9][0-9]*|d%|d)\s*/)
  def next_token
    @prev_token = @token
    return @token = nil   if @expr_str.empty?
    match = @@regex.match(@expr_str)
    if !match
      raise "parser error: cannot tokenize input #{@expr_str}"
    end
    @expr_str = @expr_str[match.to_s.length, @expr_str.length]
    @fulltoken = match.to_s # for "tokens left" error message only...
    @token = match[1]
  end

  # "parser"
  # bit lengthy but basically straightforward
  def number() # number or parenthesized expression
    raise "unexpeced >)<" if ( @token == ')' )
    if ( @token == '(' )
      next_token
      val = addend
      raise "parser error: parenthesis error, expected ) got #{@token}"
if @token != ')'
      next_token
      return val
    end
    raise "parse error: number expected, got #{@token}" if @token !~
/^[0-9]*$/
    next_token
    @prev_token
  end

  def dice()
    if ( @token == 'd' )
      rolls = 1
    else
      rolls = number()
    end
    while ( @token == 'd' || @token == 'd%' )
      if @token == 'd%'
        rolls = DiceExpr.new(rolls, 100)
        next_token
      else
        next_token
        sides = number()
        raise "parser error: missing sides expression" if !sides
        rolls = DiceExpr.new(rolls, sides)
      end
    end
    rolls
  end

  def factor()
    lhs = dice()
    while ( @token == '*' || @token == '/' )
      op = @token
      next_token
      rhs = dice()
      raise "parser error: missing factor" if !rhs
      lhs = Expr.new(lhs, rhs, op)
    end
    lhs
  end

  def addend()
    lhs = factor()
    while ( @token == '+' || @token == '-' )
      op = @token
      next_token
      rhs = factor()
      raise "parser error: missing addend" if !rhs
      lhs = Expr.new(lhs, rhs, op)
    end
    lhs
  end

  def to_s
    "#{@expr_org} -> #{@expr.to_s}"
  end

  def roll
    @expr.to_i
  end

end

d = Dice.new(ARGV[0])

#puts d.to_s

(ARGV[1] || 1).to_i.times { print "#{d.roll} " }
puts
This topic is locked and can not be replied to.