Is there a way to limit 7 randomized variables to a sum of 40?

Viewed 77

I want to make a randomizer for the SPECIAL stats from Fallout New Vegas, i've built most of the code, but there's cases that the sum of the variables exceed/are below the cap of 40.

Is there a way to limit them, or in cases that the sum is below or over 40, distribute the difference?

strength = random.randint(1, 10)
perception = random.randint(1, 10)
endurance = random.randint(1, 10)
charisma = random.randint(1, 10)
intelligence = random.randint(1, 10)
agility = random.randint(1, 10)
luck = random.randint(1, 10)
sum = strength + perception + endurance + charisma + intelligence + agility + luck

diff = 40 - sum
if diff < 0:
    diff = (diff * - 1)

print("=======================")
print("Strength:", strength)
print("Perception:", perception)
print("Endurance:", endurance)
print("Charisma:", charisma)
print("Intelligence:", intelligence)
print("Agility:", agility)
print("Luck:", luck)
print("Total:", sum)
print("Difference:", diff)
print("=======================")
4 Answers

Instead of generating seven independent random numbers, generate seven numbers less than 40, and use their differences to generate your stats.

import random

STATMAX = 10

# generate six random numbers, sorted, to use as dividers in the range
rand_numbers = sorted(random.choices(range(40), k=6))
# calculate stats by taking the differences between them
stat_numbers = [(j - i) for (i, j) in zip([0] + rand_numbers, rand_numbers + [40])]
# for values higher than 10, dump the excess values into other stats
excess_points = sum(max(s - STATMAX, 0) for s in stat_numbers)
# also, drop stats above 10 before redistributing the points
stat_numbers = [min(s, STATMAX) for s in stat_numbers]
while excess_points > 0:
    idx = random.randint(0, len(stat_numbers) - 1)
    # this approach favors balanced stats by only adding one point at a time
    # an alternate approach would be to add as many points as possible, which
    # would favor imbalanced stats (i.e. minmaxing)
    if stat_numbers[idx] < STATMAX:
        stat_numbers[idx] += 1
        excess_points -= 1

strength, perception, endurance, charisma, intelligence, agility, luck = stat_numbers

You can tailor this approach in a few different ways. For example, if you want a chance of rolling less than 40 total stats, you could generate seven random numbers instead, and use the last random number as an endpoint instead of 40.

I would not recommend generating the attributes independently from each other (Green Cloak Guy has provided an instructive answer on how this can be done better).

That said, if for some reason you want to do it nonetheless, you can distribute the differences across the attributes as follows:

import random

strength = random.randint(1, 10)
perception = random.randint(1, 10)
endurance = random.randint(1, 10)
charisma = random.randint(1, 10)
intelligence = random.randint(1, 10)
agility = random.randint(1, 10)
luck = random.randint(1, 10)

list_attributes = [strength, perception, endurance, charisma, intelligence, agility, luck]
sum_attributes = sum(list_attributes)
diff = 40 - sum_attributes

if diff != 0:
    diff_partial = diff/len(list_attributes)
    for i, attribute in enumerate(list_attributes):
        list_attributes[i] = attribute + diff_partial

strength, perception, endurance, charisma, intelligence, agility, luck = list_attributes

Keep in mind that, since random returns float values, your updated attributes will be floats as well. In case you need them as int, you could, for example, use int() inside the for loop.

If your aim is <=40 instead of aiming directly at 40, one option to consider is to determine the overage and spread it across your number of stats. The math/logic used to apply the overage-spread could be tweaked to not be so heavy handed, but it feels close to what you are after.

import random
import math

specialStats={"strength":random.randint(1, 10),
    "perception": random.randint(1, 10),
    "endurance": random.randint(1, 10),
    "charisma": random.randint(1, 10),
    "intelligence": random.randint(1, 10),
    "agility": random.randint(1, 10),
    "luck": random.randint(1, 10)
    }

sum=0
for stat in specialStats:
    sum += specialStats[stat]

#determine, if there is an overage, what the spread would be to subtract from stats
distributeOverage=0
if sum > 40: 
    distributeOverage = math.ceil((sum-40)/len(specialStats))

#apply difference from overage spread and print
print("=======================")
sum=0
for stat in specialStats:
    specialStats[stat] = specialStats[stat] -(distributeOverage * (specialStats[stat]>distributeOverage))
    sum += specialStats[stat] 
    print(stat+":", specialStats[stat])

print("Total:", sum)
print("=======================")

I would just generate the seven stats, and depending on the sum total, add or subtract randomly until the sum equals 40...

import random
stats = []

## Generate the stats
for x in range(7):
    stats.append(random.randint(1,10))
    
## If the sum total is 40, leave em.
if sum(stats) == 40:
    pass

## If it is less than 40, add +1's randomly until you reach 40.
elif sum(stats) < 40:
    while sum(stats) != 40:
        stat = random.randint(0,6)
        if stats[stat] != 10:
            stats[stat] = stats[stat] + 1

## If it is more than 40, subtract randomly until you reach 40.
elif sum(stats) > 40:
    while sum(stats) != 40:
        stat = random.randint(0,6)
        if stats[stat] != 1:
            stats[stat] = stats[stat] - 1
            
strength = stats[0]
perception = stats[1]
endurance = stats[2]
charisma = stats[3]
intelligence = stats[4]
agility = stats[5]
luck = stats[6]
Related