From: Robert Uhl
Subject: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m37jdr474x.fsf@4dv.net>
Anyone care to critique this first attempt at a dice-rolling function &
macro pair?  This isn't, as one might at first assume, a homework
assignment--I've been out of school for years--but is instead something
I'm using to generate GURPS Traveller solar systems.

I discovered that I had a lot of code which looked like this:

  (let ((roll (die)))
       (cond ((= roll 1) (foo))
             ((<= roll 3) (bar))
             (t (baz))))

And so I wrote a macro which can condense it down to this:

  (roll ()
        ((1) (foo))
        ((2 3) (bar))
        (t (baz)))

I made it a little better, in that sometimes it's necessary to get the
value of the roll, so the name of a variable can be passed in:

  (roll (roll)
        (t (print roll)))

And there are a few other optional params just in case one wants
multi-sided dice or multiple rolls.  E.g. one could toss a coin thus:

  (roll (toss 1 2)
        ((1) 'heads)
        ((2) 'tails))

Any suggestions on how the code might be improved?  I'm thinking that it
might make sense for exact matches to be specified as (1 (foo)) rather
than ((1) (foo)); other than that I'm drawing a blank.

  (defun die (&optional (rolls 1) (sides 6))
    (let ((acc 0))
      (dotimes (i rolls)
        (incf acc (+ 1 (random sides))))
      acc))
  
  (defmacro roll (params &body body)
    "(ROLL (&optional (ROLL-VAR 'ROLL) (ROLLS 1) (SIDES 6)) CONDITION ...)
  
  Roll ROLLS SIDES-sided dice, assigning the total to locally-bound
  ROLL-VAR.  Then check each CONDITION, in order, until one is true.
  
  Each CONDITION may be a single-item list, in which case it matches if
  ROLL-VAR is = to that item, or it may be a two-item list, in which case
  it matches if ROLL-VAR is >= the first item and <= the second item, or
  it may be t, in which case it always matches.
  
  If no condition matches, the ROLL will signal an UNHANDLED-RESULT
  warning."
    
    (let (items)
      (destructuring-bind (&optional (roll-var 'roll) (rolls 1) (sides 6))
  	params
        (dolist (item body)
  	(let ((condition (first item))
  	      (actions (rest item)))
  	  (cond ((eq condition t)
  		 (push `(t ,@actions) items))
  		((= (length condition) 2)
  		 (push `((and (>= ,roll-var ,(first condition))
                              (<= ,roll-var ,(second condition)))
                         ,@actions) items))
  		((= (length condition) 1)
  		 (push `((= ,roll-var ,(car condition)) ,@actions) items)))))
        (setf items (reverse items))
        `(let ((,roll-var (die ,rolls ,sides)))
  	(cond ,@items (t (warn "Roll of ~A was unhandled." ,roll-var)))))))

-- 
Robert Uhl <http://public.xdi.org/=ruhl>
Or, to put it another way, if you see a long line of rats streaming off
of a ship, the correct assumption is *not* `gosh, I bet that's a real
nice boat now that those rats are gone.'                 --Mike Sphar 

From: Peter Lewerin
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <1126216742.287378.221030@g14g2000cwa.googlegroups.com>
Robert Uhl wrote:

> And so I wrote a macro which can condense it down to this:
>
>   (roll ()
>         ((1) (foo))
>         ((2 3) (bar))
>         (t (baz)))

My first thought was: why not

    (case (...)
      (1 (foo))
      ((2 3) (bar))
      (t (baz)))

> I made it a little better, in that sometimes it's necessary to get the
> value of the roll, so the name of a variable can be passed in:

    (defmacro roll ((&optional &key var) &body body)
      (unless var
        (setf var (gensym)))
      `(let ((,var (die)))
         (case ,var ,@body)))

    (roll (:var foo)
      (1 (foo))
      ((2 3) (bar))
      (t (print foo)))

> And there are a few other optional params just in case one wants
> multi-sided dice or multiple rolls.  E.g. one could toss a coin thus:

    (defmacro roll ((&optional &key var (dice 1) (sides 6)) &body body)
      (unless var
        (setf var (gensym)))
      `(let ((,var (die ,dice ,sides)))
         (case ,var ,@body)))

    (roll (:dice 1 :sides 2)
      (1 'head)
      (2 'tail))
From: Marco Antoniotti
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <YE2Ue.1$pa3.372@typhoon.nyu.edu>
Peter Lewerin wrote:
> 
> 
>>And there are a few other optional params just in case one wants
>>multi-sided dice or multiple rolls.  E.g. one could toss a coin thus:
> 
> 
>     (defmacro roll ((&optional &key var (dice 1) (sides 6)) &body body)
>       (unless var
>         (setf var (gensym)))
>       `(let ((,var (die ,dice ,sides)))
>          (case ,var ,@body)))
> 
>     (roll (:dice 1 :sides 2)
>       (1 'head)
>       (2 'tail))
> 

Simpler.

	(defmacro roll ((&key (var (gensym "ROLL-VAR-"))
                               (dice 1)
                               (sides 6))
	                &body body)
	   `(let ((,var (die ,dice ,sides)))
	      (case ,var ,@body)))

Cheers
--
Marco
From: Thomas A. Russ
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <ymi8xy5ex4p.fsf@sevak.isi.edu>
Marco Antoniotti <·······@cs.nyu.edu> writes:

> Simpler.
> 
> 	(defmacro roll ((&key (var (gensym "ROLL-VAR-"))
>                                (dice 1)
>                                (sides 6))
> 	                &body body)
> 	   `(let ((,var (die ,dice ,sides)))
> 	      (case ,var ,@body)))

Although simpler, it doesn't have quite the same interpretation of
clauses as the original poster (not Peter Lewerin, who also used a
CASE-based approach).

I had thought that CASE would be an obvious answer for the examples
given, with coin tosses and six-sided dice, but the original
specification where (x y) denoted the range (<= x die-roll y) makes more
sense if one considers rolling percentile dice.  In that case, one may
want to say something like

  (roll (:sides 100)
     (1  'rare)
     ((2 50) 'common)
     ((51 75) 'middle)
     ((76 100) 'rest))


-- 
Thomas A. Russ,  USC/Information Sciences Institute
From: Alexander Schmolck
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <yfsr7bztfmv.fsf@black4.ex.ac.uk>
Robert Uhl <·········@NOSPAMgmail.com> writes:

> Anyone care to critique this first attempt at a dice-rolling function &
> macro pair?  This isn't, as one might at first assume, a homework
> assignment--I've been out of school for years--but is instead something
> I'm using to generate GURPS Traveller solar systems.
>
> I discovered that I had a lot of code which looked like this:
>
>   (let ((roll (die)))
>        (cond ((= roll 1) (foo))
>              ((<= roll 3) (bar))
>              (t (baz))))
>
> And so I wrote a macro which can condense it down to this:
>
>   (roll ()
>         ((1) (foo))
>         ((2 3) (bar))
>         (t (baz)))

how about just using

(case (die) 
  (1 (foo)) 
  ((2 3) (bar))
  (t (baz)))

?

'as
From: Robert Uhl
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m38xy6qgca.fsf@4dv.net>
Alexander Schmolck <··········@gmx.net> writes:
>
>>   (roll ()
>>         ((1) (foo))
>>         ((2 3) (bar))
>>         (t (baz)))
>
> how about just using
>
> (case (die) 
>   (1 (foo)) 
>   ((2 3) (bar))
>   (t (baz)))

That particular example does indeed work the same, but if you look at
the code I do one more thing: a condition like (2 6) with ROLL is the
equivalent of the condition (2 3 4 5 6) with CASE.  This isn't so big
a deal with simple one-die rolls, but with triple or greater rolls it
could become a hassle.  Also, if I wanted to reference the DIE result
I'd need to write something like this using CASE:

  (let ((roll (die 2))
     (case (die)
           ((2 3 4) (foo roll))
           ((5 6)   (bar roll))
           ((7 8 9 10 11 12) (baz roll))))

Whereas it would look like this with ROLL:

  (roll (roll 2)
        ((2 4) (foo roll))
        ((5 6) (bar roll))
        ((7 12) (baz roll)))

Which is a bit prettier.  Not perfect, I'll admit, but a bit better.

Also, I'd forgotten about the existence of CASE:-)

-- 
Robert Uhl <http://public.xdi.org/=ruhl>
The South oppressed 30% of its population.  The Union oppressed 100%
of of the South's population.  Think about it.
From: GP lisper
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <1126303213.5e8fbcfbd4fd8ceb6e6262b7b404e5a0@teranews>
On Fri, 09 Sep 2005 11:52:05 -0600, <·········@NOSPAMgmail.com> wrote:
>
>
> Alexander Schmolck <··········@gmx.net> writes:
>>
>>>   (roll ()
>>>         ((1) (foo))
>>>         ((2 3) (bar))
>>>         (t (baz)))
>>
>> how about just using
>>
>> (case (die) 
>>   (1 (foo)) 
>>   ((2 3) (bar))
>>   (t (baz)))
>
> That particular example does indeed work the same, but if you look at
> the code I do one more thing: a condition like (2 6) with ROLL is the
> equivalent of the condition (2 3 4 5 6) with CASE.  This isn't so big
> a deal with simple one-die rolls, but with triple or greater rolls it
> could become a hassle.  Also, if I wanted to reference the DIE result
> I'd need to write something like this using CASE:
>
>   (let ((roll (die 2))
>      (case (die)
>            ((2 3 4) (foo roll))
>            ((5 6)   (bar roll))
>            ((7 8 9 10 11 12) (baz roll))))
>
> Whereas it would look like this with ROLL:
>
>   (roll (roll 2)
>         ((2 4) (foo roll))
>         ((5 6) (bar roll))
>         ((7 12) (baz roll)))
>
> Which is a bit prettier.  Not perfect, I'll admit, but a bit better.
>
> Also, I'd forgotten about the existence of CASE:-)
>


-- 
You can always tell a really good idea by the enemies it makes.
From: Peter Lewerin
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <1126305040.832107.205530@g49g2000cwa.googlegroups.com>
Robert Uhl wrote:

> Whereas it would look like this with ROLL:
>
>   (roll (roll 2)
>         ((2 4) (foo roll))
>         ((5 6) (bar roll))
>         ((7 12) (baz roll)))

    (defmacro roll ((&key (var (gensym)) (dice 1) (sides 6)) &body
body)
      `(let ((,var (die ,dice ,sides)))
         (rest (find-if (lambda (x)
                          (if (consp x)
                            (<= (first x) ,var (second x))
                            (= x ,var))) ',body :key #'first))))
From: Peter Lewerin
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <1126629553.531046.26400@g44g2000cwa.googlegroups.com>
No, that doesn't work.  It returns a list of the forms to be evaluated,
instead of the result of evaluating them.  I'll get back to this.

I think I will try some kind of mapping from "roll-clauses" to COND
clauses instead.
From: Lars Brinkhoff
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <853boaqwrs.fsf@junk.nocrew.org>
Robert Uhl <·········@NOSPAMgmail.com> writes:
> if you look at the code I do one more thing: a condition like (2 6)
> with ROLL is the equivalent of the condition (2 3 4 5 6) with CASE.

I have sometimes experimented with using improper lists in syntax.  In
this case, you could have (2 6) match only 2 and 6, but have (2 . 6)
be equivalent with (2 3 4 5 6).  E.g. (1 4 . 6) would be equivalent
with (1 4 5 6), etc.  This way, you would have both the CASE behaviour,
and be able to specify a range.

I haven't seen this elsewhere, nor mentioned in comp.lang.lisp, so
perhaps it's not considered good style.
From: rydis (Martin Rydstr|m) @CD.Chalmers.SE
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <w4c3bo99qtw.fsf@boris.cd.chalmers.se>
Lars Brinkhoff <·········@nocrew.org> writes:
> Robert Uhl <·········@NOSPAMgmail.com> writes:
> > if you look at the code I do one more thing: a condition like (2 6)
> > with ROLL is the equivalent of the condition (2 3 4 5 6) with CASE.
> 
> I have sometimes experimented with using improper lists in syntax.  In
> this case, you could have (2 6) match only 2 and 6, but have (2 . 6)
> be equivalent with (2 3 4 5 6).  E.g. (1 4 . 6) would be equivalent
> with (1 4 5 6), etc.  This way, you would have both the CASE behaviour,
> and be able to specify a range.
> 
> I haven't seen this elsewhere, nor mentioned in comp.lang.lisp, so
> perhaps it's not considered good style.

I've used that as well, though not in anything anyone has seen, as far
as I know. I imagine it's just something that feels obvious, if you
want to use a cons-based representation.

(In similar situations, I've also used lists, e.g. (1 (4 6)) would
be the same as your (1 4 . 6), allowing stuff like (2 (4 6) 8).)

'mr

-- 
[Emacs] is written in Lisp, which is the only computer language that is
beautiful.  -- Neal Stephenson, _In the Beginning was the Command Line_
From: Barry Fishman
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m3r7btdmcx.fsf@barry_fishman.att.net>
Lars Brinkhoff <·········@nocrew.org> writes:
> I have sometimes experimented with using improper lists in syntax.  In
> this case, you could have (2 6) match only 2 and 6, but have (2 . 6)
> be equivalent with (2 3 4 5 6).  E.g. (1 4 . 6) would be equivalent
> with (1 4 5 6), etc.  This way, you would have both the CASE behaviour,
> and be able to specify a range.
>
> I haven't seen this elsewhere, nor mentioned in comp.lang.lisp, so
> perhaps it's not considered good style.

The Emacs Gnus package stores article numbers in ordered lists where cons
are used to store ranges of consecutive values so:

  (1 (4 . 6)) -> (1 4 5 6)

But you can do things like:

  (1 (3 . 5) 7 (9 . 13)) -> (1 3 4 5 7 9 10 11 12 13)
From: Alexander Schmolck
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <yfsbr2yqrhu.fsf@black4.ex.ac.uk>
Robert Uhl <·········@NOSPAMgmail.com> writes:

> Alexander Schmolck <··········@gmx.net> writes:
>>
>>>   (roll ()
>>>         ((1) (foo))
>>>         ((2 3) (bar))
>>>         (t (baz)))
>>
>> how about just using
>>
>> (case (die) 
>>   (1 (foo)) 
>>   ((2 3) (bar))
>>   (t (baz)))
>
> That particular example does indeed work the same, but if you look at
> the code I do one more thing: a condition like (2 6) with ROLL is the
> equivalent of the condition (2 3 4 5 6) with CASE.

TYPECASE :)

Or something like:

(defun between (start end) (loop for i from start to end collect i))
(case (die)
      (#.(between 7 12) (foo roll))
...


> This isn't so big a deal with simple one-die rolls, but with triple or
> greater rolls it could become a hassle. Also, if I wanted to reference the
> DIE result I'd need to write something like this using CASE:
>
>   (let ((roll (die 2))
>      (case (die)
>            ((2 3 4) (foo roll))
>            ((5 6)   (bar roll))
>            ((7 8 9 10 11 12) (baz roll))))
>
> Whereas it would look like this with ROLL:
>
>   (roll (roll 2)
>         ((2 4) (foo roll))
>         ((5 6) (bar roll))
>         ((7 12) (baz roll)))
>
> Which is a bit prettier.  Not perfect, I'll admit, but a bit better.

I'm not so sure. 

The first form is immediately clear to anyone who knows lisp. The second
requires wading through some (admittedly fairly simple) macro code and
memorizing the various (non-obvious nor self-documenting) design choices (e.g.
(2 4) is a range not a set; it is also inclusive rather than half open, the
multiple uses of ROLL etc.) -- it also introduces one more potential source of
error to the program (macros can be broken in more subtle ways than
functions).

The minor difference in conciseness doesn't seem to justify this burden to me;
the only thing that CASE alone doesn't seem to handle sufficiently to me are
ranges for dice with more than 6 sides but that's easily fixed and in a more
explicit way that is also applicable beyond the narrow domain of dice-rolling.

'as
From: Robert Uhl
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m3u0gohpxp.fsf@4dv.net>
Alexander Schmolck <··········@gmx.net> writes:
>
>> This isn't so big a deal with simple one-die rolls, but with triple
>> or greater rolls it could become a hassle. Also, if I wanted to
>> reference the DIE result I'd need to write something like this using
>> CASE:
>>
>>   (let ((roll (die 2))
>>      (case (die)
>>            ((2 3 4) (foo roll))
>>            ((5 6)   (bar roll))
>>            ((7 8 9 10 11 12) (baz roll))))
>>
>> Whereas it would look like this with ROLL:
>>
>>   (roll (roll 2)
>>         ((2 4) (foo roll))
>>         ((5 6) (bar roll))
>>         ((7 12) (baz roll)))

[snip]

> The minor difference in conciseness doesn't seem to justify this
> burden to me; the only thing that CASE alone doesn't seem to handle
> sufficiently to me are ranges for dice with more than 6 sides but
> that's easily fixed and in a more explicit way that is also applicable
> beyond the narrow domain of dice-rolling.

Well, for the simple case of 1 single-sided die it's not so bad--but
consider the case of half-a-dozen.  It would get _very_ annoying to
enumerate all the numbers from 6 to 72; the condition becomes worse yet
for percentile dice and the like.

-- 
Robert Uhl <http://public.xdi.org/=ruhl>
An American thinks 100 years is a long time;
A European thinks 100 miles is a long distance.
From: Joe Marshall
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <k6hkiknf.fsf@alum.mit.edu>
Robert Uhl <·········@NOSPAMgmail.com> writes:

> Well, for the simple case of 1 single-sided die it's not so bad--but
> consider the case of half-a-dozen.

Um... wouldn't that always come out to 6?  Do they make single-sided
dice?

> It would get _very_ annoying to enumerate all the numbers from 6 to 72;

You'd only have to that once, besides how many numbers *are* there
between 6 and 72?
From: Robert Uhl
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m3d5nbuzwc.fsf@4dv.net>
Joe Marshall <·········@alum.mit.edu> writes:
>
>> Well, for the simple case of 1 single-sided die it's not so bad--but
>> consider the case of half-a-dozen.
>
> Um... wouldn't that always come out to 6?  Do they make single-sided
> dice?

meant to type 'six,' but my fingers got lost after the 'si.'

>> It would get _very_ annoying to enumerate all the numbers from 6 to
>> 72;
>
> You'd only have to that once, besides how many numbers *are* there
> between 6 and 72?

One would have to do it every time one rolled 6d6!  When writing an app
to randomly generate star systems, there are a _lot_ of random rolls:
determining luminosity; gravity; mass of planetoids and so forth.

-- 
Robert Uhl <http://public.xdi.org/=ruhl>
12 is the number of months in the year and the number of signs of the
Zodiac.  There were 12 apostles of Christ.
From: Christopher C. Stacy
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <uoe6vtl93.fsf@news.dtpq.com>
Robert Uhl <·········@NOSPAMgmail.com> writes:

> Joe Marshall <·········@alum.mit.edu> writes:
> >
> >> Well, for the simple case of 1 single-sided die it's not so bad--but
> >> consider the case of half-a-dozen.
> >
> > Um... wouldn't that always come out to 6?  Do they make single-sided
> > dice?
> 
> meant to type 'six,' but my fingers got lost after the 'si.'
> 
> >> It would get _very_ annoying to enumerate all the numbers from 6 to
> >> 72;
> >
> > You'd only have to that once, besides how many numbers *are* there
> > between 6 and 72?
> 
> One would have to do it every time one rolled 6d6!  When writing an app
> to randomly generate star systems, there are a _lot_ of random rolls:
> determining luminosity; gravity; mass of planetoids and so forth.

Klingons, starbases, ...
From: Thomas A. Russ
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <ymi7jdjdw9t.fsf@sevak.isi.edu>
Alexander Schmolck <··········@gmx.net> writes:
> Robert Uhl <·········@NOSPAMgmail.com> writes:
> > That particular example does indeed work the same, but if you look at
> > the code I do one more thing: a condition like (2 6) with ROLL is the
> > equivalent of the condition (2 3 4 5 6) with CASE.


> The first form is immediately clear to anyone who knows lisp. The second
> requires wading through some (admittedly fairly simple) macro code and
> memorizing the various (non-obvious nor self-documenting) design choices (e.g.
> (2 4) is a range not a set;

Hmmm.  This suggests to me that adding slightly more syntax would
perhaps help a bit, and also make it easy to specify sets of numbers.

What about using  (2 -> 4) for the range and (2 4) for the set {2, 4}.
Then there would be a documenting marker, the symbol -> for a range of
values.

> it is also inclusive rather than half open, the
> multiple uses of ROLL etc.) -- it also introduces one more potential source of
> error to the program (macros can be broken in more subtle ways than
> functions).

-- 
Thomas A. Russ,  USC/Information Sciences Institute
From: Jens Axel Søgaard
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <4320b9c8$0$37083$edfadb0f@dread12.news.tele.dk>
Robert Uhl wrote:
> Anyone care to critique this first attempt at a dice-rolling function &
> macro pair?  


If you are interested in more putting more features into your roll
macro, then take a look at the documentation for Torben �gidius 
Mogensen's Dice program/compiler.

     <http://www.diku.dk/~torbenm/Dice.zip>

-- 
Jens Axel S�gaard
From: Robert Uhl
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m3zmqmoy9k.fsf@4dv.net>
Jens Axel Søgaard <······@soegaard.net> writes:
>
> If you are interested in more putting more features into your roll
> macro, then take a look at the documentation for Torben Ægidius
> Mogensen's Dice program/compiler.
>
>      <http://www.diku.dk/~torbenm/Dice.zip>

Interesting little language, although to be honest it's almost certainly
overkill for what I'm doing here (mostly rolling 3d6 and assigning
values based on the result--GURPS Traveller:First In has a pretty
straightforward die-rolling mechanic within a rather complex creation
sequence).

-- 
Robert Uhl <http://public.xdi.org/=ruhl>
Oatmeal is, of course, divine food of the gods or horsefood dressed up
for human consumption, depending on your disposition.  It's one of
those Marmite things.                                   --Sherilyn
From: Jock Cooper
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m37jdq2ihk.fsf@jcooper02.sagepub.com>
Robert Uhl <·········@NOSPAMgmail.com> writes:

> Anyone care to critique this first attempt at a dice-rolling function &
> macro pair?  This isn't, as one might at first assume, a homework
> assignment--I've been out of school for years--but is instead something
> I'm using to generate GURPS Traveller solar systems.
> 
> I discovered that I had a lot of code which looked like this:
> 
>   (let ((roll (die)))
>        (cond ((= roll 1) (foo))
>              ((<= roll 3) (bar))
>              (t (baz))))
> 
> And so I wrote a macro which can condense it down to this:
> 
>   (roll ()
>         ((1) (foo))
>         ((2 3) (bar))
>         (t (baz)))
> 

 I wrote some toy code that is a little more generalized but may give 
some ideas.  It returns a keyword but could be modified to perform an 
action.  One is uses fixed values and the other lets you specify them
at the time of the call.

(defmacro fixed-random-generator (events &key (default :default))
  "Events is ((eventkw1 percent1) (eventkw2 percent2) ...).
   Percents should add up to more than 100 -- if less a default 
   case (specify with default kw) will be chosen.
   return is the eventkw chosen.  See test-fixed-random-generator for example"
  (flet ((%frg-cond-generator ()
	   (loop for (event percentage) in events
		 with low-end = 0 
		 collect `((<= ,low-end rnd ,(+ low-end percentage)) ,event) 
                    into forms
		 do (setq low-end (+ 1 low-end percentage))
		 finally (return (nconc forms `((t ,default)))))))
    `(let ((rnd (random 100)))
      (cond ,@(%frg-cond-generator)))))

(defun test-fixed-random-generator (&optional (iter 10000))
  (let ((result-hash (make-hash-table)))
    (dotimes (i iter)
      (let* ((item (fixed-random-generator ((:door 10) (:secret 5) (:foo 1)
					    (:bar 2)) :default :haha))
	     (current (gethash item result-hash)))
	(setf (gethash item result-hash) (or (and current (1+ current))
						 1))))
    (loop for key being each hash-key in result-hash
	  using (hash-value count)
	  do (format t "~a = ~a~%" key count))))

(defmacro variable-random-generator (events &key (default :default))
  "Like fixed-random-generator, except events is (eventkw1 eventkw2 ...) 
   and the return is a lambda. the lambda takes as args the percent values 
   for the keywords specified.  See test-variable-random-generator for example"
  (flet ((%vrg-get-lambda-args () 
	   (loop for event in events
		 collect (gensym (symbol-name event))))
	 (%vrg-get-computed-ranges (syms)
	   (loop for sym in syms
		 with last
		 for gsym = (gensym)
		 collect `(,gsym (+ ,sym ,(or last 0)))
		 do (setq last gsym)))
	 (%vrg-cond-generator (event-pairs)
	   (loop for (event percentage) in event-pairs
		 with low-end = 0 
		 collect `((<= ,low-end rnd ,percentage) ,event) into forms
		 do (setq low-end `(1+ ,percentage))
		 finally (return (nconc forms `((t ,default)))))))
    (let* ((%args (%vrg-get-lambda-args))
	   (%computed (%vrg-get-computed-ranges %args))
	   (%events (loop for event in events
			  for (sym nil) in %computed
			  collect (list event sym)))
	   )
      `(lambda ,%args
	(let* ((rnd (random 100))
	       ,@%computed)
	  (cond ,@(%vrg-cond-generator %events)))))))

(defun test-variable-random-generator (&optional (iter 10000))
  (let* ((result-hash (make-hash-table))
	 (the-fn (variable-random-generator (:door :secret :foo :BAR) 
                   :default :haha)))
    (dotimes (i iter)
      (let* ((item (funcall the-fn 10 5 1 2))
	     (current (gethash item result-hash)))
	(setf (gethash item result-hash) (or (and current (1+ current))
						 1))))
    (loop for key being each hash-key in result-hash
	  using (hash-value count)
	  do (format t "~a = ~a~%" key count))))
From: Robert Uhl
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m3mzmhenrk.fsf@4dv.net>
Jock Cooper <·····@mail.com> writes:
>
>  I wrote some toy code that is a little more generalized but may give 
> some ideas.  It returns a keyword but could be modified to perform an 
> action.  One is uses fixed values and the other lets you specify them
> at the time of the call.

[mass snippage of a very cool little bit o' code]

Any license on your code?  I'd love to incorporate it...

-- 
Robert Uhl <http://public.xdi.org/=ruhl>
And I'd say: so what?  I mean, Picasso might paint someone square and
blue, but if you held a gun to the bastard's head he could do proper
art.                                               --Terry Pratchett
From: Jock Cooper
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m33bo83frf.fsf@jcooper02.sagepub.com>
Robert Uhl <·········@NOSPAMgmail.com> writes:

> Jock Cooper <·····@mail.com> writes:
> >
> >  I wrote some toy code that is a little more generalized but may give 
> > some ideas.  It returns a keyword but could be modified to perform an 
> > action.  One is uses fixed values and the other lets you specify them
> > at the time of the call.
> 
> [mass snippage of a very cool little bit o' code]
> 
> Any license on your code?  I'd love to incorporate it...
> 

nope no license.. use however you like..  I was playing around trying
to generate dungeon mazes one day and wanted something that would do 
a sort of d100...
From: Thomas A. Russ
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <ymibr32dj73.fsf@sevak.isi.edu>
Robert Uhl <·········@NOSPAMgmail.com> writes:

>   (defmacro roll (params &body body)
>     "(ROLL (&optional (ROLL-VAR 'ROLL) (ROLLS 1) (SIDES 6)) CONDITION ...)
                        ^^^^^^^^^^^^^^^^
                        I don't like this, 
                        since it introduces
                        the potential for
                        variable capture.

In particular, you can't do something like:

     (roll (roll)
         (1 (roll ()
                ((1 3) (format t "Half of ~A" roll))))
         ...)

since the second, nested ROLL variable will shadow the one you
explicitly specify.  It would be better to use (GENSYM "ROLL-")
instead as its value.

Also, you might want to replace the optional arguments to the macro
with keywords, so that you can more easily do things like:

  (roll (:sides 12)
     ...)

I would then perhaps name the variables (or at least the keywords)
something like :RESULT (instead of :ROLL-VAR) and maybe just :N instead
of :ROLLS.  :SIDES works well, unless you really want to go for brevity
and just use :D.  That gives something like

    (roll (:n 2 :d 10 :result attack)
       (... (print attack)))


>   Roll ROLLS SIDES-sided dice, assigning the total to locally-bound
>   ROLL-VAR.  Then check each CONDITION, in order, until one is true.
>   
>   Each CONDITION may be a single-item list, in which case it matches if
>   ROLL-VAR is = to that item, or it may be a two-item list, in which case
>   it matches if ROLL-VAR is >= the first item and <= the second item, or
>   it may be t, in which case it always matches.
>   
>   If no condition matches, the ROLL will signal an UNHANDLED-RESULT
>   warning."

SNIP.


Of course, if you go to keywords, you may than also want to change the
DIE function to match:

(defun die (&key (n 1) (sides 6))
    (let ((acc 0))
      (dotimes (i n)
        (incf acc (random sides)))
      (+ acc n)))


-- 
Thomas A. Russ,  USC/Information Sciences Institute
From: Robert Uhl
Subject: Re: [newbie] Dice-rolling Function & Macro
Date: 
Message-ID: <m3k6hphhro.fsf@4dv.net>
···@sevak.isi.edu (Thomas A. Russ) writes:
>
>>   (defmacro roll (params &body body)
>>     "(ROLL (&optional (ROLL-VAR 'ROLL) (ROLLS 1) (SIDES 6)) CONDITION ...)
>                         ^^^^^^^^^^^^^^^^
>                         I don't like this, 
>                         since it introduces
>                         the potential for
>                         variable capture.
>
> In particular, you can't do something like:
>
>      (roll (roll)
>          (1 (roll ()
>                 ((1 3) (format t "Half of ~A" roll))))
>          ...)
>
> since the second, nested ROLL variable will shadow the one you
> explicitly specify.  It would be better to use (GENSYM "ROLL-")
> instead as its value.

Yup--the new version does exactly that.

> Also, you might want to replace the optional arguments to the macro
> with keywords, so that you can more easily do things like:
>
>   (roll (:sides 12)
>      ...)

I considered that, but (die 1 6) is pretty obvious to someone familiar
with RPG terminology (in which 1d6 would mean 'roll 1 six-sided die').

I might switch to setting roll-var with a keyword, though.  Any
suggestions for what its name really oughta be?  There's got to be a
standard term for a variable to create within a macro so that it is
accessible to user-level code.

-- 
Robert Uhl <http://public.xdi.org/=ruhl>
> "global" connectivity (as in, access to OTHERS' PRIVATELY OWNED
> equipment) is a COURTESY and PRIVILEGE granted by the owners of
> that equipment. It is not a birthright.
                         --Chris Stassen
From: Harold
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <1126410775.698836.78960@z14g2000cwz.googlegroups.com>
What about (roll 1 :d 6)? That seems to match both Lisp and RPG style.

(defun roll (&optional (num-dice 1) &key (d 6))
   (loop for i from 1 to num-dice
         sum (1+ (random d))))
From: Robert Uhl
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <m34q8rfa2r.fsf@4dv.net>
"Harold" <·······@gmail.com> writes:
>
> What about (roll 1 :d 6)? That seems to match both Lisp and RPG style.
>
> (defun roll (&optional (num-dice 1) &key (d 6))
>    (loop for i from 1 to num-dice
>          sum (1+ (random d))))

That would work as well, although when I get to the point that I'm
generating a galaxy's worth of stars it may turn out that I prefer the
efficiency of non-keyword args.  Of course, optional args may be just as
expensive--and of course I haven't run any numbers on it yet.  Premature
optimisation is the root of all evil:-)

-- 
Robert Uhl <http://public.xdi.org/=ruhl>
What parts of `shall make no law,' `shall not be infringed,' and `shall
not be violated' don't you understand?
From: Harold
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <1126545704.237033.261700@o13g2000cwo.googlegroups.com>
I played around with LispWorks, OpenMCL and CMUCL. There does seem to
be a small performance hit (5-10%). But usually fixing algorithms can
help your scalability a lot more because you might be able to take an
O(n^2) down to O(n log n) here and there and save a lot of time, versus
worrying about a 10% speed-up on a roll.

Also, maybe if I turn on more aggressive compiler options (and/or
declare types for the parameters and return type) the compiler might be
able to get rid of that performance hit. e.g. Optional parameters cost
more because the callee has to see if a value was given and set up the
default value if needed. When compiling a caller that provides a value,
the call to ROLL should be able to just jump past the code for dealing
with missing arguments (if the callee is organized with the optional
parameter processing up front).
From: Robert Uhl
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <m3slwa6l32.fsf@4dv.net>
"Harold" <·······@gmail.com> writes:
>
> I played around with LispWorks, OpenMCL and CMUCL. There does seem to
> be a small performance hit (5-10%). But usually fixing algorithms can
> help your scalability a lot more because you might be able to take an
> O(n^2) down to O(n log n) here and there and save a lot of time,
> versus worrying about a 10% speed-up on a roll.

In this case I don't have much choice as to algorithms--I have to
implement the rules according to GURPS Traveller.  But even so, a mere
5-10% performance increase wouldn't make the difference between being
able to generate a subsector vs. a galaxy at a time.

FWIW, this is now the current code:

(defmacro roll (params &body body)
  (let (items)
    (destructuring-bind (&optional (rolls 1)
				   &key
				   ((:d sides) 6)
				   (roll-var (gensym "roll-var-")))
	params
      (dolist (item body)
	(let ((condition (first item))
	      (actions (rest item)))
	  (cond ((eq condition t)
		 (push `(t ,@actions) items))
		((numberp condition)
		 (push `((= ,roll-var ,condition) ,@actions) items))
		((not (consp condition))
		 (error "ROLL can handle numbers and conses, not ~A" condition))
		((> (length condition) 2)
		 (error "ROLL condition list must have 1 or 2 members (condition ~A has ~A)"
			condition (length condition)))
		((= (length condition) 2)
		 (push `((and (>= ,roll-var ,(first condition))
			  (<= ,roll-var ,(second condition))) ,@actions)
		       items))
		((= (length condition) 1)
		 (push `((= ,roll-var ,(car condition)) ,@actions) items)))))
      (setf items (reverse items))
      `(let ((,roll-var (die ,rolls ,sides)))
	(cond ,@items (t (warn "Roll of ~A was unhandled." ,roll-var)))))))

So a call would look like:

(roll (3 :d 6)
      (1 (foo))
      ((2 6) (bar))
      ((6 18) 'baz))

-- 
Robert Uhl <http://public.xdi.org/=ruhl>
But there ain't many troubles that a man can't fix
With seven hundred dollars and a thirty ought six.
From: Thomas A. Russ
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <ymi64t3dvkj.fsf@sevak.isi.edu>
Robert Uhl <·········@NOSPAMgmail.com> writes:

> FWIW, this is now the current code:
> 
> (defmacro roll (params &body body)
>   (let (items)
>     (destructuring-bind (&optional (rolls 1)
> 				   &key
> 				   ((:d sides) 6)
> 				   (roll-var (gensym "roll-var-")))
> 	params

  OK.  I would move the stuff from the DESTRUCTURING-BIND into the
lambda list of the macro itself.  Macro lambda lists are more flexible
than function lambda lists.  In particular, they have automatic
destructuring.   First of all it makes the macro signature clearer to
first inspection, and some development environments will helpfully show
you the lambda list when you are writing code.  That makes the
declaration look like

(defmacro roll ((&optional (rolls 1)
                 &key ((:d sides) 6)
                      (roll-var (gensym "roll-var")))
                 &body body)


>       (dolist (item body)
> 	(let ((condition (first item))
> 	      (actions (rest item)))
> 	  (cond ((eq condition t)
> 		 (push `(t ,@actions) items))
> 		((numberp condition)
> 		 (push `((= ,roll-var ,condition) ,@actions) items))
> 		((not (consp condition))
> 		 (error "ROLL can handle numbers and conses, not ~A" condition))
> 		((> (length condition) 2)
> 		 (error "ROLL condition list must have 1 or 2 members (condition ~A has ~A)"
> 			condition (length condition)))
> 		((= (length condition) 2)
> 		 (push `((and (>= ,roll-var ,(first condition))
> 			  (<= ,roll-var ,(second condition))) ,@actions)
> 		       items))

Here you can take advantage of the fact that Lisp's comparison functions
take an arbitrary number of arguments:

                 (push '((<= ,(first condition) ,roll-var ,(second condition))
                          ,@actions)
                       items)

> 		((= (length condition) 1)
> 		 (push `((= ,roll-var ,(car condition)) ,@actions) items)))))
>       (setf items (reverse items))
>       `(let ((,roll-var (die ,rolls ,sides)))
> 	(cond ,@items (t (warn "Roll of ~A was unhandled." ,roll-var)))))))
> 
> So a call would look like:
> 
> (roll (3 :d 6)
>       (1 (foo))
>       ((2 6) (bar))
>       ((6 18) 'baz))

And at this point, I would also consider renaming the macro WITH-ROLL.
There are a couple of reasons for that.  One is that it is a nice
parallel in nomenclature with the built-in Common Lisp WITH-... macros.
Also, some development environments, including Emacs have heuristic
indenting of WITH-... macro forms so that you would get an indentation
that looks more like this for free:

(with-roll (3 :d 6)
   (1 (foo))
   ((2 6) (bar))
   ((6 18) 'baz))

which nicely sets the parameter list off from the dispatching body of
the form.  It also is nicely suggestive of the fact that the macro is
doing something WITH a roll rather than just doing the roll itself.  The
roll is more the function which produces the result of the die roll.

-- 
Thomas A. Russ,  USC/Information Sciences Institute
From: Ivan Boldyrev
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <85mkv2-q9a.ln1@ibhome.cgitftp.uiggm.nsc.ru>
On 9230 day of my life Robert Uhl wrote:
> "Harold" <·······@gmail.com> writes:
>>
>> What about (roll 1 :d 6)? That seems to match both Lisp and RPG style.
>>
>> (defun roll (&optional (num-dice 1) &key (d 6))
>>    (loop for i from 1 to num-dice
>>          sum (1+ (random d))))
>
> That would work as well, although when I get to the point that I'm
> generating a galaxy's worth of stars it may turn out that I prefer the
> efficiency of non-keyword args.  Of course, optional args may be just as
> expensive--and of course I haven't run any numbers on it yet.  Premature
> optimisation is the root of all evil:-)

Use compiler macro.

(defun %roll (num-dice d)
   (loop for i from 1 to num-dice
         sum (1+ (random d))))

(defun roll (&optional (num-dice 1) &key (d 6))
    (%roll num-dice d))

(define-compiler-macro roll (&optional (num-dice 1) &key (d 6))
   `(%roll ,num-dice ,d))

-- 
Ivan Boldyrev

        Outlook has performed an illegal operation and will be shut down.
        If the problem persists, contact the program vendor.
From: Ed Symanzik
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <dga1a6$tc0$1@news.msu.edu>
Robert Uhl wrote:
> "Harold" <·······@gmail.com> writes:
> 
>>What about (roll 1 :d 6)? That seems to match both Lisp and RPG style.
>>
>>(defun roll (&optional (num-dice 1) &key (d 6))
>>   (loop for i from 1 to num-dice
>>         sum (1+ (random d))))
> 
> 
> That would work as well, although when I get to the point that I'm
> generating a galaxy's worth of stars it may turn out that I prefer the
> efficiency of non-keyword args.  Of course, optional args may be just as
> expensive--and of course I haven't run any numbers on it yet.  Premature
> optimisation is the root of all evil:-)
> 

For the sake of the uninitiated, let's say it was time to optimize this
piece.  What would you do here?

I started by moving the 1+ out of the loop and added a declare:

(defun roll (&optional (num-dice 1) &key (d 6))
   (declare (optimize (speed 3)))
   (loop for i from 1 to num-dice
         sum (random d) into total
         finally (return (+ num-dice total))))

At this point SBCL complains about all the optimizations it could do if
only it had type information.  I am finding though, that loop doesn't
like type information.  So, what now?
From: ··············@hotmail.com
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <1126738952.472594.289890@o13g2000cwo.googlegroups.com>
Ed Symanzik wrote:
>
> For the sake of the uninitiated, let's say it was time to optimize this
> piece.  What would you do here?
>
> I started by moving the 1+ out of the loop and added a declare:
>
> (defun roll (&optional (num-dice 1) &key (d 6))
>    (declare (optimize (speed 3)))
>    (loop for i from 1 to num-dice
>          sum (random d) into total
>          finally (return (+ num-dice total))))
>
> At this point SBCL complains about all the optimizations it could do if
> only it had type information.  I am finding though, that loop doesn't
> like type information.  So, what now?

Just a thought, backed by no experimental evidence.

One possibility is to recognize that the "six six-sided dice" is just a
gamer's way of specifying a particular probability density function.

There is a long history in the literature of techniques to rapidly
generate pseudo-random numbers with given probability density
functions, in order to support Monte Carlo calculations efficiently. My
recollection of one technique is that you use an approximation of the
inverse of the cumulative probability distribution, i.e. a function
that takes a uniform random variable on the range [0.0,1.0) as an
argument and returns a random number from the desired distribution. The
"efficiency" part is by using some quick-and-somewhat-dirty
approximation to the inverse distribution, instead of a expensive
special function call.

It seems to me that the answer might be to have the dice-rolling macro
change the "6d6" specifications into calls to an instance of one of
these optimized generators; the macro has the intelligence to generate
these functions from scratch; perhaps common combinations can be
supported by hand-coded generators.

Also, another thing to watch for is that the underlying random number
generator of your Lisp implementation might have favored good
statistical properties over speed. So your optimized generator might
have to rely on a higher-speed generator than the one provided by
Lisp's random.
From: Peter Seibel
Subject: Re: Dice-rolling Function & Macro
Date: 
Message-ID: <m2mzmfnz1w.fsf@gigamonkeys.com>
Ed Symanzik <···@msu.edu> writes:

> Robert Uhl wrote:
>> "Harold" <·······@gmail.com> writes:
>> 
>>>What about (roll 1 :d 6)? That seems to match both Lisp and RPG style.
>>>
>>>(defun roll (&optional (num-dice 1) &key (d 6))
>>>   (loop for i from 1 to num-dice
>>>         sum (1+ (random d))))
>> 
>> 
>> That would work as well, although when I get to the point that I'm
>> generating a galaxy's worth of stars it may turn out that I prefer the
>> efficiency of non-keyword args.  Of course, optional args may be just as
>> expensive--and of course I haven't run any numbers on it yet.  Premature
>> optimisation is the root of all evil:-)
>> 
>
> For the sake of the uninitiated, let's say it was time to optimize this
> piece.  What would you do here?
>
> I started by moving the 1+ out of the loop and added a declare:
>
> (defun roll (&optional (num-dice 1) &key (d 6))
>    (declare (optimize (speed 3)))
>    (loop for i from 1 to num-dice
>          sum (random d) into total
>          finally (return (+ num-dice total))))
>
> At this point SBCL complains about all the optimizations it could do if
> only it had type information.  I am finding though, that loop doesn't
> like type information.  So, what now?

LOOP has it's on scheme for declaring types. Check out the hyperspec.

-Peter

-- 
Peter Seibel           * ·····@gigamonkeys.com
Gigamonkeys Consulting * http://www.gigamonkeys.com/
Practical Common Lisp  * http://www.gigamonkeys.com/book/