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); 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. * 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. * 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, internalVariables: 1,
innermostVariable: variable, innermostVariable: variable,
internalIntegrate: integrateVariable, internalIntegrate: integrateVariable,
internalIntegrateInner: integrateVariableInner,
applySubstitution: ops.passthrough as unknown as SubstitutionFunction<T> 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) // 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 integral = this.getIntegralFormula().evaluate(1);
const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2); 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()) { if (!this.isIntegrable()) {
throw new Error("Cannot evaluate integral of formula without integral"); throw new Error("Cannot evaluate integral of formula without integral");
} }
return Decimal.add( return this.getIntegralFormula().evaluate(variable);
this.getIntegralFormula().evaluate(variable),
this.calculateConstantOfIntegration()
);
} }
/** /**
@ -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. * @param stack For nested formulas, a stack of operations that occur outside the complex operation.
*/ */
getIntegralFormula(stack?: SubstitutionStack): GenericFormula { getIntegralFormula(stack?: SubstitutionStack): GenericFormula {
if (this.integralFormula != null) { if (this.integralFormula != null && stack == null) {
return this.integralFormula; return this.integralFormula;
} }
if (stack == null) { if (stack == null) {
@ -245,6 +248,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
throw new Error("Cannot integrate formula without variable"); throw new Error("Cannot integrate formula without variable");
} }
} }
return this.integralFormula;
} else { } else {
// "Inner" part of the formula // "Inner" part of the formula
if (this.applySubstitution == null) { if (this.applySubstitution == null) {
@ -255,25 +259,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
this.applySubstitution!.call(this, variable, ...this.inputs) this.applySubstitution!.call(this, variable, ...this.inputs)
); );
if (this.internalIntegrateInner) { if (this.internalIntegrateInner) {
this.integralFormula = this.internalIntegrateInner.call( return this.internalIntegrateInner.call(this, stack, ...this.inputs);
this,
stack,
...this.inputs
);
} else if (this.internalIntegrate) { } else if (this.internalIntegrate) {
this.integralFormula = this.internalIntegrate.call(this, stack, ...this.inputs); return this.internalIntegrate.call(this, stack, ...this.inputs);
} else if ( } else if (
this.inputs.length === 1 && this.inputs.length === 1 &&
this.internalEvaluate == null && this.internalEvaluate == null &&
this.hasVariable() this.hasVariable()
) { ) {
// eslint-disable-next-line @typescript-eslint/no-this-alias return this;
this.integralFormula = this;
} else { } else {
throw new Error("Cannot integrate formula without variable"); 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"); 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) { export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
if (hasVariable(lhs)) { if (hasVariable(lhs)) {
return new Formula({ const x = lhs.getIntegralFormula(stack);
inputs: [lhs.getIntegralFormula(stack)], return Formula.pow10(x).div(Formula.ln(10));
evaluate: internalIntegratePow10,
invert: internalInvertIntegralPow10
});
} }
throw new Error("Could not integrate due to no input being a variable"); 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, { import Formula, {
calculateCost, calculateCost,
calculateMaxAffordable, calculateMaxAffordable,
printFormula,
unrefFormulaSource unrefFormulaSource
} from "game/formulas/formulas"; } from "game/formulas/formulas";
import type { GenericFormula, InvertibleFormula } from "game/formulas/types"; 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 { beforeAll, describe, expect, test } from "vitest";
import { ref } from "vue"; import { ref } from "vue";
import "../utils"; import "../utils";
@ -572,7 +573,13 @@ describe("Integrating", () => {
// Check if the calculated cost is within 10% of the actual cost, // Check if the calculated cost is within 10% of the actual cost,
// because this is an approximation // because this is an approximation
expect( 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); ).toBeLessThan(0.1);
}); });
@ -668,7 +675,7 @@ describe("Inverting integrals", () => {
test("Inverting integral of nested formulas", () => { test("Inverting integral of nested formulas", () => {
const formula = Formula.add(variable, constant).times(constant).pow(2).times(30); 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", () => { test("Inverting integral of nested complex formulas", () => {
@ -929,18 +936,18 @@ describe("Custom Formulas", () => {
expect( expect(
new Formula({ new Formula({
inputs: [variable], inputs: [variable],
evaluate: v1 => Decimal.add(v1, 19.5), evaluate: v1 => Decimal.add(v1, 10),
integrate: (stack, v1) => Formula.add(v1, 10) integrate: (stack, v1) => Formula.add(v1, 10)
}).evaluateIntegral() }).evaluateIntegral()
).compare_tolerance(20)); ).compare_tolerance(11));
test("Two inputs integrates correctly", () => test("Two inputs integrates correctly", () =>
expect( expect(
new Formula({ new Formula({
inputs: [variable, 10], 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) integrate: (stack, v1, v2) => Formula.add(v1, v2)
}).evaluateIntegral() }).evaluateIntegral()
).compare_tolerance(20)); ).compare_tolerance(11));
}); });
describe("Formula with invertIntegral", () => { describe("Formula with invertIntegral", () => {
@ -956,7 +963,7 @@ describe("Custom Formulas", () => {
expect( expect(
new Formula({ new Formula({
inputs: [variable], inputs: [variable],
evaluate: v1 => Decimal.add(v1, 19.5), evaluate: v1 => Decimal.add(v1, 10),
integrate: (stack, v1) => Formula.add(v1, 10) integrate: (stack, v1) => Formula.add(v1, 10)
}).invertIntegral(20) }).invertIntegral(20)
).compare_tolerance(10)); ).compare_tolerance(10));
@ -964,7 +971,7 @@ describe("Custom Formulas", () => {
expect( expect(
new Formula({ new Formula({
inputs: [variable, 10], 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) integrate: (stack, v1, v2) => Formula.add(v1, v2)
}).invertIntegral(20) }).invertIntegral(20)
).compare_tolerance(10)); ).compare_tolerance(10));
@ -1001,14 +1008,25 @@ describe("Buy Max", () => {
const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource); const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
expect(() => maxAffordable.value).toThrow(); expect(() => maxAffordable.value).toThrow();
}); });
// https://www.desmos.com/calculator/7ffthe7wi8 test("Calculates max affordable and cost correctly with 0 purchases", () => {
test("Calculates max affordable and cost correctly", () => { const purchases = ref(0);
const variable = Formula.variable(0); const variable = Formula.variable(purchases);
const formula = Formula.pow(1.05, variable).times(100); const formula = Formula.pow(1.05, variable).times(100);
const maxAffordable = calculateMaxAffordable(formula, resource); 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) .fill(null)
.reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0)); .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
const calculatedCost = calculateCost(formula, maxAffordable.value); const calculatedCost = calculateCost(formula, maxAffordable.value);
@ -1018,5 +1036,33 @@ describe("Buy Max", () => {
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber() Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
).toBeLessThan(0.1); ).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);
});
}); });
}); });