All tests pass now

This commit is contained in:
thepaperpilot 2023-04-02 15:02:43 -05:00
parent 6f9b73d0e8
commit 8dc0c6c55c
3 changed files with 75 additions and 45 deletions

View file

@ -32,6 +32,10 @@ function integrateVariable(this: GenericFormula) {
return Formula.pow(this, 2).div(2);
}
function integrateVariableInner(this: GenericFormula) {
return this;
}
/**
* A class that can be used for cost/goal functions. It can be evaluated similar to a cost function, but also provides extra features for supported formulas. For example, a lot of math functions can be inverted.
* Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently.
@ -81,6 +85,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
internalVariables: 1,
innermostVariable: variable,
internalIntegrate: integrateVariable,
internalIntegrateInner: integrateVariableInner,
applySubstitution: ops.passthrough as unknown as SubstitutionFunction<T>
};
}
@ -119,7 +124,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
};
}
private calculateConstantOfIntegration() {
/** Calculates C for the implementation of the integral formula for this formula. */
calculateConstantOfIntegration() {
// Calculate C based on the knowledge that at x=1, the integral should be the average between f(0) and f(1)
const integral = this.getIntegralFormula().evaluate(1);
const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2);
@ -189,10 +195,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
if (!this.isIntegrable()) {
throw new Error("Cannot evaluate integral of formula without integral");
}
return Decimal.add(
this.getIntegralFormula().evaluate(variable),
this.calculateConstantOfIntegration()
);
return this.getIntegralFormula().evaluate(variable);
}
/**
@ -212,7 +215,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
* @param stack For nested formulas, a stack of operations that occur outside the complex operation.
*/
getIntegralFormula(stack?: SubstitutionStack): GenericFormula {
if (this.integralFormula != null) {
if (this.integralFormula != null && stack == null) {
return this.integralFormula;
}
if (stack == null) {
@ -245,6 +248,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
throw new Error("Cannot integrate formula without variable");
}
}
return this.integralFormula;
} else {
// "Inner" part of the formula
if (this.applySubstitution == null) {
@ -255,25 +259,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
this.applySubstitution!.call(this, variable, ...this.inputs)
);
if (this.internalIntegrateInner) {
this.integralFormula = this.internalIntegrateInner.call(
this,
stack,
...this.inputs
);
return this.internalIntegrateInner.call(this, stack, ...this.inputs);
} else if (this.internalIntegrate) {
this.integralFormula = this.internalIntegrate.call(this, stack, ...this.inputs);
return this.internalIntegrate.call(this, stack, ...this.inputs);
} else if (
this.inputs.length === 1 &&
this.internalEvaluate == null &&
this.hasVariable()
) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
this.integralFormula = this;
return this;
} else {
throw new Error("Cannot integrate formula without variable");
}
}
return this.integralFormula;
}
/**

View file

@ -326,24 +326,10 @@ export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
throw new Error("Could not invert due to no input being a variable");
}
function internalIntegratePow10(lhs: DecimalSource) {
return Decimal.pow10(lhs).div(Decimal.ln(lhs));
}
function internalInvertIntegralPow10(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) {
return lhs.invert(ln10.times(value).ln().div(ln10));
}
throw new Error("Could not invert due to no input being a variable");
}
export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) {
return new Formula({
inputs: [lhs.getIntegralFormula(stack)],
evaluate: internalIntegratePow10,
invert: internalInvertIntegralPow10
});
const x = lhs.getIntegralFormula(stack);
return Formula.pow10(x).div(Formula.ln(10));
}
throw new Error("Could not integrate due to no input being a variable");
}

View file

@ -2,10 +2,11 @@ import { createResource, Resource } from "features/resources/resource";
import Formula, {
calculateCost,
calculateMaxAffordable,
printFormula,
unrefFormulaSource
} from "game/formulas/formulas";
import type { GenericFormula, InvertibleFormula } from "game/formulas/types";
import Decimal, { DecimalSource } from "util/bignum";
import Decimal, { DecimalSource, format } from "util/bignum";
import { beforeAll, describe, expect, test } from "vitest";
import { ref } from "vue";
import "../utils";
@ -572,7 +573,13 @@ describe("Integrating", () => {
// Check if the calculated cost is within 10% of the actual cost,
// because this is an approximation
expect(
Decimal.sub(actualCost, formula.evaluateIntegral()).abs().div(actualCost).toNumber()
Decimal.sub(
actualCost,
Decimal.add(formula.evaluateIntegral(), formula.calculateConstantOfIntegration())
)
.abs()
.div(actualCost)
.toNumber()
).toBeLessThan(0.1);
});
@ -668,7 +675,7 @@ describe("Inverting integrals", () => {
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, 0.01);
expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10);
});
test("Inverting integral of nested complex formulas", () => {
@ -929,18 +936,18 @@ describe("Custom Formulas", () => {
expect(
new Formula({
inputs: [variable],
evaluate: v1 => Decimal.add(v1, 19.5),
evaluate: v1 => Decimal.add(v1, 10),
integrate: (stack, v1) => Formula.add(v1, 10)
}).evaluateIntegral()
).compare_tolerance(20));
).compare_tolerance(11));
test("Two inputs integrates correctly", () =>
expect(
new Formula({
inputs: [variable, 10],
evaluate: v1 => Decimal.add(v1, 19.5),
evaluate: (v1, v2) => Decimal.add(v1, v2),
integrate: (stack, v1, v2) => Formula.add(v1, v2)
}).evaluateIntegral()
).compare_tolerance(20));
).compare_tolerance(11));
});
describe("Formula with invertIntegral", () => {
@ -956,7 +963,7 @@ describe("Custom Formulas", () => {
expect(
new Formula({
inputs: [variable],
evaluate: v1 => Decimal.add(v1, 19.5),
evaluate: v1 => Decimal.add(v1, 10),
integrate: (stack, v1) => Formula.add(v1, 10)
}).invertIntegral(20)
).compare_tolerance(10));
@ -964,7 +971,7 @@ describe("Custom Formulas", () => {
expect(
new Formula({
inputs: [variable, 10],
evaluate: v1 => Decimal.add(v1, 19.5),
evaluate: (v1, v2) => Decimal.add(v1, v2),
integrate: (stack, v1, v2) => Formula.add(v1, v2)
}).invertIntegral(20)
).compare_tolerance(10));
@ -1001,14 +1008,25 @@ describe("Buy Max", () => {
const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
expect(() => maxAffordable.value).toThrow();
});
// https://www.desmos.com/calculator/7ffthe7wi8
test("Calculates max affordable and cost correctly", () => {
const variable = Formula.variable(0);
test("Calculates 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);
expect(maxAffordable.value).compare_tolerance(7);
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(7)
const actualCost = new Array(actualAffordable)
.fill(null)
.reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
const calculatedCost = calculateCost(formula, maxAffordable.value);
@ -1018,5 +1036,33 @@ describe("Buy Max", () => {
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
).toBeLessThan(0.1);
});
test("Calculates max affordable and cost correctly 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);
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);
});
});
});