forked from profectus/Profectus
All tests pass now
This commit is contained in:
parent
6f9b73d0e8
commit
8dc0c6c55c
3 changed files with 75 additions and 45 deletions
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue