Probability of Stabilizing the Dying in “Stars without Number”

Another Foray into Probability and Computations

By on ::

, I grew curious about the probability of stabilizing a dying creature in Stars without Number: Revised Edition . In part, I was curious about how I would go about calculating the probability. I have a bachelors degree in Computer Science and Applied Mathematics, and I enjoy calculating probabilities. I see these probabilities as insight into the game and the fictional tension the base mechanics emulate.

Below, in Table 218, I’ve written up several probability scenarios of someone stabilizing a charcter in Stars without Number: Revised Edition or Worlds without Number .

Further below, I’ve included the code I used to generate Table 218.

Analysis

For reference, a character with Heal-0, Dexterity +1, using a Lazarus would consult the rows with a Modified Difficulty of 5.

I’ve made the assumption that only one person may attempt to stabilize a character during each round. There’s a implicit assumption embedded in these tables: once you start with a modified difficulty, you continue using that for each subsequent test.

In other words, if you start using Lazarus Patches, you continue to use them on subsequent rounds.

Assuming no real medical training, here are the following probabilities:

  • With 6 Lazarus Patches and:
    • without an expert: 0.963
    • with an expert re-roll: 0.990
  • With a Medical Kit and:
    • without an expert: 0.687
    • with an expert re-roll: 0.817
  • With no Supplies and:
    • without an expert: 0.257
    • with an expert re-roll: 0.381

So spend that money on Lazarus Patches for the best survival odds for your characters. And if you need to ration, use them we you don’t have a re-roll available.

The Probability of Stabilization Table

Details of the Probability of Stabilization table…

Column Definitions:

