18
\$\begingroup\$

Lucky dice rolls

In pen and paper roleplaying games dice are used for various chance calculations. The usual way to describe a roll is \$n\textbf{d}k\$ where \$n\$ is the number of dice and \$k\$ is the number of faces on a die. For example \$3d6\$ means that you need to roll the classical 6-sided die 3 times (or roll 3 dice at the same time). Both \$n\$ and \$k\$ are positive integers. Each die's output value ranges from 1 to \$k\$.

Usually the values are then summed and they are used for various game mechanics like chance to hit something or damage calculations.

A lucky roll will mean that you have Fortuna's favor on your side (or against you). Luckiness is an integer number that increases (or decreases) the sum in the following way. The roll is modified to \${(n+|luck|)}\textbf{d}{k}\$ and the sum will be the \$n\$ best (or worst) values. Each die is fair, so they will have the same probability for the outcome of the possible values per die.

The \$luck\$ can be a negative number, in this case you need to get the \$n\$ worst values for the sum.

Input

The integer values for \$n,k,luck\$ in any way.

Output

The expected value for the sum of the (un)lucky roll. The expected value is \$\sum{x_{i} p_{i}}\$ where \$x_{i}\$ is the possible outcome of the sum and \$p_{i}\$ is the probability for \$x_{i}\$ occuring, and \$i\$ indexes all possible outcomes. The output value can be float or rational number, at least 3 decimal places of accuracy or a fraction of two integer numbers, whichever suits your program better.

Examples

n,k,luck    expected value
1,6,0       3.5
2,6,0       7
2,6,-1      5.54166
2,6,1       8.45833
2,6,-5      3.34854
2,10,-1     8.525
2,10,1      13.475
6,2,15      11.98223
6,2,-15     6.01776

Scoring

Shortest code in bytes wins.

Other

With this mechanic you essentially create fake dice using only fair dice. I wonder if there is a nice formula to calculate this mathematically.

Good luck! ;)

\$\endgroup\$
11
  • 2
    \$\begingroup\$ Shouldn't the fourth example have 8.458333333333 as expected answer? Rounding should not result in a trailing 4. \$\endgroup\$
    – Abigail
    Commented Jun 15, 2020 at 15:01
  • 1
    \$\begingroup\$ @Abigail Time for you to read What every computer scientist should know about floating-point arithmetic. \$\endgroup\$
    – Noodle9
    Commented Jun 15, 2020 at 15:33
  • 3
    \$\begingroup\$ You may want to add a test case with \$|luck|>1\$. (Unless we always have \$|luck|\le1\$, in which case it should be specified.) \$\endgroup\$
    – Arnauld
    Commented Jun 15, 2020 at 15:38
  • 2
    \$\begingroup\$ @Noodle9 I expect the examples to be exact (up to rounding), and not to show some artifact from floating point arithmetic. \$\endgroup\$
    – Abigail
    Commented Jun 15, 2020 at 15:51
  • 2
    \$\begingroup\$ @Noodle9 I presume the OP is a human, not a binary device. No human should round the exact value of 1827 / 216 to 8.458333333333334. \$\endgroup\$
    – Abigail
    Commented Jun 15, 2020 at 16:12

14 Answers 14

9
\$\begingroup\$

AnyDice, 82 bytes

function:l N K L{ifL<0{result:[lowestNof(N-L)dK]}else{result:[highestNof(N+L)dK]}}

Try it online!

For the output, check the "export" view and "summary" data and take the first value next to the output name (normally the link brings you there, but if you encounter issues, you know).

Ungolfed for readability

function: l N K L {                  \ function with 3 parameters                                     \
  if L<0 {                           \  if L is negative                                              \
    result: [lowest N of (N-L)dK]    \   return the lowest N dice among (N-L) rolls of a K-sided die  \
  } else {                           \  else                                                          \
    result: [highest N of (N+L)dK]   \   return the highest N dice among (N-L) rolls of a K-sided die \
  }                                  \  end if                                                        \
}                                    \ end function                                                   \
\$\endgroup\$
1
  • \$\begingroup\$ @Memberfor3months The value is incorrect for several test cases: 2, 6, 1, 2, 10, 1 and 6, 2, 15, basically all cases where L is strictly positive. \$\endgroup\$ Commented Jun 20, 2020 at 17:56
4
\$\begingroup\$

R, 106 96 88 bytes

function(n,k,l)n*mean(apply(expand.grid(rep(list(NA,1:k),n+abs(l))),1,sort,l>0,T)[1:n,])

Try it online!

Credit to Dominic van Essen for the l>0 for the descending argument to sort, and for golfing down a lot of other bytes!

\$\endgroup\$
6
  • \$\begingroup\$ Nice. Another example where I accidentally learn something useful (expand.grid) while trying to waste time... \$\endgroup\$ Commented Jun 15, 2020 at 19:48
  • \$\begingroup\$ 99 bytes if you simply take the mean of the (selected) dice rolls. \$\endgroup\$ Commented Jun 15, 2020 at 19:51
  • \$\begingroup\$ 97 bytes with [] instead of head (assuming there aren't any annoying edge cases that get broken by this). \$\endgroup\$ Commented Jun 15, 2020 at 19:57
  • \$\begingroup\$ @DominicvanEssen neat! I saw you used that method of extracting the first n rows and the n*mean() in your code but I think I flubbed it by putting the [1:n,] after the apply() instead of after the matrix(). \$\endgroup\$
    – Giuseppe
    Commented Jun 15, 2020 at 20:17
  • 1
    \$\begingroup\$ Nice! You can just put the NA inside the list() to save some more bytes. It makes the array bigger, but who cares! \$\endgroup\$
    – Giuseppe
    Commented Jun 15, 2020 at 20:35
4
\$\begingroup\$

MATL, 24 bytes

|+i:Z^!S1G0>?P]2G:Y)XsYm

Inputs are: luck, n, k.

How it works

|      % Implicit input: luck. Absolute value
+      % Implicit input: n. Add. Gives n+|luck|
i:     % Input: k. Range [1 2 ... k]
Z^     % Cartesian power. Gives a matrix with n+|luck| columns, where each
       % row is a Cartesian tuple
!      % Transpose
S      % Sort each column in ascending order
1G     % Push first input (luck) again
0>     % Is it positive?
?      % If so
  P    %   Flip vertically: the order within each column becomes descending
]      % End
2G:    % Push second input (n) again. Range [1 2 ... n]
Y)     % Row-index. This keeps the first n rows
Xs     % Sum of each row
Ym     % Mean. Implicit display

Try it online! Or verify all test cases.

\$\endgroup\$
3
\$\begingroup\$

05AB1E, 15 bytes

L²³Ä+ãε{³.$O}ÅA

Inputs in the order \$k,n,luck\$.

Try it online or verify all test cases.

Explanation:

L             # Push a list in the range [1, (implicit) input `k`]
 ²            # Push the second input `n`
  ³Ä+         # Add the absolute value of the third input `luck`
     ã        # Take the cartesian product of the list and this value
      ε       # Map each inner list to:
       {      #  Sort the list
        ³.$   #  Drop the third input amount of leading items,
              #   `luck` = 0: no items are removed
              #   `luck` = 1: the first item is removed
              #   `luck` = -1: the last item is removed
           O  #  Sum the remaining list of values
      }ÅA     # After the map: calculate the average of this list of sums
              # (after which it is output implicitly as result)
\$\endgroup\$
5
  • \$\begingroup\$ Nice approach :-D \$\endgroup\$
    – Luis Mendo
    Commented Jun 15, 2020 at 15:10
  • \$\begingroup\$ @LuisMendo Thanks. I see it's pretty similar as your answer and the Python answer in terms of the cartesian product and average (now that you've added an explanation, since I can't read MATL). :) \$\endgroup\$ Commented Jun 15, 2020 at 15:12
  • \$\begingroup\$ Yes, I guess that enumerating the possibilities and averaging is the golfiest approach in most languages... \$\endgroup\$
    – Luis Mendo
    Commented Jun 15, 2020 at 15:18
  • \$\begingroup\$ [ I was by no means suggesting you took it from my answer. I just found the coincidence funny :-) On second thought, may be not so much a coincidence, just the straightforward approach ] \$\endgroup\$
    – Luis Mendo
    Commented Jun 16, 2020 at 9:01
  • \$\begingroup\$ @LuisMendo I know. :) I also only noticed it was similar after you added an explanation. As you said, generating all sums and taking the average is probably the best approach for most languages. \$\endgroup\$ Commented Jun 16, 2020 at 9:09
3
\$\begingroup\$

Perl 5 -MList::Util=sum -ap, 116 bytes

