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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue