The three rules of Ruby Quiz: 1. Please do not post any solutions or spoiler discussion for this quiz until 48 hours have passed from the time on this message. 2. Support Ruby Quiz by submitting ideas as often as you can: http://www.rubyquiz.com/ 3. Enjoy! -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= by Matthew D Moss Time to release your inner nerd. The task for this Ruby Quiz is to write a dice roller. You should write a program that takes two arguments: a dice expression followed by the number of times to roll it (being optional, with a default of 1). So to calculate those stats for your AD&D character, you would do this: > roll.rb "3d6" 6 72 64 113 33 78 82 Or, for something more complicated: > roll.rb "(5d5-4)d(16/d4)+3" 31 [NOTE: You'll usually want quotes around the dice expression to hide parenthesis from the shell, but the quotes are not part of the expression.] The main code of roll.rb should look something like this: d = Dice.new(ARGV[0]) (ARGV[1] || 1).to_i.times { print "#{d.roll} " } The meat of this quiz is going to be parsing the dice expression (i.e., implementing Dice.new). Let's first go over the grammar, which I present in a simplified BNF notation with some notes: <expr> := <expr> + <expr> | <expr> - <expr> | <expr> * <expr> | <expr> / <expr> | ( <expr> ) | [<expr>] d <expr> | integer * Integers are positive; never zero, never negative. * The "d" (dice) expression XdY rolls a Y-sided die (numbered from 1 to Y) X times, accumulating the results. X is optional and defaults to 1. * All binary operators are left-associative. * Operator precedence: ( ) highest d * / + - lowest [NOTE: The BNF above is simplified here for clarity and space. If requested, I will make available the full BNF description I've used in my own solution, which incorporates the association and precedence rules.] A few more things... Feel free to either craft this by hand or an available lexing/parsing library. Handling whitespace between integers and operators is nice. Some game systems use d100 quite often, and may abbreviate it as "d%" (but note that '%' is only allowed immediately after a 'd').

on 2006-01-06 19:57

on 2006-01-06 20:03

On Sat, Jan 07, 2006 at 03:56:47AM +0900, Ruby Quiz wrote: [...] } [NOTE: The BNF above is simplified here for clarity and space. If } requested, I will make available the full BNF description I've used in my } own solution, which incorporates the association and precedence rules.] I would appreciate the full BNF, please. --Greg

on 2006-01-06 20:15

On 1/6/06, Ruby Quiz <james@grayproductions.net> wrote: > Or, for something more complicated: > > > roll.rb "(5d5-4)d(16/d4)+3" > 31 I assume integer arithmetic? So if, for example, a 3 comes up on your d4, 16/d4 would be 5? Jacob Fugal

on 2006-01-06 20:30

Please don't take this the wrong way, but I've never played D&D. Would someone mind explaining the math that went into the command below to generate it's result? ~ ryan ~

on 2006-01-06 20:36

On 06/01/06, J. Ryan Sobol <ryansobol@gmail.com> wrote: > Please don't take this the wrong way, but I've never played D&D. > Would someone mind explaining the math that went into the command > below to generate it's result? I suspect user error. The correct answer will always be between 3 and 18 for 3d6. -austin

on 2006-01-06 20:39

On Jan 6, 2006, at 1:29 PM, J. Ryan Sobol wrote: >> >> The task for this Ruby Quiz 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 Gray II

on 2006-01-06 20:39

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.

on 2006-01-06 20:42

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

on 2006-01-06 20:48

On 1/6/06, Matthew Moss <matthew.moss.coder@gmail.com> wrote: > Ha ha... Must have copied the wrong line when writing up the quiz > description. > > That should look like this: > > > roll.rb "3d6" 6 > 18 18 18 18 18 18 > Actually 3d6 means roll a 6 sided die 3 times so you would have a result of 3-18 so this: > roll.rb "3d6" 6 Would actully be: (RND = Random) RND(3-18) RND(3-18) RND(3-18) RND(3-18) RND(3-18) RND(3-18) Below is 3d6 from the DnD Dice Roller on Wizards.com. The +0 would be a modifier from depending if it was an attack roll or a defense roll. For our purposes you would remove the +0 Roll(3d6)+0: 1,6,6,+0 Total:13 DnD Dice Roller: http://www.wizards.com/default.asp?x=dnd/dnd/20040517a Will -- Will Shattuck ( willshattuck.at.gmail.com ) Home Page: http://www.thewholeclan.com/will When you get to your wit's end, you'll find God lives there.

on 2006-01-06 20:51

On 06/01/06, Matthew Moss <matthew.moss.coder@gmail.com> wrote: > Ha ha... Must have copied the wrong line when writing up the quiz > description. > > That should look like this: > > > roll.rb "3d6" 6 > 18 18 18 18 18 18 Just don't tell me that the first one is 18/00. -austin

on 2006-01-06 20:54

On Jan 6, 2006, at 1:49 PM, Austin Ziegler wrote: > On 06/01/06, Matthew Moss <matthew.moss.coder@gmail.com> wrote: >> Ha ha... Must have copied the wrong line when writing up the quiz >> description. >> >> That should look like this: >> >>> roll.rb "3d6" 6 >> 18 18 18 18 18 18 > > Just don't tell me that the first one is 18/00. <dies laughing> They all were, of course. James Edward Gray II

on 2006-01-06 20:54

```
On 1/6/06, Will Shattuck <willshattuck@gmail.com> 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. ;)
```

on 2006-01-06 21:22

I guess that must be a D&D inside half-joke because I'm totally confused. Don't worry about explaining it as I just needed to know what that command, roll.rb "3d6" 6, did. ~ ryan ~ On Jan 6, 2006, at 2:39 PM, Matthew Moss wrote: >> roll.rb "3d6" 6 > 18 18 18 18 18 18 On Jan 6, 2006, at 2:49 PM, Austin Ziegler wrote: > Just don't tell me that the first one is 18/00. On Jan 6, 2006, at 2:53 PM, James Edward Gray II wrote: > <dies laughing> They all were, of course.

