I've never been very good at probability anyalisis, but I do enjoy a good game of Risk. I wanted to figure out the probabilities of the attacker losing no pieces, one piece, or two pieces under given rolls. I tried to work out how to do this using online tutorials, and quickly discovered that most web-based probability sources are aimed as continuing references for people who are already deep inside the field.

Anyway, there aren't that many combinations, so the problem is solvable using brute-force alone. Not my best code here, but it seems to be giving correct answers (or correct-looking answers, anyway :)

The output is divided into sections. The top of the section gives the number of attacker dice vs. the number of defender dice. In the following lines, the first column gives the defender's roll. The next three columns give the attacker's chance of losing none, losing one, or losing two (respectively) against that roll. The last line of the section gives the attacker's overall chance of losing none, losing one, or losing two.

I was interested to note that in the most common situation (3 attackers vs 2 defenders), the chances are almost even between the three possibilities, with the attacker being slightly favored to lose none. Also, the 1v1 roll slightly favors the defender.

#!/usr/bin/perl use strict; use warnings; # # Everything here is very brute-force and hackish # sub probability_to_beat { my @defender_roll = sort { $b <=> $a } @{ +shift }; my @attackers = @_; my $lose_none = 0; my $lose_one = 0; my $lose_two = 0; foreach my $attacker_roll (@attackers) { my @attacker_roll = sort { $b <=> $a } @$attacker_roll; if(defined($defender_roll[1]) && defined($attacker_roll[1])) { if( ($defender_roll[0] >= $attacker_roll[0]) && ($defender_roll[1] >= $attacker_roll[1]) ) { $lose_two++ } elsif( ( ($defender_roll[0] >= $attacker_roll[0]) && ($defender_roll[1] < $attacker_roll[1]) ) || ( ($defender_roll[0] < $attacker_roll[0]) && ($defender_roll[1] >= $attacker_roll[1]) ) ) { $lose_one++; } else { $lose_none++; } } else { if($defender_roll[0] >= $attacker_roll[0]) { $lose_one++; } else { $lose_none++; } } } return ( $lose_none, $lose_one, $lose_two, ); } sub one_die { return [map {[ $_ ]} 1 .. 6]; } sub two_die { return [map { my $first = $_; map {[ $first, $_ ]} 1 .. 6 } 1 .. 6]; } sub three_die { return [map { my $first = $_; map { my $second = $_; map {[ $first, $second, $_ ]} 1 .. 6 } 1 .. 6 } 1.. 6]; } sub run { my @attackers = @{ +shift }; my @defenders = @{ +shift }; my $num_attackers = scalar @{ $attackers[0] }; my $num_defenders = scalar @{ $defenders[0] }; my $total_lose_none = 0; my $total_lose_one = 0; my $total_lose_two = 0; foreach my $defender_roll (@defenders) { my ($lose_none, $lose_one, $lose_two) = probability_to_beat( $defender_roll, @attackers ); my $total = $lose_none + $lose_one + $lose_two; print join( ', ' => @$defender_roll) . "\t" . sprintf("%2.2f%%, %2.2f%%, %2.2f%%", ($lose_none / $total) * 100, ($lose_one / $total) * 100, ($lose_two / $total) * 100, ) . "\n"; $total_lose_none += $lose_none; $total_lose_one += $lose_one; $total_lose_two += $lose_two; } my $total = $total_lose_none + $total_lose_one + $total_lose_two; print "Totals:\t" . sprintf("%2.2f%%, %2.2f%%, %2.2f%%", ($total_lose_none / $total) * 100, ($total_lose_one / $total) * 100, ($total_lose_two / $total) * 100, ) . "\n"; } { print "3 v 2\n"; run( three_die, two_die ); print "\n3 v 1\n"; run( three_die, one_die ); print "\n2 v 2\n"; run( two_die, two_die ); print "\n2 v 1\n"; run( two_die, one_die ); print "\n1 v 2\n"; run( one_die, two_die ); print "\n1 v 1\n"; run( one_die, one_die ); }

"There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

Replies are listed 'Best First'.
Re: Dice Roll Probabilities in Risk
by Roy Johnson (Monsignor) on May 18, 2005 at 17:15 UTC
    I thought you'd be interested to see how much it condenses when you generalize it a bit. Instead of lose_one/two/three, there's @lose. Instead of three dice routines, a parameterized dice routine. More things could be turned into loops, but this covered the most glaring ones.

    Caution: Contents may have been coded under pressure.

      Thanks. I was trying to get my head around generalizing the dice-creation subroutines, but I couldn't think of how to do it. I figured a brute-force would work just as well, so I went with that.

      "There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

Re: Dice Roll Probabilities in Risk
by ivancho (Hermit) on May 21, 2005 at 15:09 UTC
    here's my 2c:

    We throw K dice and look for the best pair out of them.
    The probability that this best pair is (i, j), when i < j, is
    K * ( i ** (K-1) - (i-1) ** (K-1) ) / 6 ** K
    and when i ==j,
    ( i ** K - (i-1) ** K - K * (i-1) ** (K-1)) / 6 ** K.

    Where did this come from, you'd ask. Well, for the first part, we have K possible dice that could give a j, and then the other K-1 should be showing numbers no bigger than i, thus the i ** (K-1). But at least one has to be exactly i, thus we should remove the throws that have only smaller numbers - ie, (i-1) ** (K-1). Finally, divide by all possible throws, 6 ** K... If we are frisky and use dice with more, or less than 6 sides, we'd use that number instead. If we use a mixture of 6- and 20-sided dice,... well, let's not..
    the i == j case can be derived with the same logic

    trying to write it down in a common expression gives
    ((i<j) ? (K * i ** (K-1)) : (i ** K - (i-1) ** K ) - K * (i-1) ** (K-1)) / 6 ** K

    Ugly? you bet.. but on the positive side, it's the only explicit expression we'll have to write. So let's code it up

    sub n_sides {6} sub attack {3} sub defend {2} sub loss { my ($att, $def) = @_; return (($att->[0] <= $def->[0]) + ($att->[1] <= $def->[1])); } sub pair_prob { my ($K, $pair) = @_; my ($i, $j) = @{$pair}; return (((($i < $j) ? ($K * $i ** ($K-1)) : ($i ** $K - ($i-1) ** $K)) - $K * ($i-1) ** ($K-1)) / (n_sides() ** $K)); } my @pairs = map {my $j = $_; map {[$_,$j]} (1..$j) } (1..n_sides()); my @losses; foreach my $att (@pairs) { foreach my $def (@pairs) { $losses[loss($att,$def)] += pair_prob(attack(), $att) * pair_prob(defend(), $def); } } print "P(loss = $_) = ", $losses[$_], "\n" for (0..@losses-1);

    What we really want, though, is to replace the @pairs array with some sort of iterator, so we don't have to store them all, and, the bigger problem, to generalize pairs to tuples..
    Currently, we can handle attacker throwing 8 dice and defender 5, but then we only compare the top 2, while we should be comparing the top 5..

    This is where things become really hairy.. We need a tuple_prob function, that from number of dice, K, and an m-tuple i_1 <= i_2 <= ... <= i_m, gives the probability that this is our best m-tuple from K thrown dice.
    Let's call the signature of a tuple a count of how many times each element repeats. For example, the signature of (1,1,2,2,2,5) is (2,3,1).
    Now imagine that for our m-tuple, the signature is (k_1,k_2,..k_s).. Our answer, then, has a coefficient of (K choose k_2,k_3,..,k_s), which is in how many ways we can throw the non-smallest parts of the tuple, and now we have to figure out what i_1 and k_1 do.

    Well, if we call that part F(i,k,K), we have that
    F(i,k,K) = F(i,k-1,K-1) + (i-1) * F(i,k,K-1)
    with some boundary conditions, ie, F(i,1,K) = i ** (K-m+1) - (i-1) ** (K-m+1).
    Add: These are the polynomials in i that we had above, just with more terms

    We can recurse this and get our tuple_prob. Should probably cache the Fs, they will repeat a lot. We can then wrap it up with a tuple iterator and have a horribly over-generalized and useless Risk calculator.

    So, if you actually made it down to here, and are now wondering what the heck does all this have to do with you, Perl, or anything, rest assured, absolutely nothing.. I had time to waste and holli wrote 'Because I think there must be a "formula" approach instead of brute force.'
    Pure formulae solution this is not, but we'd gain a few orders of magnitude on brute force, if we have some weirdos that play with bagfuls of dice...

Re: Dice Roll Probabilities in Risk
by scmason (Monk) on May 20, 2005 at 19:15 UTC
Re: Dice Roll Probabilities in Risk
by Anonymous Monk on May 25, 2005 at 02:56 UTC
    For 2 dice ...
    1
    21
    321
    4321
    54321
    654321
     65432
      6543
       654
        65
         6
    
    For 3 ...
    1
    21    1
    321   21
    4321  321
    54321 4321
    65432154321
     65432654321    any so on...
      6543 54321
       654  4321
        65   321
         6    21
               1