Modified Difficulty
The base DC 📖 for the skill check modified by the skill rank, attribute bonus, and situational modifiers. This does not include the number of rounds since dropping to 0 HP . For example, a medic with Heal-2 and a Dexterity modifier of +1 using a Lazurus Patch (base DC 6) would have a Modified Difficulty of 3.
Dice Rolled
While most characters roll 2d6 for their skill checks, some healers might roll 3d6 and keep the best two dice. I chose not to add scenarios for 4d6.
Helpers
The number of helpers assisting the main character. I wrote about Group Rolls for “Stars without Number” and “Worlds without Number”.
Reroll
Does the primary medic have a re-roll for their Heal check?
Round 0 through 5
Once the victim dropped to 0 HP , what was the round in which everyone attempts to stabilize the victim? The values in these cells represent the probability that the medical effort will eventually stabilize the victim.
Table 218: Probability of Stabilization in SWN
Modified Difficulty Dice Rolled Helpers Reroll Round 0 Round 1 Round 2 Round 3 Round 4 Round 5
0 2d6 0 false 1.000 1.000 1.000 1.000 0.986 0.833
0 2d6 0 true 1.000 1.000 1.000 1.000 0.999 0.972
0 2d6 1 false 1.000 1.000 1.000 1.000 0.997 0.903
0 2d6 1 true 1.000 1.000 1.000 1.000 1.000 0.991
0 2d6 2 false 1.000 1.000 1.000 1.000 0.998 0.914
0 2d6 2 true 1.000 1.000 1.000 1.000 1.000 0.993
0 3d6 0 false 1.000 1.000 1.000 1.000 0.999 0.949
0 3d6 0 true 1.000 1.000 1.000 1.000 1.000 0.997
0 3d6 1 false 1.000 1.000 1.000 1.000 1.000 0.976
0 3d6 1 true 1.000 1.000 1.000 1.000 1.000 0.999
0 3d6 2 false 1.000 1.000 1.000 1.000 1.000 0.981
0 3d6 2 true 1.000 1.000 1.000 1.000 1.000 1.000
1 2d6 0 false 1.000 1.000 1.000 0.996 0.954 0.722
1 2d6 0 true 1.000 1.000 1.000 1.000 0.992 0.923
1 2d6 1 false 1.000 1.000 1.000 0.999 0.981 0.802
1 2d6 1 true 1.000 1.000 1.000 1.000 0.998 0.961
1 2d6 2 false 1.000 1.000 1.000 1.000 0.985 0.825
1 2d6 2 true 1.000 1.000 1.000 1.000 0.999 0.969
1 3d6 0 false 1.000 1.000 1.000 1.000 0.995 0.894
1 3d6 0 true 1.000 1.000 1.000 1.000 1.000 0.989
1 3d6 1 false 1.000 1.000 1.000 1.000 0.998 0.934
1 3d6 1 true 1.000 1.000 1.000 1.000 1.000 0.996
1 3d6 2 false 1.000 1.000 1.000 1.000 0.999 0.945
1 3d6 2 true 1.000 1.000 1.000 1.000 1.000 0.997
2 2d6 0 false 1.000 1.000 0.998 0.981 0.884 0.583
2 2d6 0 true 1.000 1.000 1.000 0.997 0.968 0.826
2 2d6 1 false 1.000 1.000 1.000 0.994 0.934 0.664
2 2d6 1 true 1.000 1.000 1.000 0.999 0.987 0.887
2 2d6 2 false 1.000 1.000 1.000 0.995 0.947 0.698
2 2d6 2 true 1.000 1.000 1.000 1.000 0.991 0.909
2 3d6 0 false 1.000 1.000 1.000 0.999 0.979 0.806
2 3d6 0 true 1.000 1.000 1.000 1.000 0.998 0.962
2 3d6 1 false 1.000 1.000 1.000 1.000 0.991 0.857
2 3d6 1 true 1.000 1.000 1.000 1.000 0.999 0.980
2 3d6 2 false 1.000 1.000 1.000 1.000 0.993 0.878
2 3d6 2 true 1.000 1.000 1.000 1.000 1.000 0.985
3 2d6 0 false 1.000 0.999 0.989 0.932 0.757 0.417
3 2d6 0 true 1.000 1.000 0.998 0.981 0.899 0.660
3 2d6 1 false 1.000 1.000 0.997 0.966 0.828 0.486
3 2d6 1 true 1.000 1.000 1.000 0.993 0.942 0.736
3 2d6 2 false 1.000 1.000 0.998 0.975 0.857 0.527
3 2d6 2 true 1.000 1.000 1.000 0.996 0.957 0.776
3 3d6 0 false 1.000 1.000 1.000 0.993 0.938 0.681
3 3d6 0 true 1.000 1.000 1.000 0.999 0.988 0.898
3 3d6 1 false 1.000 1.000 1.000 0.997 0.962 0.733
3 3d6 1 true 1.000 1.000 1.000 1.000 0.995 0.929
3 3d6 2 false 1.000 1.000 1.000 0.998 0.971 0.763
3 3d6 2 true 1.000 1.000 1.000 1.000 0.996 0.944
4 2d6 0 false 0.999 0.992 0.951 0.824 0.579 0.278
4 2d6 0 true 1.000 0.999 0.986 0.927 0.754 0.478
4 2d6 1 false 1.000 0.998 0.977 0.882 0.649 0.316
4 2d6 1 true 1.000 1.000 0.995 0.960 0.819 0.533
4 2d6 2 false 1.000 0.999 0.984 0.906 0.690 0.344
4 2d6 2 true 1.000 1.000 0.997 0.972 0.853 0.570
4 3d6 0 false 1.000 1.000 0.997 0.970 0.848 0.523
4 3d6 0 true 1.000 1.000 1.000 0.994 0.951 0.773
4 3d6 1 false 1.000 1.000 0.999 0.983 0.884 0.567
4 3d6 1 true 1.000 1.000 1.000 0.998 0.969 0.812
4 3d6 2 false 1.000 1.000 0.999 0.988 0.905 0.598
4 3d6 2 true 1.000 1.000 1.000 0.999 0.977 0.839
5 2d6 0 false 0.993 0.959 0.854 0.649 0.398 0.167
5 2d6 0 true 0.999 0.989 0.939 0.795 0.565 0.306
5 2d6 1 false 0.998 0.981 0.904 0.714 0.443 0.185
5 2d6 1 true 1.000 0.996 0.968 0.853 0.619 0.336
5 2d6 2 false 0.999 0.987 0.925 0.752 0.476 0.201
5 2d6 2 true 1.000 0.998 0.977 0.883 0.656 0.361
5 3d6 0 false 1.000 0.998 0.981 0.902 0.693 0.356
5 3d6 0 true 1.000 1.000 0.996 0.969 0.854 0.586
5 3d6 1 false 1.000 0.999 0.990 0.929 0.733 0.384
5 3d6 1 true 1.000 1.000 0.999 0.981 0.884 0.621
5 3d6 2 false 1.000 1.000 0.993 0.944 0.762 0.407
5 3d6 2 true 1.000 1.000 0.999 0.987 0.904 0.649
6 2d6 0 false 0.963 0.866 0.678 0.448 0.236 0.083
6 2d6 0 true 0.990 0.944 0.812 0.602 0.363 0.160
6 2d6 1 false 0.983 0.913 0.740 0.493 0.259 0.090
6 2d6 1 true 0.997 0.971 0.866 0.654 0.396 0.172
6 2d6 2 false 0.988 0.932 0.776 0.526 0.278 0.097
6 2d6 2 true 0.998 0.980 0.894 0.689 0.423 0.184
6 3d6 0 false 0.998 0.985 0.921 0.754 0.485 0.199
6 3d6 0 true 1.000 0.997 0.975 0.883 0.668 0.359
6 3d6 1 false 0.999 0.992 0.944 0.790 0.515 0.212
6 3d6 1 true 1.000 0.999 0.985 0.909 0.701 0.379
6 3d6 2 false 1.000 0.995 0.956 0.815 0.540 0.224
6 3d6 2 true 1.000 0.999 0.990 0.926 0.728 0.398
7 2d6 0 false 0.870 0.687 0.464 0.257 0.109 0.028
7 2d6 0 true 0.946 0.817 0.613 0.381 0.183 0.055
7 2d6 1 false 0.915 0.747 0.508 0.280 0.117 0.029
7 2d6 1 true 0.972 0.870 0.664 0.414 0.197 0.058
7 2d6 2 false 0.934 0.783 0.541 0.300 0.124 0.031
7 2d6 2 true 0.980 0.897 0.699 0.441 0.209 0.061
7 3d6 0 false 0.986 0.927 0.772 0.523 0.258 0.074
7 3d6 0 true 0.997 0.977 0.891 0.693 0.406 0.143
7 3d6 1 false 0.993 0.948 0.806 0.553 0.273 0.078
7 3d6 1 true 0.999 0.986 0.916 0.724 0.427 0.149
7 3d6 2 false 0.995 0.960 0.830 0.577 0.287 0.081
7 3d6 2 true 0.999 0.990 0.932 0.750 0.447 0.155
8 2d6 0 false 0.687 0.464 0.257 0.109 0.028 0.000
8 2d6 0 true 0.817 0.613 0.381 0.183 0.055 0.000
8 2d6 1 false 0.747 0.508 0.280 0.117 0.029 0.000
8 2d6 1 true 0.870 0.664 0.414 0.197 0.058 0.000
8 2d6 2 false 0.783 0.541 0.300 0.124 0.031 0.000
8 2d6 2 true 0.897 0.699 0.441 0.209 0.061 0.000
8 3d6 0 false 0.927 0.772 0.523 0.258 0.074 0.000
8 3d6 0 true 0.977 0.891 0.693 0.406 0.143 0.000
8 3d6 1 false 0.948 0.806 0.553 0.273 0.078 0.000
8 3d6 1 true 0.986 0.916 0.724 0.427 0.149 0.000
8 3d6 2 false 0.960 0.830 0.577 0.287 0.081 0.000
8 3d6 2 true 0.990 0.932 0.750 0.447 0.155 0.000
9 2d6 0 false 0.464 0.257 0.109 0.028 0.000 0.000
9 2d6 0 true 0.613 0.381 0.183 0.055 0.000 0.000
9 2d6 1 false 0.508 0.280 0.117 0.029 0.000 0.000
9 2d6 1 true 0.664 0.414 0.197 0.058 0.000 0.000
9 2d6 2 false 0.541 0.300 0.124 0.031 0.000 0.000
9 2d6 2 true 0.699 0.441 0.209 0.061 0.000 0.000
9 3d6 0 false 0.772 0.523 0.258 0.074 0.000 0.000
9 3d6 0 true 0.891 0.693 0.406 0.143 0.000 0.000
9 3d6 1 false 0.806 0.553 0.273 0.078 0.000 0.000
9 3d6 1 true 0.916 0.724 0.427 0.149 0.000 0.000
9 3d6 2 false 0.830 0.577 0.287 0.081 0.000 0.000
9 3d6 2 true 0.932 0.750 0.447 0.155 0.000 0.000
10 2d6 0 false 0.257 0.109 0.028 0.000 0.000 0.000
10 2d6 0 true 0.381 0.183 0.055 0.000 0.000 0.000
10 2d6 1 false 0.280 0.117 0.029 0.000 0.000 0.000
10 2d6 1 true 0.414 0.197 0.058 0.000 0.000 0.000
10 2d6 2 false 0.300 0.124 0.031 0.000 0.000 0.000
10 2d6 2 true 0.441 0.209 0.061 0.000 0.000 0.000
10 3d6 0 false 0.523 0.258 0.074 0.000 0.000 0.000
10 3d6 0 true 0.693 0.406 0.143 0.000 0.000 0.000
10 3d6 1 false 0.553 0.273 0.078 0.000 0.000 0.000
10 3d6 1 true 0.724 0.427 0.149 0.000 0.000 0.000
10 3d6 2 false 0.577 0.287 0.081 0.000 0.000 0.000
10 3d6 2 true 0.750 0.447 0.155 0.000 0.000 0.000
11 2d6 0 false 0.109 0.028 0.000 0.000 0.000 0.000
11 2d6 0 true 0.183 0.055 0.000 0.000 0.000 0.000
11 2d6 1 false 0.117 0.029 0.000 0.000 0.000 0.000
11 2d6 1 true 0.197 0.058 0.000 0.000 0.000 0.000
11 2d6 2 false 0.124 0.031 0.000 0.000 0.000 0.000
11 2d6 2 true 0.209 0.061 0.000 0.000 0.000 0.000
11 3d6 0 false 0.258 0.074 0.000 0.000 0.000 0.000
11 3d6 0 true 0.406 0.143 0.000 0.000 0.000 0.000
11 3d6 1 false 0.273 0.078 0.000 0.000 0.000 0.000
11 3d6 1 true 0.427 0.149 0.000 0.000 0.000 0.000
11 3d6 2 false 0.287 0.081 0.000 0.000 0.000 0.000
11 3d6 2 true 0.447 0.155 0.000 0.000 0.000 0.000
12 2d6 0 false 0.028 0.000 0.000 0.000 0.000 0.000
12 2d6 0 true 0.055 0.000 0.000 0.000 0.000 0.000
12 2d6 1 false 0.029 0.000 0.000 0.000 0.000 0.000
12 2d6 1 true 0.058 0.000 0.000 0.000 0.000 0.000
12 2d6 2 false 0.031 0.000 0.000 0.000 0.000 0.000
12 2d6 2 true 0.061 0.000 0.000 0.000 0.000 0.000
12 3d6 0 false 0.074 0.000 0.000 0.000 0.000 0.000
12 3d6 0 true 0.143 0.000 0.000 0.000 0.000 0.000
12 3d6 1 false 0.078 0.000 0.000 0.000 0.000 0.000
12 3d6 1 true 0.149 0.000 0.000 0.000 0.000 0.000
12 3d6 2 false 0.081 0.000 0.000 0.000 0.000 0.000
12 3d6 2 true 0.155 0.000 0.000 0.000 0.000 0.000

