@swrpg-online/monte-carlo
Version:
A library for performing Monte Carlo simulations with the Star Wars RPG narrative dice system by Fantasy Flight Games
884 lines (882 loc) • 43.7 kB
JavaScript
// node_modules/@swrpg-online/dice/dist/bundle.esm.js
var e = { SUCCESS: "SUCCESS", FAILURE: "FAILURE", ADVANTAGE: "ADVANTAGE", THREAT: "THREAT", TRIUMPH: "TRIUMPH", DESPAIR: "DESPAIR", LIGHT: "LIGHT", DARK: "DARK" };
var t = [{ description: "Recover one strain (may be applied more than once).", cost: { [e.ADVANTAGE]: 1, [e.TRIUMPH]: 1 } }, { description: "Add a boost die to the next allied active character's check.", cost: { [e.ADVANTAGE]: 1, [e.TRIUMPH]: 1 } }, { description: "Notice a single important point in the ongoing conflict, such as the location of a blast door's control panel or a weak point on an attack speeder.", cost: { [e.ADVANTAGE]: 1, [e.TRIUMPH]: 1 } }, { description: "Inflict a Critical Injury with a successful attack that deals damage past soak (Advantage cost may vary).", cost: { [e.ADVANTAGE]: 1, [e.TRIUMPH]: 1 } }, { description: "Activate a weapon quality (Advantage cost may vary).", cost: { [e.ADVANTAGE]: 1, [e.TRIUMPH]: 1 } }, { description: "Perform an immediate free maneuver that does not exceed the two maneuver per turn limit.", cost: { [e.ADVANTAGE]: 2, [e.TRIUMPH]: 1 } }, { description: "Add a setback die to the targeted character's next check.", cost: { [e.ADVANTAGE]: 2, [e.TRIUMPH]: 1 } }, { description: "Add a boost die to any allied character's next check, including that of the active character.", cost: { [e.ADVANTAGE]: 2, [e.TRIUMPH]: 1 } }, { description: "Negate the targeted enemy's defensive bonuses (such as the defense gained from cover, equipment, or performing the Guarded Stance maneuver) util the end of the current round.", cost: { [e.ADVANTAGE]: 3, [e.TRIUMPH]: 1 } }, { description: "Ignore penalizing environmental effects such as inclement weather, zero gravity, or similar circumstances until the end of the active character's next turn.", cost: { [e.ADVANTAGE]: 3, [e.TRIUMPH]: 1 } }, { description: "When dealing damage to a target, have the attack disable the opponent or one piece of gear rather than dealing wounds or strain. This could include hobbling them temporarily with a shot to the leg, or disabling their comlink. This should be agreed upon by the player and the GM, and the effects are up to the GM (although Table 6-10: Critical Injury Result is a god resource to consult for possible effects). The effects should be temporary and not too excessive.", cost: { [e.ADVANTAGE]: 3, [e.TRIUMPH]: 1 } }, { description: "Gain + 1 melee or ranged defense until the end of the active character's next turn.", cost: { [e.ADVANTAGE]: 3, [e.TRIUMPH]: 1 } }, { description: "Force the target to drop a melee or ranged weapon they are wielding.", cost: { [e.ADVANTAGE]: 3, [e.TRIUMPH]: 1 } }, { description: "Upgrade the difficulty of the targeted character's next check.", cost: { [e.TRIUMPH]: 1 } }, { description: "Do something vital, such as shooting the controls to the nearby blast doors to seal them shut.", cost: { [e.TRIUMPH]: 1 } }, { description: "Upgrade any allied character's next check, including that of the current active character.", cost: { [e.TRIUMPH]: 1 } }, { description: "When dealing damage to a target, have the attack destroy a piece of equipment the target is using, such as blowing up his blaster or destroying a personal shield generator.", cost: { [e.TRIUMPH]: 2 } }, { description: "The active character suffers 1 strain.", cost: { [e.THREAT]: 1, [e.DESPAIR]: 1 } }, { description: "The active character loses the benefits of a prior maneuver (such as from taking cover or assuming a Guarded Stance) until they perform the maneuver again.", cost: { [e.THREAT]: 1, [e.DESPAIR]: 1 } }, { description: "An opponent may immediately perform one free maneuver in response to the active character's check.", cost: { [e.THREAT]: 2, [e.DESPAIR]: 1 } }, { description: "Add a boost die to the targeted character's next check.", cost: { [e.THREAT]: 1, [e.DESPAIR]: 1 } }, { description: "The active character or an allied character suffers a setback die on their next action.", cost: { [e.THREAT]: 2, [e.DESPAIR]: 1 } }, { description: "The active character falls prone.", cost: { [e.THREAT]: 3, [e.DESPAIR]: 1 } }, { description: "The active character grants the enemy a significant advantage in the ongoing encounter, such as accidentally blasting the controls to a bridge the active character was planning to use for their escape.", cost: { [e.THREAT]: 3, [e.DESPAIR]: 1 } }, { description: "The character's ranged weapon imediately runs out of ammunition and may not be used for the remainder of the encounter.", cost: { [e.DESPAIR]: 1 } }, { description: "Upgrade the difficulty of an allied character's next check, including that of the current active character.", cost: { [e.DESPAIR]: 1 } }, { description: "The tool or melee weapon the character is using becomes damaged.", cost: { [e.DESPAIR]: 1 } }];
var a = { 1: {}, 2: {}, 3: { successes: 1 }, 4: { successes: 1, advantages: 1 }, 5: { advantages: 2 }, 6: { advantages: 1 } };
var s = { 1: {}, 2: {}, 3: { failures: 1 }, 4: { failures: 1 }, 5: { threats: 1 }, 6: { threats: 1 } };
var i = { 1: {}, 2: { successes: 1 }, 3: { successes: 1 }, 4: { successes: 2 }, 5: { advantages: 1 }, 6: { advantages: 1 }, 7: { successes: 1, advantages: 1 }, 8: { advantages: 2 } };
var r = { 1: {}, 2: { failures: 1 }, 3: { failures: 2 }, 4: { threats: 1 }, 5: { threats: 1 }, 6: { threats: 1 }, 7: { threats: 2 }, 8: { failures: 1, threats: 1 } };
var c = { 1: {}, 2: { successes: 1 }, 3: { successes: 1 }, 4: { successes: 2 }, 5: { successes: 2 }, 6: { advantages: 1 }, 7: { successes: 1, advantages: 1 }, 8: { successes: 1, advantages: 1 }, 9: { successes: 1, advantages: 1 }, 10: { advantages: 2 }, 11: { advantages: 2 }, 12: { triumphs: 1 } };
var n = { 1: {}, 2: { failures: 1 }, 3: { failures: 1 }, 4: { failures: 2 }, 5: { failures: 2 }, 6: { threats: 1 }, 7: { threats: 1 }, 8: { failures: 1, threats: 1 }, 9: { failures: 1, threats: 1 }, 10: { threats: 2 }, 11: { threats: 2 }, 12: { despairs: 1 } };
var o = { 1: { darkSide: 1 }, 2: { darkSide: 1 }, 3: { darkSide: 1 }, 4: { darkSide: 1 }, 5: { darkSide: 1 }, 6: { darkSide: 1 }, 7: { darkSide: 2 }, 8: { lightSide: 1 }, 9: { lightSide: 1 }, 10: { lightSide: 2 }, 11: { lightSide: 2 }, 12: { lightSide: 2 } };
var l = { [e.SUCCESS]: "successes", [e.FAILURE]: "failures", [e.ADVANTAGE]: "advantages", [e.THREAT]: "threats", [e.TRIUMPH]: "triumphs", [e.DESPAIR]: "despair", [e.LIGHT]: "lightSide", [e.DARK]: "darkSide" };
var u = (e2) => Math.floor(Math.random() * e2) + 1;
var h = (e2) => {
const t2 = a[e2];
return { successes: t2.successes || 0, failures: t2.failures || 0, advantages: t2.advantages || 0, threats: t2.threats || 0, triumphs: t2.triumphs || 0, despair: t2.despairs || 0, lightSide: t2.lightSide || 0, darkSide: t2.darkSide || 0 };
};
var p = (e2) => {
const t2 = s[e2];
return { successes: t2.successes || 0, failures: t2.failures || 0, advantages: t2.advantages || 0, threats: t2.threats || 0, triumphs: t2.triumphs || 0, despair: t2.despairs || 0, lightSide: t2.lightSide || 0, darkSide: t2.darkSide || 0 };
};
var g = (e2) => {
const t2 = i[e2];
return { successes: t2.successes || 0, failures: t2.failures || 0, advantages: t2.advantages || 0, threats: t2.threats || 0, triumphs: t2.triumphs || 0, despair: t2.despairs || 0, lightSide: t2.lightSide || 0, darkSide: t2.darkSide || 0 };
};
var f = (e2) => {
const t2 = r[e2];
return { successes: t2.successes || 0, failures: t2.failures || 0, advantages: t2.advantages || 0, threats: t2.threats || 0, triumphs: t2.triumphs || 0, despair: t2.despairs || 0, lightSide: t2.lightSide || 0, darkSide: t2.darkSide || 0 };
};
var m = (e2) => {
const t2 = c[e2];
return { successes: t2.successes || 0, failures: t2.failures || 0, advantages: t2.advantages || 0, threats: t2.threats || 0, triumphs: t2.triumphs || 0, despair: t2.despairs || 0, lightSide: t2.lightSide || 0, darkSide: t2.darkSide || 0 };
};
var v = (e2) => {
const t2 = n[e2];
return { successes: t2.successes || 0, failures: t2.failures || 0, advantages: t2.advantages || 0, threats: t2.threats || 0, triumphs: t2.triumphs || 0, despair: t2.despairs || 0, lightSide: t2.lightSide || 0, darkSide: t2.darkSide || 0 };
};
var A = (e2) => {
const t2 = o[e2];
return { successes: t2.successes || 0, failures: t2.failures || 0, advantages: t2.advantages || 0, threats: t2.threats || 0, triumphs: t2.triumphs || 0, despair: t2.despairs || 0, lightSide: t2.lightSide || 0, darkSide: t2.darkSide || 0 };
};
var y = (e2, a2) => {
var s2, i2, r2, c2, n2, o2, d, y2, S;
const D = ((e3) => {
const t2 = { ...e3 };
if (e3.upgradeAbility && e3.upgradeAbility > 0) {
let a3 = e3.upgradeAbility;
const s3 = t2.abilityDice || 0, i3 = Math.min(s3, a3);
t2.abilityDice = s3 - i3, t2.proficiencyDice = (t2.proficiencyDice || 0) + i3, a3 -= i3, a3 > 0 && (t2.proficiencyDice = (t2.proficiencyDice || 0) + a3);
}
if (e3.upgradeDifficulty && e3.upgradeDifficulty > 0) {
let a3 = e3.upgradeDifficulty;
const s3 = t2.difficultyDice || 0, i3 = Math.min(s3, a3);
t2.difficultyDice = s3 - i3, t2.challengeDice = (t2.challengeDice || 0) + i3, a3 -= i3, a3 > 0 && (t2.challengeDice = (t2.challengeDice || 0) + a3);
}
if (e3.downgradeProficiency && e3.downgradeProficiency > 0) {
const a3 = t2.proficiencyDice || 0, s3 = Math.min(a3, e3.downgradeProficiency);
t2.proficiencyDice = a3 - s3, t2.abilityDice = (t2.abilityDice || 0) + s3;
}
if (e3.downgradeChallenge && e3.downgradeChallenge > 0) {
const a3 = t2.challengeDice || 0, s3 = Math.min(a3, e3.downgradeChallenge);
t2.challengeDice = a3 - s3, t2.difficultyDice = (t2.difficultyDice || 0) + s3;
}
return t2;
})(e2), T = null !== (s2 = D.boostDice) && void 0 !== s2 ? s2 : 0, b = null !== (i2 = D.abilityDice) && void 0 !== i2 ? i2 : 0, k = null !== (r2 = D.proficiencyDice) && void 0 !== r2 ? r2 : 0, M = null !== (c2 = D.setBackDice) && void 0 !== c2 ? c2 : 0, R = null !== (n2 = D.difficultyDice) && void 0 !== n2 ? n2 : 0, E = null !== (o2 = D.challengeDice) && void 0 !== o2 ? o2 : 0, I = null !== (d = D.forceDice) && void 0 !== d ? d : 0, P = null !== (y2 = null == a2 ? void 0 : a2.maxDicePerType) && void 0 !== y2 ? y2 : 100, x = null !== (S = null == a2 ? void 0 : a2.maxTotalDice) && void 0 !== S ? S : 500, H = Math.max(0, Math.min(T, P)), w = Math.max(0, Math.min(b, P)), U = Math.max(0, Math.min(k, P)), G = Math.max(0, Math.min(M, P)), N = Math.max(0, Math.min(R, P)), C = Math.max(0, Math.min(E, P)), V = Math.max(0, Math.min(I, P)), $ = T > P || b > P || k > P || M > P || R > P || E > P || I > P, L = H + w + U + G + N + C + V;
if (L > x) throw new Error(`Total dice count (${L}) exceeds maximum allowed (${x}). Please reduce the number of dice in your pool.`);
if ($ && (null == a2 ? void 0 : a2.throwOnLimitExceeded)) {
const e3 = [];
throw T > P && e3.push(`boost: ${T}`), b > P && e3.push(`ability: ${b}`), k > P && e3.push(`proficiency: ${k}`), M > P && e3.push(`setback: ${M}`), R > P && e3.push(`difficulty: ${R}`), E > P && e3.push(`challenge: ${E}`), I > P && e3.push(`force: ${I}`), new Error(`Dice counts exceed per-type limit (${P}): ${e3.join(", ")}. Dice counts have been capped to the maximum.`);
}
const F = [];
for (let e3 = 0; e3 < H; e3++) {
const e4 = u(6);
F.push({ type: "boost", roll: e4, result: h(e4) });
}
for (let e3 = 0; e3 < w; e3++) {
const e4 = u(8);
F.push({ type: "ability", roll: e4, result: g(e4) });
}
for (let e3 = 0; e3 < U; e3++) {
const e4 = u(12);
F.push({ type: "proficiency", roll: e4, result: m(e4) });
}
for (let e3 = 0; e3 < G; e3++) {
const e4 = u(6);
F.push({ type: "setback", roll: e4, result: p(e4) });
}
for (let e3 = 0; e3 < N; e3++) {
const e4 = u(8);
F.push({ type: "difficulty", roll: e4, result: f(e4) });
}
for (let e3 = 0; e3 < C; e3++) {
const e4 = u(12);
F.push({ type: "challenge", roll: e4, result: v(e4) });
}
for (let e3 = 0; e3 < V; e3++) {
const e4 = u(12);
F.push({ type: "force", roll: e4, result: A(e4) });
}
const j = { successes: e2.automaticSuccesses, failures: e2.automaticFailures, advantages: e2.automaticAdvantages, threats: e2.automaticThreats, triumphs: e2.automaticTriumphs, despairs: e2.automaticDespairs, lightSide: e2.automaticLightSide, darkSide: e2.automaticDarkSide }, O = ((e3, t2) => {
const a3 = e3.reduce((e4, t3) => ({ successes: e4.successes + t3.successes + t3.triumphs, failures: e4.failures + t3.failures + t3.despair, advantages: e4.advantages + t3.advantages, threats: e4.threats + t3.threats, triumphs: e4.triumphs + t3.triumphs, despair: e4.despair + t3.despair, lightSide: e4.lightSide + (t3.lightSide || 0), darkSide: e4.darkSide + (t3.darkSide || 0) }), { successes: ((null == t2 ? void 0 : t2.successes) || 0) + ((null == t2 ? void 0 : t2.triumphs) || 0), failures: ((null == t2 ? void 0 : t2.failures) || 0) + ((null == t2 ? void 0 : t2.despairs) || 0), advantages: (null == t2 ? void 0 : t2.advantages) || 0, threats: (null == t2 ? void 0 : t2.threats) || 0, triumphs: (null == t2 ? void 0 : t2.triumphs) || 0, despair: (null == t2 ? void 0 : t2.despairs) || 0, lightSide: (null == t2 ? void 0 : t2.lightSide) || 0, darkSide: (null == t2 ? void 0 : t2.darkSide) || 0 });
let s3 = 0, i3 = 0;
a3.successes === a3.failures ? (s3 = 0, i3 = 0) : a3.successes > a3.failures ? s3 = a3.successes - a3.failures : i3 = a3.failures - a3.successes;
let r3 = 0, c3 = 0;
return a3.advantages === a3.threats ? (r3 = 0, c3 = 0) : a3.advantages > a3.threats ? r3 = a3.advantages - a3.threats : c3 = a3.threats - a3.advantages, { successes: s3, failures: i3, advantages: r3, threats: c3, triumphs: a3.triumphs, despair: a3.despair, lightSide: a3.lightSide, darkSide: a3.darkSide };
})(F.map((e3) => e3.result), j);
if (null == a2 ? void 0 : a2.hints) {
const e3 = t.filter((e4) => {
const { cost: t2 } = e4;
return Object.entries(t2).some(([e5, t3]) => {
const a3 = l[e5];
if (!a3) return false;
const s3 = O[a3];
return "number" == typeof s3 && (void 0 !== t3 && t3 > 0 && s3 >= t3);
});
});
O.hints = e3.map((e4) => `${(function(e5) {
if (!e5.cost || 0 === Object.keys(e5.cost).length) return "No cost";
const t2 = Object.entries(e5.cost).filter(([e6, t3]) => t3 && t3 > 0).map(([e6, t3]) => `${t3} ${e6.charAt(0).toUpperCase() + e6.toLowerCase().slice(1)}${t3 > 1 ? "s" : ""}`);
return t2.length > 1 ? t2.join(" OR ") : t2.length > 0 ? t2[0] : "No cost";
})(e4)} - ${e4.description}`);
}
return { results: F, summary: O };
};
// src/MonteCarlo.ts
var MonteCarloError = class extends Error {
constructor(message) {
super(message);
this.name = "MonteCarloError";
}
};
var _MonteCarlo = class _MonteCarlo {
constructor(dicePoolOrConfig, iterations = 1e4, runSimulate = true) {
this.histogram = {
netSuccesses: {},
netAdvantages: {},
triumphs: {},
despairs: {},
failures: {},
threats: {},
lightSide: {},
darkSide: {},
netForce: {}
};
// Theoretical maximum total Force pips (2 per Force die + automatic pips);
// computed per run in simulate(). Used for the "Max Force Pips %" metric.
this.maxForcePips = 0;
this.hasForceDice = false;
this.statsCache = /* @__PURE__ */ new Map();
this.modifierStats = {
automaticSymbolContribution: {
successes: 0,
failures: 0,
advantages: 0,
threats: 0,
triumphs: 0,
despairs: 0,
lightSide: 0,
darkSide: 0
},
rolledSymbolContribution: {
successes: 0,
failures: 0,
advantages: 0,
threats: 0,
triumphs: 0,
despairs: 0,
lightSide: 0,
darkSide: 0
},
upgradeImpact: {
abilityUpgrades: 0,
difficultyUpgrades: 0,
proficiencyDowngrades: 0,
challengeDowngrades: 0
}
};
this.runningStats = {
successCount: 0,
criticalSuccessCount: 0,
criticalFailureCount: 0,
netPositiveCount: 0,
sumSuccesses: 0,
sumAdvantages: 0,
sumTriumphs: 0,
sumFailures: 0,
sumThreats: 0,
sumDespair: 0,
sumLightSide: 0,
sumDarkSide: 0,
sumNetForce: 0,
sumSquaredSuccesses: 0,
sumSquaredAdvantages: 0,
sumSquaredThreats: 0,
sumSquaredFailures: 0,
sumSquaredDespair: 0,
sumSquaredLightSide: 0,
sumSquaredDarkSide: 0,
sumSquaredTriumphs: 0,
sumSquaredNetForce: 0,
maxForcePipsCount: 0
};
this.results = [];
if (this.isSimulationConfig(dicePoolOrConfig)) {
this.config = dicePoolOrConfig;
this.dicePool = dicePoolOrConfig.dicePool;
this.iterations = dicePoolOrConfig.iterations || iterations;
this.modifiers = dicePoolOrConfig.modifiers || this.mergeModifiers(
dicePoolOrConfig.playerModifiers,
dicePoolOrConfig.oppositionModifiers
);
} else {
this.dicePool = dicePoolOrConfig;
this.iterations = iterations;
}
this.validateDicePool(this.dicePool);
this.validateIterations(this.iterations);
this.resetRunningStats();
if (runSimulate) {
this.simulate();
}
}
isSimulationConfig(obj) {
return obj && typeof obj === "object" && "dicePool" in obj;
}
mergeModifiers(player, opposition) {
if (!player && !opposition) return void 0;
const merged = {};
if (player) {
merged.automaticSuccesses = player.automaticSuccesses;
merged.automaticAdvantages = player.automaticAdvantages;
merged.automaticTriumphs = player.automaticTriumphs;
merged.automaticLightSide = player.automaticLightSide;
merged.upgradeAbility = player.upgradeAbility;
merged.downgradeProficiency = player.downgradeProficiency;
}
if (opposition) {
merged.automaticFailures = opposition.automaticFailures;
merged.automaticThreats = opposition.automaticThreats;
merged.automaticDespairs = opposition.automaticDespairs;
merged.automaticDarkSide = opposition.automaticDarkSide;
merged.upgradeDifficulty = opposition.upgradeDifficulty;
merged.downgradeChallenge = opposition.downgradeChallenge;
}
return merged;
}
applyModifiers(pool) {
if (!this.modifiers) return pool;
const modifiedPool = { ...pool };
if (this.modifiers.automaticSuccesses)
modifiedPool.automaticSuccesses = (modifiedPool.automaticSuccesses || 0) + this.modifiers.automaticSuccesses;
if (this.modifiers.automaticFailures)
modifiedPool.automaticFailures = (modifiedPool.automaticFailures || 0) + this.modifiers.automaticFailures;
if (this.modifiers.automaticAdvantages)
modifiedPool.automaticAdvantages = (modifiedPool.automaticAdvantages || 0) + this.modifiers.automaticAdvantages;
if (this.modifiers.automaticThreats)
modifiedPool.automaticThreats = (modifiedPool.automaticThreats || 0) + this.modifiers.automaticThreats;
if (this.modifiers.automaticTriumphs)
modifiedPool.automaticTriumphs = (modifiedPool.automaticTriumphs || 0) + this.modifiers.automaticTriumphs;
if (this.modifiers.automaticDespairs)
modifiedPool.automaticDespairs = (modifiedPool.automaticDespairs || 0) + this.modifiers.automaticDespairs;
if (this.modifiers.automaticLightSide)
modifiedPool.automaticLightSide = (modifiedPool.automaticLightSide || 0) + this.modifiers.automaticLightSide;
if (this.modifiers.automaticDarkSide)
modifiedPool.automaticDarkSide = (modifiedPool.automaticDarkSide || 0) + this.modifiers.automaticDarkSide;
if (this.modifiers.upgradeAbility)
modifiedPool.upgradeAbility = (modifiedPool.upgradeAbility || 0) + this.modifiers.upgradeAbility;
if (this.modifiers.upgradeDifficulty)
modifiedPool.upgradeDifficulty = (modifiedPool.upgradeDifficulty || 0) + this.modifiers.upgradeDifficulty;
if (this.modifiers.downgradeProficiency)
modifiedPool.downgradeProficiency = (modifiedPool.downgradeProficiency || 0) + this.modifiers.downgradeProficiency;
if (this.modifiers.downgradeChallenge)
modifiedPool.downgradeChallenge = (modifiedPool.downgradeChallenge || 0) + this.modifiers.downgradeChallenge;
this.modifierStats.upgradeImpact.abilityUpgrades = this.modifiers.upgradeAbility || 0;
this.modifierStats.upgradeImpact.difficultyUpgrades = this.modifiers.upgradeDifficulty || 0;
this.modifierStats.upgradeImpact.proficiencyDowngrades = this.modifiers.downgradeProficiency || 0;
this.modifierStats.upgradeImpact.challengeDowngrades = this.modifiers.downgradeChallenge || 0;
return modifiedPool;
}
validateDicePool(dicePool) {
if (!dicePool || typeof dicePool !== "object") {
throw new MonteCarloError(
"Invalid dice pool: must be a valid DicePool object"
);
}
const diceTypes = [
"abilityDice",
"proficiencyDice",
"boostDice",
"setBackDice",
"difficultyDice",
"challengeDice",
"forceDice"
];
const hasAnyDice = diceTypes.some(
(type) => dicePool[type] && dicePool[type] > 0
);
if (!hasAnyDice) {
throw new MonteCarloError(
"Invalid dice pool: must contain at least one die"
);
}
diceTypes.forEach((type) => {
const count = dicePool[type];
if (count !== void 0 && (count < 0 || !Number.isInteger(count))) {
throw new MonteCarloError(
`Invalid ${type}: must be a non-negative integer`
);
}
});
}
validateIterations(iterations) {
if (!Number.isInteger(iterations)) {
throw new MonteCarloError("Iterations must be an integer");
}
if (iterations < _MonteCarlo.MIN_ITERATIONS) {
throw new MonteCarloError(
`Iterations must be at least ${_MonteCarlo.MIN_ITERATIONS}`
);
}
if (iterations > _MonteCarlo.MAX_ITERATIONS) {
throw new MonteCarloError(
`Iterations must not exceed ${_MonteCarlo.MAX_ITERATIONS}`
);
}
}
calculateHistogramStats(histogram, totalCount) {
let sum = 0;
let sumSquares = 0;
let count = 0;
for (const [value, freq] of Object.entries(histogram)) {
const val = parseInt(value);
sum += val * freq;
sumSquares += val * val * freq;
count += freq;
}
const mean = sum / count;
const variance = sumSquares / count - mean * mean;
const stdDev = Math.sqrt(Math.max(0, variance));
return { mean, stdDev, sum, sumSquares };
}
calculateSkewness(histogram, stats) {
if (stats.stdDev === 0) return 0;
let sumCubedDeviations = 0;
let totalCount = 0;
for (const [value, freq] of Object.entries(histogram)) {
const deviation = (parseInt(value) - stats.mean) / stats.stdDev;
sumCubedDeviations += Math.pow(deviation, 3) * freq;
totalCount += freq;
}
return sumCubedDeviations / totalCount;
}
calculateKurtosis(histogram, stats) {
if (stats.stdDev === 0) return 0;
let sumFourthPowerDeviations = 0;
let totalCount = 0;
for (const [value, freq] of Object.entries(histogram)) {
const deviation = (parseInt(value) - stats.mean) / stats.stdDev;
sumFourthPowerDeviations += Math.pow(deviation, 4) * freq;
totalCount += freq;
}
return sumFourthPowerDeviations / totalCount - 3;
}
findOutliers(histogram, stats) {
if (stats.stdDev === 0) return [];
const threshold = 2;
return Object.entries(histogram).filter(
([value]) => Math.abs(parseInt(value) - stats.mean) > threshold * stats.stdDev
).map(([value]) => parseInt(value));
}
analyzeDistribution(histogram, totalCount) {
const stats = this.calculateHistogramStats(histogram, totalCount);
return {
skewness: this.calculateSkewness(histogram, stats),
kurtosis: this.calculateKurtosis(histogram, stats),
outliers: this.findOutliers(histogram, stats),
modes: this.findModes(histogram),
percentiles: this.calculatePercentiles(histogram, totalCount)
};
}
average(selector) {
const selectorName = typeof selector === "function" ? selector.name || "custom" : selector.name;
const cacheKey = `avg_${selectorName}`;
if (this.statsCache.has(cacheKey)) {
return this.statsCache.get(cacheKey);
}
let sum = 0;
if (typeof selector === "function") {
sum = this.results.reduce((acc, roll) => {
const value = selector(roll);
if (typeof value !== "number" || isNaN(value)) {
throw new MonteCarloError(`Invalid selector result: ${value}`);
}
return acc + value;
}, 0);
} else {
switch (selector.name) {
case "successes":
sum = this.runningStats.sumSuccesses;
break;
case "advantages":
sum = this.runningStats.sumAdvantages;
break;
case "triumphs":
sum = this.runningStats.sumTriumphs;
break;
case "failures":
sum = this.runningStats.sumFailures;
break;
case "threats":
sum = this.runningStats.sumThreats;
break;
case "despair":
sum = this.runningStats.sumDespair;
break;
case "lightSide":
sum = this.runningStats.sumLightSide;
break;
case "darkSide":
sum = this.runningStats.sumDarkSide;
break;
default:
throw new MonteCarloError(`Unknown selector: ${selector.name}`);
}
}
const avg = sum / this.iterations;
this.statsCache.set(cacheKey, avg);
return avg;
}
standardDeviation(selector) {
const selectorName = typeof selector === "function" ? selector.name || "custom" : selector.name;
const cacheKey = `std_${selectorName}`;
if (this.statsCache.has(cacheKey)) {
return this.statsCache.get(cacheKey);
}
const avg = this.average(selector);
let squareSum = 0;
if (typeof selector === "function") {
squareSum = this.results.reduce((acc, roll) => {
const value = selector(roll);
if (typeof value !== "number" || isNaN(value)) {
throw new MonteCarloError(`Invalid selector result: ${value}`);
}
return acc + value * value;
}, 0);
} else {
switch (selector.name) {
case "successes":
squareSum = this.runningStats.sumSquaredSuccesses;
break;
case "advantages":
squareSum = this.runningStats.sumSquaredAdvantages;
break;
case "threats":
squareSum = this.runningStats.sumSquaredThreats;
break;
case "triumphs":
squareSum = this.runningStats.sumSquaredTriumphs;
break;
case "failures":
squareSum = this.runningStats.sumSquaredFailures;
break;
case "despair":
squareSum = this.runningStats.sumSquaredDespair;
break;
case "lightSide":
squareSum = this.runningStats.sumSquaredLightSide;
break;
case "darkSide":
squareSum = this.runningStats.sumSquaredDarkSide;
break;
default:
throw new MonteCarloError(`Unknown selector: ${selector.name}`);
}
}
const stdDev = Math.sqrt(Math.abs(squareSum / this.iterations - avg * avg));
this.statsCache.set(cacheKey, stdDev);
return stdDev;
}
resetRunningStats() {
this.runningStats = {
successCount: 0,
criticalSuccessCount: 0,
criticalFailureCount: 0,
netPositiveCount: 0,
sumSuccesses: 0,
sumAdvantages: 0,
sumTriumphs: 0,
sumFailures: 0,
sumThreats: 0,
sumDespair: 0,
sumLightSide: 0,
sumDarkSide: 0,
sumNetForce: 0,
sumSquaredSuccesses: 0,
sumSquaredAdvantages: 0,
sumSquaredThreats: 0,
sumSquaredFailures: 0,
sumSquaredDespair: 0,
sumSquaredLightSide: 0,
sumSquaredDarkSide: 0,
sumSquaredTriumphs: 0,
sumSquaredNetForce: 0,
maxForcePipsCount: 0
};
}
resetModifierStats() {
this.modifierStats = {
automaticSymbolContribution: {
successes: 0,
failures: 0,
advantages: 0,
threats: 0,
triumphs: 0,
despairs: 0,
lightSide: 0,
darkSide: 0
},
rolledSymbolContribution: {
successes: 0,
failures: 0,
advantages: 0,
threats: 0,
triumphs: 0,
despairs: 0,
lightSide: 0,
darkSide: 0
},
upgradeImpact: {
abilityUpgrades: 0,
difficultyUpgrades: 0,
proficiencyDowngrades: 0,
challengeDowngrades: 0
}
};
}
trackModifierContribution(result) {
if (!this.modifiers) return;
const poolModifiers = this.applyModifiers(this.dicePool);
const autoSuccesses = poolModifiers.automaticSuccesses || 0;
const autoFailures = poolModifiers.automaticFailures || 0;
const autoAdvantages = poolModifiers.automaticAdvantages || 0;
const autoThreats = poolModifiers.automaticThreats || 0;
const autoTriumphs = poolModifiers.automaticTriumphs || 0;
const autoDespairs = poolModifiers.automaticDespairs || 0;
const autoLightSide = poolModifiers.automaticLightSide || 0;
const autoDarkSide = poolModifiers.automaticDarkSide || 0;
this.modifierStats.automaticSymbolContribution.successes += autoSuccesses;
this.modifierStats.automaticSymbolContribution.failures += autoFailures;
this.modifierStats.automaticSymbolContribution.advantages += autoAdvantages;
this.modifierStats.automaticSymbolContribution.threats += autoThreats;
this.modifierStats.automaticSymbolContribution.triumphs += autoTriumphs;
this.modifierStats.automaticSymbolContribution.despairs += autoDespairs;
this.modifierStats.automaticSymbolContribution.lightSide += autoLightSide;
this.modifierStats.automaticSymbolContribution.darkSide += autoDarkSide;
this.modifierStats.rolledSymbolContribution.successes += Math.max(
0,
result.successes - autoSuccesses
);
this.modifierStats.rolledSymbolContribution.failures += Math.max(
0,
result.failures - autoFailures
);
this.modifierStats.rolledSymbolContribution.advantages += Math.max(
0,
result.advantages - autoAdvantages
);
this.modifierStats.rolledSymbolContribution.threats += Math.max(
0,
result.threats - autoThreats
);
this.modifierStats.rolledSymbolContribution.triumphs += Math.max(
0,
result.triumphs - autoTriumphs
);
this.modifierStats.rolledSymbolContribution.despairs += Math.max(
0,
result.despair - autoDespairs
);
this.modifierStats.rolledSymbolContribution.lightSide += Math.max(
0,
result.lightSide - autoLightSide
);
this.modifierStats.rolledSymbolContribution.darkSide += Math.max(
0,
result.darkSide - autoDarkSide
);
}
updateHistogram(result) {
const netSuccesses = result.successes - result.failures;
this.histogram.netSuccesses[netSuccesses] = (this.histogram.netSuccesses[netSuccesses] || 0) + 1;
const netAdvantages = result.advantages - result.threats;
this.histogram.netAdvantages[netAdvantages] = (this.histogram.netAdvantages[netAdvantages] || 0) + 1;
this.histogram.triumphs[result.triumphs] = (this.histogram.triumphs[result.triumphs] || 0) + 1;
this.histogram.despairs[result.despair] = (this.histogram.despairs[result.despair] || 0) + 1;
this.histogram.lightSide[result.lightSide] = (this.histogram.lightSide[result.lightSide] || 0) + 1;
this.histogram.darkSide[result.darkSide] = (this.histogram.darkSide[result.darkSide] || 0) + 1;
this.histogram.failures[result.failures] = (this.histogram.failures[result.failures] || 0) + 1;
this.histogram.threats[result.threats] = (this.histogram.threats[result.threats] || 0) + 1;
const netForce = result.lightSide - result.darkSide;
this.histogram.netForce[netForce] = (this.histogram.netForce[netForce] || 0) + 1;
this.runningStats.sumSuccesses += result.successes;
this.runningStats.sumAdvantages += result.advantages;
this.runningStats.sumTriumphs += result.triumphs;
this.runningStats.sumFailures += result.failures;
this.runningStats.sumThreats += result.threats;
this.runningStats.sumDespair += result.despair;
this.runningStats.sumLightSide += result.lightSide;
this.runningStats.sumDarkSide += result.darkSide;
this.runningStats.sumNetForce += netForce;
this.runningStats.sumSquaredSuccesses += result.successes * result.successes;
this.runningStats.sumSquaredAdvantages += result.advantages * result.advantages;
this.runningStats.sumSquaredThreats += result.threats * result.threats;
this.runningStats.sumSquaredFailures += result.failures * result.failures;
this.runningStats.sumSquaredDespair += result.despair * result.despair;
this.runningStats.sumSquaredLightSide += result.lightSide * result.lightSide;
this.runningStats.sumSquaredDarkSide += result.darkSide * result.darkSide;
this.runningStats.sumSquaredTriumphs += result.triumphs * result.triumphs;
this.runningStats.sumSquaredNetForce += netForce * netForce;
if (this.hasForceDice && result.lightSide + result.darkSide === this.maxForcePips) {
this.runningStats.maxForcePipsCount++;
}
if (netSuccesses > 0) {
this.runningStats.successCount++;
if (netAdvantages > 0) {
this.runningStats.netPositiveCount++;
}
}
if (result.triumphs > 0) this.runningStats.criticalSuccessCount++;
if (result.despair > 0) this.runningStats.criticalFailureCount++;
}
simulate() {
try {
this.resetHistogram();
this.resetRunningStats();
this.resetModifierStats();
this.statsCache.clear();
this.results = [];
const modifiedPool = this.applyModifiers(this.dicePool);
const forceDiceCount = modifiedPool.forceDice || 0;
this.hasForceDice = forceDiceCount > 0;
this.maxForcePips = 2 * forceDiceCount + (modifiedPool.automaticLightSide || 0) + (modifiedPool.automaticDarkSide || 0);
for (let i2 = 0; i2 < this.iterations; i2++) {
const rollResult = y(modifiedPool);
this.results.push(rollResult.summary);
this.updateHistogram(rollResult.summary);
this.trackModifierContribution(rollResult.summary);
}
const successProbability = this.runningStats.successCount / this.iterations;
const criticalSuccessProbability = this.runningStats.criticalSuccessCount / this.iterations;
const criticalFailureProbability = this.runningStats.criticalFailureCount / this.iterations;
const netPositiveProbability = this.runningStats.netPositiveCount / this.iterations;
const averages = {
successes: this.runningStats.sumSuccesses / this.iterations,
advantages: this.runningStats.sumAdvantages / this.iterations,
triumphs: this.runningStats.sumTriumphs / this.iterations,
failures: this.runningStats.sumFailures / this.iterations,
threats: this.runningStats.sumThreats / this.iterations,
despair: this.runningStats.sumDespair / this.iterations,
lightSide: this.runningStats.sumLightSide / this.iterations,
darkSide: this.runningStats.sumDarkSide / this.iterations
};
const standardDeviations = {
successes: Math.sqrt(
Math.max(
0,
this.runningStats.sumSquaredSuccesses / this.iterations - averages.successes * averages.successes
)
),
advantages: Math.sqrt(
Math.max(
0,
this.runningStats.sumSquaredAdvantages / this.iterations - averages.advantages * averages.advantages
)
),
triumphs: Math.sqrt(
Math.max(
0,
this.runningStats.sumSquaredTriumphs / this.iterations - averages.triumphs * averages.triumphs
)
),
failures: Math.sqrt(
Math.max(
0,
this.runningStats.sumSquaredFailures / this.iterations - averages.failures * averages.failures
)
),
threats: Math.sqrt(
Math.max(
0,
this.runningStats.sumSquaredThreats / this.iterations - averages.threats * averages.threats
)
),
despair: Math.sqrt(
Math.max(
0,
this.runningStats.sumSquaredDespair / this.iterations - averages.despair * averages.despair
)
),
lightSide: Math.sqrt(
Math.max(
0,
this.runningStats.sumSquaredLightSide / this.iterations - averages.lightSide * averages.lightSide
)
),
darkSide: Math.sqrt(
Math.max(
0,
this.runningStats.sumSquaredDarkSide / this.iterations - averages.darkSide * averages.darkSide
)
)
};
const medians = {
successes: this.calculateMedianFromHistogram(
this.histogram.netSuccesses
),
advantages: this.calculateMedianFromHistogram(
this.histogram.netAdvantages
),
triumphs: this.calculateMedianFromHistogram(this.histogram.triumphs),
failures: this.calculateMedianFromHistogram(this.histogram.failures),
threats: this.calculateMedianFromHistogram(this.histogram.threats),
despair: this.calculateMedianFromHistogram(this.histogram.despairs),
lightSide: this.calculateMedianFromHistogram(this.histogram.lightSide),
darkSide: this.calculateMedianFromHistogram(this.histogram.darkSide)
};
const averageNetForce = this.runningStats.sumNetForce / this.iterations;
const forceStatistics = {
averageNetForce,
medianNetForce: this.calculateMedianFromHistogram(
this.histogram.netForce
),
standardDeviationNetForce: Math.sqrt(
Math.max(
0,
this.runningStats.sumSquaredNetForce / this.iterations - averageNetForce * averageNetForce
)
),
maxForcePipsProbability: this.hasForceDice ? this.runningStats.maxForcePipsCount / this.iterations : 0
};
const analysis = {
netSuccesses: this.analyzeDistribution(
this.histogram.netSuccesses,
this.iterations
),
netAdvantages: this.analyzeDistribution(
this.histogram.netAdvantages,
this.iterations
),
triumphs: this.analyzeDistribution(
this.histogram.triumphs,
this.iterations
),
despairs: this.analyzeDistribution(
this.histogram.despairs,
this.iterations
),
lightSide: this.analyzeDistribution(
this.histogram.lightSide,
this.iterations
),
darkSide: this.analyzeDistribution(
this.histogram.darkSide,
this.iterations
),
netForce: this.analyzeDistribution(
this.histogram.netForce,
this.iterations
)
};
const result = {
averages,
medians,
standardDeviations,
successProbability,
criticalSuccessProbability,
criticalFailureProbability,
netPositiveProbability,
forceStatistics,
histogram: this.histogram,
analysis
};
if (this.modifiers) {
const iterations = this.iterations;
result.modifierAnalysis = {
automaticSymbolContribution: {
successes: this.modifierStats.automaticSymbolContribution.successes / iterations,
failures: this.modifierStats.automaticSymbolContribution.failures / iterations,
advantages: this.modifierStats.automaticSymbolContribution.advantages / iterations,
threats: this.modifierStats.automaticSymbolContribution.threats / iterations,
triumphs: this.modifierStats.automaticSymbolContribution.triumphs / iterations,
despairs: this.modifierStats.automaticSymbolContribution.despairs / iterations,
lightSide: this.modifierStats.automaticSymbolContribution.lightSide / iterations,
darkSide: this.modifierStats.automaticSymbolContribution.darkSide / iterations
},
rolledSymbolContribution: {
successes: this.modifierStats.rolledSymbolContribution.successes / iterations,
failures: this.modifierStats.rolledSymbolContribution.failures / iterations,
advantages: this.modifierStats.rolledSymbolContribution.advantages / iterations,
threats: this.modifierStats.rolledSymbolContribution.threats / iterations,
triumphs: this.modifierStats.rolledSymbolContribution.triumphs / iterations,
despairs: this.modifierStats.rolledSymbolContribution.despairs / iterations,
lightSide: this.modifierStats.rolledSymbolContribution.lightSide / iterations,
darkSide: this.modifierStats.rolledSymbolContribution.darkSide / iterations
},
upgradeImpact: this.modifierStats.upgradeImpact
};
}
return result;
} catch (error) {
if (error instanceof Error) {
throw new MonteCarloError(`Simulation failed: ${error.message}`);
}
throw new MonteCarloError("Simulation failed with unknown error");
}
}
resetHistogram() {
this.histogram = {
netSuccesses: {},
netAdvantages: {},
triumphs: {},
despairs: {},
failures: {},
threats: {},
lightSide: {},
darkSide: {},
netForce: {}
};
}
calculateMedianFromHistogram(histogram) {
const entries = Object.entries(histogram).map(([value, count]) => ({ value: parseInt(value), count })).sort((a2, b) => a2.value - b.value);
if (entries.length === 0) {
return 0;
}
let runningCount = 0;
const targetCount = this.iterations / 2;
for (const { value, count } of entries) {
runningCount += count;
if (runningCount >= targetCount) {
return value;
}
}
return entries[entries.length - 1].value;
}
findModes(histogram) {
const entries = Object.entries(histogram);
if (entries.length === 0) return [];
const maxCount = Math.max(...entries.map(([, count]) => count));
return entries.filter(([, count]) => count === maxCount).map(([value]) => parseInt(value));
}
calculatePercentiles(histogram, totalCount) {
const sortedEntries = Object.entries(histogram).map(([value, count]) => ({ value: parseInt(value), count })).sort((a2, b) => a2.value - b.value);
if (sortedEntries.length === 0) {
return {};
}
const percentiles = {};
let runningCount = 0;
const targetPercentiles = [25, 50, 75, 90];
let currentTargetIndex = 0;
for (const { value, count } of sortedEntries) {
runningCount += count;
const currentPercentile = runningCount / totalCount * 100;
while (currentTargetIndex < targetPercentiles.length && currentPercentile >= targetPercentiles[currentTargetIndex]) {
percentiles[targetPercentiles[currentTargetIndex]] = value;
currentTargetIndex++;
}
}
const maxValue = sortedEntries[sortedEntries.length - 1].value;
while (currentTargetIndex < targetPercentiles.length) {
percentiles[targetPercentiles[currentTargetIndex]] = maxValue;
currentTargetIndex++;
}
return percentiles;
}
};
_MonteCarlo.MIN_ITERATIONS = 100;
_MonteCarlo.MAX_ITERATIONS = 1e6;
var MonteCarlo = _MonteCarlo;
export {
MonteCarlo,
MonteCarloError
};
//# sourceMappingURL=index.esm.js.map