@r=1..$F[1];$_=(sum map{(sort{$F[2]<0?$a-$b:$b-$a}/\d+/g)[0.."@F"-1]}@g=glob join$"=',',("{@r}")x("@F"+abs$F[2]))/@g

Try it online!

Enumerates all possible rolls, selects the top (bottom) entries from each list, adds all of those up, and divides by the number of combinations.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Nice as ever! Can't think of a different approach right now, but you can save a few bytes with a little re-organisation: Try it online! \$\endgroup\$ Commented Jun 16, 2020 at 12:35
3
\$\begingroup\$

J, 45 bytes

Takes input as k on the left side and n, luck on the right side.

[:(+/%#){:@]+/@}.&|:1+[:/:~"1[#.inv(i.@^+&|/)

Try it online!

How it works

[:(+/%#){:@]+/@}.&|:1+[:/:~"1[#.inv(i.@^+&|/)
                                    i.@^+&|/ 0..k^(|n| + |luck|)
                             [#.inv          to base k 0 0 0..5 5 5
                        /:~"1                sort each roll
                    1+                       0 0 0 -> 1 1 1
        {:@]   }.&|:                         transpose and drop luck rows
                                             negative values drop from end
            +/                               sum each roll
  (+/%#)                                     average of all rolls                   
\$\endgroup\$
3
\$\begingroup\$

R, 152 127 109 bytes

function(n,k,l,w=n+abs(l))n*mean(apply(cbind(NA,mapply(rep,list(1:k),e=k^(w:1-1),l=k^w)),1,sort,l>0,T)[1:n,])

Try it online!

Edit: -18 bytes thanks to Giuseppe for a really nice bit of programming! Note that this solution avoids a key R built-in function expand.grid, but Giuseppe's improvement manages to close-the-gap on his own solution (that uses the function) quite a lot.

Commented:

lucky_total=function(n,k,l){
    m=n+abs(l)          # number of rolls including lucky rolls
    a=matrix(NA))       # initial (empty) matrix of roll results
    for(r in 1:m){      # do all the rolls & combine results in matrix
        a=cbind(a[rep(seq(d<-k^(r-1)),k),],rep(1:k,e=d))
    }
    mean(               # get the mean result of...
        apply(a,1,function(b)
                        # all the rolls, but only keeping
                        # the highest/lowest 'lucky' dice
                        # (using luck>0 to decide whether to sort 
                        # increasing or decreasing)
            sum(sort(b,l>0)[1:n])
        )
    )
}
\$\endgroup\$
3
  • \$\begingroup\$ struggled mightily to get apply to do what I wanted for n=1; I see you went with na.last to get around that, but I ended up just reshaping the output as a matrix again, which seems to have worked, and I did steal your decreasing argument to sort in the apply :-) \$\endgroup\$
    – Giuseppe
    Commented Jun 15, 2020 at 19:40
  • \$\begingroup\$ 109 bytes -- wanted to see if you could do it without expand.grid and ended up golfing it out more than I thought I could... \$\endgroup\$
    – Giuseppe
    Commented Jun 16, 2020 at 17:39
  • \$\begingroup\$ Incredible! I'd given-up trying to improve this after realising I'd never beat your version with expand.grid! The argument-recycling trick using mapply is brilliant: I had to run it several times to understand what's going on! \$\endgroup\$ Commented Jun 16, 2020 at 20:19
2
\$\begingroup\$

Python 2, 131 bytes

from itertools import*
n,k,l=input()
w=n+abs(l)
print sum(sum(sorted(x)[l>0and-n:][:n])for x in product(*[range(1,k+1)]*w))*1./k**w

Try it online!

\$\endgroup\$
1
  • \$\begingroup\$ range(1,k+1) -> range(k) and +n at the end: sum(sum(sorted(x)[l>0and-n:][:n])for x in product(*[range(k)]*w))*1./k**w+n \$\endgroup\$
    – tsh
    Commented Jun 16, 2020 at 3:50
2
\$\begingroup\$

R, 131 119 bytes

function(Z,Y,l,E=Z*(1+Y)/2,`[`=pbinom)(sum(1:Y*((K=rep(1:Z-1,e=Y))[X<-abs(l)+Z,J<-1-1:Y/Y]-K[X,J+1/Y]))-E)*(-1)^(l<0)+E

Try it online!

Quite a fast implementation; calculates the value directly. It's binomials all the way down.

The key is the identity found here, for the expected value of a rolling \$X\$ d\$Y\$ and keeping the highest \$Z\$ of them. I rearranged it slightly to

$$\sum_{j=1}^{Y}j \sum_{k=0}^{Z-1} \sum_{l=0}^k \binom{X}{l}\left(\left(\frac{Y-j}{Y}\right)^l\left(\frac{j}{Y}\right)^{X-l} - \left(\frac{Y-j+1}{Y}\right)^l\left(\frac{j-1}{Y}\right)^{X-l}\right). $$

Recognizing the innermost sum as the difference of two binomial CDFs, it's implemented as

sum(1:Y*(p(K<-rep(1:Z-1,e=Y),X,J)-p(K,X,J+1/Y)))

for maximum (ab)use of R's recycling rules. There is then an adjustment for the fact that we may wish to keep the lowest n dice, but that's easy due to the symmetry of the binomial distribution.

\$\endgroup\$
1
  • \$\begingroup\$ nice find! I searched everywhere for a formula but this seems to be the one I was looking for :) \$\endgroup\$ Commented Jun 18, 2020 at 15:40
1
\$\begingroup\$

perl -alp -MList::Util=sum, 144 bytes

@,=map{@;=sort{$a<=>$b}/\d+/g;pop@;for$F[2]..-1;shift@;for 1..$F[2];sum @;}glob join",",("{".join(",",1..$F[1])."}")x($_+abs$F[2]);$_=sum(@,)/@,

Try it online!

More readable written:

use 5.026;

use strict;
use warnings;
no  warnings 'syntax';

my ($n, $k, $luck) = @F;

my @a = map {         # Iterate over all possible rolls
    my @b = sort {$a <=> $b} /\d+/g;  # Grab the digits, sort them.
    pop @b for $luck .. -1;           # Remove the -luck best rolls.
    shift @b for 1 .. $luck;          # Remove the  luck worst rolls.
    sum @b;                           # Sum the remaining pips.
}
glob       #  Glob expansion (as the shell would do)
join ",",  #  Separate the results of each die in a roll.
           #  Almost any character will do, as long as it's
           #  not special for glob expansion, and not a digit
     (
        "{" .      # "{" introduces a set of things glob can choose from
            join (",", 1 .. $k) .   # 1 to number of faces
         "}"       # matching "}"
     ) x ($n + abs $luck);  # Number of dice in a roll

$_ = sum (@a) / @a;  # Sum the results of each different roll,
                     # and divide by the number of rolls; $_ is
                     # printed at the end of the program.

__END__

Reads space separated numbers from STDIN. Writes results to STDOUT.

\$\endgroup\$
1
\$\begingroup\$

JavaScript (ES10), 143 bytes

A naive, straightforward approach.

(n,k,l)=>eval([...Array(N=k**(t=l<0?n-l:n+l))].flatMap((_,v)=>[...Array(t)].map((_,i)=>-~(v/k**i%k)).sort((a,b)=>(a-b)*l).slice(-n)).join`+`)/N

Try it online!

How?

We generate \$N=k^{n+|l|}\$ arrays of length \$n+|l|\$ corresponding to all possible rolls, keeping only the \$n\$ best or \$n\$ worst die in each array.

We turn that into a single flat list of values, compute its sum and divide it by \$N\$.

\$\endgroup\$
1
\$\begingroup\$

JavaScript (Node.js), 116 bytes

k=>l=>g=(n,w=[],h=i=>i&&g(n-1,[...w,i])+h(i-1),L=l<0?-l:l)=>n+L?h(k)/k:eval(w.sort((a,b)=>(a-b)*l).slice(L).join`+`)

Try it online!

\$\endgroup\$
1
\$\begingroup\$

Charcoal, 57 bytes

NθNηNζ≧⁺↔ζθ≔XηθεFε«≔⊕…⮌↨⁺ιεηθδF↔ζ≔Φδ⁻μ⌕δ÷⌊×δζζδ⊞υΣδ»I∕Συε

Try it online! Link is to verbose version of code. Explanation:

NθNηNζ

Input n, k and l.

≧⁺↔ζθ

Add |l| to n.

≔Xηθε

Calculate the number of possible outcomes of rolling n+|l| k-sided dice.

Fε«

Loop over each outcome index.

≔⊕…⮌↨⁺ιεηθδ

Generate the next outcome by converting to base k padded to length n+|l|.

F↔ζ

For each element of luck, ...

≔Φδ⁻μ⌕δ÷⌊×δζζδ

... remove the lowest or highest value from the outcome.

⊞υΣδ

Save the sum of the remaining dice.

»I∕Συε

Output the average sum.

41 bytes if l is limited to -1, 0 or 1:

NθNηNζ≧⁺↔ζθ≔XηθεI∕ΣEEε⊕…⮌↨⁺ιεηθ⁻Σι×⌊×ιζζε

Try it online! Link is to verbose version of code. Explanation:

NθNηN

Input n, k and l.

≧⁺↔ζθ

Add |l| to n.

≔Xηθε

Calculate the number of possible outcomes of rolling n+|l| k-sided dice.

I∕ΣEEε⊕…⮌↨⁺ιεηθ⁻Σι×⌊×ιζζε

Generate all the possible outcomes, but if the luck is -1 or 1 then subtract the largest or smallest entry from the sum, finally calculating the average sum.

\$\endgroup\$
1
\$\begingroup\$

APL (Dyalog Unicode), 61 58 57 64 61 bytesSBCS

Full program, input order is k, luck and n.

(⊢⌹=⍨){w←1∘/⍵⋄1⊥w[⍒w]↑⍨n×(¯1*<∘0)l}¨(,∘.,)⍣(¯1+(n←⎕)+|l←⎕)⍨⍳⎕

Try it online! (with two extra bytes to print in TIO) or check all test cases!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.