The Code Used to Generate the Table

Details of the Ruby code…
desc "Calculate the probability of stabilization with treatment"
task :probability_swn_stabilize do
  # This class encapsulates the probability calculations based on
  # the given :distribution.
  class Universe
    def initialize(label:, distribution:)
      @label = label
      @distribution = distribution
      @max = distribution.keys.max
      @size = distribution.values.sum.to_f
    end
    attr_reader :label

    # Given the :modified_target, what is the chance of a result
    # equal to or greater than that target?
    #
    # @param modified_target [Integer] What is the roll we're looking
    # for?
    #
    # @return [Float] the chance, with a range between 0 and 1.
    def chance_of_gt_or_eq_to(modified_target)
      @distribution.slice(*(modified_target..@max)).values.sum /
        @size
    end

    # Given the :modified_target, what is the chance of a result
    # equal to exactly the modified target?
    #
    # @param modified_target [Integer] What is the roll we're looking
    # for?
    #
    # @return [Float] the chance, with a range between 0 and 1.
    def chance_of_exactly(modified_target)
      @distribution.fetch(modified_target, 0) / @size
    end
  end

  # Think to your Settlers of Catan board game.  The "dot" on each
  # of the number chits is the chance in 36 of rolling that number
  # on 2d6.
  universe_2d6 = Universe.new(
    label: "2d6",
    distribution: {
      2 => 1, 3 => 2, 4 => 3,
      5 => 4, 6 => 5, 7 => 6,
      8 => 5, 9 => 4, 10 => 3,
      11 => 2, 12 => 1
    })

  # This universe is the distribution of all possible rolls in which
  # we throw 3 six-sided dice and keep the best 2 values.
  universe_3d6 = Universe.new(
    label: "3d6",
    distribution: {
      2  => 1, 3  => 3, 4  => 7,
      5  => 12, 6  => 19, 7  => 27,
      8  => 34, 9  => 36, 10 => 34,
      11 => 27, 12 => 16
    })

  # This class encapsulates the probability
  #
  # A key assumption in helping is that all helpers are helping with
  # their preferred skill check, and thus the modified_difficulty is
  # the same as the person performing the primary test.
  #
  # @note I tested this using a coin toss universe distribution
  # (e.g. heads or tails)
  class Helper
    # @param number [Integer] the number of helpers
    #
    # @param universe [Universe] the universe of possible dice
    # results for each of the helpers
    def initialize(number:, universe:)
      @number = number
      @universe = universe
    end
    attr_reader :number

    # @param modified_difficulty [Integer] the modified dice roll that
    # the helpers are trying to achieve.
    #
    # @return [Float] the probability that one of the helpers
    # succeeds.
    def chance_someone_succeeds_at(modified_difficulty)
      success = @universe.chance_of_gt_or_eq_to(modified_difficulty)
      accumulate(success: success)
    end

    private

    def accumulate(success:, accumulator: 0, number: @number)
      return accumulator if number <= 0

      chance_of_next_helper = 1 - accumulator
      accumulator += chance_of_next_helper * success
      accumulate(
        success: success,
        number: number - 1,
        accumulator: accumulator)
    end
  end

  # Originally extracted as a parameter object (because the
  # `success_probability_for_given_check_or_later` method had too
  # many parameters), this object helps encapsulate the data used
  # to calculate each round's probability of success.
  class Check
    PARAMETERS = [
      :universe,
      :modified_difficulty,
      :reroll,
      :chance_we_need_this_round,
      :round,
      :helpers
    ]

    def initialize(**kwargs)
      PARAMETERS.each do |param|
        instance_variable_set("@#{param}", kwargs.fetch(param))
      end
    end

    # @return [Universe] The universe of possible modified dice rolls.
    attr_reader :universe

    # @return [Integer] The check's Difficulty Class (DC) plus all of
    # the modifiers (excluding those for rounds since dropping to 0
    # HP) affecting the 2d6 roll.
    #
    # @note for a Heal-2 medic with a Dex of +1 using a Lazurus
    # Patch (DC 6) would have a modified_difficulty of
    # 3. (e.g. 6 - 2 - 1 = 3)
    #
    # @note for an untrained medic with a Dex of -1 using a Lazurus
    # Patch (DC 6) would have a modified_difficulty of
    # 3. (e.g. 6 - (-1) - (-1) = 9)
    attr_reader :modified_difficulty

    # @return [Boolean] True if we allow a reroll.
    attr_reader :reroll

    # @return [Float] The probability that we need this round, range
    # between 0.0 and 1.0.
    attr_reader :chance_we_need_this_round

    # @return [Integer] The round in which we made a check
    attr_reader :round

    # @return [Helper] Who are the helpers for this task
    attr_reader :helpers

    def next(**kwargs)
      new_kwargs = {}
      PARAMETERS.each do |param|
        new_kwargs[param] = kwargs.fetch(param) do
          instance_variable_get("@#{param}")
        end
      end
      self.class.new(**new_kwargs)
    end

    # @return [Float]
    def probability_of_success_this_round
      universe.chance_of_gt_or_eq_to(modified_difficulty + round)
    end

    # @return [Float]
    def chance_of_help_making_the_difference
      universe.chance_of_exactly(modified_difficulty + round - 1) *
        helpers.chance_someone_succeeds_at(modified_difficulty + round)
    end

    # @return [Float]
    def probability_of_reroll_makes_difference(prob:)
      return 0.0 unless reroll
      (1 - prob) * prob
    end
  end


  # @param check [Check] The current round's Check context.
  #
  # @param ttl [Integer] "Time to Live" - After this many rounds
  # without treatment, a character dies.
  #
  # @return [Float] The probability of success in this round or all
  # future rounds. Range between 0.0 and 1.0
  #
  # @note Per SWN rules, characters die after 6 rounds without
  # treatment.
  def success_probability_for_given_check_or_later(check, ttl: 6)
    return 0 if check.round >= ttl

    prob = check.probability_of_success_this_round

    # Using the assumption that the best character is making the
    # check, and everyone that could be helping is using their best
    # check to help.  With the assumption that everyone has the same
    # modifier as the base check.
    prob += check.chance_of_help_making_the_difference

    # I believe the interaction of helpers and reroll is correct.
    # We only attempt a re-roll if the helpers didn't succeed.  And
    # the re-roll has the same probability of success
    prob += check.probability_of_reroll_makes_difference(prob: prob)

    # Assumption that we’ll re-roll with the best odds.
    next_check = check.next(
      reroll: false,
      round: check.round + 1,
      chance_we_need_this_round: (1 - prob)
    )
    check.chance_we_need_this_round * (
      prob +
      success_probability_for_given_check_or_later(next_check)
    )
  end

  all_helpers = [
    Helper.new(number: 0, universe: universe_2d6),
    Helper.new(number: 1, universe: universe_2d6),
    Helper.new(number: 2, universe: universe_2d6)
  ]

  number_of_rounds = (0..5)
  header_template = "| %-10s | %-4s | %7s | %6s" +
                    " | %5s" * number_of_rounds.size + " |"
  divider_template = "|------------+------+---------+--------" +
                     ("+-------" * number_of_rounds.size) + "|"
  line_template = "| %-10s | %-4s | %-7d | %6s" +
                  " | %.3f" * number_of_rounds.size + " |"
  header = sprintf(header_template, "Mod Diff", "Dice", "Helpers",
                   "Reroll",
                   *number_of_rounds.to_a.map {|i| "Rnd #{i}"})
  puts header
  puts divider_template

  rows = []
  (0..12).each do |modified_difficulty|
    [universe_2d6, universe_3d6].each do |universe|
      all_helpers.each do |helpers|
        [false, true].each do |reroll|
          rounds = number_of_rounds.map do |round|
            check = Check.new(
              reroll: reroll,
              universe: universe,
              modified_difficulty: modified_difficulty,
              chance_we_need_this_round: 1.0,
              helpers: helpers,
              round: round
            )
            success_probability_for_given_check_or_later(check)
          end
          rows << {
            "Modified Difficulty" => modified_difficulty,
            "Dice Rolled" => universe.label,
            "Helpers" => helpers.number,
            "Reroll" => reroll,
            "Round 0" => sprintf("%.3f", rounds[0]),
            "Round 1" => sprintf("%.3f", rounds[1]),
            "Round 2" => sprintf("%.3f", rounds[2]),
            "Round 3" => sprintf("%.3f", rounds[3]),
            "Round 4" => sprintf("%.3f", rounds[4]),
            "Round 5" => sprintf("%.3f", rounds[5])
          }
          puts sprintf(line_template,
                       modified_difficulty,
                       universe.label,
                       helpers.number,
                       reroll,
                       *rounds)
        end
      end
    end
  end

  # Below is used for writing a YAML file that I use to auto-generate
  # the table for a blog post.
  pwd = if defined?(TakeOnRules::PROJECT_PATH)
          File.join(TakeOnRules::PROJECT_PATH, "data/probabilities")
        else
          Dir.pwd
        end

  column_names = [
    "Modified Difficulty",
    "Dice Rolled",
    "Helpers",
    "Reroll",
    "Round 0",
    "Round 1",
    "Round 2",
    "Round 3",
    "Round 4",
    "Round 5"
  ]

  columns = []
  column_names.each do |name|
    columns << { "label" => name, "key" => name }
  end

  require 'psych'
  File.open(File.join(pwd, "swn_stabilize.yml"), "w+") do |file|
    file.puts(
      Psych.dump(
        "name" => 'Probability of Stabilization in SWN',
        "columns" => columns,
        "rows" => rows
      )
    )
  end
end

update

Conclusion

Instead of applying a brute force check on each possible dice combination, I pre-compiled some of that information into what I dubbed the Universe.

The Universe represents the distribution of possible dice rolls. From that distribution, I then determine the probability. This new method appears to be 100 times faster than the brute force method I used in Thinking Through Group Rolls for Stars and Worlds without Number.

On a lark, I may go back and revisit that script to use the “universe” approach.