To make a little more intuitive code as Bill has mentioned:
diceNum=10 # number of dices
ss = 6**diceNum # number of elements of the sample space: throw n dices and write points of each dice. (n**N)
diceSum = []
diceThrow = []
def roll_dice(diceThrow_n1)->list: # diceThrow_n1 is the list for n dices thrown
#check if it's the first dice
if diceThrow_n1 == []:
diceThrow_n2=np.array([[i] for i in range(1,7)])
else:
diceThrow_n2 = [] # list for n+1 thrown dices
for d in diceThrow_n1:
for t in range(1,7): # throw the n+1 dice
diceThrow_n2.append([*d,t])
return diceThrow_n2
for d in range(diceNum): # Throw diceNum dices
diceThrow = roll_dice(diceThrow)
diceSum = [sum(elm) for elm in diceThrow] # Sum each element of the sample space
roll_dice takes your diceThrown list does nothing other than take all List entrys and add the end the result of another dice throw. (so every time the function is executed, the entries in diceThrow is multiplied by 6).
Let us check this for diceNum = 2:
In the first execute d = 0 (first dice throw), we provide roll_dice a empty list (diceThrow = []). So roll_dice populate diceThrow with 1 ...6 (diceThrow = [[1],[2],[3],[4],[5],[6]]).
So now d = 1:
roll_dice starts with diceThrow = [[1],[2],[3],[4],[5],[6]]:
so we go to the else part of the function. Now we iterate over all entries in the list. Starting with d = [1]. (* in front of a list gives all elements (*[a,b,c]=a,b,c)) It takes every thrown dice and add the result of the next throw. So [1] gets to [[1,1],[1,2],[1,3],...,[1,6]]. But 1 was only one possible result of the first throw, so we do this for every other possible result and end so with:
diceThrow = [[1 1]
[1 2]
[1 3]
[1 4]
[1 5]
[1 6]
[2 1]
[2 2]
[2 3]
[2 4]
[2 5]
[2 6]
[3 1]
[3 2]
[3 3]
[3 4]
[3 5]
[3 6]
[4 1]
[4 2]
[4 3]
[4 4]
[4 5]
[4 6]
[5 1]
[5 2]
[5 3]
[5 4]
[5 5]
[5 6]
[6 1]
[6 2]
[6 3]
[6 4]
[6 5]
[6 6]]
This can be repeated now for every additional dice.
At the end we sum about every entry as you did before, but instead doing this every time we create a new entry in diceThrow we do it at the end to prevent us doing this multiple times.
But here the problems begins. This is very ineffective, because lists in python are not the fastest. And we do this again and again. create a list, create a bigger list, ....
A better way but much less intuitive is using numpy.
diceNum=2
diceThrow = np.array(np.meshgrid(*([np.array([1,2,3,4,5,6])]*diceNum))).T.reshape(-1,diceNum)
diceSum = [sum(elm) for elm in diceThrow]
In principle you give the meshgrid function diceNum np.arrays with 1,2,...,6] and reshape it afterwards in a way you gain the diceNum dices.
A good explanation(and a little inspiration for me xD) for whats going on here, you can find here Numpy: efficient way to generate combinations from given ranges and Using numpy to build an array of all combinations of two arrays
But even with this I get into long runtimes with diceNum > 10. Maybe anyone else has a good idea to keep the practical approach and not using any analytic theories.