13
\$\begingroup\$

I'm looking to model an alternate to the "4d6, drop the lowest" mechanic for determining ability scores.

The proposed system works as follows:

  1. 4 players roll 4d6, drop the lowest x 6 [24 total].
  2. Combine these results into a stat pool.
  3. Players then roll initiative (1d20).
  4. Players select stats in "snake order" [1, 2, 3, 4, 4, 3, 2, 1, 1, 2...]
  5. Players assign stats to ability scores as desired.

(I am thinking this is a good way to have fun rolling dice without the inter-party balance issues of straight 4d6, drop 1.)

Obviously for modeling, this can be simplified:

  1. Roll 24 d [highest 3 of 4d6]
  2. Rank the dice
  3. Loop Player 1..4 and assign values per "snake order", assuming highest available is always selected
  4. Output of 6 abilities per player (or of only player 1 vs player 4)

How do I model the probability distribution of the above in Anydice? I'm getting hung up on the syntax.

\$\endgroup\$
10
  • 2
    \$\begingroup\$ Are you trying to just expedite this process, or looking for graphs of the probabilities for this method? If the latter, do you want sum of stats for each player? What are you trying to output? \$\endgroup\$ Apr 11, 2018 at 21:50
  • 1
    \$\begingroup\$ Correct me if I'm wrong, but my understanding of AnyDice is that it just calculates broad probability. \$\endgroup\$
    – Tanthos
    Apr 11, 2018 at 21:55
  • \$\begingroup\$ @Tanthos You could use Anydice to find probabilities of a number of things related to this method, but there is no easy way to just get an output of the resulting ability scores. \$\endgroup\$ Apr 11, 2018 at 21:59
  • \$\begingroup\$ Anydice has an inherent problem with pools larger than 10 as far as I experienced \$\endgroup\$
    – Trish
    Apr 11, 2018 at 22:28
  • \$\begingroup\$ I think this is pretty clear; there's even a sketch of what the code would be like. The OP clearly misunderstands how Anydice works, and that misunderstanding can be addressed in an answer. \$\endgroup\$ Apr 12, 2018 at 2:03

4 Answers 4

12
\$\begingroup\$

Overall this seems to benefit the first player, but it depends how you measure it.

I thought I would model this programatically as it will be a bit more flexible than using AnyDice. I've written a script which carries out the process a large number of times and averages the values for each player.

The main difficulty here is how you actually interpret the data: what makes one array of ability scores better than another one? There are multiple ways to judge this. I'll include some values for different methods.

Total of all ability scores
Player 1 has mean ability score value 72.95522.
Player 2 has mean ability score value 73.43975.
Player 3 has mean ability score value 73.64131.
Player 4 has mean ability score value 73.72761.
Verdict: Being later in the order is better

Total of all ability scores except the lowest one
Player 1 has mean ability score value 66.49585.
Player 2 has mean ability score value 65.71347.
Player 3 has mean ability score value 65.10333.
Player 4 has mean ability score value 64.57315.
Verdict: Being earlier in the order is better

Total points buy cost of all ability scores
(I've used invented points buy scores for numbers outside the normal allowed range: 18 = 19, 17 = 15, 16 = 12, 6-7 = -1, 4-5 = -2, 3 = -1)
Player 1 has mean ability score value 33.43838.
Player 2 has mean ability score value 31.80692.
Player 3 has mean ability score value 31.00389.
Player 4 has mean ability score value 30.64647.
Verdict: Being earlier in the order is better

Total points buy cost of all ability scores except the lowest one
Player 1 has mean ability score value 34.33727.
Player 2 has mean ability score value 31.88918.
Player 3 has mean ability score value 30.39722.
Player 4 has mean ability score value 29.45149.
Verdict: Being earlier in the order is much better

I've included my horrible amateur code here so you can try it out.

#!/usr/bin/perl

use strict;
use warnings;
use List::Util qw(sum);
use Data::Dumper;
use POSIX;
use 5.010;

my $die_size = 6;
my $number_of_dice = 4;
my $number_of_players = 4;
my $number_of_runs = 10000;

sub get_single_ability_score {
    my @rolls;
    for (1..$number_of_dice) {
        my $roll = 1 + int rand($die_size);
        push @rolls, $roll;
    }
    @rolls = sort {$a <=> $b} @rolls;
    for (1..$number_of_dice - 3) {
        shift @rolls;
    }
    my $ability_score = sum(@rolls);
    return $ability_score;
}

sub get_total_values {
    my @group_ability_scores;
    for (1..$number_of_players * 6) {
        push @group_ability_scores, get_single_ability_score();
    }
    @group_ability_scores = sort { $b <=> $a } @group_ability_scores;
    my @player_order = (1..$number_of_players);
    my @reverse_player_order = sort { $b <=> $a } @player_order;
    @player_order = (@player_order, @reverse_player_order, @player_order, @reverse_player_order, @player_order, @reverse_player_order);
    my @player_ability_scores;
    foreach my $player (@player_order) {
        my @ability_scores;
        my $chosen_ability_score = shift @group_ability_scores;
        push @ability_scores, $chosen_ability_score;
        push @{ $player_ability_scores[$player-1] }, @ability_scores;
    }
    my @total_values;
    foreach my $player (1..$number_of_players) {
        my @ability_scores = sort { $a <=> $b } @{ $player_ability_scores[$player-1] };
        my $total_value = 0;
        shift @ability_scores; # One dump stat is fine so discard the lowest ability score
        foreach my $ability_score (@ability_scores) {
            #$total_value += $ability_score;                    # Uses the score as the value
            #$total_value += floor(($ability_score - 10) / 2);  # Uses the modifier as the value
            given ($ability_score) {
                when ($_ == 18) {$total_value += 19}
                when ($_ == 17) {$total_value += 15}
                when ($_ == 16) {$total_value += 12}
                when ($_ == 15) {$total_value +=  9}
                when ($_ == 14) {$total_value +=  7}
                when ($_ == 13) {$total_value +=  5}
                when ($_ == 12) {$total_value +=  4}
                when ($_ == 11) {$total_value +=  3}
                when ($_ == 10) {$total_value +=  2}
                when ($_ ==  9) {$total_value +=  1}
                when ($_ ==  8) {$total_value +=  0}
                when ($_ ==  7) {$total_value += -1}
                when ($_ ==  6) {$total_value += -2}
                when ($_ ==  5) {$total_value += -3}
                when ($_ ==  4) {$total_value += -4}
                when ($_ ==  3) {$total_value += -5}
            }
        }
        push @total_values, $total_value;
    }
    return @total_values;
}

my @all_values;
for (1..$number_of_runs) {
    my @total_values = get_total_values();
    foreach my $player (1..$number_of_players) {
        push @{ $all_values[$player-1] }, $total_values[$player-1];
    }
}

for my $player (1..$number_of_players) {
    my $total_value;
    foreach my $value (@{ $all_values[$player-1] }) {
        $total_value += $value;
    }
    my $mean_value = $total_value / $number_of_runs;
    print "Player $player has mean ability score value $mean_value.\n";
}

Try it online!

\$\endgroup\$
3
  • \$\begingroup\$ This is brilliant! One thing I would be very interested in is the modifier values. Dropping the lowest stat is a good call, to account for the dump stat as being less important. 10,000 iterations looks to be plenty, since the CVs are less than 1%. The Point Buy is a good call, since it provides the weighting for higher ability scores. \$\endgroup\$
    – chazman
    Apr 13, 2018 at 16:00
  • 1
    \$\begingroup\$ Another thought - try dropping the lowest 2 stats. This will more balanced, since dropping an odd number of stats will always benefit the 1st player. \$\endgroup\$
    – chazman
    Apr 13, 2018 at 16:11
  • \$\begingroup\$ This seems to mostly agree with what I observed; specifically that Player 1 gets "better minmaxing" with a higher primary stat and a lower dump stat. "Cost to buy that stat line in point buy" seems like a good objective way to compare them, if we assume WotC did their own research to make point buy balanced. \$\endgroup\$ Apr 13, 2018 at 18:03
5
\$\begingroup\$

After a bit of finagling, I wrote an anydice calculation that should produce the probability distribution of the highest absolute ability score from this method. Unfortunately, it doesn't actually run on anydice.com due to a low runtime limit; I don't know if a local equivalent exists that could be allowed to run longer and obtain those results.

If it is possible to run with a lot more efficiency or runtime, this calculation should produce probability graphs for each player's highest and total scores. Similar graphs could be produced for second highest, third highest etc.

While, as I mentioned, I haven't been able to run all those calculations, a few points are immediately obvious. First: Player 1 will always have the highest "primary stat", but the lowest "secondary stat", as well as the largest variance between their highest and lowest stats. That would lead Player 1 to perform best at a class where their primary stat is pretty much all they need. On the other hand, Player 4 would have the lowest primary stat and the highest secondary stat, as well as the lowest variance between highest and lowest stats, performing best in any class with their two best stats very close in importance and/or where their dump stat needs to be a little higher than terrible. Players 2 and 3 would be between them.

Overall I'd say Player 1 gets the best out of it; their first and third highest stats are the best, with their second best on the low end, while Player 4 has the worst first and third stats, and a higher second seems unlikely to make up for that. This might be subjective, but to me there's a very major gap between the values of the highest 3 and lowest 3 ability scores, so Player 4 having the best 4th and 6th highest scores has almost no impact in my opinion.

Finally, in an attempt to get some useful graphs at all, I made a simpler simulation for just the "Highest stat". First I tried checking distributions for a few alternatives. The one that most closely ended up matching the distribution of "top ~17% scores" on the normal rolling system was the one called "skewed high distribution", so I made this calculation intended to estimate what scores each player would get for their highest score. Keeping in mind that I just eyeballed the distribution for that, it shows Player 1 with over 70% chance to have their highest score be 17 or 18, while Player 4 has over 70% chance for their highest score to be only 15. Checking the alternative distributions gives similar results: Player 1 usually has at least 60% chance to get a 17 or 18, while Player 4 has less than 1% chance.

For an alternative measure, I've done some math by hand to generate a "typical" pool, i.e. probability of generating a score * 24 attempts = expected number of that score, and assigned them in accordance with your procedure to get these results:

  1. 17, 14, 13, 11, 11, 6
  2. 16, 14, 13, 11, 10, 7
  3. 16, 14, 13, 12, 10, 8
  4. 15, 15, 12, 12, 9, 9

Going at the calculation from this angle, we can see that the advantage Player 1 gets in their Highest stat is significant since there's likely to be only one 17 or 18 score in the entire pool, but differences between players in the second through 4th stats is minimal because 11-14 are very common in 4d6 drop 1. There is an even larger disparity in the lowest stat because of a flatter tail, but a "dump stat" is commonly desirable and I doubt the other players would be unhappy to move points from their lowest to their highest stat.

\$\endgroup\$
1
  • \$\begingroup\$ Great work! I wonder what a "modified snake order" of [1..4],[4..1],[4..1],[4..1],[1..4],[4..1] would look like. Hello, rabbit hole! \$\endgroup\$
    – chazman
    Apr 12, 2018 at 21:07
3
\$\begingroup\$

This is not the answer to your question because I'm not sure how this could be done using Anydice, but I know that it can be done with Excel pretty easily.

I've quickly thrown together this spreadsheet which will simulate the 24 x 4d6 (drop-the-lowest) rolls, and then assign the ability scores to the players in "snake order" and calculate their respective ability mods.

https://docs.google.com/spreadsheets/d/1_IDyd-fih6m9GeQ77f_aIH5e4dMWtydTaazQbt35EV4/edit?usp=sharing

PS. The randomised rolls will update every time that a cell is refreshed, might have to download the sheet to do that. You can type ctrl+R to refresh without updating a cell.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Maybe note that you can type ctrl+r to refresh without updating a cell \$\endgroup\$ Apr 11, 2018 at 23:56
  • \$\begingroup\$ Thanks! That's exactly what I did in Excel prior to posting the question. I was also looking at the sum and spread of the modifiers. My interest lays in modeling the variance (swinginess) of the stat spread of player 1 vs player 4. \$\endgroup\$
    – chazman
    Apr 12, 2018 at 13:07
2
\$\begingroup\$

Exact distributions for first and last player

Here are graphs of the distribution of the highest, 2nd highest, ... score for the first and last players out of 4. Note that the curves aren't independent; in particular, the first and last players are likely to have pairs of the same number when they take two picks in a row.

Exact distribution for first and last player

Commentary

As with the other answers here, I'd say the first player has the most advantageous position, since a high top score tends to help more than a low bottom score hurts. I like OP's commented idea of flipping the 3rd round against the first player, so the first player only gets the highest score in the 1st and 5th rounds.

Algorithm

This was computed using my hdroller Python (heh, "snake draft") library, which uses combinatorics and dynamic programming to find the exact result in just seconds. You can try this computation (including tables of mean arrays for 1-6 players) in your browser using this JupyterLite notebook.

Script:

import hdroller
import matplotlib.pyplot as plt

def snake_draft_graph(num_players,
                      player,
                      ability_score=hdroller.d6.keep_highest(4, 3),
                      reverses=(False, True, False, True, False, True)):
    """
    Args:
        num_players: The number of players. Six times this many scores will be rolled.
        player: The player to graph.
        ability_score: A die representing the distribution of a single score.
        reverses: A 6-tuple representing whether the player order should be flipped
            for that round of drafting. In other words, each element determines
            whether a column of the result table will be flipped.
            The default performs a snake draft.
    """
    total_rolls = 6 * num_players
    fig, ax = plt.subplots(figsize=(8, 4))
    for score_rank, reverse in enumerate(reverses):
        if reverse:
            index = (score_rank + 1) * num_players - 1 - player
        else:
            index = score_rank * num_players + player
        rank_die = ability_score.keep(total_rolls, index)
        ax.plot(rank_die.outcomes(), rank_die.pmf(percent=True))
    ax.grid(True)
    ax.set_xlim(3, 18)
    ax.set_xticks(range(3, 19))
    ax.set_ylim(0)
    ax.set_title(f'Player {player+1} of {num_players}')
    ax.set_xlabel('Score')
    ax.set_ylabel('Chance (%)')
    plt.show()

snake_draft_graph(4, 0)
snake_draft_graph(4, 3)
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .