Skip to content

Commit

Permalink
Use a calculated population max rather than a sample max (#31)
Browse files Browse the repository at this point in the history
## API

- Use a calculated population max (The actual max) rather than basing it on the max value of the simulated sample (fixes: #30)
- Remove Median as the way the data is distributed, it is usually extremely close to mean anyway
- Calculate frequency table on the fly, rather than creating a large results array first (fixes #32)
  • Loading branch information
damonhook committed Feb 13, 2020
1 parent 2ab829b commit 28aeb9c
Show file tree
Hide file tree
Showing 42 changed files with 458 additions and 447 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
- run: npm install -g yarn@1.19.2
- name: Install Dependencies
run: yarn setup
- name: Build
run: yarn heroku-postbuild
- name: Lint and Test
run: |
yarn lint
Expand Down
27 changes: 13 additions & 14 deletions api/controllers/statsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ export const compareUnits = ({ units, target }) => {
};
};

const buildCumulative = (probabilities: any, unitNames: string[], metrics: any) => {
const maxDamage = Math.max(...Object.keys(probabilities).map(n => Number(n)));
const buildCumulative = (
probabilities: any,
unitNames: string[],
metrics: {
mean: { [name: string]: number };
max: { [name: string]: number };
},
) => {
const maxDamage = Math.max(...Object.values(metrics.max));
const sums = unitNames.reduce((acc, name) => ({ ...acc, [name]: 0 }), {});
const cumulative = [...Array(maxDamage + 1)].map((_, damage) => {
const map = probabilities[damage] ?? {};
Expand All @@ -49,14 +56,13 @@ const buildCumulative = (probabilities: any, unitNames: string[], metrics: any)

const buildProbability = ({ save, ...unitResults }) => {
const probabilities = {};
const metrics = { mean: {}, median: {}, max: {} };
const metrics = { mean: {}, max: {} };
Object.keys(unitResults).forEach(name => {
unitResults[name].buckets.forEach(({ damage, probability }) => {
if (probabilities[damage] == null) probabilities[damage] = {};
probabilities[damage][name] = probability;
});
metrics.mean[name] = unitResults[name].metrics.mean;
metrics.median[name] = unitResults[name].metrics.median;
metrics.max[name] = unitResults[name].metrics.max;
});
const buckets = Object.keys(probabilities)
Expand All @@ -74,18 +80,12 @@ const buildProbability = ({ save, ...unitResults }) => {
};
};

export const simulateUnitsForSave = ({
units,
save,
target,
numSimulations = 1000,
includeOutcomes = false,
}) => {
export const simulateUnitsForSave = ({ units, save, target, numSimulations = 1000 }) => {
const unitList: Unit[] = units.map(({ name, weapon_profiles }) => new Unit(name, weapon_profiles));
const targetClass = new Target(save, target ? target.modifiers : []);
const results = unitList.reduce(
(acc, unit) => {
acc[unit.name] = unit.runSimulations(targetClass, numSimulations, includeOutcomes);
acc[unit.name] = unit.runSimulations(targetClass, numSimulations);
return acc;
},
{ save: save ? save.toString() : 'None' },
Expand All @@ -99,7 +99,7 @@ export const simulateUnitsForSave = ({
};
};

export const simulateUnits = ({ units, target, numSimulations = 1000, includeOutcomes = false }) => {
export const simulateUnits = ({ units, target, numSimulations = 1000 }) => {
const unitList = units.map(({ name, weapon_profiles }) => new Unit(name, weapon_profiles));
const data = SAVES.reduce(
(acc, save) => {
Expand All @@ -108,7 +108,6 @@ export const simulateUnits = ({ units, target, numSimulations = 1000, includeOut
save,
target,
numSimulations,
includeOutcomes,
});
return {
results: [...acc.results, saveData.results],
Expand Down
2 changes: 1 addition & 1 deletion api/models/dice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getRandomInt } from '../utils/StatsUtils';
import { getRandomInt } from '../utils/mathUtils';

/**
* A class used to represent a single dice (e.g: D3, D6)
Expand Down
23 changes: 19 additions & 4 deletions api/models/diceValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,43 @@ class DiceValue {
* summed with the additions)
* */
get average(): number {
const averageAditions = this.additions.reduce<number>(
const averageAdditions = this.additions.reduce<number>(
(acc, item) => (item instanceof Dice ? acc + item.average : acc + Number(item)),
0,
);
const averageSubtractions = this.subtractions.reduce<number>(
(acc, item) => (item instanceof Dice ? acc + item.average : acc + Number(item)),
0,
);
return averageAditions - averageSubtractions;
return averageAdditions - averageSubtractions;
}

/**
* Get the maximum of this dice value
*/
get max(): number {
const maxAdditions = this.additions.reduce<number>(
(acc, item) => (item instanceof Dice ? acc + item.sides : acc + Number(item)),
0,
);
const minSubtractions = this.subtractions.reduce<number>(
(acc, item) => (item instanceof Dice ? acc + 1 : acc + Number(item)),
0,
);
return maxAdditions - minSubtractions;
}

/** Roll the this dice value (combination of dice rolls summed with additions) */
roll(): number {
const rolledAditions = this.additions.reduce<number>(
const rolledAdditions = this.additions.reduce<number>(
(acc, item) => (item instanceof Dice ? acc + item.roll() : acc + Number(item)),
0,
);
const rolledSubtractions = this.subtractions.reduce<number>(
(acc, item) => (item instanceof Dice ? acc + item.roll() : acc + Number(item)),
0,
);
return rolledAditions - rolledSubtractions;
return rolledAdditions - rolledSubtractions;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion api/models/modifiers/BaseModifier.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Characteristic, getCharacteristic } from '../../constants';
import { choiceOption } from '../../utils/ModifierOptions';
import { choiceOption } from '../../utils/modifierUtils';

export default class BaseModifier {
['constructor']: typeof BaseModifier;
Expand Down
2 changes: 1 addition & 1 deletion api/models/modifiers/Bonus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Characteristic as C } from '../../constants';
import { numberOption } from '../../utils/ModifierOptions';
import { numberOption } from '../../utils/modifierUtils';
import DiceValue from '../diceValue';
import WeaponProfile from '../weaponProfile';
import BaseModifier from './BaseModifier';
Expand Down
2 changes: 1 addition & 1 deletion api/models/modifiers/ConditionalBonus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Characteristic as C, getCharacteristic } from '../../constants';
import { booleanOption, choiceOption, numberOption, rollOption } from '../../utils/ModifierOptions';
import { booleanOption, choiceOption, numberOption, rollOption } from '../../utils/modifierUtils';
import { D6 } from '../dice';
import DiceValue from '../diceValue';
import WeaponProfile from '../weaponProfile';
Expand Down
2 changes: 1 addition & 1 deletion api/models/modifiers/Exploding.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Characteristic as C } from '../../constants';
import { booleanOption, numberOption, rollOption } from '../../utils/ModifierOptions';
import { booleanOption, numberOption, rollOption } from '../../utils/modifierUtils';
import { D6 } from '../dice';
import DiceValue from '../diceValue';
import WeaponProfile from '../weaponProfile';
Expand Down
2 changes: 1 addition & 1 deletion api/models/modifiers/LeaderBonus.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Characteristic as C } from '../../constants';
import { numberOption } from '../../utils/ModifierOptions';
import { numberOption } from '../../utils/modifierUtils';
import DiceValue from '../diceValue';
import WeaponProfile from '../weaponProfile';
import BaseModifier from './BaseModifier';
Expand Down
4 changes: 2 additions & 2 deletions api/models/modifiers/MortalWounds.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Characteristic as C } from '../../constants';
import { booleanOption, numberOption, rollOption } from '../../utils/ModifierOptions';
import { booleanOption, numberOption, rollOption } from '../../utils/modifierUtils';
import { D6 } from '../dice';
import DiceValue from '../diceValue';
import WeaponProfile from '../weaponProfile';
Expand All @@ -13,7 +13,7 @@ export default class MortalWounds extends BaseModifier {
unmodified: boolean;
inAddition: boolean;

constructor({ characteristic, on = 6, mortalWounds = 1, unmodified = true, inAddition = false }) {
constructor({ characteristic, on = 6, mortalWounds, unmodified = true, inAddition = false }) {
super({ characteristic });
this.on = Number(on);
this.mortalWounds = DiceValue.parse(mortalWounds);
Expand Down
2 changes: 1 addition & 1 deletion api/models/targetModifiers/TargetFeelNoPain.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { rollOption } from '../../utils/ModifierOptions';
import { rollOption } from '../../utils/modifierUtils';
import { D6 } from '../dice';
import Target from '../target';
import WeaponProfile from '../weaponProfile';
Expand Down
2 changes: 1 addition & 1 deletion api/models/targetModifiers/TargetMortalNegate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { rollOption } from '../../utils/ModifierOptions';
import { rollOption } from '../../utils/modifierUtils';
import { D6 } from '../dice';
import Target from '../target';
import WeaponProfile from '../weaponProfile';
Expand Down
59 changes: 40 additions & 19 deletions api/models/unit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Average from '../processors/average';
import Simulation from '../processors/simulation';
import { getMetrics } from '../utils/StatsUtils';
import AverageDamageProcessor from '../processors/averageDamageProcessor';
import MaxDamageProcessor from '../processors/maxDamageProcessor';
import SimulationProcessor from '../processors/simulationProcessor';
import { range } from '../utils/mathUtils';
import Target from './target';
import WeaponProfile from './weaponProfile';

Expand Down Expand Up @@ -37,39 +38,59 @@ class Unit {
*/
averageDamage(target: Target): number {
return this.weaponProfiles.reduce(
(acc, profile) => acc + new Average(profile, target).getAverageDamage(),
(acc, profile) => acc + new AverageDamageProcessor(profile, target).getAverageDamage(),
0,
);
}

runSimulations(target: Target, numSimulations = 1000, includeOutcomes = true) {
const results = [...Array(numSimulations)].map(() =>
this.weaponProfiles.reduce((acc, profile) => {
const sim = new Simulation(profile, target);
const simResult = sim.simulate();
return acc + simResult;
}, 0),
maxDamage(): number {
return this.weaponProfiles.reduce(
(acc, profile) => acc + new MaxDamageProcessor(profile).getMaxDamage(),
0,
);
}

const counts = results.reduce<{ [damage: number]: any }>((acc, n) => {
acc[n] = acc[n] ? acc[n] + 1 : 1;
return acc;
}, {});
runSimulations(target: Target, numSimulations = 1000) {
const max = this.maxDamage();
const mean = this.averageDamage(target);

let variance = 0;
const counts: { [damage: number]: number } = {};
[...Array(numSimulations)].forEach(() => {
const result = this.weaponProfiles.reduce<number>((acc, profile) => {
const sim = new SimulationProcessor(profile, target);
const simResult = sim.simulate();
return acc + simResult;
}, 0);
variance += (result - mean) ** 2;
counts[result] = counts[result] ? counts[result] + 1 : 1;
});
variance /= numSimulations;

const buckets = Object.keys(counts)
.sort((x, y) => Number(x) - Number(y))
.map(Number)
.sort((x, y) => x - y)
.map(damage => ({
damage,
count: counts[damage],
probability: parseFloat(((counts[damage] * 100) / numSimulations).toFixed(2)),
}));

const data = includeOutcomes ? { results } : {};
const sampleMax = Math.max(...Object.keys(counts).map(Number));
const step = Math.max(Math.floor(((max - sampleMax) / max) * 10), 1);
[...range(sampleMax + 1, max, step)].forEach(i => {
buckets.push({ damage: i, count: 0, probability: 0 });
});
buckets.push({ damage: max, count: 0, probability: 0 });

return {
...data,
buckets,
metrics: getMetrics(results),
metrics: {
max,
mean,
variance,
standardDeviation: Math.sqrt(variance),
},
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MODIFIERS as m } from '../models/modifiers';
import Target from '../models/target';
import WeaponProfile from '../models/weaponProfile';

export default class Average {
export default class AverageDamageProcessor {
profile: WeaponProfile;
target: Target;

Expand All @@ -30,7 +30,7 @@ export default class Average {
leaderModifiers.map(mod => mod.getAsBonusModifier()),
);
leaderProfile.numModels = numLeaders;
const leaderProcessor = new Average(leaderProfile, this.target);
const leaderProcessor = new AverageDamageProcessor(leaderProfile, this.target);
leaderDamage += leaderProcessor.getAverageDamage();
}
}
Expand Down Expand Up @@ -59,7 +59,7 @@ export default class Average {
if (cbModifier) {
const newProfile = this.profile.getSplitProfile([cbModifier], [cbModifier.getAsBonusModifier()]);
const cbModHits = attacks * cbModifier.resolve(this.profile);
const splitProcessor = new Average(newProfile, this.target);
const splitProcessor = new AverageDamageProcessor(newProfile, this.target);
splitDamage = splitProcessor.resolveWounds(cbModHits);
hits -= cbModHits;
}
Expand Down Expand Up @@ -87,7 +87,7 @@ export default class Average {
if (cbModifier) {
const newProfile = this.profile.getSplitProfile([cbModifier], [cbModifier.getAsBonusModifier()]);
const cbModWounds = hits * cbModifier.resolve(this.profile);
const splitProcessor = new Average(newProfile, this.target);
const splitProcessor = new AverageDamageProcessor(newProfile, this.target);
splitDamage = splitProcessor.resolveSaves(cbModWounds);
wounds -= cbModWounds;
}
Expand Down

0 comments on commit 28aeb9c

Please sign in to comment.