From 3078584043798ed794d7d65dce028ce5a565ed35 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 11:43:44 -0500 Subject: [PATCH] Rewrite integration to handle nested formulas properly And more clearly defines which formulas are supported --- src/game/formulas.ts | 472 ++++++++++++++++++++++++++++-------- tests/game/formulas.test.ts | 52 +++- 2 files changed, 416 insertions(+), 108 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 3c0b379..7fd489b 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1,7 +1,7 @@ import { Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; -import { computed, ComputedRef, ref, Ref, unref } from "vue"; +import { computed, ComputedRef, ref, unref } from "vue"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type GenericFormula = Formula; @@ -16,6 +16,8 @@ export type InvertibleIntegralFormula = GenericFormula & { invertIntegral: (value: DecimalSource) => DecimalSource; }; +export type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined; + export type FormulaOptions = | { variable: ProcessedComputable; @@ -34,6 +36,18 @@ export type FormulaOptions = integrate?: ( this: Formula, variable: DecimalSource | undefined, + stack: SubstitutionStack | undefined, + ...inputs: T + ) => DecimalSource; + integrateInner?: ( + this: Formula, + variable: DecimalSource | undefined, + stack: SubstitutionStack | undefined, + ...inputs: T + ) => DecimalSource; + applySubstitution?: ( + this: Formula, + variable: DecimalSource, ...inputs: T ) => DecimalSource; invertIntegral?: (this: Formula, value: DecimalSource, ...inputs: T) => DecimalSource; @@ -60,6 +74,17 @@ function passthrough(value: DecimalSource) { return value; } +function integrateVariable(variable: DecimalSource) { + return Decimal.pow(variable, 2).div(2); +} + +function integrateVariableInner(this: GenericFormula, variable: DecimalSource | undefined) { + if (variable == null && this.innermostVariable == null) { + throw "Cannot integrate non-existent variable"; + } + return variable ?? unref(this.innermostVariable); +} + function invertNeg(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.neg(value)); @@ -67,8 +92,19 @@ function invertNeg(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateNeg(variable: DecimalSource | undefined, lhs: FormulaSource) { - return Decimal.pow(unrefFormulaSource(lhs, variable), 2).div(2).neg(); +function integrateNeg( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + return Decimal.neg(lhs.evaluateIntegral(variable, stack)); + } + throw "Could not integrate due to no input being a variable"; +} + +function applySubstitutionNeg(value: DecimalSource) { + return Decimal.neg(value); } function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -80,17 +116,40 @@ function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateAdd(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateAdd( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.pow(x, 2) - .div(2) - .add(Decimal.times(unrefFormulaSource(rhs), x)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.times( + unrefFormulaSource(rhs), + variable ?? unref(lhs.innermostVariable) ?? 0 + ).add(x); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); - return Decimal.pow(x, 2) - .div(2) - .add(Decimal.times(unrefFormulaSource(lhs), x)); + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.times( + unrefFormulaSource(lhs), + variable ?? unref(rhs.innermostVariable) ?? 0 + ).add(x); + } + throw "Could not integrate due to no input being a variable"; +} + +function integrateInnerAdd( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.add(x, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.add(x, unrefFormulaSource(lhs)); } throw "Could not integrate due to no input being a variable"; } @@ -115,15 +174,40 @@ function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateSub(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateSub( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.pow(x, 2) - .div(2) - .add(Decimal.times(unrefFormulaSource(rhs), x).neg()); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sub( + x, + Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0) + ); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); - return Decimal.sub(unrefFormulaSource(lhs), Decimal.div(x, 2)).times(x); + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.times( + unrefFormulaSource(lhs), + variable ?? unref(rhs.innermostVariable) ?? 0 + ).sub(x); + } + throw "Could not integrate due to no input being a variable"; +} + +function integrateInnerSub( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sub(x, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.sub(x, unrefFormulaSource(lhs)); } throw "Could not integrate due to no input being a variable"; } @@ -148,17 +232,31 @@ function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateMul(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateMul( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(rhs)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.times(x, unrefFormulaSource(rhs)); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); - return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(lhs)); + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.times(x, unrefFormulaSource(lhs)); } throw "Could not integrate due to no input being a variable"; } +function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.div(value, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + return Decimal.div(value, unrefFormulaSource(lhs)); + } + throw "Could not apply substitution due to no input being a variable"; +} + function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { const b = unrefFormulaSource(rhs); @@ -179,17 +277,31 @@ function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateDiv(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateDiv( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(rhs))); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.div(x, unrefFormulaSource(rhs)); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); - return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(lhs))); + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.div(unrefFormulaSource(lhs), x); } throw "Could not integrate due to no input being a variable"; } +function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.mul(value, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + return Decimal.mul(value, unrefFormulaSource(lhs)); + } + throw "Could not apply substitution due to no input being a variable"; +} + function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { const b = unrefFormulaSource(rhs); @@ -208,9 +320,13 @@ function invertRecip(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateRecip(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateRecip( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.ln(x); } throw "Could not integrate due to no input being a variable"; @@ -230,10 +346,14 @@ function invertLog10(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateLog10(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateLog10( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(10))); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); } throw "Could not integrate due to no input being a variable"; } @@ -256,13 +376,18 @@ function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateLog(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateLog( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.times( - x, - Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(unrefFormulaSource(rhs))) - ); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x) + .sub(1) + .times(x) + .div(Decimal.ln(unrefFormulaSource(rhs))); } throw "Could not integrate due to no input being a variable"; } @@ -282,10 +407,14 @@ function invertLog2(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateLog2(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateLog2( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(2))); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2)); } throw "Could not integrate due to no input being a variable"; } @@ -304,10 +433,14 @@ function invertLn(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateLn(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateLn( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.times(x, Decimal.ln(x).sub(1)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x); } throw "Could not integrate due to no input being a variable"; } @@ -328,13 +461,18 @@ function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integratePow(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integratePow( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); const pow = Decimal.add(unrefFormulaSource(rhs), 1); return Decimal.pow(x, pow).div(pow); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); + const x = rhs.evaluateIntegral(variable, stack); const b = unrefFormulaSource(lhs); return Decimal.pow(b, x).div(Decimal.ln(b)); } @@ -359,9 +497,13 @@ function invertPow10(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integratePow10(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integratePow10( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); } throw "Could not integrate due to no input being a variable"; @@ -387,15 +529,18 @@ function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSou function integratePowBase( variable: DecimalSource | undefined, + stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs, variable); - return Decimal.pow(b, unrefFormulaSource(lhs)).div(Decimal.ln(b)); + const x = lhs.evaluateIntegral(variable, stack); + const b = unrefFormulaSource(rhs); + return Decimal.pow(b, x).div(Decimal.ln(b)); } else if (hasVariable(rhs)) { - const denominator = Decimal.add(unrefFormulaSource(lhs, variable), 1); - return Decimal.pow(unrefFormulaSource(rhs), denominator).div(denominator); + const x = rhs.evaluateIntegral(variable, stack); + const denominator = Decimal.add(unrefFormulaSource(lhs), 1); + return Decimal.pow(x, denominator).div(denominator); } throw "Could not integrate due to no input being a variable"; } @@ -422,14 +567,14 @@ function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource function integrateRoot( variable: DecimalSource | undefined, + stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return Decimal.pow(unrefFormulaSource(lhs, variable), Decimal.recip(b).add(1)) - .times(b) - .div(Decimal.add(b, 1)); + const x = lhs.evaluateIntegral(variable, stack); + const a = unrefFormulaSource(rhs); + return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1)); } throw "Could not integrate due to no input being a variable"; } @@ -454,9 +599,14 @@ function invertExp(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateExp(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateExp( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.exp(unrefFormulaSource(lhs, variable)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.exp(x); } throw "Could not integrate due to no input being a variable"; } @@ -580,9 +730,14 @@ function invertSin(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateSin(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateSin( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.cos(unrefFormulaSource(lhs, variable)).neg(); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.cos(x).neg(); } throw "Could not integrate due to no input being a variable"; } @@ -594,9 +749,14 @@ function invertCos(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateCos(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateCos( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.sin(unrefFormulaSource(lhs, variable)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sin(x); } throw "Could not integrate due to no input being a variable"; } @@ -608,9 +768,14 @@ function invertTan(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateTan(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateTan( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.cos(unrefFormulaSource(lhs, variable)).ln().neg(); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.cos(x).ln().neg(); } throw "Could not integrate due to no input being a variable"; } @@ -622,9 +787,13 @@ function invertAsin(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAsin(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAsin( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.asin(x) .times(x) .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); @@ -639,9 +808,13 @@ function invertAcos(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAcos(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAcos( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.acos(x) .times(x) .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); @@ -656,9 +829,13 @@ function invertAtan(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAtan(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAtan( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.atan(x) .times(x) .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2)); @@ -673,9 +850,13 @@ function invertSinh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateSinh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateSinh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.cosh(x); } throw "Could not integrate due to no input being a variable"; @@ -688,9 +869,13 @@ function invertCosh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateCosh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateCosh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.sinh(x); } throw "Could not integrate due to no input being a variable"; @@ -703,9 +888,13 @@ function invertTanh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateTanh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateTanh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.cosh(x).ln(); } throw "Could not integrate due to no input being a variable"; @@ -718,9 +907,13 @@ function invertAsinh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAsinh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAsinh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt()); } throw "Could not integrate due to no input being a variable"; @@ -733,9 +926,13 @@ function invertAcosh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAcosh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAcosh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.acosh(x) .times(x) .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt())); @@ -750,9 +947,13 @@ function invertAtanh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAtanh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAtanh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.atanh(x) .times(x) .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2)); @@ -776,7 +977,21 @@ export default class Formula { | ((value: DecimalSource, ...inputs: T) => DecimalSource) | undefined; private readonly internalIntegrate: - | ((variable: DecimalSource | undefined, ...inputs: T) => DecimalSource) + | (( + variable: DecimalSource | undefined, + stack: SubstitutionStack | undefined, + ...inputs: T + ) => DecimalSource) + | undefined; + private readonly internalIntegrateInner: + | (( + variable: DecimalSource | undefined, + stack: SubstitutionStack | undefined, + ...inputs: T + ) => DecimalSource) + | undefined; + private readonly applySubstitution: + | ((variable: DecimalSource, ...inputs: T) => DecimalSource) | undefined; private readonly internalInvertIntegral: | ((value: DecimalSource, ...inputs: T) => DecimalSource) @@ -791,6 +1006,11 @@ export default class Formula { this.inputs = [options.variable] as T; this.internalHasVariable = true; this.innermostVariable = options.variable; + this.internalIntegrate = + integrateVariable as unknown as Formula["internalIntegrate"]; + this.internalIntegrateInner = + integrateVariableInner as unknown as Formula["internalIntegrateInner"]; + this.applySubstitution = passthrough as unknown as Formula["applySubstitution"]; return; } // Constant case @@ -803,7 +1023,16 @@ export default class Formula { return; } - const { inputs, evaluate, invert, integrate, invertIntegral, hasVariable } = options; + const { + inputs, + evaluate, + invert, + integrate, + integrateInner, + applySubstitution, + invertIntegral, + hasVariable + } = options; if (invert == null && invertIntegral == null && hasVariable) { throw "A formula cannot be marked as having a variable if it is not invertible"; } @@ -811,6 +1040,8 @@ export default class Formula { this.inputs = inputs; this.internalEvaluate = evaluate; this.internalIntegrate = integrate; + this.internalIntegrateInner = integrateInner; + this.applySubstitution = applySubstitution; const numVariables = inputs.filter( input => input instanceof Formula && input.hasVariable() @@ -874,7 +1105,7 @@ export default class Formula { unrefFormulaSource(input, variable) ) as GuardedFormulasToDecimals) ) ?? - variable ?? + (this.internalHasVariable ? variable : null) ?? unrefFormulaSource(this.inputs[0]) ); } @@ -894,17 +1125,57 @@ export default class Formula { } /** - * Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable + * Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable. The formula can only have one "complex" operation (anything besides +,-,*,/). * @param variable Optionally override the value of the variable while evaluating + * @param stack The list of callbacks to run to handle simple operations inside the complex operation. Used in nested formulas * @see {@link isIntegrable} */ - evaluateIntegral(variable?: DecimalSource): DecimalSource { - if (this.internalIntegrate) { - return this.internalIntegrate.call(this, variable, ...this.inputs); - } else if (this.inputs.length === 1 && this.internalHasVariable) { - return variable ?? unrefFormulaSource(this.inputs[0]); + evaluateIntegral(variable?: DecimalSource, stack?: SubstitutionStack): DecimalSource { + if (stack == null) { + // "Outer" part of the formula + if (this.applySubstitution == null) { + // We're the complex operation of this formula + stack = []; + if (this.internalIntegrate == null) { + throw "Cannot integrate formula with non-existent operation"; + } + let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs); + stack.forEach(func => (value = func(value))); + return value; + } else { + // Continue digging into the formula + if (this.internalIntegrate) { + return this.internalIntegrate.call(this, variable, undefined, ...this.inputs); + } else if (this.inputs.length === 1 && this.internalHasVariable) { + return integrateVariable(variable ?? unrefFormulaSource(this.inputs[0])); + } + throw "Cannot integrate formula without variable"; + } + } else { + // "Inner" part of the formula + if (this.applySubstitution == null) { + throw "Cannot have two complex operations in an integrable formula"; + } + stack.push((variable: DecimalSource) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.applySubstitution!.call(this, variable, ...this.inputs) + ); + if (this.internalIntegrateInner) { + return this.internalIntegrateInner.call(this, variable, stack, ...this.inputs); + } else if (this.internalIntegrate) { + return this.internalIntegrate.call(this, variable, stack, ...this.inputs); + } else if (this.inputs.length === 1 && this.internalHasVariable) { + return variable ?? unrefFormulaSource(this.inputs[0]); + } + throw "Cannot integrate formula without variable"; } - throw "Cannot integrate formula without variable"; + } + + calculateConstantOfIntegration() { + // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase + const integral = this.evaluateIntegral(1); + const actualCost = this.evaluate(0); + return Decimal.sub(actualCost, integral); } /** @@ -1070,6 +1341,7 @@ export default class Formula { inputs: [value], evaluate: Decimal.neg, invert: invertNeg, + applySubstitution: applySubstitutionNeg, integrate: integrateNeg }); } @@ -1116,6 +1388,8 @@ export default class Formula { evaluate: Decimal.add, invert: invertAdd, integrate: integrateAdd, + integrateInner: integrateInnerAdd, + applySubstitution: passthrough, invertIntegral: invertIntegrateAdd }); } @@ -1135,6 +1409,8 @@ export default class Formula { evaluate: Decimal.sub, invert: invertSub, integrate: integrateSub, + integrateInner: integrateInnerSub, + applySubstitution: passthrough, invertIntegral: invertIntegrateSub }); } @@ -1160,6 +1436,7 @@ export default class Formula { evaluate: Decimal.mul, invert: invertMul, integrate: integrateMul, + applySubstitution: applySubstitutionMul, invertIntegral: invertIntegrateMul }); } @@ -1185,6 +1462,7 @@ export default class Formula { evaluate: Decimal.div, invert: invertDiv, integrate: integrateDiv, + applySubstitution: applySubstitutionDiv, invertIntegral: invertIntegrateDiv }); } @@ -2180,7 +2458,7 @@ export default class Formula { * Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers. * @param formula The formula to use for calculating buy max from * @param resource The resource used when purchasing (is only read from) - * @param spendResources Whether or not to count spent resources on each purchase or not + * @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards fewer purchases */ export function calculateMaxAffordable( formula: InvertibleFormula, @@ -2219,7 +2497,7 @@ export function calculateMaxAffordable( * Utility for calculating the cost of a formula for a given amount of purchases. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers. * @param formula The formula to use for calculating buy max from * @param amountToBuy The amount of purchases to calculate the cost for - * @param spendResources Whether or not to count spent resources on each purchase or not + * @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards higher cost */ export function calculateCost( formula: InvertibleFormula, diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 43d876f..b45112d 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -8,7 +8,7 @@ import Formula, { } from "game/formulas"; import Decimal, { DecimalSource } from "util/bignum"; import { beforeAll, describe, expect, test } from "vitest"; -import { ref } from "vue"; +import { ref, unref } from "vue"; import "../utils"; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; @@ -486,7 +486,7 @@ describe("Integrating", () => { let variable: GenericFormula; let constant: GenericFormula; beforeAll(() => { - variable = Formula.variable(10); + variable = Formula.variable(ref(10)); constant = Formula.constant(10); }); @@ -564,8 +564,24 @@ describe("Integrating", () => { describe.todo("Integrable formulas integrate correctly"); test("Integrating nested formulas", () => { - const formula = Formula.add(variable, constant).times(constant); - expect(formula.evaluateIntegral()).compare_tolerance(1500); + const formula = Formula.add(variable, constant).times(constant).pow(2).times(30); + const actualCost = new Array(10) + .fill(null) + .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0)); + const calculatedCost = Decimal.add( + formula.evaluateIntegral(), + formula.calculateConstantOfIntegration() + ); + // 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("Integrating nested complex formulas", () => { + const formula = Formula.pow(1.05, variable).times(100).pow(0.5); + expect(() => formula.evaluateIntegral()).toThrow(); }); }); @@ -652,8 +668,13 @@ describe("Inverting integrals", () => { describe.todo("Invertible Integral formulas invert correctly"); test("Inverting integral of nested formulas", () => { - const formula = Formula.add(variable, constant).times(constant); - expect(formula.invertIntegral(1500)).compare_tolerance(10); + const formula = Formula.add(variable, constant).times(constant).pow(2).times(30); + expect(formula.invertIntegral(7000000)).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)).toThrow(); }); describe("Inverting integral pass-throughs", () => { @@ -875,7 +896,7 @@ describe("Custom Formulas", () => { describe("Formula with integrate", () => { test("Zero input integrates correctly", () => - expect(() => + expect( new Formula({ inputs: [], evaluate: () => 10, @@ -887,7 +908,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable], evaluate: () => 10, - integrate: (val, v1) => val ?? 20 + integrate: (val, stack, v1) => val ?? 20 }).evaluateIntegral() ).compare_tolerance(20)); test("Two inputs integrates correctly", () => @@ -941,7 +962,7 @@ describe("Buy Max", () => { const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false); expect(() => maxAffordable.value).toThrow(); }); - // https://www.desmos.com/calculator/5vgletdc1p + // https://www.desmos.com/calculator/7ffthe7wi8 test("Calculates max affordable and cost correctly", () => { const variable = Formula.variable(0); const formula = Formula.pow(1.05, variable).times(100); @@ -957,13 +978,22 @@ describe("Buy Max", () => { const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource); expect(() => maxAffordable.value).toThrow(); }); - // https://www.desmos.com/calculator/5vgletdc1p + // https://www.desmos.com/calculator/7ffthe7wi8 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); expect(maxAffordable.value).compare_tolerance(7); - expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(735); + + const actualCost = new Array(7) + .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); }); }); });