in reply to Dice calcs?

OK. I've taken a look at this and there's are a problem with your order of preference: @[] has to be at the top otherwise 6@[3d8] will roll an 8 sided dice three times and return that value six times. Thus you'll get something like {6, 6, 6}. With the @[] at the top it will run 3d6 three times and you'll get three different values.

Below is an example of a dice calculator using your given rules. There are no symbol defs though and its not interactive. There's also a lot of code in there for verbose output. You might want to can it by setting $verbose=0 :)

You'll notice I've changed the precedence to (), @, <>, &, d, *, /, +, - in order to get your examples to work.

#!/usr/bin/perl -w use strict; my $verbose=1; my $indent=0; my @tests = ( ['3d6', 'Returns a value 3 to 18'], ['(1d4)d20','Rolls a 20 sided die 1 to 4 times and sums the results +'], ['6@[3d6]','Returns 6 values of 3 to 18 in a list'], ['6>12@[3d6]','Returns the best 6 of 12 3d6\'s in a list'], ['6@[&3>4@[(1d5)+1]]','Equivelant to the ol\' "Roll 6 stats -- 4d6, + re-roll 1\'s, drop the lowest die."'], ); foreach my $part (@tests) { $indent = 0; my ($test,$text) = @$part; print "$test : $text\n"; print 'RESULT: ' . determine($test) . "\n"; print '-' x 80; print "\n\n"; } sub determine { my $term = shift; print indent(0,+3) . "Determining '$term'.\n" if $verbose; # Loop until all calcs are done or we're 100 deep. my $loop = 0; while (($term=~/[\@<>\(d\*\/\+\-&]/) && ($loop < 100)) { # Precedence 5: '@' #$term=~s/(\d+)@\[([^\[\]]+)\]/rpt($1,$2)/eg; $term=~s/(\d+)@\[(.*)\]/rpt($1,$2)/eg; # Precedence 6: '<,>' $term=~s/(\d+)([<>])\{([^\{\}]+)\}/gtlt($1,$2,$3)/eg; # Precedence 7: '&'; $term=~s/\&\{([^\{\}]+)\}/sum($1)/eg; # Precedence 1: Parenthesis $term=~s/\(([^\(\)]+)\)/determine($1)/eg; # Precedence 2: 'd' $term=~s/(\d+)d(\d+)/roll($1,$2)/eg; # Precedence 3: '*,/' $term=~s/(\d+)\*(\d+)/($1*$2)/eg; $term=~s|(\d+)/(\d+)|($1/$2)|eg; # Precedence 4: '+,-' $term=~s/(\d+)\+(\d+)/($1+$2)/eg; $term=~s/(\d+)\-(\d+)/($1-$2)/eg; } print indent(-3,0) . "Got $term.\n" if $verbose; return $term; } sub rpt { my ($repeat,$term) = @_; print indent(0,+3) . "Running '$term' $repeat times.\n" if $verbose + >= 2; my $result='{'; for (1 .. $repeat) { my $oneresult = determine($term); $result .= $oneresult . ', '; } $result =~s/,\s*$/}/; print indent(-3,0) . "Returning $result.\n" if $verbose >= 2; return $result; } sub gtlt { my ($items,$sign,$list) = @_; my $result; if ($sign eq '<') { print indent(0,+3) . "Finding lowest $items items in {$list}.\n" + if $verbose >= 2; my @list = split(/,\s*/,$list); print indent(0,0) . "Split: @list\n" if $verbose >= 4; @list = sort {$a <=> $b} @list; print indent(0,0) . "Sort: @list\n" if $verbose >= 4; @list = @list[0..($items-1)]; print indent(0,0) . "Sub: @list\n" if $verbose >= 4; $result = '{' . join(', ', @list) . '}'; } elsif ($sign eq '>') { print indent(0,+3) . "Finding highest $items items in {$list}.\n +" if $verbose >= 2; my @list = split(/,\s*/,$list); print indent(0,0) . "Split: @list\n" if $verbose >= 4; @list = reverse sort {$a <=> $b} @list; print indent(0,0) . "Sort: @list\n" if $verbose >= 4; @list = @list[0..($items-1)]; print indent(0,0) . "Sub: @list\n" if $verbose >= 4; $result = '{' . join(', ', @list) . '}'; } print indent(-3,0) . "Returning $result.\n" if $verbose >= 2; return $result; } sub sum { my $list = shift; print indent(0,+3) . "Summing {$list}.\n" if $verbose >= 2; my @list = split(/,\s*/,$list); my $result; foreach my $item (@list) { $result += $item; } print indent(-3,0) . "Returning $result.\n" if $verbose >= 2; return $result; } sub roll { my ($repeat,$sides) = @_; print indent(0,+3) . "Rolling a $sides sided dice $repeat times and + summing the results.\n" if $verbose; my $result; for (1 .. $repeat) { my $roll = int(rand($sides)+1); print indent(0,0) . "Got a $roll.\n" if $verbose >= 2; $result += $roll; } print indent(-3,0) . "Returning $result.\n" if $verbose; return $result; } sub indent { my ($pre,$post) = @_; $indent += $pre; my $result = ' ' x $indent; $indent += $post; return $result; #" [$indent] "; }

Replies are listed 'Best First'.
Re: Re: Dice calcs?
by Anonymous Monk on Sep 18, 2002 at 23:21 UTC
    Hmmmm.... I hadn't thought about in-place substitutions. My original assignment was an infix-to-postfix calculator, and the precedence order I listed was required for that algorithm to work.

    I did find a few bugs in your solution. I realize your code was just an example, and I appreciate the insight, I just want to make sure anyone that downloads it realizes there are issues. For example, your code wouldn't allow for multiple @[] operators at the same level (i.e. &3>4@[1d6]+&1>4@[1d100]). Division should be integer. Negative numbers (and thus unary -) need to be implemented. There's no general syntax checking.

    I went ahead with the in-place substitution idea and should have some code ready to post soon. I've addressed all of the above issues and a few more. There are still issues with precedence lists, though.... with this method it is impossible/difficult for operators to share the same precedence. For example, normally something like 5*20/3*3 should equal 99, but in this case it comes up 11 (100/9). That's not a problem as long as it's documented.

      Oh, good bug-find. Here's a couple of fixes that you've prolly already done

      1. To get it to do the multiple @ operators at the same level you need to

      use Regexp::Common
      Then replace the 'Precedence 5' regexp with:
      $term=~s/(\d+)@($RE{balanced}{-parens=>'[]'})/rpt($1,$2)/eg;

      2.To get it to do the * and / from left to right, change 'Precedence 3' to this single regexp:

      $term=~s/(\d+)([\*\/])(\d+)/int(eval("$1$2$3"))/eg;
      and change 'Precedence 4' to
      $term=~s/(\d+)([+-])(\d+)/eval("$1$2$3")/eg;

      Hope these help you with your efforts. There is still no syntax checking but I figure you can handle stopping people from putting 3*+/4. Oh, and you probably want to handle negative numbers before 'Precedence 4'

        I handled the matching parens in the subroutine.

        Your fix to precedence 3 still isn't going to work:

        my $x = '20*5/3*3'; $x =~ s/(\d+)([\*\/])(\d+)/int(eval("$1$2$3"))/eg; print "$x\n"; Output: 100/9

        I'm okay for the moment with non-shared precedence. That's what parens are for. :-)

        I also changed some of the logic in the determine sub, because as you can see from above, one s/// pass isn't always enough. I didn't want lower-precedence operators being evaluated before all of the higher-precedences had been taken care of. That helped with the unary - problem... I made - a higher precedence than + and tweaked the rx's slightly.

        The only bit I have left to do (when I can find time) is to add a pre-parser similar to my infix-to-postfix program that will go a long way to validating syntax (but this isn't exactly a mission-critical module either :-)

        Thanx!
        Matt Stum