thepaperpilot
83d41428eb
- Removed `jsx()` and `JSXFunction`. You can now use `JSX.Element` like any other `Computable` value - `joinJSX` now always requires a joiner. Just pass the array of elements or wrap them in `<>` and `</>` if there's no joiner - Removed `coerceComponent`, `computeComponent`, and `computeOptionalComponent`; just use the `render` function now - It's recommended to now do `<MyComponent />` instead of `<component :is="myComponent" />` - All features no longer take the options as a type parameter, and all generic forms have been removed as a result - Fixed `forceHideGoBack` not being respected - Removed `deepUnref` as now things don't get unreffed before being passed into vue components by default - Moved MarkNode to new wrapper, and removed existing `mark` properties - Moved Tooltip to new wrapper, and made it take an options function instead of raw object - VueFeature component now wraps all vue features, and applies styling, classes, and visibility in the wrapping div. It also adds the Node component so features don't need to - `mergeAdjacent` now works with grids (perhaps should've used scss to reduce the amount of css this took) - `CoercableComponent` renamed to `Renderable` since it should be used with `render` - Replaced `isCoercableComponent` with `isJSXElement` - Replaced `Computable` and `ProcessedComputable` with the vue built-ins `MaybeRefOrGetter` and `MaybeRef` - `convertComputable` renamed to `processGetter` - Also removed `GetComputableTypeWithDefault` and `GetComputableType`, which can similarly be replaced - `dontMerge` is now a property on rows and columns rather than an undocumented css class you'd have to include on every feature within the row or column - Fixed saves manager not being imported in addiction warning component - Created `vueFeatureMixin` for simplifying the vue specific parts of a feature. Passes the component's properties in explicitly and directly from the feature itself - All features should now return an object that includes props typed to omit the options object and satisfies the feature. This will ensure type correctness and pass-through custom properties. (see existing features for more thorough examples of changes) - Replaced decorators with mixins, which won't require casting. Bonus amount decorators converted into generic bonus amount mixin. Removed effect decorator - All `render` functions now return `JSX.Element`. The `JSX` variants (e.g. `renderJSX`) (except `joinJSX`) have been removed - Moved all features that use the clickable component into the clickable folder - Removed `small` property from clickable, since its a single css rule (`min-height: unset`) (you could add a small css class and pass small to any vue feature's classes property, though) - Upgrades now use the clickable component - Added ConversionType symbol - Removed setDefault, just use `??=` - Added isType function that uses a type symbol to check - General cleanup
1291 lines
53 KiB
TypeScript
1291 lines
53 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
import { createResource, Resource } from "features/resources/resource";
|
|
import Formula, {
|
|
calculateCost,
|
|
calculateMaxAffordable,
|
|
unrefFormulaSource
|
|
} from "game/formulas/formulas";
|
|
import type { GenericFormula, IntegrableFormula, InvertibleFormula } from "game/formulas/types";
|
|
import Decimal, { DecimalSource } from "util/bignum";
|
|
import { beforeAll, describe, expect, test } from "vitest";
|
|
import { ref } from "vue";
|
|
import "../utils";
|
|
import { InvertibleIntegralFormula } from "game/formulas/types";
|
|
|
|
type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
|
|
|
|
const testValues = [-2, "0", new Decimal(10.5)] as const;
|
|
|
|
const invertibleZeroParamFunctionNames = [
|
|
"round",
|
|
"floor",
|
|
"ceil",
|
|
"trunc",
|
|
"neg",
|
|
"recip",
|
|
"log10",
|
|
"log2",
|
|
"ln",
|
|
"pow10",
|
|
"exp",
|
|
"sqr",
|
|
"sqrt",
|
|
"cube",
|
|
"cbrt",
|
|
"lambertw",
|
|
"ssqrt",
|
|
"sin",
|
|
"cos",
|
|
"tan",
|
|
"asin",
|
|
"acos",
|
|
"atan",
|
|
"sinh",
|
|
"cosh",
|
|
"tanh",
|
|
"asinh",
|
|
"acosh",
|
|
"atanh",
|
|
"slog",
|
|
"tetrate",
|
|
"iteratedexp"
|
|
] as const;
|
|
const nonInvertibleZeroParamFunctionNames = [
|
|
"abs",
|
|
"sign",
|
|
"pLog10",
|
|
"absLog10",
|
|
"factorial",
|
|
"gamma",
|
|
"lngamma"
|
|
] as const;
|
|
const integrableZeroParamFunctionNames = [
|
|
"neg",
|
|
"recip",
|
|
"log10",
|
|
"log2",
|
|
"ln",
|
|
"pow10",
|
|
"exp",
|
|
"sqr",
|
|
"sqrt",
|
|
"cube",
|
|
"cbrt",
|
|
"sin",
|
|
"cos",
|
|
"tan",
|
|
"asin",
|
|
"acos",
|
|
"atan",
|
|
"sinh",
|
|
"cosh",
|
|
"tanh",
|
|
"asinh",
|
|
"acosh",
|
|
"atanh"
|
|
] as const;
|
|
const nonIntegrableZeroParamFunctionNames = [
|
|
...nonInvertibleZeroParamFunctionNames,
|
|
"round",
|
|
"floor",
|
|
"ceil",
|
|
"trunc",
|
|
"lambertw",
|
|
"ssqrt"
|
|
] as const;
|
|
const invertibleIntegralZeroPramFunctionNames = [
|
|
"recip",
|
|
"log10",
|
|
"log2",
|
|
"ln",
|
|
"pow10",
|
|
"sqr",
|
|
"sqrt",
|
|
"cube",
|
|
"cbrt",
|
|
"neg",
|
|
"exp",
|
|
"sin",
|
|
"cos",
|
|
"tan",
|
|
"sinh",
|
|
"cosh",
|
|
"tanh"
|
|
] as const;
|
|
const nonInvertibleIntegralZeroPramFunctionNames = [
|
|
...nonIntegrableZeroParamFunctionNames,
|
|
"asin",
|
|
"acos",
|
|
"atan",
|
|
"asinh",
|
|
"acosh",
|
|
"atanh"
|
|
] as const;
|
|
|
|
const invertibleOneParamFunctionNames = [
|
|
"add",
|
|
"sub",
|
|
"mul",
|
|
"div",
|
|
"log",
|
|
"pow",
|
|
"root",
|
|
"layeradd"
|
|
] as const;
|
|
const nonInvertibleOneParamFunctionNames = ["layeradd10"] as const;
|
|
const integrableOneParamFunctionNames = ["add", "sub", "mul", "div", "log", "pow", "root"] as const;
|
|
const nonIntegrableOneParamFunctionNames = [...nonInvertibleOneParamFunctionNames, "slog"] as const;
|
|
const invertibleIntegralOneParamFunctionNames = integrableOneParamFunctionNames;
|
|
const nonInvertibleIntegralOneParamFunctionNames = nonIntegrableOneParamFunctionNames;
|
|
|
|
const nonInvertibleTwoParamFunctionNames = ["iteratedlog", "pentate"] as const;
|
|
const nonIntegrableTwoParamFunctionNames = nonInvertibleTwoParamFunctionNames;
|
|
const nonInvertibleIntegralTwoParamFunctionNames = nonIntegrableTwoParamFunctionNames;
|
|
|
|
describe("Formula Equality Checking", () => {
|
|
describe("Equality Checks", () => {
|
|
test("Equals", () => expect(Formula.add(1, 1).equals(Formula.add(1, 1))).toBe(true));
|
|
test("Not Equals due to inputs", () =>
|
|
expect(Formula.add(1, 1).equals(Formula.add(1, 0))).toBe(false));
|
|
test("Not Equals due to functions", () =>
|
|
expect(Formula.add(1, 1).equals(Formula.sub(1, 1))).toBe(false));
|
|
test("Not Equals due to hasVariable", () =>
|
|
expect(Formula.constant(1).equals(Formula.variable(1))).toBe(false));
|
|
});
|
|
|
|
describe("Formula aliases", () => {
|
|
function testAliases<T extends FormulaFunctions>(
|
|
aliases: T[],
|
|
args: Parameters<(typeof Formula)[T]>
|
|
) {
|
|
describe(aliases[0], () => {
|
|
let formula: GenericFormula;
|
|
beforeAll(() => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
formula = Formula[aliases[0]](...args);
|
|
});
|
|
|
|
aliases.slice(1).forEach(alias => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
test(alias, () => expect(Formula[alias](...args).equals(formula)).toBe(true));
|
|
});
|
|
});
|
|
}
|
|
testAliases(["neg", "negate", "negated"], [1]);
|
|
testAliases(["recip", "reciprocal", "reciprocate"], [1]);
|
|
testAliases(["sign", "sgn"], [1]);
|
|
testAliases(["add", "plus"], [1, 1]);
|
|
testAliases(["sub", "subtract", "minus"], [1, 1]);
|
|
testAliases(["mul", "multiply", "times"], [1, 1]);
|
|
testAliases(["div", "divide"], [1, 1]);
|
|
testAliases(["log", "logarithm"], [1, 1]);
|
|
});
|
|
|
|
describe("Instance vs Static methods", () => {
|
|
let formula: GenericFormula;
|
|
beforeAll(() => {
|
|
formula = Formula.constant(10);
|
|
});
|
|
[...invertibleZeroParamFunctionNames, ...nonInvertibleZeroParamFunctionNames].forEach(
|
|
name => {
|
|
test(name, () => {
|
|
const instanceFormula = formula[name]();
|
|
const staticFormula = Formula[name](formula);
|
|
expect(instanceFormula.equals(staticFormula)).toBe(true);
|
|
});
|
|
}
|
|
);
|
|
|
|
[...invertibleOneParamFunctionNames, ...nonInvertibleOneParamFunctionNames].forEach(
|
|
name => {
|
|
test(name, () => {
|
|
const instanceFormula = formula[name](10);
|
|
const staticFormula = Formula[name](formula, 10);
|
|
expect(instanceFormula.equals(staticFormula)).toBe(true);
|
|
});
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Creating Formulas", () => {
|
|
describe("Constants", () => {
|
|
function testConstant(
|
|
desc: string,
|
|
formulaFunc: () => InvertibleFormula,
|
|
expectedValue: DecimalSource = 10
|
|
) {
|
|
describe(desc, () => {
|
|
let formula: GenericFormula;
|
|
beforeAll(() => {
|
|
formula = formulaFunc();
|
|
});
|
|
test("Is not invertible", () => expect(formula.isInvertible()).toBe(false));
|
|
test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false));
|
|
test("Integral is not invertible", () =>
|
|
expect(formula.isIntegralInvertible()).toBe(false));
|
|
test("Is not marked as having a variable", () =>
|
|
expect(formula.hasVariable()).toBe(false));
|
|
test("Evaluates correctly", () =>
|
|
expect(formula.evaluate()).compare_tolerance(expectedValue));
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
/* @ts-ignore */
|
|
test("Invert errors", () => expect(() => formula.invert(25)).toLogError());
|
|
test("Integrate errors", () =>
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
/* @ts-ignore */
|
|
expect(() => formula.evaluateIntegral()).toLogError());
|
|
test("Invert integral errors", () =>
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
/* @ts-ignore */
|
|
expect(() => formula.invertIntegral(25)).toLogError());
|
|
});
|
|
}
|
|
testConstant("number", () => Formula.constant(10));
|
|
testConstant("string", () => Formula.constant("10"));
|
|
testConstant("decimal", () => Formula.constant(new Decimal("1e400")), "1e400");
|
|
testConstant("ref", () => Formula.constant(ref(10)));
|
|
});
|
|
|
|
function checkFormula<T extends FormulaFunctions>(
|
|
functionName: T,
|
|
args: Readonly<Parameters<(typeof Formula)[T]>>
|
|
) {
|
|
let formula: GenericFormula;
|
|
beforeAll(() => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
formula = Formula[functionName](...args);
|
|
});
|
|
// None of these formulas have variables, so they should all behave the same
|
|
test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false));
|
|
test("Is not invertible", () => expect(formula.isInvertible()).toBe(false));
|
|
test(`Formula errors if trying to invert`, () =>
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
/* @ts-ignore */
|
|
expect(() => formula.invert(10)).toLogError());
|
|
test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false));
|
|
test("Has a non-invertible integral", () =>
|
|
expect(formula.isIntegralInvertible()).toBe(false));
|
|
}
|
|
|
|
// Utility function that will test all the different
|
|
// It's a lot of tests, but I'd rather be exhaustive
|
|
function testFormulaCall<T extends FormulaFunctions>(
|
|
functionName: T,
|
|
args: Readonly<Parameters<(typeof Formula)[T]>>
|
|
) {
|
|
if ((functionName === "slog" || functionName === "layeradd") && args[0] === -1) {
|
|
// These cases in particular take a long time, so skip them
|
|
// We still have plenty of coverage
|
|
return;
|
|
}
|
|
let testName = functionName + "(";
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (i !== 0) {
|
|
testName += ", ";
|
|
}
|
|
testName += args[i];
|
|
}
|
|
testName += ") evaluates correctly";
|
|
test(testName, () => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
const formula = Formula[functionName](...args);
|
|
|
|
try {
|
|
const expectedEvaluation = Decimal[functionName](
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
...args.map(i => unrefFormulaSource(i))
|
|
);
|
|
if (expectedEvaluation != null) {
|
|
expect(formula.evaluate()).compare_tolerance(expectedEvaluation);
|
|
}
|
|
} catch {
|
|
// If this is an invalid Decimal operation, then ignore this test case
|
|
}
|
|
});
|
|
}
|
|
|
|
describe("0-param", () => {
|
|
[...invertibleZeroParamFunctionNames, ...nonInvertibleZeroParamFunctionNames].forEach(
|
|
names =>
|
|
describe(names, () => {
|
|
checkFormula(names, [0] as const);
|
|
testValues.forEach(i => testFormulaCall(names, [i] as const));
|
|
})
|
|
);
|
|
});
|
|
describe("1-param", () => {
|
|
(
|
|
[
|
|
...invertibleOneParamFunctionNames,
|
|
...nonInvertibleOneParamFunctionNames,
|
|
"max",
|
|
"min",
|
|
"maxabs",
|
|
"minabs",
|
|
"clampMin",
|
|
"clampMax"
|
|
] as const
|
|
).forEach(names =>
|
|
describe(names, () => {
|
|
checkFormula(names, [0, 0] as const);
|
|
testValues.forEach(i =>
|
|
testValues.forEach(j => testFormulaCall(names, [i, j] as const))
|
|
);
|
|
})
|
|
);
|
|
});
|
|
describe("2-param", () => {
|
|
([...nonInvertibleTwoParamFunctionNames, "clamp"] as const).forEach(names =>
|
|
describe(names, () => {
|
|
checkFormula(names, [0, 0, 0] as const);
|
|
testValues.forEach(i =>
|
|
testValues.forEach(j =>
|
|
testValues.forEach(k => testFormulaCall(names, [i, j, k] as const))
|
|
)
|
|
);
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Variables", () => {
|
|
let variable: GenericFormula;
|
|
let constant: GenericFormula;
|
|
beforeAll(() => {
|
|
variable = Formula.variable(10);
|
|
constant = Formula.constant(10);
|
|
});
|
|
|
|
test("Created variable is marked as a variable", () =>
|
|
expect(variable.hasVariable()).toBe(true));
|
|
test("evaluate() returns variable's value", () =>
|
|
expect(variable.evaluate()).compare_tolerance(10));
|
|
test("evaluate(variable) overrides variable value", () =>
|
|
expect(variable.add(10).evaluate(20)).compare_tolerance(30));
|
|
|
|
test("Nested variable is marked as having a variable", () =>
|
|
expect(variable.add(10).div(3).pow(2).hasVariable()).toBe(true));
|
|
test("Nested non-variable is marked as not having a variable", () =>
|
|
expect(constant.add(10).div(3).pow(2).hasVariable()).toBe(false));
|
|
});
|
|
|
|
describe("Inverting", () => {
|
|
let variable: IntegrableFormula;
|
|
let constant: GenericFormula;
|
|
beforeAll(() => {
|
|
variable = Formula.variable(10);
|
|
constant = Formula.constant(10);
|
|
});
|
|
|
|
test("variable.invert() is pass-through", () =>
|
|
expect(variable.invert(100)).compare_tolerance(100));
|
|
|
|
describe("Invertible Formulas correctly calculate when they contain a variable", () => {
|
|
function checkFormula(formula: GenericFormula, expectedBool = true) {
|
|
expect(formula.isInvertible()).toBe(expectedBool);
|
|
expect(formula.hasVariable()).toBe(expectedBool);
|
|
}
|
|
invertibleZeroParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var) is marked as invertible and having a variable`, () =>
|
|
checkFormula(Formula[name](variable)));
|
|
});
|
|
});
|
|
invertibleOneParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var, const) is marked as invertible and having a variable`, () =>
|
|
checkFormula(Formula[name](variable, constant)));
|
|
test(`${name}(const, var) is marked as invertible and having a variable`, () =>
|
|
checkFormula(Formula[name](constant, variable)));
|
|
test(`${name}(var, var) is marked as not invertible and not having a variable`, () =>
|
|
checkFormula(Formula[name](variable, variable), false));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Non-invertible formulas marked as such", () => {
|
|
function checkFormula(formula: GenericFormula) {
|
|
expect(formula.isInvertible()).toBe(false);
|
|
expect(formula.isIntegrable()).toBe(false);
|
|
expect(formula.isIntegralInvertible()).toBe(false);
|
|
}
|
|
nonInvertibleZeroParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](variable)));
|
|
});
|
|
});
|
|
nonInvertibleOneParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var, const) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](variable, constant)));
|
|
test(`${name}(const, var) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](constant, variable)));
|
|
test(`${name}(var, var) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](variable, variable)));
|
|
});
|
|
});
|
|
nonInvertibleTwoParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var, const, const) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](variable, constant, constant)));
|
|
test(`${name}(const, var, const) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](constant, variable, constant)));
|
|
test(`${name}(const, const, var) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](constant, constant, variable)));
|
|
test(`${name}(var, var, const) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](variable, variable, constant)));
|
|
test(`${name}(var, const, var) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](variable, constant, variable)));
|
|
test(`${name}(const, var, var) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](constant, variable, variable)));
|
|
test(`${name}(var, var, var) is marked as not invertible`, () =>
|
|
checkFormula(Formula[name](variable, variable, variable)));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Inverting calculates the value of the variable", () => {
|
|
let variable: IntegrableFormula;
|
|
let constant: IntegrableFormula;
|
|
beforeAll(() => {
|
|
variable = Formula.variable(2);
|
|
constant = Formula.constant(3);
|
|
});
|
|
invertibleOneParamFunctionNames.forEach(name =>
|
|
describe(name, () => {
|
|
test(`${name}(var, const).invert()`, () => {
|
|
const formula = Formula[name](variable, constant);
|
|
const result = formula.evaluate();
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
expect(formula.invert!(result)).compare_tolerance(2);
|
|
});
|
|
if (name !== "layeradd") {
|
|
test(`${name}(const, var).invert()`, () => {
|
|
const formula = Formula[name](constant, variable);
|
|
const result = formula.evaluate();
|
|
expect(formula.invert(result)).compare_tolerance(2);
|
|
});
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
describe("Inverting pass-throughs", () => {
|
|
test("max", () => expect(Formula.max(variable, constant).invert(10)).compare_tolerance(10));
|
|
test("min", () => expect(Formula.min(variable, constant).invert(10)).compare_tolerance(10));
|
|
test("minabs", () =>
|
|
expect(Formula.minabs(variable, constant).invert(10)).compare_tolerance(10));
|
|
test("maxabs", () =>
|
|
expect(Formula.maxabs(variable, constant).invert(10)).compare_tolerance(10));
|
|
test("clampMax", () =>
|
|
expect(Formula.clampMax(variable, constant).invert(10)).compare_tolerance(10));
|
|
test("clampMin", () =>
|
|
expect(Formula.clampMin(variable, constant).invert(10)).compare_tolerance(10));
|
|
test("clamp", () =>
|
|
expect(Formula.clamp(variable, constant, constant).invert(10)).compare_tolerance(10));
|
|
});
|
|
|
|
test("Inverting nested formulas", () => {
|
|
const formula = Formula.add(variable, constant).times(constant).floor();
|
|
expect(formula.invert(100)).compare_tolerance(0);
|
|
});
|
|
|
|
describe("Inverting with non-invertible sections", () => {
|
|
test("Non-invertible constant", () => {
|
|
const formula = Formula.add(variable, constant.sign());
|
|
expect(formula.isInvertible()).toBe(true);
|
|
expect(() => formula.invert(10)).not.toLogError();
|
|
});
|
|
test("Non-invertible variable", () => {
|
|
const formula = Formula.add(variable.sign(), constant);
|
|
expect(formula.isInvertible()).toBe(false);
|
|
expect(() => formula.invert(10)).toLogError();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Integrating", () => {
|
|
let variable: IntegrableFormula;
|
|
let constant: IntegrableFormula;
|
|
beforeAll(() => {
|
|
variable = Formula.variable(ref(10));
|
|
constant = Formula.constant(10);
|
|
});
|
|
|
|
test("variable.evaluateIntegral() calculates correctly", () =>
|
|
expect(variable.evaluateIntegral()).compare_tolerance(Decimal.pow(10, 2).div(2)));
|
|
test("variable.evaluateIntegral(variable) overrides variable value", () =>
|
|
expect(variable.evaluateIntegral(20)).compare_tolerance(Decimal.pow(20, 2).div(2)));
|
|
|
|
describe("Integrable functions marked as such", () => {
|
|
function checkFormula(formula: IntegrableFormula) {
|
|
expect(formula.isIntegrable()).toBe(true);
|
|
expect(() => formula.evaluateIntegral()).to.not.throw();
|
|
}
|
|
integrableZeroParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var) is marked as integrable`, () =>
|
|
checkFormula(Formula[name](variable)));
|
|
});
|
|
});
|
|
integrableOneParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var, const) is marked as integrable`, () =>
|
|
checkFormula(Formula[name](variable, constant)));
|
|
if (name !== "log" && name !== "root") {
|
|
test(`${name}(const, var) is marked as integrable`, () =>
|
|
checkFormula(Formula[name](constant, variable)));
|
|
}
|
|
test(`${name}(var, var) is marked as not integrable`, () =>
|
|
expect(Formula[name](variable, variable).isIntegrable()).toBe(false));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Non-Integrable functions marked as such", () => {
|
|
function checkFormula(formula: GenericFormula) {
|
|
expect(formula.isIntegrable()).toBe(false);
|
|
}
|
|
nonIntegrableZeroParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](variable)));
|
|
});
|
|
});
|
|
nonIntegrableOneParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var, const) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](variable, constant)));
|
|
test(`${name}(const, var) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](constant, variable)));
|
|
test(`${name}(var, var) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](variable, variable)));
|
|
});
|
|
});
|
|
nonIntegrableTwoParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var, const, const) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](variable, constant, constant)));
|
|
test(`${name}(const, var, const) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](constant, variable, constant)));
|
|
test(`${name}(const, const, var) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](constant, constant, variable)));
|
|
test(`${name}(var, var, const) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](variable, variable, constant)));
|
|
test(`${name}(var, const, var) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](variable, constant, variable)));
|
|
test(`${name}(const, var, var) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](constant, variable, variable)));
|
|
test(`${name}(var, var, var) is marked as not integrable`, () =>
|
|
checkFormula(Formula[name](variable, variable, variable)));
|
|
});
|
|
});
|
|
});
|
|
|
|
// TODO I think these tests will require writing at least one known example for every function
|
|
describe.todo("Integrable formulas integrate correctly");
|
|
|
|
test("Integrating nested formulas", () => {
|
|
const formula = Formula.add(variable, constant).times(constant).pow(2).times(30).add(10);
|
|
const actualCost = new Array(10)
|
|
.fill(null)
|
|
.reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
|
|
// Check if the calculated cost is within 10% of the actual cost,
|
|
// because this is an approximation
|
|
expect(
|
|
Decimal.sub(
|
|
actualCost,
|
|
Decimal.add(formula.evaluateIntegral(), formula.calculateConstantOfIntegration())
|
|
)
|
|
.abs()
|
|
.div(actualCost)
|
|
.toNumber()
|
|
).toBeLessThan(0.1);
|
|
});
|
|
|
|
test("Integrating nested formulas with overidden variable", () => {
|
|
const formula = Formula.add(variable, constant).times(constant).pow(2).times(30).add(10);
|
|
const actualCost = new Array(20)
|
|
.fill(null)
|
|
.reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
|
|
// Check if the calculated cost is within 10% of the actual cost,
|
|
// because this is an approximation
|
|
expect(
|
|
Decimal.sub(
|
|
actualCost,
|
|
Decimal.add(formula.evaluateIntegral(20), formula.calculateConstantOfIntegration())
|
|
)
|
|
.abs()
|
|
.div(actualCost)
|
|
.toNumber()
|
|
).toBeLessThan(0.1);
|
|
});
|
|
|
|
test("Integrating nested complex formulas", () => {
|
|
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
|
|
expect(() => formula.evaluateIntegral()).toLogError();
|
|
});
|
|
|
|
describe("Integrating with non-integrable sections", () => {
|
|
test("Non-integrable constant", () => {
|
|
const formula = Formula.add(variable, constant.ceil());
|
|
expect(formula.isIntegrable()).toBe(true);
|
|
expect(() => formula.evaluateIntegral()).not.toLogError();
|
|
});
|
|
test("Non-integrable variable", () => {
|
|
const formula = Formula.add(variable.ceil(), constant);
|
|
expect(formula.isIntegrable()).toBe(false);
|
|
expect(() => formula.evaluateIntegral()).toLogError();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Inverting integrals", () => {
|
|
let variable: InvertibleIntegralFormula;
|
|
let constant: InvertibleIntegralFormula;
|
|
beforeAll(() => {
|
|
variable = Formula.variable(10);
|
|
constant = Formula.constant(10);
|
|
});
|
|
|
|
test("variable.invertIntegral() calculates correctly", () =>
|
|
expect(variable.invertIntegral(20)).compare_tolerance(
|
|
Decimal.sqrt(20).times(Decimal.sqrt(2))
|
|
));
|
|
|
|
describe("Invertible Integral functions marked as such", () => {
|
|
function checkFormula(formula: InvertibleIntegralFormula) {
|
|
expect(formula.isIntegralInvertible()).toBe(true);
|
|
expect(() => formula.invertIntegral(10)).not.toLogError();
|
|
}
|
|
invertibleIntegralZeroPramFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var) is marked as having an invertible integral`, () =>
|
|
checkFormula(Formula[name](variable)));
|
|
});
|
|
});
|
|
invertibleIntegralOneParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var, const) is marked as having an invertible integral`, () =>
|
|
checkFormula(Formula[name](variable, constant)));
|
|
if (name !== "log" && name !== "root") {
|
|
test(`${name}(const, var) is marked as having an invertible integral`, () =>
|
|
checkFormula(Formula[name](constant, variable)));
|
|
}
|
|
test(`${name}(var, var) is marked as not having an invertible integral`, () => {
|
|
const formula = Formula[name](variable, variable);
|
|
expect(formula.isIntegralInvertible()).toBe(false);
|
|
expect(() => formula.invertIntegral(10)).toLogError();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Non-Invertible integral functions marked as such", () => {
|
|
function checkFormula(formula: GenericFormula) {
|
|
expect(formula.isIntegralInvertible()).toBe(false);
|
|
}
|
|
nonInvertibleIntegralZeroPramFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](variable)));
|
|
});
|
|
});
|
|
nonInvertibleIntegralOneParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var, const) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](variable, constant)));
|
|
test(`${name}(const, var) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](constant, variable)));
|
|
test(`${name}(var, var) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](variable, variable)));
|
|
});
|
|
});
|
|
nonInvertibleIntegralTwoParamFunctionNames.forEach(name => {
|
|
describe(name, () => {
|
|
test(`${name}(var, const, const) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](variable, constant, constant)));
|
|
test(`${name}(const, var, const) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](constant, variable, constant)));
|
|
test(`${name}(const, const, var) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](constant, constant, variable)));
|
|
test(`${name}(var, var, const) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](variable, variable, constant)));
|
|
test(`${name}(var, const, var) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](variable, constant, variable)));
|
|
test(`${name}(const, var, var) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](constant, variable, variable)));
|
|
test(`${name}(var, var, var) is marked as not having an invertible integral`, () =>
|
|
checkFormula(Formula[name](variable, variable, variable)));
|
|
});
|
|
});
|
|
});
|
|
|
|
// TODO I think these tests will require writing at least one known example for every function
|
|
describe.todo("Invertible Integral formulas invert correctly");
|
|
|
|
test("Inverting integral of nested formulas", () => {
|
|
const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
|
|
expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10);
|
|
});
|
|
|
|
test("Inverting integral of nested complex formulas", () => {
|
|
const formula = Formula.pow(1.05, variable).times(100).pow(0.5);
|
|
expect(() => formula.invertIntegral(100)).toLogError();
|
|
});
|
|
});
|
|
|
|
describe("Step-wise", () => {
|
|
let variable: GenericFormula;
|
|
let constant: GenericFormula;
|
|
beforeAll(() => {
|
|
variable = Formula.variable(10);
|
|
constant = Formula.constant(10);
|
|
});
|
|
|
|
test("Formula without variable is marked as such", () => {
|
|
expect(Formula.step(constant, 10, value => Formula.sqrt(value)).isInvertible()).toBe(false);
|
|
expect(Formula.step(constant, 10, value => Formula.sqrt(value)).hasVariable()).toBe(false);
|
|
});
|
|
|
|
test("Formula with variable is marked as such", () => {
|
|
expect(Formula.step(variable, 10, value => Formula.sqrt(value)).isInvertible()).toBe(true);
|
|
expect(Formula.step(variable, 10, value => Formula.sqrt(value)).hasVariable()).toBe(true);
|
|
});
|
|
|
|
test("Non-invertible formula modifier marks formula as such", () => {
|
|
expect(Formula.step(constant, 10, value => Formula.abs(value)).isInvertible()).toBe(false);
|
|
expect(Formula.step(constant, 10, value => Formula.abs(value)).hasVariable()).toBe(false);
|
|
});
|
|
|
|
test("Formula never marked integrable", () => {
|
|
expect(Formula.step(constant, 10, value => Formula.add(value, 10)).isIntegrable()).toBe(
|
|
false
|
|
);
|
|
expect(() =>
|
|
Formula.step(constant, 10, value => Formula.add(value, 10)).evaluateIntegral()
|
|
).toLogError();
|
|
});
|
|
|
|
test("Formula never marked as having an invertible integral", () => {
|
|
expect(
|
|
Formula.step(constant, 10, value => Formula.add(value, 10)).isIntegralInvertible()
|
|
).toBe(false);
|
|
expect(() =>
|
|
Formula.step(constant, 10, value => Formula.add(value, 10)).invertIntegral(10)
|
|
).toLogError();
|
|
});
|
|
|
|
test("Formula modifiers with variables mark formula as non-invertible", () => {
|
|
expect(
|
|
Formula.step(constant, 10, value => Formula.add(value, variable)).isInvertible()
|
|
).toBe(false);
|
|
expect(
|
|
Formula.step(constant, 10, value => Formula.add(value, variable)).hasVariable()
|
|
).toBe(false);
|
|
});
|
|
|
|
describe("Pass-through underneath start", () => {
|
|
test("Evaluates correctly", () =>
|
|
expect(
|
|
Formula.step(constant, 20, value => Formula.sqrt(value)).evaluate()
|
|
).compare_tolerance(10));
|
|
test("Inverts correctly with variable in input", () =>
|
|
expect(
|
|
Formula.step(variable, 20, value => Formula.sqrt(value)).invert(10)
|
|
).compare_tolerance(10));
|
|
});
|
|
|
|
describe("Pass-through at boundary", () => {
|
|
test("Evaluates correctly", () =>
|
|
expect(
|
|
Formula.step(constant, 10, value => Formula.sqrt(value)).evaluate()
|
|
).compare_tolerance(10));
|
|
test("Inverts correctly with variable in input", () =>
|
|
expect(
|
|
Formula.step(variable, 10, value => Formula.sqrt(value)).invert(10)
|
|
).compare_tolerance(10));
|
|
});
|
|
|
|
describe("Evaluates correctly beyond start", () => {
|
|
test("Evaluates correctly", () =>
|
|
expect(
|
|
Formula.step(variable, 8, value => Formula.add(value, 2)).evaluate()
|
|
).compare_tolerance(12));
|
|
test("Inverts correctly", () =>
|
|
expect(
|
|
Formula.step(variable, 8, value => Formula.add(value, 2)).invert(12)
|
|
).compare_tolerance(10));
|
|
});
|
|
|
|
describe("Evaluates correctly when nested", () => {
|
|
test("Evaluates correctly", () =>
|
|
expect(
|
|
Formula.add(variable, constant)
|
|
.step(10, value => Formula.mul(value, 2))
|
|
.sub(10)
|
|
.evaluate()
|
|
).compare_tolerance(20));
|
|
test("Inverts correctly", () =>
|
|
expect(
|
|
Formula.add(variable, constant)
|
|
.step(10, value => Formula.mul(value, 2))
|
|
.sub(10)
|
|
.invert(30)
|
|
).compare_tolerance(15));
|
|
});
|
|
});
|
|
|
|
describe("Conditionals", () => {
|
|
let variable: GenericFormula;
|
|
let constant: GenericFormula;
|
|
beforeAll(() => {
|
|
variable = Formula.variable(10);
|
|
constant = Formula.constant(10);
|
|
});
|
|
|
|
test("Formula without variable is marked as such", () => {
|
|
expect(Formula.if(constant, true, value => Formula.sqrt(value)).isInvertible()).toBe(false);
|
|
expect(Formula.if(constant, true, value => Formula.sqrt(value)).hasVariable()).toBe(false);
|
|
});
|
|
|
|
test("Formula with variable is marked as such", () => {
|
|
expect(Formula.if(variable, true, value => Formula.sqrt(value)).isInvertible()).toBe(true);
|
|
expect(Formula.if(variable, true, value => Formula.sqrt(value)).hasVariable()).toBe(true);
|
|
});
|
|
|
|
test("Non-invertible formula modifier marks formula as such", () => {
|
|
expect(Formula.if(constant, true, value => Formula.abs(value)).isInvertible()).toBe(false);
|
|
expect(Formula.if(constant, true, value => Formula.abs(value)).hasVariable()).toBe(false);
|
|
});
|
|
|
|
test("Formula never marked integrable", () => {
|
|
expect(Formula.if(constant, true, value => Formula.add(value, 10)).isIntegrable()).toBe(
|
|
false
|
|
);
|
|
expect(() =>
|
|
Formula.if(constant, true, value => Formula.add(value, 10)).evaluateIntegral()
|
|
).toLogError();
|
|
});
|
|
|
|
test("Formula never marked as having an invertible integral", () => {
|
|
expect(
|
|
Formula.if(constant, true, value => Formula.add(value, 10)).isIntegralInvertible()
|
|
).toBe(false);
|
|
expect(() =>
|
|
Formula.if(constant, true, value => Formula.add(value, 10)).invertIntegral(10)
|
|
).toLogError();
|
|
});
|
|
|
|
test("Formula modifiers with variables mark formula as non-invertible", () => {
|
|
expect(
|
|
Formula.if(constant, true, value => Formula.add(value, variable)).isInvertible()
|
|
).toBe(false);
|
|
expect(
|
|
Formula.if(constant, true, value => Formula.add(value, variable)).hasVariable()
|
|
).toBe(false);
|
|
});
|
|
|
|
describe("Pass-through with condition false", () => {
|
|
test("Evaluates correctly", () =>
|
|
expect(
|
|
Formula.if(constant, false, value => Formula.sqrt(value)).evaluate()
|
|
).compare_tolerance(10));
|
|
test("Inverts correctly with variable in input", () =>
|
|
expect(
|
|
Formula.if(variable, false, value => Formula.sqrt(value)).invert(10)
|
|
).compare_tolerance(10));
|
|
});
|
|
describe("Evaluates correctly with condition false and else statement", () => {
|
|
test("Evaluates correctly", () =>
|
|
expect(
|
|
Formula.if(
|
|
constant,
|
|
false,
|
|
value => Formula.sqrt(value),
|
|
value => value.times(2)
|
|
).evaluate()
|
|
).compare_tolerance(20));
|
|
test("Inverts correctly with variable in input", () =>
|
|
expect(
|
|
Formula.if(
|
|
variable,
|
|
false,
|
|
value => Formula.sqrt(value),
|
|
value => value.times(2)
|
|
).invert(20)
|
|
).compare_tolerance(10));
|
|
});
|
|
|
|
describe("Evaluates correctly with condition true", () => {
|
|
test("Evaluates correctly", () =>
|
|
expect(
|
|
Formula.if(variable, true, value => Formula.add(value, 2)).evaluate()
|
|
).compare_tolerance(12));
|
|
test("Inverts correctly", () =>
|
|
expect(
|
|
Formula.if(variable, true, value => Formula.add(value, 2)).invert(12)
|
|
).compare_tolerance(10));
|
|
});
|
|
|
|
describe("Evaluates correctly when nested", () => {
|
|
test("Evaluates correctly", () =>
|
|
expect(
|
|
Formula.add(variable, constant)
|
|
.if(true, value => Formula.add(value, 2))
|
|
.div(2)
|
|
.evaluate()
|
|
).compare_tolerance(11));
|
|
test("Inverts correctly", () =>
|
|
expect(
|
|
Formula.add(variable, constant)
|
|
.if(true, value => Formula.add(value, 2))
|
|
.div(2)
|
|
.invert(12)
|
|
).compare_tolerance(12));
|
|
});
|
|
});
|
|
|
|
describe("Custom Formulas", () => {
|
|
let variable: InvertibleIntegralFormula;
|
|
beforeAll(() => {
|
|
variable = Formula.variable(1);
|
|
});
|
|
|
|
describe("Formula with evaluate", () => {
|
|
test("Zero input evaluates correctly", () =>
|
|
expect(new Formula({ inputs: [], evaluate: () => 10 }).evaluate()).compare_tolerance(
|
|
10
|
|
));
|
|
test("One input evaluates correctly", () =>
|
|
expect(
|
|
new Formula({ inputs: [1], evaluate: value => value }).evaluate()
|
|
).compare_tolerance(1));
|
|
test("Two inputs evaluates correctly", () =>
|
|
expect(
|
|
new Formula({ inputs: [1, 2], evaluate: (v1, v2) => v1 }).evaluate()
|
|
).compare_tolerance(1));
|
|
});
|
|
|
|
describe("Formula with invert", () => {
|
|
test("Zero input does not invert", () =>
|
|
expect(() =>
|
|
new Formula({
|
|
inputs: [],
|
|
evaluate: () => 6,
|
|
invert: value => value
|
|
}).invert(10)
|
|
).toLogError());
|
|
test("One input inverts correctly", () =>
|
|
expect(
|
|
new Formula({
|
|
inputs: [variable],
|
|
evaluate: () => 10,
|
|
invert: (value, v1) => v1.evaluate()
|
|
}).invert(10)
|
|
).compare_tolerance(1));
|
|
test("Two inputs inverts correctly", () =>
|
|
expect(
|
|
new Formula({
|
|
inputs: [variable, 2],
|
|
evaluate: () => 10,
|
|
invert: (value, v1, v2) => v2
|
|
}).invert(10)
|
|
).compare_tolerance(2));
|
|
});
|
|
|
|
describe("Formula with integrate", () => {
|
|
test("Zero input cannot integrate", () =>
|
|
expect(() =>
|
|
new Formula({
|
|
inputs: [],
|
|
evaluate: () => 0,
|
|
integrate: stack => variable
|
|
}).evaluateIntegral()
|
|
).toLogError());
|
|
test("One input integrates correctly", () =>
|
|
expect(
|
|
new Formula({
|
|
inputs: [variable],
|
|
evaluate: v1 => Decimal.add(v1, 10),
|
|
integrate: (stack, v1) => Formula.add(v1, 10)
|
|
}).evaluateIntegral()
|
|
).compare_tolerance(11));
|
|
test("Two inputs integrates correctly", () =>
|
|
expect(
|
|
new Formula({
|
|
inputs: [variable, 10],
|
|
evaluate: (v1, v2) => Decimal.add(v1, v2),
|
|
integrate: (stack, v1, v2) => Formula.add(v1, v2)
|
|
}).evaluateIntegral()
|
|
).compare_tolerance(11));
|
|
});
|
|
|
|
describe("Formula with invertIntegral", () => {
|
|
test("Zero input does not invert integral", () =>
|
|
expect(() =>
|
|
new Formula({
|
|
inputs: [],
|
|
evaluate: () => 0,
|
|
integrate: stack => variable
|
|
}).invertIntegral(20)
|
|
).toLogError());
|
|
test("One input inverts integral correctly", () =>
|
|
expect(
|
|
new Formula({
|
|
inputs: [variable],
|
|
evaluate: v1 => Decimal.add(v1, 10),
|
|
integrate: (stack, v1) => Formula.add(v1, 10)
|
|
}).invertIntegral(20)
|
|
).compare_tolerance(10));
|
|
test("Two inputs inverts integral correctly", () =>
|
|
expect(
|
|
new Formula({
|
|
inputs: [variable, 10],
|
|
evaluate: (v1, v2) => Decimal.add(v1, v2),
|
|
integrate: (stack, v1, v2) => Formula.add(v1, v2)
|
|
}).invertIntegral(20)
|
|
).compare_tolerance(10));
|
|
});
|
|
|
|
describe("Formula as input", () => {
|
|
let customFormula: InvertibleIntegralFormula;
|
|
beforeAll(() => {
|
|
customFormula = new Formula({
|
|
inputs: [variable],
|
|
evaluate: v1 => v1,
|
|
invert: value => value,
|
|
integrate: (stack, v1) => v1.getIntegralFormula(stack)
|
|
});
|
|
});
|
|
test("Evaluate correctly", () =>
|
|
expect(customFormula.add(10).evaluate()).compare_tolerance(11));
|
|
test("Invert correctly", () =>
|
|
expect(customFormula.add(10).invert(20)).compare_tolerance(10));
|
|
test("Integrate correctly", () =>
|
|
expect(customFormula.add(10).evaluateIntegral(10)).compare_tolerance(20));
|
|
});
|
|
});
|
|
|
|
describe("Buy Max", () => {
|
|
let resource: Resource;
|
|
beforeAll(() => {
|
|
resource = createResource(ref(100000));
|
|
});
|
|
describe("Without cumulative cost", () => {
|
|
test("Errors on calculating max affordable of non-invertible formula", () => {
|
|
const purchases = ref(1);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = Formula.abs(variable);
|
|
const maxAffordable = calculateMaxAffordable(formula, resource, false);
|
|
expect(() => maxAffordable.value).toLogError();
|
|
});
|
|
test("Errors on calculating cost of non-invertible formula", () => {
|
|
const purchases = ref(1);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = Formula.abs(variable);
|
|
expect(() => calculateCost(formula, 5, false, 0)).toLogError();
|
|
});
|
|
test("Calculates max affordable and cost correctly", () => {
|
|
const variable = Formula.variable(0);
|
|
const formula = Formula.pow(1.05, variable).times(100);
|
|
const maxAffordable = calculateMaxAffordable(formula, resource, false);
|
|
expect(maxAffordable.value).compare_tolerance(141);
|
|
expect(calculateCost(formula, maxAffordable.value, false)).compare_tolerance(
|
|
Decimal.pow(1.05, 141).times(100)
|
|
);
|
|
});
|
|
test("Calculates max affordable and cost correctly with direct sum", () => {
|
|
const variable = Formula.variable(0);
|
|
const formula = Formula.pow(1.05, variable).times(100);
|
|
const maxAffordable = calculateMaxAffordable(formula, resource, false, 4);
|
|
expect(maxAffordable.value).compare_tolerance(141 - 4);
|
|
|
|
const actualCost = new Array(4)
|
|
.fill(null)
|
|
.reduce((acc, _, i) => acc.add(formula.evaluate(133 + i)), new Decimal(0));
|
|
const calculatedCost = calculateCost(formula, maxAffordable.value, false, 4);
|
|
expect(calculatedCost).compare_tolerance(actualCost);
|
|
});
|
|
});
|
|
describe("With cumulative cost", () => {
|
|
test("Errors on calculating max affordable of non-invertible formula", () => {
|
|
const purchases = ref(1);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = Formula.abs(variable);
|
|
const maxAffordable = calculateMaxAffordable(formula, resource, true);
|
|
expect(() => maxAffordable.value).toLogError();
|
|
});
|
|
test("Errors on calculating cost of non-invertible formula", () => {
|
|
const purchases = ref(1);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = Formula.abs(variable);
|
|
expect(() => calculateCost(formula, 5, true, 0)).toLogError();
|
|
});
|
|
test("Estimates max affordable and cost correctly with 0 purchases", () => {
|
|
const purchases = ref(0);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = Formula.pow(1.05, variable).times(100);
|
|
const maxAffordable = calculateMaxAffordable(formula, resource, true, 0);
|
|
let actualAffordable = 0;
|
|
let summedCost = Decimal.dZero;
|
|
while (true) {
|
|
const nextCost = formula.evaluate(actualAffordable);
|
|
if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
|
|
actualAffordable++;
|
|
summedCost = Decimal.add(summedCost, nextCost);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
expect(maxAffordable.value).compare_tolerance(actualAffordable);
|
|
|
|
const actualCost = new Array(actualAffordable)
|
|
.fill(null)
|
|
.reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
|
|
const calculatedCost = calculateCost(formula, maxAffordable.value);
|
|
// Check if the calculated cost is within 10% of the actual cost,
|
|
// because this is an approximation
|
|
expect(
|
|
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
|
|
).toBeLessThan(0.1);
|
|
});
|
|
test("Estimates max affordable and cost with 1 purchase", () => {
|
|
const purchases = ref(1);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = Formula.pow(1.05, variable).times(100);
|
|
const maxAffordable = calculateMaxAffordable(formula, resource, true, 0);
|
|
let actualAffordable = 0;
|
|
let summedCost = Decimal.dZero;
|
|
while (true) {
|
|
const nextCost = formula.evaluate(Decimal.add(actualAffordable, 1));
|
|
if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
|
|
actualAffordable++;
|
|
summedCost = Decimal.add(summedCost, nextCost);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
expect(maxAffordable.value).compare_tolerance(actualAffordable);
|
|
|
|
const actualCost = new Array(actualAffordable)
|
|
.fill(null)
|
|
.reduce((acc, _, i) => acc.add(formula.evaluate(i + 1)), new Decimal(0));
|
|
const calculatedCost = calculateCost(formula, maxAffordable.value);
|
|
// Check if the calculated cost is within 10% of the actual cost,
|
|
// because this is an approximation
|
|
expect(
|
|
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
|
|
).toBeLessThan(0.1);
|
|
});
|
|
test("Estimates max affordable and cost more accurately with direct sum", () => {
|
|
const purchases = ref(1);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = Formula.pow(1.05, variable).times(100);
|
|
const maxAffordable = calculateMaxAffordable(formula, resource);
|
|
let actualAffordable = 0;
|
|
let summedCost = Decimal.dZero;
|
|
while (true) {
|
|
const nextCost = formula.evaluate(Decimal.add(actualAffordable, 1));
|
|
if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
|
|
actualAffordable++;
|
|
summedCost = Decimal.add(summedCost, nextCost);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
expect(maxAffordable.value).compare_tolerance(actualAffordable);
|
|
|
|
const actualCost = new Array(actualAffordable)
|
|
.fill(null)
|
|
.reduce((acc, _, i) => acc.add(formula.evaluate(i + 1)), new Decimal(0));
|
|
const calculatedCost = calculateCost(formula, maxAffordable.value);
|
|
// Since we're summing the last few purchases, this has a tighter deviation allowed
|
|
expect(
|
|
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
|
|
).toBeLessThan(0.02);
|
|
});
|
|
test("Handles direct sum when making few purchases", () => {
|
|
const purchases = ref(90);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = Formula.pow(1.05, variable).times(100);
|
|
const maxAffordable = calculateMaxAffordable(formula, resource);
|
|
let actualAffordable = 0;
|
|
let summedCost = Decimal.dZero;
|
|
while (true) {
|
|
const nextCost = formula.evaluate(Decimal.add(actualAffordable, purchases.value));
|
|
if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
|
|
actualAffordable++;
|
|
summedCost = Decimal.add(summedCost, nextCost);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
expect(maxAffordable.value).compare_tolerance(actualAffordable);
|
|
|
|
const actualCost = new Array(actualAffordable)
|
|
.fill(null)
|
|
.reduce(
|
|
(acc, _, i) => acc.add(formula.evaluate(i + purchases.value)),
|
|
new Decimal(0)
|
|
);
|
|
const calculatedCost = calculateCost(formula, maxAffordable.value);
|
|
// Since we're summing all the purchases this should be equivalent
|
|
expect(calculatedCost).compare_tolerance(actualCost);
|
|
});
|
|
test("Handles direct sum when making very few purchases", () => {
|
|
const purchases = ref(0);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = variable.add(1);
|
|
const resource = createResource(ref(3));
|
|
const maxAffordable = calculateMaxAffordable(formula, resource, true);
|
|
expect(maxAffordable.value).compare_tolerance(2);
|
|
|
|
const actualCost = new Array(2)
|
|
.fill(null)
|
|
.reduce(
|
|
(acc, _, i) => acc.add(formula.evaluate(i + purchases.value)),
|
|
new Decimal(0)
|
|
);
|
|
const calculatedCost = calculateCost(formula, maxAffordable.value);
|
|
// Since we're summing all the purchases this should be equivalent
|
|
expect(calculatedCost).compare_tolerance(actualCost);
|
|
});
|
|
test("Handles direct sum when over e308 purchases", () => {
|
|
resource.value = "1ee308";
|
|
const purchases = ref(0);
|
|
const variable = Formula.variable(purchases);
|
|
const formula = variable;
|
|
const maxAffordable = calculateMaxAffordable(formula, resource);
|
|
const calculatedCost = calculateCost(formula, maxAffordable.value);
|
|
expect(Decimal.isNaN(calculatedCost)).toBe(false);
|
|
expect(Decimal.isFinite(calculatedCost)).toBe(true);
|
|
resource.value = 100000;
|
|
});
|
|
test("Handles direct sum of non-integrable formula", () => {
|
|
const purchases = ref(0);
|
|
const formula = Formula.variable(purchases).abs();
|
|
expect(() => calculateCost(formula, 10)).not.toLogError();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Stringifies", () => {
|
|
test("Nested formula", () => {
|
|
const variable = Formula.variable(ref(0));
|
|
expect(variable.add(5).pow(Formula.constant(10)).stringify()).toBe(
|
|
"pow(add(x, 5.00), 10.00)"
|
|
);
|
|
});
|
|
test("Indeterminate", () => {
|
|
expect(Formula.if(10, true, f => f.add(5)).stringify()).toBe("indeterminate");
|
|
expect(Formula.step(10, 5, f => f.add(5)).stringify()).toBe("indeterminate");
|
|
});
|
|
});
|