on 2006-01-06 21:28

On Jan 6, 2006, at 2:21 PM, J. Ryan Sobol wrote: > I guess that must be a D&D inside half-joke because I'm totally > confused. 18 was the best stat a starting character could have. If you got one, they let you roll d% and put it after the slash (the higher the better). 00 == 100. So characters with 18/00 had some damn lucky die rolls. :) James Edward Gray II

on 2006-01-06 21:31

On 06/01/06, James Edward Gray II <james@grayproductions.net> wrote: > On Jan 6, 2006, at 2:21 PM, J. Ryan Sobol wrote: >> I guess that must be a D&D inside half-joke because I'm totally >> confused. > 18 was the best stat a starting character could have. If you got > one, they let you roll d% and put it after the slash (the higher the > better). 00 == 100. So characters with 18/00 had some damn lucky > die rolls. :) In most versions of D&D/AD&D, this was also limited to Strength attributes only. This may have changed recently. ;) -austin

on 2006-01-06 21:37

On Jan 6, 2006, at 2:29 PM, Austin Ziegler 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 Gray II

on 2006-01-06 21:40

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

on 2006-01-06 21:43

forgive my ignorance... BNF? w On 1/6/06, Matthew Moss <matthew.moss.coder@gmail.com> 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 Shattuck ( willshattuck.at.gmail.com ) Home Page: http://www.thewholeclan.com/will When you get to your wit's end, you'll find God lives there.

on 2006-01-06 21:52

> forgive my ignorance... BNF? http://www.garshol.priv.no/download/text/bnf.html http://en.wikipedia.org/wiki/Backus-Naur_form HTH, Bill

on 2006-01-06 22:10

On Fri, 06 Jan 2006 18:56:47 -0000, Ruby Quiz <james@grayproductions.net> wrote: > > Time to release your inner nerd. > > The task for this Ruby Quiz is to write a dice roller. Well, I'm no D&Der, but I think I'm gonna hand in my solution for this one as my first Ruby Quiz entry :) Cheers,

on 2006-01-06 22:55

On Jan 6, 2006, at 12:56 PM, Ruby Quiz 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?

on 2006-01-06 23:31

On 06/01/06, Jim Freeze <jim@freeze.org> 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

on 2006-01-06 23:46

Ruby Quiz schrieb: > > Huhu. How do you parse 5d6d7? As (5d6)d7 or 5d(6d7) since there is no "Assoziativgesetz" like (AdB)dC == Ad(BdC). - aTdHvAaNnKcSe

on 2006-01-06 23:52

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

on 2006-01-07 00:03

Moin, Austin Ziegler wrote: > No. d00/d%/d100 all refer to values from 1 to 100. It should be > considered impossible to get a value of 0 from dice. Strictly speaking, > d100 should be a special case simulated where you are rolling two d10 > values and treating one of them as the 10s and one of them as the 1s. > Again, it results in a slightly different curve than a pure d100 result > would be. How exactly would those d10s differ from a d100? < One gaming system developed by Gary Gygax after he was ousted > from TSR in the mid-80s used what he termed d10x, which was d10*d10, > resulting in values from 1 - 100 with a radically different probability > curve than a normal d100. Not only a different curve, but also some values would be impossible to get (as 13 and 51) *Sascha

on 2006-01-07 00:13

On Jan 6, 2006, at 5:04 PM, Sascha Abel wrote: > < One gaming system developed by Gary Gygax after he was ousted >> from TSR in the mid-80s used what he termed d10x, which was d10*d10, >> resulting in values from 1 - 100 with a radically different >> probability >> curve than a normal d100. > > Not only a different curve, but also some values would be > impossible to > get (as 13 and 51) Na, if you get a 1 on the tens die and a 3 on the ones die, you have rolled a 13. James Edward Gray II

on 2006-01-07 00:16

On 06/01/06, Sascha Abel <sascha.abel@ewetel.name> wrote: > Austin Ziegler wrote: >> No. d00/d%/d100 all refer to values from 1 to 100. It should be >> considered impossible to get a value of 0 from dice. Strictly >> speaking, d100 should be a special case simulated where you are >> rolling two d10 values and treating one of them as the 10s and one of >> them as the 1s. Again, it results in a slightly different curve than >> a pure d100 result would be. > How exactly would those d10s differ from a d100? In the same way that 3d6 is different than rand(16)+3. It's not necessarily as dramatic a difference, but IME, the incidences of the very lows (01-19) and very highs (81-00) are not as common as those in the middle. >> One gaming system developed by Gary Gygax after he was ousted from >> TSR in the mid-80s used what he termed d10x, which was d10*d10, >> resulting in values from 1 - 100 with a radically different >> probability curve than a normal d100. > Not only a different curve, but also some values would be impossible > to get (as 13 and 51) Yes. -austin

on 2006-01-07 00:22

On 1/6/06, Matthew Moss <matthew.moss.coder@gmail.com> 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

on 2006-01-07 00:25

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

on 2006-01-07 00:28

In article <20060106185354.LPAB613.centrmmtao03.cox.net@localhost.localdomain>, Ruby Quiz <james@grayproductions.net> writes: > Or, for something more complicated: > > > roll.rb "(5d5-4)d(16/d4)+3" > 31 What's the execution order in this case? Do 5d5-4 rolls with 5d5-4 probably different dices having 16/d4 sides (number of sides calculated for each roll individually) or should one choose the number of sides once for all rolls? I guess it doesn't make much difference but it should be specified... Morus

on 2006-01-07 00:31

On 06/01/06, James Edward Gray II <james@grayproductions.net> wrote: > On Jan 6, 2006, at 5:04 PM, Sascha Abel wrote: > I wrote: > >> One gaming system developed by Gary Gygax after he was ousted from > >> TSR in the mid-80s used what he termed d10x, which was d10*d10, > >> resulting in values from 1 - 100 with a radically different > >> probability curve than a normal d100. >> Not only a different curve, but also some values would be impossible >> to get (as 13 and 51) > Na, if you get a 1 on the tens die and a 3 on the ones die, you have > rolled a 13. That's for d%; I was referring to "Cyborg Commando" which had a d10x, which is (d10)*(d10), making a 1,3 combination 3 always. You'd never get a prime number larger than 7 under the d10x system. combo = Hash.new(0) 1.upto(10) { |i| 1.upto(10) { |j| combo[i * j] += 1 } } There are 42 possible values here, and 9 values (6, 8, 10, 12, 18, 20, 24, 30, 40) appear four times each. Four values (4, 9, 16, 36) appear three times each, 23 values twice each, and 6 values once. It was a truly fucked up system. I think it's because he was mad to be ousted. -austin

on 2006-01-07 00:37

On Jan 6, 2006, at 4:31 PM, Austin Ziegler 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

on 2006-01-07 00:43

```
On Jan 6, 2006, at 5:35 PM, Jim Freeze wrote:
> What do you need to roll to get a 0 and 100?
A zero on the tens dice is 10. On the one's dice, it's zero. 00 is
100.
James Edward Gray II
```

on 2006-01-07 00:46

On Jan 6, 2006, at 5:41 PM, James Edward Gray II wrote: > On Jan 6, 2006, at 5:35 PM, Jim Freeze wrote: > >> What do you need to roll to get a 0 and 100? > > A zero on the tens dice is 10. On the one's dice, it's zero. 00 > is 100. > That doesn't jive with what was said earlier. There should be no zero on the tens dice. Only 1..10. Jim

on 2006-01-07 01:19

On 1/6/06, James Edward Gray II <james@grayproductions.net> wrote: > On Jan 6, 2006, at 5:35 PM, Jim Freeze wrote: > > > What do you need to roll to get a 0 and 100? > > A zero on the tens dice is 10. On the one's dice, it's zero. 00 is > 100. Clarification: presented in short, long and practical. :) Short clarification: Actually, when rolled together, both dice are zero-based. The double-nought is the only special combination of 00 -> 100. When rolled singly, a d10 has 0 -> 10. Rolling a 0 is never possible. Long clarification: Normally, a d% is rolled as a combination of a "d100" and a d10. "d100" is in quotes, because it's actually just a special d10 -- 10 sided die, that is -- except the numbers on the "d100" are 00, 10, 20... 90. The numbers on the d10 are 0, 1, 2... 9. Rolling the two together and adding you have a range from 0..99. However, since the tables that require a d% roll are normally 1-based (1..100), the 'double-nought' -- a 00 on the "d100" and 0 on the d10 -- is considered 100, everything else is face value. Some examples: 00 / 5 -> 5 10 / 5 -> 15 20 / 0 -> 20 00 / 0 -> 100 Similarly, when asked to roll a d10, the face numbers are 0..9, but are interpreted as 1..10 by making the 0 a 10 and leaving the other faces at face value. All other dice (in my experience) are always interpreted as face value (the sides being 1-based). Regarding the probability curve of a d% versus a true d100 (100-sided die), they are the same. Consider the d100: there are 100 faces, each with a 1% probability. With a d% roll ("d100" + d10), each integer between 1 and 100 (again, double-nought counting as 100) is produced exactly once, and with the same probability. 53 (produced only by 50 + 3) is no more likely than 99 (90 + 9) or 1 (00 + 1). So for all intents and purposes, a d% is equivalent to a d100. Practical clarification: As mentioned above, rolling two ten-sided dice versus rolling a 100-sided dice produce the same distribution (given the method of combination). Rolling a ten-sided zero-based die then converting 0 to 10 versus rolling a ten-sided one-based die produce the same distribution. So if you see dM then rand(M) + 1 will produce the correct distribution. d% counts as d100. Now if you'll excuse me, I'm late for an appointment with *my* dice. Your die-rolling lesson for the day was brought to you by the numbers 3, 5 and the letter D. :) Jacob Fugal

on 2006-01-07 01:28

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

on 2006-01-07 01:37

On Jan 6, 2006, at 6:17 PM, Jacob Fugal 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

on 2006-01-07 02:10

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

on 2006-01-07 02:37

On 06/01/06, Jim Freeze <jim@freeze.org> wrote: > No wonder I don't play D&D. I don't think I am smart enough. > > What does 0 -> 10 mean. Does it mean a dice can have the > values 0,1,2,3...10? > > If so, why is a 0 never possible? > > And why does d10 have 0 -> 10 while a d6 has 1 -> 6? d10 has size 0..9, generally because of size (the print on them is typically only large enough to have one decimal digit). 0 is rarely a useful number in gaming, so it is treated as a 10 result. Therefore rand(10) + 1 is sufficient to represent d10. When used as d100, you'll get the values 00 .. 99, but again, 00 is not a useful value so it is treated as 100. So rand(100) + 1 is sufficient to represent d100. -austin

on 2006-01-07 02:37

On Jan 6, 2006, at 6:17 PM, Jacob Fugal 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 Gray II

on 2006-01-07 02:46

On Jan 6, 2006, at 6:35 PM, Jim Freeze 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 Gray II

on 2006-01-07 02:49

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

on 2006-01-07 02:55

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

on 2006-01-07 03:01

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

on 2006-01-07 03:16

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

on 2006-01-07 04:19

On Sat, 07 Jan 2006 01:44:26 -0000, James Edward Gray II <james@grayproductions.net> wrote: [snip involved dice discussion] > Honestly though, I won't think less of you if you generate a random > number between 1 and 100. :) > Phew :D

on 2006-01-07 04:28

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 ~

on 2006-01-07 05:20

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.

on 2006-01-07 05:44

On Jan 6, 2006, at 9:26 PM, J. Ryan Sobol wrote: > Thank you all for pitching in your explanation of dice rollers to > non-D&D players like my self. However, there's a considerable > amount of noise for this post (already) and I'm not 100% confident > I could parse the dice syntax in English let alone ruby. Would it > be possible to summarize this discussion and post it as an addendum > at http://www.rubyquiz.com/quiz61.html ? Feel free to summarize here, but I generally don't add the discussion to the quizzes themselves. I like to keep them pretty basic an we can always go to the archives as needed, I figure. I did correct the error on the site though. :) James Edward Gray II

on 2006-01-07 06:47

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_

on 2006-01-07 09:14

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

on 2006-01-07 09:14

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

on 2006-01-07 09:14

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

on 2006-01-07 10:32

On Fri, 2006-01-06 at 23:45, Robert Retzbach wrote: > > > Huhu. > How do you parse 5d6d7? > As (5d6)d7 or 5d(6d7) since there is no "Assoziativgesetz" like (AdB)dC > == Ad(BdC). > - > aTdHvAaNnKcSe With the game systems I know, and I admit I haven't played for a couple of years, 5d6d7 would not be a legal expression, and would raise an exception. /Henrik -- http://www.henrikmartensson.org/ - Reflections on software development

on 2006-01-07 13:06

"Matthew D Moss" <matthew.moss.coder@gmail.com> writes: > ___ d ___ 4 16 ___ d ___ > 5 5 1 4 So what are the maximum and minimum values of this?

on 2006-01-07 13:09

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

on 2006-01-07 13:27

"Ross Bamford" <rosco@roscopeco.remove.co.uk> writes: > @asserts = { ... > '(5d5-4)d(16/d4)+3' => 87, #25d4 + 3 > } This is wrong, the maximum is 339: (25-4)d(16/1)+3.

on 2006-01-07 14:14

On Sat, 07 Jan 2006 12:27:05 -0000, Christian Neukirchen <chneukirchen@gmail.com> 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 Bamford - rosco@roscopeco.remove.co.uk I don't understand that. I get: (5d5-4)d(16/d4)+3 = 87 (5d5-4)d(16/d1)+3 = 339 I read the first as 21 rolls (25 - 4) of a four sided (16 / 4) dice plus 3, while the second is 21 rolls (25 - 4) of a 16 sided (16 / 1) dice, plus 3. Right?

on 2006-01-07 14:53

> 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

on 2006-01-07 14:53

"Ross Bamford" <rosco@roscopeco.remove.co.uk> 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.

on 2006-01-07 15:02

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

on 2006-01-07 15:14

On Sat, 07 Jan 2006 13:51:51 -0000, Christian Neukirchen <chneukirchen@gmail.com> 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.

on 2006-01-07 17:57

On Jan 7, 2006, at 5:03 AM, Christian Neukirchen 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.

on 2006-01-07 18:00

In article <m2fyo0gs8o.fsf@lilith.local>, Christian Neukirchen <chneukirchen@gmail.com> 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

on 2006-01-07 18:24

```
On Jan 7, 2006, at 9:55 AM, Gavin Kistner wrote:
> Min: (5*1-4)*(1)+3 = 1*1+3 = 5
Wow. That's some brilliant math.
1+3, of course, equals 4. No matter how many 1's are multiplied to
result in 1. :p
```

on 2006-01-07 18:27

On Jan 7, 2006, at 9:55 AM, Gavin Kistner wrote: > On Jan 7, 2006, at 5:03 AM, Christian Neukirchen wrote: >> "Matthew D Moss" <matthew.moss.coder@gmail.com> writes: >>> Parse tree for: (5d5-4)d(16/d4)+3 >> >> So what are the maximum and minimum values of this? > > Max: (5*5+4)*(16/4)+3 = 29*4+3 = 119 Man, *and* I flipped the sign on that -4. *shakes head sadly* One more shot. Min: (5*1-4)*(1)+3 = 1*1+3 = 4 Max: (5*5-4)*(16/4)+3 = 21*4+3 = 87

on 2006-01-07 18:27

On Jan 7, 2006, at 9:58 AM, Reinder Verlinde wrote: > ([1,21])d(16/[1,4])+3 = (c/[x,y] = [c/y,c/x] if x and y > 0) > > ([1,21])d([4,16])+3 = ([a,b]d[c,d] = [a*c,b*d] if a,b,c, and d > > 0) I'm agree with you on that top line, but I'm not sure how you got to the second line from there. I think you meant: ([1,21])d([1,4])+3 = ... which results in: [1,84]+3 [4,87]

on 2006-01-07 19:33

Gavin Kistner <gavin@refinery.com> 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.

on 2006-01-07 19:36

From: "Gavin Kistner" <gavin@refinery.com> > > Min: (5*1-4)*(1)+3 = 1*1+3 = 4 > Max: (5*5-4)*(16/4)+3 = 21*4+3 = 87 For max, should be (16/1) not (16/4), no?

on 2006-01-07 19:55

Gavin Kistner wrote: > On Jan 7, 2006, at 9:58 AM, Reinder Verlinde wrote: >> ([1,21])d(16/[1,4])+3 = (c/[x,y] = [c/y,c/x] if x and y > 0) >> >> ([1,21])d([4,16])+3 = ([a,b]d[c,d] = [a*c,b*d] if a,b,c, and d >> > 0) > > I'm agree with you on that top line, but I'm not sure how you got to > the second line from there. > I think you meant: > > ([1,21])d([1,4])+3 = ... > > which results in: > > [1,84]+3 > [4,87] 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

on 2006-01-07 20:00

On Jan 7, 2006, at 11:36 AM, Bill Kelly wrote: > From: "Gavin Kistner" <gavin@refinery.com> >> Min: (5*1-4)*(1)+3 = 1*1+3 = 4 >> Max: (5*5-4)*(16/4)+3 = 21*4+3 = 87 > > For max, should be (16/1) not (16/4), no? You're all too damned smart. :) Brilliant!

on 2006-01-07 20:28

On Jan 7, 2006, at 18:32, Christian Neukirchen 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.

on 2006-01-07 22:58

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

on 2006-01-07 23:04

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.

on 2006-01-08 00:17

On 1/7/06, Christer Nilsson <janchrister.nilsson@gmail.com> 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.

on 2006-01-08 02:17

Austin Ziegler wrote: > Novelty dice created in the past include: > > d30, d100 > > The latter is quite unwieldy. > > Strictly speaking, it is not possible to make a die (polyhedron) with an > odd number of faces, Uh, of course you can make such a polyhedron. Consider the Egyptian and Mayan pyramids as examples of 5-sided polyhedron (four triangles on the sides and a square on the bottom). Adjusting the steepness of the sides can make it as fair or unfair as you'd want. Sure, they're not regular polyhedra, but neither is the d30 you spoke of.

on 2006-01-08 14:50

Matthew Moss a Ã©crit : > > Well, why do you say it's invalid ? Given the simplified BNF it must be read as : 3d(d(d(d6))) and it is perfectly valid as there is no other way to understand that ... Pierre

on 2006-01-08 15:17

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?

on 2006-01-08 15:26

On Sun, Jan 08, 2006 at 10:48:01PM +0900, Pierre Barbier de Reuille wrote: } Matthew Moss 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

on 2006-01-08 15:51

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

on 2006-01-08 20:07

Attached is my submission. It looks pretty cool to me, but then this is only my second-ever Ruby program. Meta-comment: if [QUIZ] opens the quiz, then surely [/QUIZ] should close it. Luke Blanshard

on 2006-01-08 20:13

Hi, I finally finished a Ruby Quiz! Albeit by means of a goofy method_missing hack. <grin> But it was fun. Here 'tis: ------------------------------------------------------------------ #!/usr/bin/env ruby expr = ARGV[0] || abort('Please specify expression, such as "(5d5-4)d(16/d4)+3"') expr = expr.dup # unfreeze class Object def method_missing(name, *args) # Intercept dieroll-method calls, like :_5d5, and compute # their value: if name.to_s =~ /^_(\d*)d(\d+)$/ rolls = [1, $1.to_i].max nsides = $2.to_i (1..rolls).inject(0) {|sum,n| sum + (rand(nsides) + 1)} else raise NameError, [name, *args].inspect end end end class String def die_to_meth # Prepend underscore to die specs, like (5d5-4) -> (_5d5-4) # making them grist for our method_missing mill: self.gsub(/\b([0-9]*d[0-9]*)\b/, '_\1') end end expr.gsub!(/d%/,"d100") # d% support # inner->outer reduce true while expr.gsub!(/\(([^()]*)\)/) {eval($1.die_to_meth)} p eval(expr.die_to_meth)

on 2006-01-08 20:17

> Leexer/parsers? We ain't got no Leexer/parsers. We don't need no > Leexer/parsers. I don't have to show you any steenking Leexer/parser. > Just call eval and use Ruby's fine lexer/parser (apologies to Mel > Brooks, John Huston and Banditos Mexicanos everywhere). > > This approach uses regex substitutions to first munge the input > expression to deal with the default cases (like d6 to 1d6 and 1% to > 1d100), then it substitutes ** for d and hands it over to the > evaluator and prints the result. > > Conveniently, ** has the desired > precedence relative to the other operators, plus it is binary and > left-associative. This feels so evil. Seduced by the Dark Side I am. > I used the same approach, but found that ** is right-associative (as it's generally defined outside of Ruby). To confirm the associativity for yourself, try this: 2**3**4. If it's left associative, it should equal 8**4 (4096), right-associativity gives 2**81 (a lot). I ended up doing a lot more redefining and mucking about: Dice Ruby d * * + / - + << - >> Interestingly, the difference between a left-associating and a right- associating 'd' operator isn't particularly visible from the 'loaded- dice' testing common on the list. For example, 2d2d6 gives a maximum of 24 whichever associativity is used, but the distributions of the two solutions are vastly different; the left-associative result has a minimum value of 4, the right-associative result has a minimum of 2. Here's my solution, which maintains correct associativity for 'd' according to the initial quiz, but does a lot more mucking about with Fixnum: matthew smillie. #!/usr/local/bin/ruby class Fixnum alias old_mult * alias old_div / alias old_plus + alias old_minus - def >>(arg) old_minus(arg) end def <<(arg) old_plus(arg) end def -(arg) old_div(arg) end def +(arg) old_mult(arg) end def *(arg) sum = 0 self.times do sum = sum.old_plus(rand(arg).old_plus(1)) end sum end end class Dice def initialize(str) # make assumed '1's explicit - do it twice to cover cases # like '3ddd6' which would otherwise miss one match. @dice = str.gsub(/([+\-*\/d])(d)/) { |s| "#{$1}1#{$2}" } @dice = @dice.gsub(/([+\-*\/d])(d)/) { |s| "#{$1}1#{$2}" } # sub all the operators. @dice = @dice.gsub(/\+/, "<<") @dice = @dice.gsub(/-/, ">>") @dice = @dice.gsub(/\*/, "+") @dice = @dice.gsub(/\//, "-") @dice = @dice.gsub(/d/, "*") end def roll eval(@dice) end end d = Dice.new(ARGV[0]) (ARGV[1] || 1).to_i.times { print "#{d.roll} " } ---- Matthew Smillie <M.B.Smillie@sms.ed.ac.uk> Institute for Communicating and Collaborative Systems University of Edinburgh

on 2006-01-08 20:29

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

on 2006-01-08 20:35

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

on 2006-01-08 20:44

Hi, This is my quiz entry for Ruby Quiz 61 (Dice Roller). It's actually the second idea I had, after starting out with Antlr (I still finished that one, because I wanted to get to grips with Antlr anyway - I happened to be playing with it when this quiz came out :)). I've bundled both this entry and that one at: http://roscopeco.co.uk/code/ruby-quiz-entries/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 Quiz 61, the quick way # by Ross Bamford # Just a debugging helper module Kernel def dbg(*s) puts(*s) if $VERBOSE|| @dice_debug end attr_writer :dice_debug def dice_debug?; @dice_debug; end end # Need to implement the 'rolls' method. Wish it didn't have to # be on Fixnum but for this it makes the parsing *lots* easier. class Fixnum def self.roll_proc=(blk) @roll_proc = blk end def self.roll_proc @roll_proc ||= method(:rand).to_proc end def rolls(sides) (1..self).inject(0) { |s,v| s + Fixnum.roll_proc[sides] } end end # Here's the roller. class DiceRoller class << self # Completely wrap up a roll def roll(expr, count = 1, debug = false) new(expr,debug).roll(count) end # The main 'parse' method. Just really coerces the code to Ruby # and then compiles to a block that returns the result. def parse(expr) # very general check here. Will pass lots of invalid syntax, # but hopefully that won't compile later. This removes the # possibility of using variables and the like, but that wasn't # required anyway. The regexps would be a bit more difficult # if we wanted to do that. raise SyntaxError, "'#{expr}' is not a valid dice expression", [] if expr =~ /[^d\d\(\)\+\-\*\/\%]|[^d]%|d-|\*\*/ # Rubify! s = expr.gsub( /([^\d\)])d|^d/, '\11d') # fix e.g. 'd5' and '33+d3' to '1.d5' and '33+1d3' s.gsub!( /d%/, 'd(100)' ) # fix e.g. 'd%' to 'd(100)' s.gsub!( /d([\+\-]?\d+)/, '.rolls(\1)') # fix e.g. '3d8' to '3.rolls(8) (*) s.gsub!( /d\(/, '.rolls(') # fix e.g. '2d(5+5)' to '2.rolls(5+5)' # (*) This line treats + or - straight after 'd' as a unary sign, # so you can have '3d-8*7' => '3.rolls(+8)-7' # This would throw a runtime error from rolls, though. # Make a block. Doing it this way gets Ruby to compile it now # so we'll reliably get fail fast on bad syntax. dbg "PARS: #{expr} => #{s}" begin eval("lambda { #{s} }") rescue Exception => ex raise SyntaxError, "#{expr} is not a valid dice expression", [] end end end # Create a new roller that rolls the specified dice expression def initialize(expr, debug = false) dbg "NEW : #{to_s}: #{expr} => #{expr_code}" @expr_code, @expr, @debug = expr, DiceRoller.parse(expr), debug end # Get hold of the original expression and compiled block, respectively attr_reader :expr_code, :expr # Roll this roller count times def roll(count = 1) dbg " ROLL: #{to_s}: #{count} times" r = (1..count).inject([]) do |totals,v| this_r = begin expr.call rescue Exception => ex raise RuntimeError, "'#{expr_code}' raised: #{ex}", [] end dbg " r#{v}: rolled #{this_r}" totals << this_r end r.length < 2 ? r[0] : r end end # Library usage: # # require 'roll' # # # is the default: # # Fixnum.roll_proc = lambda { |sides| rand(sides) + 1 } # # DiceRoller.roll('1+2*d6') # # d = DiceRoller.new('((3d%)+8*(d(5*5)))') # d.roll(5) # # d = DiceRoller.new('45*10d3') # debug # # # ... or # one_roll = d.expr.call # # command-line usage if $0 == __FILE__ unless expr = ARGV[0] puts "Usage: ruby [--verbose] roll.rb expr [count]" else (ARGV[1] || 1).to_i.times { print "#{DiceRoller.roll(expr)} " } print "\n" end end ===================================== -----------[UTIL: minmax.rb]---------- #!/usr/local/bin/ruby require 'roll' LOW_DICE = lambda { |sides| 1 } HIGH_DICE = lambda { |sides| sides } # Adds a 'minmax' method that uses loaded dice to find # min/max achievable for a given expression. # # Obviously not thread safe, but then neither is the # whole thing ;D class DiceRoller def self.minmax(expr) old_proc = Fixnum.roll_proc Fixnum.roll_proc = LOW_DICE low = DiceRoller.roll(expr) Fixnum.roll_proc = HIGH_DICE high = DiceRoller.roll(expr) Fixnum.roll_proc = old_proc [low,high] end end if $0 == __FILE__ if expr = ARGV[0] min, max = DiceRoller.minmax(expr) puts "Expression: #{expr} ; min / max = #{min} / #{max}" else puts "Usage: minmax.rb <expr>" end end ===================================== -----------[TEST: test.rb]---------- #!/usr/local/bin/ruby # # Ruby Quiz, number 61 - Dice roller # This entry by Ross Bamford (rosco<at>roscopeco.co.uk) require 'test/unit' require 'roll' ASSERTS = { '1' => 1, '1+2' => 3, '1+3*4' => 13, '1*2+4/8-1' => 1, 'd1' => 1, '1d1' => 1, 'd10' => 10, '1d10' => 10, '10d10' => 100, 'd3*2' => 6, '5d6d7' => 210, # left assoc '2d3+8' => 14, # not 22 '(2d(3+8))' => 22, # not 14 'd3+d3' => 6, '33+d3+10' => 46, 'd2*2d4' => 16, 'd(2*2)+d4' => 8, 'd%' => 100, '2d%' => 200, 'd%*7' => 700, '14+3*10d2' => 74, '(5d5-4)d(16/d4)+3' => 87, #25d4 + 3 '3d+8/8' => 3 #3d(+8)/8 } ERRORS = { # Bad input, all should raise exception 'd' => SyntaxError, '3d' => SyntaxError, '3d-8' => SyntaxError, # - # of sides '3ddd6' => SyntaxError, '3%2' => SyntaxError, '%d' => SyntaxError, '+' => SyntaxError, '4**3' => SyntaxError } # bit messy, but can't get class methods on Fixnum Fixnum.roll_proc = lambda { |sides| sides } class TestDiceRoller < Test::Unit::TestCase def initialize(*args) super end ASSERTS.each do |expr, expect| eval <<-EOC def test_good_#{expr.hash.abs} expr, expect = #{expr.inspect}, #{expect.inspect} puts "\n-----------------------\n\#{expr} => \#{expect}" if $VERBOSE res = DiceRoller.roll(expr) puts "Returned \#{res}\n-----------------------" if $VERBOSE assert_equal expect, res end EOC end ERRORS.each do |expr, expect| eval <<-EOC def test_error_#{expr.hash.abs} expr, expect = #{expr.inspect}, #{expect.inspect} assert_raise(#{expect}) do puts "\n-----------------------\n\#{expr} => \#{expect}" if $VERBOSE res = DiceRoller.roll(expr) puts "Returned \#{res}\n-----------------------" if $VERBOSE end end EOC end end =====================================

on 2006-01-08 20:50

On Sun, 08 Jan 2006 19:33:18 -0000, Ross Bamford <rosco@roscopeco.remove.co.uk> wrote: > This is implemented in the DiceRoller.parse method, which returns the > string. Sorry, I changed that. It gives a block now.

on 2006-01-08 21:01

Very interesting, and different solutions, this time! Here's my recursive descent solution with histogram: =begin Ruby Quiz #61 by Matthew D Moss Solution by Christer Nilsson "3d6" gives 3..18 randomly "(5d5-4)d(16/d4)+3" Backus Naur Form: expr: term ['+' expr | '-' expr] term: fact ['*' term | '/' term] fact: [unit] 'd' dice unit: '(' expr ')' | integer dice: '%' | term integer: digit [integer] digit: /[0-9]/ * Integers are positive * The "d" (dice) expression XdY rolls a Y-sided die (numbered from 1 to Y) X times, accumulating the results. X is optional and defaults to 1. * All binary operators are left-associative. * Operator precedence: ( ) highest d * / + - lowest Some game systems use d100 quite often, and may abbreviate it as "d%" (but note that '%' is only allowed immediately after a 'd'). =end class String def behead return ['',''] if self == '' [self[0..0], self[1...self.size]] end end class Array def sum inject(0) {|sum,e| sum += e} end def histogram(header="") width = 100 each_index {|i| self[i]=0 if self[i].nil?} sum = self.sum max = self.max if max.nil? s = " " + header + "\n" each_with_index do |x,i| label = " " + format("%2.1f",100.0*x/sum)+"%" s += format("%2d",i) + " " + "*" * ((x-min) * width / (max-min)) + label + "\n" end s += "\n" end end class Dice def statistics(expr, n=1000) prob = [] n.times do value = evaluate(expr) prob[value]=0 if prob[value].nil? prob[value] += 1 end prob end def evaluate s @sym, @s = s.behead @stack = [] expr pop end def drop (pattern) raise 'syntax error: expected ' + pattern unless pattern === @sym @sym, @s = @s.behead end def push(x) @stack.push x end def top2() @stack[-2] end def top() @stack[-1] end def pop() @stack.pop end def calc value pop push value end def try symbol return nil unless @sym == symbol drop symbol case symbol when '+' then expr; calc top2 + pop when '-' then expr; calc top2 - pop when '*' then term; calc top2 * pop when '/' then term; calc top2 / pop when '%' then push 100 when '(' then expr; drop ')' #when 'd' then dice; calc top2 * pop # debug mode when 'd' # release mode dice sum = 0 sides = pop count = pop count.times {sum += rand(sides) + 1} push sum end end def expr term try('+') or try('-') end def term fact try('*') or try('/') end def fact @sym == 'd' ? push(1) : unit # implicit 1 try('d') end def dice #unit unless try('%')# if 5d6d7 is not accepted term unless try('%') # if 5d6d7 is accepted end def unit integer @sym.to_i unless try('(') end def integer(i) return if @sym == '' digit = /[0-9]/ drop(digit) digit === @sym ? integer( 10 * i + @sym.to_i ) : push(i) end end require 'test/unit' class TestDice < Test::Unit::TestCase def t (actual, expect) assert_equal expect, actual end def test_all t(/[0-9]/==="0", true) t(/[0-9]/==="a", false) t "abc".behead, ["a","bc"] t "a".behead, ["a",""] t "".behead, ["",""] dice = Dice.new() print dice.statistics("d6").histogram("d6") print dice.statistics("2d6").histogram("2d6") print dice.statistics("(d6)d6",10000).histogram("(d6)d6") #t dice.evaluate("(6)"), 6 #t dice.evaluate("12+34"), 46 #t dice.evaluate("3*4+2"), 14 #t dice.evaluate("5+6+7"), 18 #t dice.evaluate("5+6-7"), 4 #t dice.evaluate("(5+6)+7"), 18 #t dice.evaluate("5"), 5 #t dice.evaluate("5+(6+7)"), 18 #t dice.evaluate("(5+6+7)"), 18 #t dice.evaluate("5*6*7"), 210 #t dice.evaluate("2+3*4"), 14 #t dice.evaluate("12+13*14"), 194 #t dice.evaluate("(2+3)*4"), 20 #t dice.evaluate("(5d5-4)d(16/1d4)+3"), 45 #t dice.evaluate("(5d5-4)d(400/1d%)+3"), 87 #t dice.evaluate("1"), 1 #t dice.evaluate("1+2"),3 #t dice.evaluate("1+3*4"),13 #t dice.evaluate("1*2+4/8-1"), 1 #t dice.evaluate("d1"),1 #t dice.evaluate("1d1"),1 #t dice.evaluate("1d10"), 10 #t dice.evaluate("10d10"),100 #t dice.evaluate("d3*2"), 6 #t dice.evaluate("2d3+8"), 14 #t dice.evaluate("(2*(3+8))"),22 #t dice.evaluate("d3+d3"),6 #t dice.evaluate("d2*2d4"),16 #t dice.evaluate("2d%"),200 #t dice.evaluate("14+3*10d2"), 74 #t dice.evaluate("(5d5-4)d(16/d4)+3"),87 #t dice.evaluate("d10"), 10 #t dice.evaluate("d%"),100 #t dice.evaluate("d(2*2)+d4"),8 #t dice.evaluate("(5d6)d7"), 210 #t dice.evaluate("5d(6d7)"), 210 #t dice.evaluate("5d6d7)"), 210 #t dice.evaluate("12d13d14)"), 2184 #t dice.evaluate("12*d13)"), 156 #t dice.evaluate("12+d13)"), 25 end end

on 2006-01-08 21:05

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

on 2006-01-08 21:14

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

on 2006-01-08 21:29

Here is my submission. Yes, this is my first completed Ruby Quiz ;) Thanks to Eric Mahurin's syntax.rb for making this work. I've attached it as well, because it's not easily accessible otherwise ;) -austin

on 2006-01-08 22:45

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

on 2006-01-08 22:51

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

on 2006-01-08 22:54

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

on 2006-01-08 23:24

On Jan 8, 2006, at 3:42 PM, Pablo Hoch wrote: > Here is my solution. I convert the expression into RPN (using the > algorithm > described in the Wikipedia article) and then calculate it (I have > added a > 'd' method to Fixnum so that I can use it like the standard arithmetic > operators). Wow. That is very cool. Thanks for sharing! James Edward Gray II

on 2006-01-08 23:30

On Jan 8, 2006, at 3:53 PM, Dennis Ranke wrote: > For this I implemented a simple recursive descent parser class that > allows the tokens and the grammar to be defined in a very clean > ruby syntax. Awesome! > I think I'd really like to see a production quality parser > (generator) using something like this grammar format. I agree. This is fantastic. So what do we have to do to get you to add the polish and make it available? :) James Edward Gray II

on 2006-01-08 23:39

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

on 2006-01-09 01:46

Here is my solution. It's a recursive descent parser, that parses the full BNF posted by Matthew Moss. It doesn't "compile" the expresion into nodes or something similar, instead it evaluates the expression while parsing (so it has to be reparsed for every dice rolling). It uses StringScanner, which was quite handy for this task. (And it also uses eval() ;-) Dominik require "strscan" class Dice def initialize(expr) @expr = expr.gsub(/\s+/, "") end def roll s = StringScanner.new(@expr) res = expr(s) raise "garbage after end of expression" unless s.eos? res end private def split_expr(s, sub_expr, sep) expr = [] loop do expr << send(sub_expr, s) break unless s.scan(sep) expr << s[1] if s[1] end expr end def expr(s) eval(split_expr(s, :fact, /([+\-])/).join) end def fact(s) eval(split_expr(s, :term, /([*\/])/).join) end def term(s) first_rolls = s.match?(/d/) ? 1 : unit(s) dices = s.scan(/d/) ? split_expr(s, :dice, /d/) : [] dices.inject(first_rolls) do |rolls, dice| raise "invalid dice (#{dice})" unless dice > 0 (1..rolls).inject(0) { |sum, _| sum + rand(dice) + 1 } end end def dice(s) s.scan(/%/) ? 100 : unit(s) end def unit(s) if s.scan(/(\d+)/) s[1].to_i else unless s.scan(/\(/) && (res = expr(s)) && s.scan(/\)/) raise "error in expression" end res end end end if $0 == __FILE__ begin d = Dice.new(ARGV[0]) puts (1..(ARGV[1] || 1).to_i).map { d.roll }.join(" ") rescue => e puts e end end

on 2006-01-09 03:28

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

on 2006-01-09 08:03

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.

on 2006-01-09 11:28

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

on 2006-01-09 16:45

On 1/6/06, Jim Freeze <jim@freeze.org> wrote: > > > On 1/6/06, James Edward Gray II <james@grayproductions.net> 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 Fugal

on 2006-01-09 16:54

On 1/6/06, Jacob Fugal <lukfugl@gmail.com> 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 Freeze <jim@freeze.org> 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 Fugal <lukfugl@gmail.com> 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 Freeze. 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 Fugal

on 2006-01-09 17:00

On 1/7/06, Reinder Verlinde <reinder@verlinde.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 Fugal

on 2006-01-09 17:06

On 1/7/06, Ron M <rm_rails@cheapcomplexdevices.com> 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 Fugal

on 2006-01-09 17:18

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

on 2006-01-09 18:22

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

on 2006-01-09 18:34

On Sat, Jan 07, 2006 at 04:00:52AM +0900, Gregory Seidman wrote: } On Sat, Jan 07, 2006 at 03:56:47AM +0900, Ruby Quiz wrote: } [...] } } [NOTE: The BNF above is simplified here for clarity and space. If } } requested, I will make available the full BNF description I've used in my } } own solution, which incorporates the association and precedence rules.] } } I would appreciate the full BNF, please. 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 Moss 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

on 2006-01-09 20:59

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

on 2006-01-10 14:50

James Edward Gray 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

on 2006-01-10 20:19

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 (1..@rolls.to_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