Skip to content

Instantly share code, notes, and snippets.

@SimDing
Last active December 22, 2023 03:54
Show Gist options
  • Save SimDing/a067718caffbdad042ed3b6c8d0d77ff to your computer and use it in GitHub Desktop.
Save SimDing/a067718caffbdad042ed3b6c8d0d77ff to your computer and use it in GitHub Desktop.
Baldurs Gate 3 - Great Weapon Fighting and Savage Attacker Calculations
// DSL for producing distributions of dice rolls
type Cont<T> = (state: T) => void;
const rollDie = <T, U>(faces: number, state: T, cont: (faces: number, state: T, next: Cont<U>) => void, next: Cont<U>) => {
for (let i = 1; i <= faces; i++) {
//console.log(state, 'rolled', i);
cont(i, { ...state }, next);
}
};
const cWhile = <T>(cond: (state: T) => boolean, state: T, inner: (state: T, next: Cont<T>) => void, next: Cont<T>) => {
if (cond(state)) {
inner(state, state => cWhile(cond, state, inner, next));
} else {
next(state);
}
};
const forOf = <T, V>(arr: V[], state: T, inner: (v: V, state: T, next: Cont<T>) => void, next: Cont<T>) => {
arr.reduceRight((a, b) => state => inner(b, state, a), next)(state);
};
const cif = <T>(cond: boolean, state: T, cont: (state: T, next: Cont<T>) => void, next: Cont<T>) => {
if (cond) {
cont(state, next);
} else {
next(state);
}
};
interface Attack {
name: string;
dice: number[]; // damage dice
gwf: boolean; // great weapon fighting
sa: boolean; // savage attacker
flat: number; // bonus to damage throw
ac: number; // armor class of target
ab: number; // bonus to attack throw
critRange: number; // 1: 20, 2: 19-20, 3: 18-20...
onlyDmg?: boolean; // skip hit / crit calculation
}
const convolution = (f: ArrayLike<number>, g: ArrayLike<number>) => {
const result = new Float64Array(f.length + g.length - 1);
for (let k = 0; k < result.length; k++) {
for (let i = Math.max(0, k - g.length + 1); i < Math.min(k + 1, f.length); i++) {
const fval = f[i];
const gx = k - i;
const gval = g[gx];
result[k] += fval * gval;
}
}
return result;
};
const normalize = (arr: Float64Array) => {
const sum = arr.reduce((a, b) => a + b);
for (let k = 0; k < arr.length; k++) {
arr[k] /= sum;
}
};
const mulAdd = (mul: number, from: Float64Array, into: Float64Array) => {
for (let k = 0; k < from.length; k++) {
into[k] += mul * from[k];
}
};
const simulateAttack = (attack: Attack) => {
// The damage roll
let result = new Float64Array(attack.flat + 1); // read flat: 100%
result[attack.flat] = 1;
for (const die of attack.dice) {
/*
let result = rollDie(die);
if (attack.gwf && result < 3) {
result = rollDie(die);
}
if (attack.sa) {
result = Math.max(result, rollDie(die));
}
stats[result] += 1;
*/
const stats = new Float64Array(die + 1);
rollDie(die, null, (rolled, _, next) => {
next({ result: rolled });
}, (state: { result: number }) => {
cif(attack.gwf, state, (state, next) => {
rollDie(die, state, (rolled, state, next) => {
next( { result: state.result < 3 ? rolled : state.result });
}, next);
}, state => {
cif(attack.sa, state, (state, next) => {
rollDie(die, state, (rolled, state, next) => {
next({ result: Math.max(rolled, state.result) });
}, next);
}, state => {
stats[state.result] += 1;
});
});
});
result = convolution(result, stats);
normalize(result);
}
if (attack.onlyDmg) {
return result;
}
// crit / miss / hit
const critResult = convolution(result, result);
const finalResult = new Float64Array(critResult.length);
mulAdd(attack.critRange, critResult, finalResult);
const minRoll = Math.max(attack.ac - attack.ab, 2);
const missCases = minRoll - 1;
mulAdd(missCases, new Float64Array([1]), finalResult);
const hitCases = 20 - missCases - attack.critRange;
mulAdd(hitCases, result, finalResult);
normalize(finalResult);
return finalResult;
};
const consolidate = (stats: ArrayLike<number>) => {
let ex = 0;
let ex_sq = 0;
for (let i = 1; i < stats.length; i++) {
ex += i * stats[i];
ex_sq += i * i * stats[i];
}
const v = ex_sq - ex * ex;
return 'γ: ' + ex.toFixed(2) + ', σ: ' + Math.sqrt(v).toFixed(2);
};
const compareAttacks = (attacks: Attack[]) => {
const stats = attacks.map(simulateAttack);
for (let i = 0; i < attacks.length; i++) {
console.log(attacks[i].name + ': ' + consolidate(stats[i]));
//console.log(stats[i]);
}
};
// Example: paladin with Everburn Blade and Divine Strike
compareAttacks([{
dice: [6, 6, 8, 8, 4],
gwf: false,
name: 'nothing',
sa: false,
flat: 3,
ac: 14,
ab: 3,
critRange: 1,
}, {
dice: [6, 6, 8, 8, 4],
gwf: false,
name: 'ability score improvement',
sa: false,
flat: 4,
ac: 14,
ab: 4,
critRange: 1,
}, {
dice: [6, 6, 8, 8, 4],
gwf: false,
name: '2x ability score improvement',
sa: false,
flat: 5,
ac: 14,
ab: 5,
critRange: 1,
}, {
dice: [6, 6, 8, 8, 4],
gwf: true,
name: 'gwf + ability score improvement',
sa: false,
flat: 4,
ac: 14,
ab: 4,
critRange: 1,
},{
dice: [6, 6, 8, 8, 4],
gwf: false,
name: 'sa',
sa: true,
flat: 3,
ac: 14,
ab: 3,
critRange: 1,
}, {
dice: [6, 6, 8, 8, 4],
gwf: true,
name: 'gwf + sa',
sa: true,
flat: 3,
ac: 14,
ab: 3,
critRange: 1,
}]);
/*
nothing: γ: 11.83, σ: 13.05
ability score improvement: γ: 13.50, σ: 13.52
2x ability score improvement: γ: 15.27, σ: 13.87
gwf + ability score improvement: γ: 15.50, σ: 15.31
sa: γ: 14.68, σ: 15.94
gwf + sa: γ: 15.49, σ: 16.75
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment