Based on @Stephan’s beautiful answer, here is the Typescript code for posterity:
function probabilityOfNwithDifferentFaces(n: number, ...allDiceAndSides: [number, number][]): number {
if (!allDiceAndSides.length) return 0
// trying to avoid combinatorial explosion
const cleaned = allDiceAndSides.filter(([dice, sides]) => dice > 0 && sides > 0 && dice < 100 && sides <= 100)
.slice(0, 7)
if (cleaned.length === 1) return probabilityOfN(cleaned[0][0], cleaned[0][1], n)
const minResult = cleaned.reduce((acc, [dice]) => acc + dice, 0)
const maxResult = cleaned.reduce((acc, [dice, sides]) => acc + dice * sides, 0)
if (n < minResult || n > maxResult) return 0
cleaned.sort((a, b) => b[1] - a[1])
const allRepeats: number[] = cleaned.flatMap(([dice, sides]) => Array(dice).fill(sides))
// console.log(allRepeats)
const all_possible_combos: number[][] = []
for (let i = 1; i <= allRepeats.length; i++) {
const die_combos: number[] = []
for (let j = 1; j <= allRepeats[i - 1]; j++) {
die_combos.push(j)
}
all_possible_combos.push(die_combos)
}
const cartesian_product = (...arrays: any[]) => arrays.reduce(
(a, b) => a.flatMap((d: any) => b.map((e: any) => [d, e].flat())))
const all_combinations = cartesian_product(...all_possible_combos)
// @ts-ignore
const sum_of_rows = all_combinations.map(row => row.reduce((a, b) => a + b))
// @ts-ignore
const count_of_rows_summing_to_n = sum_of_rows.filter(sum => sum === n).length
// console.log(count_of_rows_summing_to_n)
const totalPossibleThrows = all_combinations.length
// console.log(totalPossibleThrows)
// console.log(probability)
return count_of_rows_summing_to_n / totalPossibleThrows
}
And some use cases:
function calculateAndReportWithDifferentFaces(n: number, ...allDiceAndSides: [number, number][]) {
const p = probabilityOfNwithDifferentFaces(n, ...allDiceAndSides)
const rollString = allDiceAndSides.map(([dice, sides]) => `${dice}d${sides}`).join(" + ")
console.log(`Got ${p.toFixed(3)} for ${n} with ${rollString}`)
}
function differentFacesProbabilityTest() {
console.log("=== Individual probabilities with different faces")
calculateAndReportWithDifferentFaces(2, [1, 10], [1, 6])
calculateAndReportWithDifferentFaces(9, [1, 10], [1, 6])
calculateAndReportWithDifferentFaces(14, [1, 10], [1, 6])
calculateAndReportWithDifferentFaces(15, [1, 10], [1, 6])
calculateAndReportWithDifferentFaces(16, [1, 10], [1, 6])
calculateAndReportWithDifferentFaces(31, [2, 10], [4, 8])
calculateAndReportWithDifferentFaces(41, [2, 10], [4, 8])
calculateAndReportWithDifferentFaces(41, [4, 8], [2, 10])
console.log("Same faces check:")
calculateAndReportWithDifferentFaces(2, [2, 10])
calculateAndReportWithDifferentFaces(11, [2, 10])
calculateAndReportWithDifferentFaces(19, [2, 10])
calculateAndReportWithDifferentFaces(20, [2, 10])
console.log("Incorrect inputs:")
calculateAndReportWithDifferentFaces(5, [2, 10], [4, 8])
calculateAndReportWithDifferentFaces(53, [2, 10], [4, 8])
}