From aebf318f830295c564dc197be2a5035b3cff72f1 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sat, 4 Feb 2023 17:56:05 -0600 Subject: [PATCH] Lots of formula changes Many tests fail and inverting integrals is basically non-functional because it just chains stuff Will need to implement integration by parts and/or integration by substitution for that --- package.json | 2 +- src/game/formulas.ts | 1083 ++++++++++++++++++++++++++++++----- tests/game/formulas.test.ts | 681 +++++++++++++++++----- 3 files changed, 1475 insertions(+), 291 deletions(-) diff --git a/package.json b/package.json index 4b87061..18f336b 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "jsdom": "^20.0.0", "prettier": "^2.5.1", "typescript": "^4.7.4", - "vitest": "^0.26.3", + "vitest": "^0.27.3", "vue-tsc": "^0.38.1" }, "engines": { diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 6b183e0..7e893be 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1,14 +1,45 @@ +import { Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; -import { ref, Ref, unref } from "vue"; +import { computed, ref, Ref, unref } from "vue"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type GenericFormula = Formula; export type FormulaSource = ProcessedComputable | GenericFormula; export type InvertibleFormulaSource = ProcessedComputable | InvertibleFormula; export type InvertibleFormula = GenericFormula & { - invert: (value: DecimalSource) => Decimal; + invert: (value: DecimalSource) => DecimalSource; }; +export type IntegrableFormula = GenericFormula & { + evaluateIntegral: (variable?: DecimalSource) => DecimalSource; +}; +export type InvertibleIntegralFormula = GenericFormula & { + invertIntegral: (value: DecimalSource) => DecimalSource; +}; + +export type FormulaOptions = + | { + variable: ProcessedComputable; + } + | { + inputs: [FormulaSource]; + } + | { + inputs: T; + evaluate: (this: Formula, ...inputs: GuardedFormulasToDecimals) => DecimalSource; + invert?: ( + this: Formula, + value: DecimalSource, + ...inputs: [...T, ...unknown[]] + ) => DecimalSource; + integrate?: ( + this: Formula, + variable: DecimalSource | undefined, + ...inputs: T + ) => DecimalSource; + invertIntegral?: (this: Formula, value: DecimalSource, ...inputs: T) => DecimalSource; + hasVariable?: boolean; + }; function hasVariable(value: FormulaSource): value is InvertibleFormula { return value instanceof Formula && value.hasVariable(); @@ -22,8 +53,8 @@ type FormulasToDecimals = { type TupleGuard = T extends any[] ? FormulasToDecimals : never; type GuardedFormulasToDecimals = TupleGuard; -export function unrefFormulaSource(value: FormulaSource) { - return value instanceof Formula ? value.evaluate() : unref(value); +export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSource) { + return value instanceof Formula ? value.evaluate(variable) : unref(value); } function invertNeg(value: DecimalSource, lhs: FormulaSource) { @@ -33,6 +64,10 @@ 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 invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.sub(value, unrefFormulaSource(rhs))); @@ -42,6 +77,32 @@ 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) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.pow(x, 2) + .div(2) + .add(Decimal.times(unrefFormulaSource(rhs), x)); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + return Decimal.pow(x, 2) + .div(2) + .add(Decimal.times(unrefFormulaSource(lhs), x)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); + } + throw "Could not invert due to no input being a variable"; +} + function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs))); @@ -51,6 +112,30 @@ 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) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.pow(x, 2) + .div(2) + .add(Decimal.times(unrefFormulaSource(rhs), x).neg()); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + return Decimal.sub(unrefFormulaSource(lhs), Decimal.div(x, 2)).times(x); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); + } + throw "Could not invert due to no input being a variable"; +} + function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs))); @@ -60,6 +145,28 @@ 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) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(lhs)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); + } + throw "Could not invert due to no input being a variable"; +} + function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs))); @@ -69,6 +176,28 @@ 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) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(lhs))); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); + } + throw "Could not invert due to no input being a variable"; +} + function invertRecip(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.recip(value)); @@ -76,6 +205,21 @@ function invertRecip(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateRecip(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.ln(x); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateRecip(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(value)); + } + throw "Could not invert due to no input being a variable"; +} + function invertLog10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow10(value)); @@ -83,6 +227,23 @@ function invertLog10(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateLog10(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(10))); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1)) + ); + } + throw "Could not invert due to no input being a variable"; +} + function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value)); @@ -92,6 +253,25 @@ 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) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.times( + x, + Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(unrefFormulaSource(rhs))) + ); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value); + return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); + } + throw "Could not invert due to no input being a variable"; +} + function invertLog2(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow(2, value)); @@ -99,6 +279,21 @@ function invertLog2(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateLog2(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(2))); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1))); + } + throw "Could not invert due to no input being a variable"; +} + function invertLn(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.exp(value)); @@ -106,6 +301,21 @@ function invertLn(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateLn(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.times(x, Decimal.ln(x).sub(1)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1))); + } + throw "Could not invert due to no input being a variable"; +} + function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs))); @@ -115,6 +325,30 @@ 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) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + const pow = Decimal.add(unrefFormulaSource(rhs), 1); + return Decimal.pow(x, pow).div(pow); + } else if (hasVariable(rhs)) { + const x = unrefFormulaSource(rhs, variable); + const b = unrefFormulaSource(lhs); + return Decimal.pow(b, x).div(Decimal.ln(b)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.negate(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); + } else if (hasVariable(rhs)) { + const denominator = Decimal.ln(unrefFormulaSource(lhs)); + return rhs.invert(Decimal.times(denominator, value).ln().div(denominator)); + } + throw "Could not invert due to no input being a variable"; +} + function invertPow10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, 10)); @@ -122,6 +356,23 @@ function invertPow10(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integratePow10(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp() + ); + } + throw "Could not invert due to no input being a variable"; +} + function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs))); @@ -131,6 +382,32 @@ function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSou throw "Could not invert due to no input being a variable"; } +function integratePowBase( + variable: DecimalSource | undefined, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs, variable); + return Decimal.pow(b, unrefFormulaSource(lhs)).div(Decimal.ln(b)); + } else if (hasVariable(rhs)) { + const denominator = Decimal.add(unrefFormulaSource(lhs, variable), 1); + return Decimal.pow(unrefFormulaSource(rhs), denominator).div(denominator); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegratePowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.ln(b).times(value).ln().div(Decimal.ln(b))); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); + } + throw "Could not invert due to no input being a variable"; +} + function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs)))); @@ -140,6 +417,33 @@ function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource throw "Could not invert due to no input being a variable"; } +function integrateRoot( + variable: DecimalSource | undefined, + 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)); + } + throw "Could not integrate due to no input being a variable"; +} + +function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert( + Decimal.add(b, 1) + .times(value) + .div(b) + .pow(Decimal.div(b, Decimal.add(b, 1))) + ); + } + throw "Could not invert due to no input being a variable"; +} + function invertExp(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.ln(value)); @@ -147,6 +451,13 @@ function invertExp(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateExp(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.exp(unrefFormulaSource(lhs, variable)); + } + throw "Could not integrate due to no input being a variable"; +} + function tetrate( value: DecimalSource, height: DecimalSource = 2, @@ -268,6 +579,13 @@ function invertSin(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateSin(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.cos(unrefFormulaSource(lhs, variable)).neg(); + } + throw "Could not integrate due to no input being a variable"; +} + function invertCos(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.acos(value)); @@ -275,6 +593,13 @@ function invertCos(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateCos(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.sin(unrefFormulaSource(lhs, variable)); + } + throw "Could not integrate due to no input being a variable"; +} + function invertTan(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.atan(value)); @@ -282,6 +607,13 @@ function invertTan(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateTan(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.cos(unrefFormulaSource(lhs, variable)).ln().neg(); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAsin(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.sin(value)); @@ -289,6 +621,16 @@ function invertAsin(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAsin(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.asin(x) + .times(x) + .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAcos(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.cos(value)); @@ -296,6 +638,16 @@ function invertAcos(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAcos(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.acos(x) + .times(x) + .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAtan(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.tan(value)); @@ -303,6 +655,16 @@ function invertAtan(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAtan(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.atan(x) + .times(x) + .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2)); + } + throw "Could not integrate due to no input being a variable"; +} + function invertSinh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.asinh(value)); @@ -310,6 +672,14 @@ function invertSinh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateSinh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.cosh(x); + } + throw "Could not integrate due to no input being a variable"; +} + function invertCosh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.acosh(value)); @@ -317,6 +687,14 @@ function invertCosh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateCosh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.sinh(x); + } + throw "Could not integrate due to no input being a variable"; +} + function invertTanh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.atanh(value)); @@ -324,6 +702,14 @@ function invertTanh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateTanh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.cosh(x).ln(); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAsinh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.sinh(value)); @@ -331,6 +717,14 @@ function invertAsinh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAsinh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + 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"; +} + function invertAcosh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.cosh(value)); @@ -338,6 +732,16 @@ function invertAcosh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAcosh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.acosh(x) + .times(x) + .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt())); + } + throw "Could not integrate due to no input being a variable"; +} + function invertAtanh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.tanh(value)); @@ -345,6 +749,16 @@ function invertAtanh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } +function integrateAtanh(variable: DecimalSource | undefined, lhs: FormulaSource) { + if (hasVariable(lhs)) { + const x = unrefFormulaSource(lhs, variable); + return Decimal.atanh(x) + .times(x) + .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2)); + } + throw "Could not integrate due to no input being a variable"; +} + export default class Formula { readonly inputs: T; @@ -354,66 +768,114 @@ export default class Formula { private readonly internalInvert: | ((value: DecimalSource, ...inputs: T) => DecimalSource) | undefined; + private readonly internalIntegrate: + | ((variable: DecimalSource | undefined, ...inputs: T) => DecimalSource) + | undefined; + private readonly internalInvertIntegral: + | ((value: DecimalSource, ...inputs: T) => DecimalSource) + | undefined; private readonly internalHasVariable: boolean; - constructor( - inputs: T, - evaluate?: (this: Formula, ...inputs: GuardedFormulasToDecimals) => DecimalSource, - invert?: ( - this: Formula, - value: DecimalSource, - ...inputs: [...T, ...unknown[]] - ) => DecimalSource, - hasVariable = false - ) { - if (inputs.length !== 1 && evaluate == null) { - throw "Evaluate function is required if inputs is not length 1"; + constructor(options: FormulaOptions) { + // Variable case + if ("variable" in options) { + this.inputs = [options.variable] as T; + this.internalHasVariable = true; + return; } - if (inputs.length !== 1 && invert == null && hasVariable) { - throw "A formula cannot be marked as having a variable if it is not invertible and inputs is not length 1"; - } - - this.inputs = inputs; - this.internalEvaluate = evaluate; - - if ( - inputs.some(input => input instanceof Formula && !input.isInvertible()) || - (hasVariable === false && evaluate != null && invert == null) - ) { + // Constant case + if (!("evaluate" in options)) { + if (options.inputs.length !== 1) { + throw "Evaluate function is required if inputs is not length 1"; + } + this.inputs = options.inputs as T; this.internalHasVariable = false; return; } + const { inputs, evaluate, invert, integrate, invertIntegral, hasVariable } = options; + if (invert == null && invertIntegral == null && hasVariable) { + throw "A formula cannot be marked as having a variable if it is not invertible"; + } + + this.inputs = inputs; + this.internalEvaluate = evaluate; + this.internalIntegrate = integrate; + const numVariables = inputs.filter( input => input instanceof Formula && input.hasVariable() ).length; + const variable = inputs.find(input => input instanceof Formula && input.hasVariable()) as + | GenericFormula + | undefined; - // ??? + this.internalHasVariable = + numVariables === 1 || (numVariables === 0 && hasVariable === true); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - this.internalInvert = numVariables <= 1 ? invert : undefined; - this.internalHasVariable = numVariables === 1 || (numVariables === 0 && hasVariable); + this.internalInvert = + this.internalHasVariable && variable?.isInvertible() ? invert : undefined; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.internalInvertIntegral = + this.internalHasVariable && variable?.isIntegralInvertible() ? invert : undefined; } isInvertible(): this is InvertibleFormula { - return this.internalInvert != null || this.internalEvaluate == null; + return ( + this.internalHasVariable && + (this.internalInvert != null || this.internalEvaluate == null) + ); + } + + isIntegrable(): this is IntegrableFormula { + return this.internalHasVariable && this.internalIntegrate != null; + } + + isIntegralInvertible(): this is InvertibleIntegralFormula { + return this.internalHasVariable && this.internalInvertIntegral != null; } hasVariable(): boolean { return this.internalHasVariable; } - evaluate(): DecimalSource { + /** + * Evaluate the current result of the formula + * @param variable Optionally override the value of the variable while evaluating. Ignored if there is not variable + */ + evaluate(variable?: DecimalSource): DecimalSource { return ( this.internalEvaluate?.call( this, - ...(this.inputs.map(unrefFormulaSource) as GuardedFormulasToDecimals) - ) ?? unrefFormulaSource(this.inputs[0]) + ...(this.inputs.map(input => + unrefFormulaSource(input, variable) + ) as GuardedFormulasToDecimals) + ) ?? + variable ?? + unrefFormulaSource(this.inputs[0]) ); } invert(value: DecimalSource): DecimalSource { - return this.internalInvert?.call(this, value, ...this.inputs) ?? value; + if (this.internalInvert) { + return this.internalInvert.call(this, value, ...this.inputs); + } else if (this.inputs.length === 1 && this.internalHasVariable) { + return value; + } + throw "Cannot invert non-invertible formula"; + } + + evaluateIntegral(variable?: DecimalSource): DecimalSource { + return ( + this.internalIntegrate?.call(this, variable, ...this.inputs) ?? + variable ?? + unrefFormulaSource(this.inputs[0]) + ); + } + + invertIntegral(value: DecimalSource): DecimalSource { + return this.internalInvertIntegral?.call(this, value, ...this.inputs) ?? value; } equals(other: GenericFormula): boolean { @@ -428,10 +890,24 @@ export default class Formula { ) && this.internalEvaluate === other.internalEvaluate && this.internalInvert === other.internalInvert && + this.internalIntegrate === other.internalIntegrate && + this.internalInvertIntegral === other.internalInvertIntegral && this.internalHasVariable === other.internalHasVariable ); } + public static constant( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula { + return new Formula({ inputs: [value] }) as InvertibleFormula; + } + + public static variable( + value: ProcessedComputable + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula { + return new Formula({ variable: value }) as InvertibleFormula; + } + /** * Creates a step-wise formula. After {@ref start} the formula will have an additional modifier. * This function assumes the incoming {@ref value} will be continuous and monotonically increasing. @@ -443,7 +919,7 @@ export default class Formula { value: FormulaSource, start: Computable, formulaModifier: (value: Ref) => GenericFormula - ) { + ): GenericFormula { const lhsRef = ref(0); const formula = formulaModifier(lhsRef); const processedStart = convertComputable(start); @@ -466,18 +942,18 @@ export default class Formula { } throw "Could not invert due to no input being a variable"; } - return new Formula( - [value], - evalStep, - formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined - ); + return new Formula({ + inputs: [value], + evaluate: evalStep, + invert: formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined + }); } public static if( value: FormulaSource, condition: Computable, formulaModifier: (value: Ref) => GenericFormula - ) { + ): GenericFormula { const lhsRef = ref(0); const formula = formulaModifier(lhsRef); const processedCondition = convertComputable(condition); @@ -499,11 +975,11 @@ export default class Formula { return lhs.invert(value); } } - return new Formula( - [value], - evalStep, - formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined - ); + return new Formula({ + inputs: [value], + evaluate: evalStep, + invert: formula.isInvertible() && !formula.hasVariable() ? invertStep : undefined + }); } public static conditional( value: FormulaSource, @@ -513,22 +989,19 @@ export default class Formula { return Formula.if(value, condition, formulaModifier); } - public static constant(value: InvertibleFormulaSource): InvertibleFormula { - return new Formula([value]) as InvertibleFormula; - } - - public static variable(value: ProcessedComputable): InvertibleFormula { - return new Formula([value], undefined, undefined, true) as InvertibleFormula; - } - public static abs(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.abs); + return new Formula({ inputs: [value], evaluate: Decimal.abs }); } - public static neg(value: InvertibleFormulaSource): InvertibleFormula; + public static neg(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static neg(value: FormulaSource): GenericFormula; public static neg(value: FormulaSource) { - return new Formula([value], Decimal.neg, invertNeg); + return new Formula({ + inputs: [value], + evaluate: Decimal.neg, + invert: invertNeg, + integrate: integrateNeg + }); } public static negate(value: FormulaSource) { return Formula.neg(value); @@ -538,35 +1011,41 @@ export default class Formula { } public static sign(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.sign); + return new Formula({ inputs: [value], evaluate: Decimal.sign }); } public static sgn(value: FormulaSource) { return Formula.sign(value); } public static round(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.round); + return new Formula({ inputs: [value], evaluate: Decimal.round }); } public static floor(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.floor); + return new Formula({ inputs: [value], evaluate: Decimal.floor }); } public static ceil(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.ceil); + return new Formula({ inputs: [value], evaluate: Decimal.ceil }); } public static trunc(value: FormulaSource): GenericFormula { - return new Formula([value], Decimal.trunc); + return new Formula({ inputs: [value], evaluate: Decimal.trunc }); } public static add( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static add(value: FormulaSource, other: FormulaSource): GenericFormula; public static add(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.add, invertAdd); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.add, + invert: invertAdd, + integrate: integrateAdd, + invertIntegral: invertIntegrateAdd + }); } public static plus(value: FormulaSource, other: FormulaSource) { return Formula.add(value, other); @@ -575,10 +1054,16 @@ export default class Formula { public static sub( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static sub(value: FormulaSource, other: FormulaSource): GenericFormula; public static sub(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.sub, invertSub); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.sub, + invert: invertSub, + integrate: integrateSub, + invertIntegral: invertIntegrateSub + }); } public static subtract(value: FormulaSource, other: FormulaSource) { return Formula.sub(value, other); @@ -590,10 +1075,16 @@ export default class Formula { public static mul( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static mul(value: FormulaSource, other: FormulaSource): GenericFormula; public static mul(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.mul, invertMul); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.mul, + invert: invertMul, + integrate: integrateMul, + invertIntegral: invertIntegrateMul + }); } public static multiply(value: FormulaSource, other: FormulaSource) { return Formula.mul(value, other); @@ -605,19 +1096,33 @@ export default class Formula { public static div( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static div(value: FormulaSource, other: FormulaSource): GenericFormula; public static div(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.div, invertDiv); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.div, + invert: invertDiv, + integrate: integrateDiv, + invertIntegral: invertIntegrateDiv + }); } public static divide(value: FormulaSource, other: FormulaSource) { return Formula.div(value, other); } - public static recip(value: InvertibleFormulaSource): InvertibleFormula; + public static recip( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static recip(value: FormulaSource): GenericFormula; public static recip(value: FormulaSource) { - return new Formula([value], Decimal.recip, invertRecip); + return new Formula({ + inputs: [value], + evaluate: Decimal.recip, + invert: invertRecip, + integrate: integrateRecip, + invertIntegral: invertIntegrateRecip + }); } public static reciprocal(value: FormulaSource): GenericFormula { return Formula.recip(value); @@ -627,19 +1132,19 @@ export default class Formula { } public static max(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula([value, other], Decimal.max); + return new Formula({ inputs: [value, other], evaluate: Decimal.max }); } public static min(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula([value, other], Decimal.min); + return new Formula({ inputs: [value, other], evaluate: Decimal.min }); } public static minabs(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula([value, other], Decimal.minabs); + return new Formula({ inputs: [value, other], evaluate: Decimal.minabs }); } public static maxabs(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula([value, other], Decimal.maxabs); + return new Formula({ inputs: [value, other], evaluate: Decimal.maxabs }); } public static clamp( @@ -647,108 +1152,169 @@ export default class Formula { min: FormulaSource, max: FormulaSource ): GenericFormula { - return new Formula([value, min, max], Decimal.clamp); + return new Formula({ inputs: [value, min, max], evaluate: Decimal.clamp }); } public static clampMin(value: FormulaSource, min: FormulaSource): GenericFormula { - return new Formula([value, min], Decimal.clampMin); + return new Formula({ inputs: [value, min], evaluate: Decimal.clampMin }); } public static clampMax(value: FormulaSource, max: FormulaSource): GenericFormula { - return new Formula([value, max], Decimal.clampMax); + return new Formula({ inputs: [value, max], evaluate: Decimal.clampMax }); } public static pLog10(value: InvertibleFormulaSource): InvertibleFormula; public static pLog10(value: FormulaSource): GenericFormula; public static pLog10(value: FormulaSource) { - return new Formula([value], Decimal.pLog10); + return new Formula({ inputs: [value], evaluate: Decimal.pLog10 }); } public static absLog10(value: InvertibleFormulaSource): InvertibleFormula; public static absLog10(value: FormulaSource): GenericFormula; public static absLog10(value: FormulaSource) { - return new Formula([value], Decimal.absLog10); + return new Formula({ inputs: [value], evaluate: Decimal.absLog10 }); } - public static log10(value: InvertibleFormulaSource): InvertibleFormula; + public static log10( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static log10(value: FormulaSource): GenericFormula; public static log10(value: FormulaSource) { - return new Formula([value], Decimal.log10, invertLog10); + return new Formula({ + inputs: [value], + evaluate: Decimal.log10, + invert: invertLog10, + integrate: integrateLog10, + invertIntegral: invertIntegrateLog10 + }); } public static log( value: InvertibleFormulaSource, base: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static log(value: FormulaSource, base: FormulaSource): GenericFormula; public static log(value: FormulaSource, base: FormulaSource) { - return new Formula([value, base], Decimal.log, invertLog); + return new Formula({ + inputs: [value, base], + evaluate: Decimal.log, + invert: invertLog, + integrate: integrateLog, + invertIntegral: invertIntegrateLog + }); } public static logarithm(value: FormulaSource, base: FormulaSource) { return Formula.log(value, base); } - public static log2(value: InvertibleFormulaSource): InvertibleFormula; + public static log2( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static log2(value: FormulaSource): GenericFormula; public static log2(value: FormulaSource) { - return new Formula([value], Decimal.log2, invertLog2); + return new Formula({ + inputs: [value], + evaluate: Decimal.log2, + invert: invertLog2, + integrate: integrateLog2, + invertIntegral: invertIntegrateLog2 + }); } - public static ln(value: InvertibleFormulaSource): InvertibleFormula; + public static ln( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static ln(value: FormulaSource): GenericFormula; public static ln(value: FormulaSource) { - return new Formula([value], Decimal.ln, invertLn); + return new Formula({ + inputs: [value], + evaluate: Decimal.ln, + invert: invertLn, + integrate: integrateLn, + invertIntegral: invertIntegrateLn + }); } public static pow( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static pow(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.pow, invertPow); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.pow, + invert: invertPow, + integrate: integratePow, + invertIntegral: invertIntegratePow + }); } - public static pow10(value: InvertibleFormulaSource): InvertibleFormula; + public static pow10( + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static pow10(value: FormulaSource): GenericFormula; public static pow10(value: FormulaSource) { - return new Formula([value], Decimal.pow10, invertPow10); + return new Formula({ + inputs: [value], + evaluate: Decimal.pow10, + invert: invertPow10, + integrate: integratePow10, + invertIntegral: invertIntegratePow10 + }); } public static pow_base( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static pow_base(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow_base(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.pow_base, invertPowBase); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.pow_base, + invert: invertPowBase, + integrate: integratePowBase, + invertIntegral: invertIntegratePowBase + }); } public static root( value: InvertibleFormulaSource, other: InvertibleFormulaSource - ): InvertibleFormula; + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; public static root(value: FormulaSource, other: FormulaSource): GenericFormula; public static root(value: FormulaSource, other: FormulaSource) { - return new Formula([value, other], Decimal.root, invertRoot); + return new Formula({ + inputs: [value, other], + evaluate: Decimal.root, + invert: invertRoot, + integrate: integrateRoot, + invertIntegral: invertIntegrateRoot + }); } public static factorial(value: FormulaSource) { - return new Formula([value], Decimal.factorial); + return new Formula({ inputs: [value], evaluate: Decimal.factorial }); } public static gamma(value: FormulaSource) { - return new Formula([value], Decimal.gamma); + return new Formula({ inputs: [value], evaluate: Decimal.gamma }); } public static lngamma(value: FormulaSource) { - return new Formula([value], Decimal.lngamma); + return new Formula({ inputs: [value], evaluate: Decimal.lngamma }); } - public static exp(value: InvertibleFormulaSource): InvertibleFormula; + public static exp(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static exp(value: FormulaSource): GenericFormula; public static exp(value: FormulaSource) { - return new Formula([value], Decimal.exp, invertExp); + return new Formula({ + inputs: [value], + evaluate: Decimal.exp, + invert: invertExp, + integrate: integrateExp + }); } public static sqr(value: FormulaSource) { @@ -782,7 +1348,11 @@ export default class Formula { height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - return new Formula([value, height, payload], tetrate, invertTetrate); + return new Formula({ + inputs: [value, height, payload], + evaluate: tetrate, + invert: invertTetrate + }); } public static iteratedexp( @@ -800,7 +1370,11 @@ export default class Formula { height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - return new Formula([value, height, payload], iteratedexp, invertIteratedExp); + return new Formula({ + inputs: [value, height, payload], + evaluate: iteratedexp, + invert: invertIteratedExp + }); } public static iteratedlog( @@ -808,7 +1382,7 @@ export default class Formula { base: FormulaSource = 10, times: FormulaSource = 1 ): GenericFormula { - return new Formula([value, base, times], iteratedLog); + return new Formula({ inputs: [value, base, times], evaluate: iteratedLog }); } public static slog( @@ -817,11 +1391,11 @@ export default class Formula { ): InvertibleFormula; public static slog(value: FormulaSource, base?: FormulaSource): GenericFormula; public static slog(value: FormulaSource, base: FormulaSource = 10) { - return new Formula([value, base], slog, invertSlog); + return new Formula({ inputs: [value, base], evaluate: slog, invert: invertSlog }); } public static layeradd10(value: FormulaSource, diff: FormulaSource): GenericFormula { - return new Formula([value, diff], Decimal.layeradd10); + return new Formula({ inputs: [value, diff], evaluate: Decimal.layeradd10 }); } public static layeradd( @@ -835,19 +1409,23 @@ export default class Formula { base?: FormulaSource ): GenericFormula; public static layeradd(value: FormulaSource, diff: FormulaSource, base: FormulaSource = 10) { - return new Formula([value, diff, base], layeradd, invertLayeradd); + return new Formula({ + inputs: [value, diff, base], + evaluate: layeradd, + invert: invertLayeradd + }); } public static lambertw(value: InvertibleFormulaSource): InvertibleFormula; public static lambertw(value: FormulaSource): GenericFormula; public static lambertw(value: FormulaSource) { - return new Formula([value], Decimal.lambertw, invertLambertw); + return new Formula({ inputs: [value], evaluate: Decimal.lambertw, invert: invertLambertw }); } public static ssqrt(value: InvertibleFormulaSource): InvertibleFormula; public static ssqrt(value: FormulaSource): GenericFormula; public static ssqrt(value: FormulaSource) { - return new Formula([value], Decimal.ssqrt, invertSsqrt); + return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: invertSsqrt }); } public static pentate( @@ -855,79 +1433,159 @@ export default class Formula { height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ) { - return new Formula([value, height, payload], pentate); + return new Formula({ inputs: [value, height, payload], evaluate: pentate }); } - public static sin(value: InvertibleFormulaSource): InvertibleFormula; + public static sin(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static sin(value: FormulaSource): GenericFormula; public static sin(value: FormulaSource) { - return new Formula([value], Decimal.sin, invertAsin); + return new Formula({ + inputs: [value], + evaluate: Decimal.sin, + invert: invertAsin, + integrate: integrateSin + }); } - public static cos(value: InvertibleFormulaSource): InvertibleFormula; + public static cos(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static cos(value: FormulaSource): GenericFormula; public static cos(value: FormulaSource) { - return new Formula([value], Decimal.cos, invertAcos); + return new Formula({ + inputs: [value], + evaluate: Decimal.cos, + invert: invertAcos, + integrate: integrateCos + }); } - public static tan(value: InvertibleFormulaSource): InvertibleFormula; + public static tan(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static tan(value: FormulaSource): GenericFormula; public static tan(value: FormulaSource) { - return new Formula([value], Decimal.tan, invertAtan); + return new Formula({ + inputs: [value], + evaluate: Decimal.tan, + invert: invertAtan, + integrate: integrateTan + }); } - public static asin(value: InvertibleFormulaSource): InvertibleFormula; + public static asin(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static asin(value: FormulaSource): GenericFormula; public static asin(value: FormulaSource) { - return new Formula([value], Decimal.asin, invertSin); + return new Formula({ + inputs: [value], + evaluate: Decimal.asin, + invert: invertSin, + integrate: integrateAsin + }); } - public static acos(value: InvertibleFormulaSource): InvertibleFormula; + public static acos(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static acos(value: FormulaSource): GenericFormula; public static acos(value: FormulaSource) { - return new Formula([value], Decimal.acos, invertCos); + return new Formula({ + inputs: [value], + evaluate: Decimal.acos, + invert: invertCos, + integrate: integrateAcos + }); } - public static atan(value: InvertibleFormulaSource): InvertibleFormula; + public static atan(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static atan(value: FormulaSource): GenericFormula; public static atan(value: FormulaSource) { - return new Formula([value], Decimal.atan, invertTan); + return new Formula({ + inputs: [value], + evaluate: Decimal.atan, + invert: invertTan, + integrate: integrateAtan + }); } - public static sinh(value: InvertibleFormulaSource): InvertibleFormula; + public static sinh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static sinh(value: FormulaSource): GenericFormula; public static sinh(value: FormulaSource) { - return new Formula([value], Decimal.sinh, invertAsinh); + return new Formula({ + inputs: [value], + evaluate: Decimal.sinh, + invert: invertAsinh, + integrate: integrateSinh + }); } - public static cosh(value: InvertibleFormulaSource): InvertibleFormula; + public static cosh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static cosh(value: FormulaSource): GenericFormula; public static cosh(value: FormulaSource) { - return new Formula([value], Decimal.cosh, invertAcosh); + return new Formula({ + inputs: [value], + evaluate: Decimal.cosh, + invert: invertAcosh, + integrate: integrateCosh + }); } - public static tanh(value: InvertibleFormulaSource): InvertibleFormula; + public static tanh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static tanh(value: FormulaSource): GenericFormula; public static tanh(value: FormulaSource) { - return new Formula([value], Decimal.tanh, invertAtanh); + return new Formula({ + inputs: [value], + evaluate: Decimal.tanh, + invert: invertAtanh, + integrate: integrateTanh + }); } - public static asinh(value: InvertibleFormulaSource): InvertibleFormula; + public static asinh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static asinh(value: FormulaSource): GenericFormula; public static asinh(value: FormulaSource) { - return new Formula([value], Decimal.asinh, invertSinh); + return new Formula({ + inputs: [value], + evaluate: Decimal.asinh, + invert: invertSinh, + integrate: integrateAsinh + }); } - public static acosh(value: InvertibleFormulaSource): InvertibleFormula; + public static acosh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static acosh(value: FormulaSource): GenericFormula; public static acosh(value: FormulaSource) { - return new Formula([value], Decimal.acosh, invertCosh); + return new Formula({ + inputs: [value], + evaluate: Decimal.acosh, + invert: invertCosh, + integrate: integrateAcosh + }); } - public static atanh(value: InvertibleFormulaSource): InvertibleFormula; + public static atanh(value: InvertibleFormulaSource): InvertibleFormula & IntegrableFormula; public static atanh(value: FormulaSource): GenericFormula; public static atanh(value: FormulaSource) { - return new Formula([value], Decimal.atanh, invertTanh); + return new Formula({ + inputs: [value], + evaluate: Decimal.atanh, + invert: invertTanh, + integrate: integrateAtanh + }); + } + + public step( + start: Computable, + formulaModifier: (value: Ref) => GenericFormula + ) { + return Formula.step(this, start, formulaModifier); + } + + public if( + condition: Computable, + formulaModifier: (value: Ref) => GenericFormula + ) { + return Formula.if(this, condition, formulaModifier); + } + public conditional( + condition: Computable, + formulaModifier: (value: Ref) => GenericFormula + ) { + return Formula.if(this, condition, formulaModifier); } public abs() { @@ -967,42 +1625,102 @@ export default class Formula { return Formula.trunc(this); } + public add( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public add(this: FormulaSource, value: FormulaSource): GenericFormula; public add(value: FormulaSource) { return Formula.add(this, value); } + public plus( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public plus(this: FormulaSource, value: FormulaSource): GenericFormula; public plus(value: FormulaSource) { return Formula.add(this, value); } + public sub( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public sub(this: FormulaSource, value: FormulaSource): GenericFormula; public sub(value: FormulaSource) { return Formula.sub(this, value); } + public subtract( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public subtract(this: FormulaSource, value: FormulaSource): GenericFormula; public subtract(value: FormulaSource) { return Formula.sub(this, value); } + public minus( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public minus(this: FormulaSource, value: FormulaSource): GenericFormula; public minus(value: FormulaSource) { return Formula.sub(this, value); } + public mul( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public mul(this: FormulaSource, value: FormulaSource): GenericFormula; public mul(value: FormulaSource) { return Formula.mul(this, value); } + public multiply( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public multiply(this: FormulaSource, value: FormulaSource): GenericFormula; public multiply(value: FormulaSource) { return Formula.mul(this, value); } + public times( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public times(this: FormulaSource, value: FormulaSource): GenericFormula; public times(value: FormulaSource) { return Formula.mul(this, value); } + public div( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public div(this: FormulaSource, value: FormulaSource): GenericFormula; public div(value: FormulaSource) { return Formula.div(this, value); } + public divide( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public divide(this: FormulaSource, value: FormulaSource): GenericFormula; public divide(value: FormulaSource) { return Formula.div(this, value); } + public divideBy( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public divideBy(this: FormulaSource, value: FormulaSource): GenericFormula; public divideBy(value: FormulaSource) { return Formula.div(this, value); } + public dividedBy( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public dividedBy(this: FormulaSource, value: FormulaSource): GenericFormula; public dividedBy(value: FormulaSource) { return Formula.div(this, value); } @@ -1057,9 +1775,19 @@ export default class Formula { return Formula.log10(this); } + public log( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public log(this: FormulaSource, value: FormulaSource): GenericFormula; public log(value: FormulaSource) { return Formula.log(this, value); } + public logarithm( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public logarithm(this: FormulaSource, value: FormulaSource): GenericFormula; public logarithm(value: FormulaSource) { return Formula.log(this, value); } @@ -1072,6 +1800,11 @@ export default class Formula { return Formula.ln(this); } + public pow( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public pow(this: FormulaSource, value: FormulaSource): GenericFormula; public pow(value: FormulaSource) { return Formula.pow(this, value); } @@ -1080,10 +1813,20 @@ export default class Formula { return Formula.pow10(this); } + public pow_base( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public pow_base(this: FormulaSource, value: FormulaSource): GenericFormula; public pow_base(value: FormulaSource) { return Formula.pow_base(this, value); } + public root( + this: InvertibleFormulaSource, + value: InvertibleFormulaSource + ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula; + public root(this: FormulaSource, value: FormulaSource): GenericFormula; public root(value: FormulaSource) { return Formula.root(this, value); } @@ -1118,6 +1861,16 @@ export default class Formula { return Formula.root(this, 3); } + public tetrate( + this: InvertibleFormulaSource, + height: InvertibleFormulaSource, + payload: InvertibleFormulaSource + ): InvertibleFormula; + public tetrate( + this: FormulaSource, + height: FormulaSource, + payload: FormulaSource + ): GenericFormula; public tetrate( height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) @@ -1125,6 +1878,16 @@ export default class Formula { return Formula.tetrate(this, height, payload); } + public iteratedexp( + this: InvertibleFormulaSource, + height: InvertibleFormulaSource, + payload: InvertibleFormulaSource + ): InvertibleFormula; + public iteratedexp( + this: FormulaSource, + height: FormulaSource, + payload: FormulaSource + ): GenericFormula; public iteratedexp( height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) @@ -1136,6 +1899,8 @@ export default class Formula { return Formula.iteratedlog(this, base, times); } + public slog(this: InvertibleFormulaSource, base?: InvertibleFormulaSource): InvertibleFormula; + public slog(this: FormulaSource, base?: FormulaSource): GenericFormula; public slog(base: FormulaSource = 10) { return Formula.slog(this, base); } @@ -1144,6 +1909,12 @@ export default class Formula { return Formula.layeradd10(this, diff); } + public layeradd( + this: InvertibleFormulaSource, + diff: InvertibleFormulaSource, + base?: InvertibleFormulaSource + ): InvertibleFormula; + public layeradd(this: FormulaSource, diff: FormulaSource, base?: FormulaSource): GenericFormula; public layeradd(diff: FormulaSource, base: FormulaSource) { return Formula.layeradd(this, diff, base); } @@ -1211,3 +1982,43 @@ export default class Formula { return Formula.atanh(this); } } + +/** + * 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. Returns a ref of how many can be bought, as well as how much that will cost. + * @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 + */ +export function calculateMaxAffordable( + formula: GenericFormula, + resource: Resource, + spendResources: Computable = true +) { + const computedSpendResources = convertComputable(spendResources); + const maxAffordable = computed(() => { + if (unref(computedSpendResources)) { + if (!formula.isIntegrable() || !formula.isIntegralInvertible()) { + throw "Cannot calculate max affordable of formula with non-invertible integral"; + } + return Decimal.floor( + formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral())) + ); + } else { + if (!formula.isInvertible()) { + throw "Cannot calculate max affordable of non-invertible formula"; + } + return Decimal.floor((formula as InvertibleFormula).invert(resource.value)); + } + }); + const cost = computed(() => { + if (unref(computedSpendResources)) { + return Decimal.sub( + formula.evaluateIntegral(maxAffordable.value), + formula.evaluateIntegral() + ); + } else { + return formula.evaluate(maxAffordable.value); + } + }); + return { maxAffordable, cost }; +} diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 94ba563..f8b4490 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1,7 +1,13 @@ -import Formula, { GenericFormula, InvertibleFormula, unrefFormulaSource } from "game/formulas"; +import { createResource, Resource } from "features/resources/resource"; +import Formula, { + calculateMaxAffordable, + GenericFormula, + InvertibleFormula, + unrefFormulaSource +} from "game/formulas"; import Decimal, { DecimalSource, format } from "util/bignum"; import { beforeAll, describe, expect, test } from "vitest"; -import { Ref, ref } from "vue"; +import { ref } from "vue"; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; @@ -43,98 +49,6 @@ declare global { } } -function testConstant( - desc: string, - formulaFunc: () => InvertibleFormula, - expectedValue: DecimalSource = 10 -) { - describe(desc, () => { - let formula: GenericFormula; - beforeAll(() => { - formula = formulaFunc(); - }); - test("Evaluates correctly", () => - expect(formula.evaluate()).compare_tolerance(expectedValue)); - test("Invert is pass-through", () => expect(formula.invert(25)).compare_tolerance(25)); - test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); - }); -} - -function testFormula( - functionName: T, - args: Readonly>, - invertible = true -) { - let formula: GenericFormula; - beforeAll(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - formula = Formula[functionName](...args); - }); - test("Formula is not marked as having a variable", () => - expect(formula.hasVariable()).toBe(false)); - test(`Formula is${invertible ? "" : " not"} invertible`, () => - expect(formula.isInvertible()).toBe(invertible)); - if (invertible) { - test(`Formula throws if inverting without any variables`, () => - expect(() => formula.invert(10)).toThrow()); - } -} - -// Utility function that will test all the different -// It's a lot of tests, but I'd rather be exhaustive -function testFormulaCall( - functionName: T, - args: Readonly> -) { - let testName = functionName + "("; - for (let i = 0; i < args.length; i++) { - if (i !== 0) { - testName += ", "; - } - testName += args[i]; - } - testName += ") evaluates correctly"; - test(testName, () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const formula = Formula[functionName](...args); - - try { - const expectedEvaluation = Decimal[functionName]( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - ...args.map(i => unrefFormulaSource(i)) - ); - if (expectedEvaluation != null) { - expect(formula.evaluate()).compare_tolerance(expectedEvaluation); - } - } catch { - // If this is an invalid Decimal operation, then ignore this test case - } - }); -} - -function testAliases( - aliases: T[], - args: Parameters -) { - describe(aliases[0], () => { - let formula: GenericFormula; - beforeAll(() => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - formula = Formula[aliases[0]](...args); - }); - - aliases.slice(1).forEach(alias => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - test(alias, () => expect(Formula[alias](...args).equals(formula)).toBe(true)); - }); - }); -} - const testValues = ["-1e400", 0, 0.25] as const; const invertibleZeroParamFunctionNames = [ @@ -164,7 +78,6 @@ const invertibleZeroParamFunctionNames = [ "acosh", "atanh" ] as const; - const nonInvertibleZeroParamFunctionNames = [ "abs", "sign", @@ -178,6 +91,64 @@ const nonInvertibleZeroParamFunctionNames = [ "gamma", "lngamma" ] as const; +const integrableZeroParamFunctionNames = [ + "neg", + "recip", + "log10", + "log2", + "ln", + "pow10", + "exp", + "sqr", + "sqrt", + "cube", + "cbrt", + "sin", + "cos", + "tan", + "asin", + "acos", + "atan", + "sinh", + "cosh", + "tanh", + "asinh", + "acosh", + "atanh" +] as const; +const nonIntegrableZeroParamFunctionNames = [ + ...nonInvertibleZeroParamFunctionNames, + "lambertw", + "ssqrt" +] as const; +const invertibleIntegralZeroPramFunctionNames = [ + "recip", + "log10", + "log2", + "ln", + "pow10", + "sqr", + "sqrt", + "cube", + "cbrt" +] as const; +const nonInvertibleIntegralZeroPramFunctionNames = [ + ...nonIntegrableZeroParamFunctionNames, + "neg", + "exp", + "sin", + "cos", + "tan", + "asin", + "acos", + "atan", + "sinh", + "cosh", + "tanh", + "asinh", + "acosh", + "atanh" +] as const; const invertibleOneParamFunctionNames = [ "add", @@ -189,7 +160,6 @@ const invertibleOneParamFunctionNames = [ "root", "slog" ] as const; - const nonInvertibleOneParamFunctionNames = [ "max", "min", @@ -199,10 +169,18 @@ const nonInvertibleOneParamFunctionNames = [ "clampMax", "layeradd10" ] as const; +const integrableOneParamFunctionNames = ["add", "sub", "mul", "div", "log", "pow", "root"] as const; +const nonIntegrableOneParamFunctionNames = [...nonInvertibleOneParamFunctionNames, "slog"] as const; +const invertibleIntegralOneParamFunctionNames = integrableOneParamFunctionNames; +const nonInvertibleIntegralOneParamFunctionNames = nonIntegrableOneParamFunctionNames; const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] as const; - const nonInvertibleTwoParamFunctionNames = ["clamp", "iteratedlog", "pentate"] as const; +const nonIntegrableTwoParamFunctionNames = [ + ...invertibleTwoParamFunctionNames, + ...nonInvertibleZeroParamFunctionNames +]; +const nonInvertibleIntegralTwoParamFunctionNames = nonIntegrableTwoParamFunctionNames; describe("Formula Equality Checking", () => { describe("Equality Checks", () => { @@ -214,6 +192,25 @@ describe("Formula Equality Checking", () => { }); describe("Formula aliases", () => { + function testAliases( + aliases: T[], + args: Parameters + ) { + describe(aliases[0], () => { + let formula: GenericFormula; + beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + formula = Formula[aliases[0]](...args); + }); + + aliases.slice(1).forEach(alias => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + test(alias, () => expect(Formula[alias](...args).equals(formula)).toBe(true)); + }); + }); + } testAliases(["neg", "negate", "negated"], [1]); testAliases(["recip", "reciprocal", "reciprocate"], [1]); testAliases(["sign", "sgn"], [1]); @@ -263,6 +260,30 @@ describe("Formula Equality Checking", () => { describe("Creating Formulas", () => { describe("Constants", () => { + function testConstant( + desc: string, + formulaFunc: () => InvertibleFormula, + expectedValue: DecimalSource = 10 + ) { + describe(desc, () => { + let formula: GenericFormula; + beforeAll(() => { + formula = formulaFunc(); + }); + test("Is not invertible", () => expect(formula.isInvertible()).toBe(false)); + test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false)); + test("Integral is not invertible", () => + expect(formula.isIntegralInvertible()).toBe(false)); + test("Is not marked as having a variable", () => + expect(formula.hasVariable()).toBe(false)); + test("Evaluates correctly", () => + expect(formula.evaluate()).compare_tolerance(expectedValue)); + test("Invert throws", () => expect(() => formula.invert(25)).toThrow()); + test("Integrate throws", () => expect(() => formula.evaluateIntegral()).toThrow()); + test("Invert integral throws", () => + expect(() => formula.invertIntegral(25)).toThrow()); + }); + } testConstant("number", () => Formula.constant(10)); testConstant("string", () => Formula.constant("10")); testConstant("formula", () => Formula.constant(Formula.constant(10))); @@ -270,10 +291,64 @@ describe("Creating Formulas", () => { testConstant("ref", () => Formula.constant(ref(10))); }); + function checkFormula( + functionName: T, + args: Readonly> + ) { + let formula: GenericFormula; + beforeAll(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + formula = Formula[functionName](...args); + }); + // None of these formulas have variables, so they should all behave the same + test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); + test("Is not invertible", () => expect(formula.isInvertible()).toBe(false)); + test(`Formula throws if trying to invert`, () => + expect(() => formula.invert(10)).toThrow()); + test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false)); + test("Has a non-invertible integral", () => + expect(formula.isIntegralInvertible()).toBe(false)); + } + + // Utility function that will test all the different + // It's a lot of tests, but I'd rather be exhaustive + function testFormulaCall( + functionName: T, + args: Readonly> + ) { + let testName = functionName + "("; + for (let i = 0; i < args.length; i++) { + if (i !== 0) { + testName += ", "; + } + testName += args[i]; + } + testName += ") evaluates correctly"; + test(testName, () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const formula = Formula[functionName](...args); + + try { + const expectedEvaluation = Decimal[functionName]( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...args.map(i => unrefFormulaSource(i)) + ); + if (expectedEvaluation != null) { + expect(formula.evaluate()).compare_tolerance(expectedEvaluation); + } + } catch { + // If this is an invalid Decimal operation, then ignore this test case + } + }); + } + describe("Invertible 0-param", () => { invertibleZeroParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0] as const); + checkFormula(names, [0] as const); testValues.forEach(i => testFormulaCall(names, [i] as const)); }) ); @@ -281,7 +356,7 @@ describe("Creating Formulas", () => { describe("Non-Invertible 0-param", () => { nonInvertibleZeroParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0] as const, false); + checkFormula(names, [0] as const); testValues.forEach(i => testFormulaCall(names, [i] as const)); }) ); @@ -289,7 +364,7 @@ describe("Creating Formulas", () => { describe("Invertible 1-param", () => { invertibleOneParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0, 0] as const); + checkFormula(names, [0, 0] as const); testValues.forEach(i => testValues.forEach(j => testFormulaCall(names, [i, j] as const)) ); @@ -299,7 +374,7 @@ describe("Creating Formulas", () => { describe("Non-Invertible 1-param", () => { nonInvertibleOneParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0, 0] as const, false); + checkFormula(names, [0, 0] as const); testValues.forEach(i => testValues.forEach(j => testFormulaCall(names, [i, j] as const)) ); @@ -309,7 +384,7 @@ describe("Creating Formulas", () => { describe("Invertible 2-param", () => { invertibleTwoParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0, 0, 0] as const); + checkFormula(names, [0, 0, 0] as const); testValues.forEach(i => testValues.forEach(j => testValues.forEach(k => testFormulaCall(names, [i, j, k] as const)) @@ -321,7 +396,7 @@ describe("Creating Formulas", () => { describe("Non-Invertible 2-param", () => { nonInvertibleTwoParamFunctionNames.forEach(names => describe(names, () => { - testFormula(names, [0, 0, 0] as const, false); + checkFormula(names, [0, 0, 0] as const); testValues.forEach(i => testValues.forEach(j => testValues.forEach(k => testFormulaCall(names, [i, j, k] as const)) @@ -342,14 +417,27 @@ describe("Variables", () => { test("Created variable is marked as a variable", () => expect(variable.hasVariable()).toBe(true)); - test("Evaluate() returns variable's value", () => + test("evaluate() returns variable's value", () => expect(variable.evaluate()).compare_tolerance(10)); - test("Invert() is pass-through", () => expect(variable.invert(100)).compare_tolerance(100)); + test("evaluate(variable) overrides variable value", () => + expect(variable.add(10).evaluate(20)).compare_tolerance(30)); test("Nested variable is marked as having a variable", () => expect(variable.add(10).div(3).pow(2).hasVariable()).toBe(true)); test("Nested non-variable is marked as not having a variable", () => expect(constant.add(10).div(3).pow(2).hasVariable()).toBe(false)); +}); + +describe("Inverting", () => { + let variable: GenericFormula; + let constant: GenericFormula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("variable.invert() is pass-through", () => + expect(variable.invert(100)).compare_tolerance(100)); describe("Invertible Formulas correctly calculate when they contain a variable", () => { function checkFormula(formula: GenericFormula, expectedBool = true) { @@ -392,42 +480,43 @@ describe("Variables", () => { }); }); - describe("Non-Invertible Formulas never marked as having a variable", () => { + describe("Non-invertible formulas marked as such", () => { function checkFormula(formula: GenericFormula) { expect(formula.isInvertible()).toBe(false); - expect(formula.hasVariable()).toBe(false); + expect(formula.isIntegrable()).toBe(false); + expect(formula.isIntegralInvertible()).toBe(false); } nonInvertibleZeroParamFunctionNames.forEach(name => { describe(name, () => { - test(`${name}(var) is marked as not invertible and not having a variable`, () => + test(`${name}(var) is marked as not invertible`, () => checkFormula(Formula[name](variable))); }); }); nonInvertibleOneParamFunctionNames.forEach(name => { describe(name, () => { - test(`${name}(var, const) is marked as not invertible and not having a variable`, () => + test(`${name}(var, const) is marked as not invertible`, () => checkFormula(Formula[name](variable, constant))); - test(`${name}(const, var) is marked as not invertible and not having a variable`, () => + test(`${name}(const, var) is marked as not invertible`, () => checkFormula(Formula[name](constant, variable))); - test(`${name}(var, var) is marked as not invertible and not having a variable`, () => + test(`${name}(var, var) is marked as not invertible`, () => checkFormula(Formula[name](variable, variable))); }); }); nonInvertibleTwoParamFunctionNames.forEach(name => { describe(name, () => { - test(`${name}(var, const, const) is marked as not invertible and not having a variable`, () => + test(`${name}(var, const, const) is marked as not invertible`, () => checkFormula(Formula[name](variable, constant, constant))); - test(`${name}(const, var, const) is marked as not invertible and not having a variable`, () => + test(`${name}(const, var, const) is marked as not invertible`, () => checkFormula(Formula[name](constant, variable, constant))); - test(`${name}(const, const, var) is marked as not invertible and not having a variable`, () => + test(`${name}(const, const, var) is marked as not invertible`, () => checkFormula(Formula[name](constant, constant, variable))); - test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => + test(`${name}(var, var, const) is marked as not invertible`, () => checkFormula(Formula[name](variable, variable, constant))); - test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () => + test(`${name}(var, const, var) is marked as not invertible`, () => checkFormula(Formula[name](variable, constant, variable))); - test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () => + test(`${name}(const, var, var) is marked as not invertible`, () => checkFormula(Formula[name](constant, variable, variable))); - test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () => + test(`${name}(var, var, var) is marked as not invertible`, () => checkFormula(Formula[name](variable, variable, variable))); }); }); @@ -476,6 +565,164 @@ describe("Variables", () => { }); }); +describe("Integrating", () => { + let variable: GenericFormula; + let constant: GenericFormula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("evaluateIntegral() returns variable's value", () => + expect(variable.evaluate()).compare_tolerance(10)); + test("evaluateIntegral(variable) overrides variable value", () => + expect(variable.add(10).evaluateIntegral(20)).compare_tolerance(400)); + + describe("Integrable functions marked as such", () => { + function checkFormula(formula: GenericFormula) { + expect(formula.isIntegrable()).toBe(true); + expect(() => formula.evaluateIntegral()).to.not.throw(); + } + integrableZeroParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as integrable`, () => + checkFormula(Formula[name](variable))); + }); + }); + integrableOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as integrable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as integrable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as integrable`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + }); + + describe("Non-Integrable functions marked as such", () => { + function checkFormula(formula: GenericFormula) { + expect(formula.isIntegrable()).toBe(false); + } + nonIntegrableZeroParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as not integrable`, () => + checkFormula(Formula[name](variable))); + }); + }); + nonIntegrableOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + nonIntegrableTwoParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant, constant))); + test(`${name}(const, var, const) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable, constant))); + test(`${name}(const, const, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, constant, variable))); + test(`${name}(var, var, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable, constant))); + test(`${name}(var, const, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant, variable))); + test(`${name}(const, var, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable, variable))); + test(`${name}(var, var, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable, variable))); + }); + }); + }); + + // TODO I think these tests will require writing at least one known example for every function + describe.todo("Integrable formulas integrate correctly"); +}); + +describe("Inverting integrals", () => { + let variable: GenericFormula; + let constant: GenericFormula; + beforeAll(() => { + variable = Formula.variable(10); + constant = Formula.constant(10); + }); + + test("variable.invertIntegral() is pass-through", () => + expect(variable.invertIntegral(20)).compare_tolerance(20)); + + describe("Invertible Integral functions marked as such", () => { + function checkFormula(formula: GenericFormula) { + expect(formula.isIntegralInvertible()).toBe(true); + expect(() => formula.invertIntegral(10)).to.not.throw(); + } + invertibleIntegralZeroPramFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as having an invertible integral`, () => + checkFormula(Formula[name](variable))); + }); + }); + invertibleIntegralOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as having an invertible integral`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as having an invertible integral`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as having an invertible integral`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + }); + + describe("Non-Invertible integral functions marked as such", () => { + function checkFormula(formula: GenericFormula) { + expect(formula.isIntegralInvertible()).toBe(false); + } + nonInvertibleIntegralZeroPramFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var) is marked as not integrable`, () => + checkFormula(Formula[name](variable))); + }); + }); + nonInvertibleIntegralOneParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant))); + test(`${name}(const, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable))); + test(`${name}(var, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable))); + }); + }); + nonInvertibleIntegralTwoParamFunctionNames.forEach(name => { + describe(name, () => { + test(`${name}(var, const, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant, constant))); + test(`${name}(const, var, const) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable, constant))); + test(`${name}(const, const, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, constant, variable))); + test(`${name}(var, var, const) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable, constant))); + test(`${name}(var, const, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, constant, variable))); + test(`${name}(const, var, var) is marked as not integrable`, () => + checkFormula(Formula[name](constant, variable, variable))); + test(`${name}(var, var, var) is marked as not integrable`, () => + checkFormula(Formula[name](variable, variable, variable))); + }); + }); + }); + + // TODO I think these tests will require writing at least one known example for every function + describe.todo("Invertible Integral formulas invert correctly"); +}); + describe("Step-wise", () => { let variable: GenericFormula; let constant: GenericFormula; @@ -485,7 +732,7 @@ describe("Step-wise", () => { }); test("Formula without variable is marked as such", () => { - expect(Formula.step(constant, 10, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.step(constant, 10, value => Formula.sqrt(value)).isInvertible()).toBe(false); expect(Formula.step(constant, 10, value => Formula.sqrt(value)).hasVariable()).toBe(false); }); @@ -499,6 +746,24 @@ describe("Step-wise", () => { expect(Formula.step(constant, 10, value => Formula.abs(value)).hasVariable()).toBe(false); }); + test("Formula never marked integrable", () => { + expect(Formula.step(constant, 10, value => Formula.add(value, 10)).isIntegrable()).toBe( + false + ); + expect(() => + Formula.step(constant, 10, value => Formula.add(value, 10)).evaluateIntegral() + ).toThrow(); + }); + + test("Formula never marked as having an invertible integral", () => { + expect( + Formula.step(constant, 10, value => Formula.add(value, 10)).isIntegralInvertible() + ).toBe(false); + expect(() => + Formula.step(constant, 10, value => Formula.add(value, 10)).invertIntegral(10) + ).toThrow(); + }); + test("Formula modifiers with variables mark formula as non-invertible", () => { expect( Formula.step(constant, 10, value => Formula.add(value, variable)).isInvertible() @@ -540,7 +805,7 @@ describe("Conditionals", () => { }); test("Formula without variable is marked as such", () => { - expect(Formula.if(constant, true, value => Formula.sqrt(value)).isInvertible()).toBe(true); + expect(Formula.if(constant, true, value => Formula.sqrt(value)).isInvertible()).toBe(false); expect(Formula.if(constant, true, value => Formula.sqrt(value)).hasVariable()).toBe(false); }); @@ -554,6 +819,24 @@ describe("Conditionals", () => { expect(Formula.if(constant, true, value => Formula.abs(value)).hasVariable()).toBe(false); }); + test("Formula never marked integrable", () => { + expect(Formula.if(constant, true, value => Formula.add(value, 10)).isIntegrable()).toBe( + false + ); + expect(() => + Formula.if(constant, true, value => Formula.add(value, 10)).evaluateIntegral() + ).toThrow(); + }); + + test("Formula never marked as having an invertible integral", () => { + expect( + Formula.if(constant, true, value => Formula.add(value, 10)).isIntegralInvertible() + ).toBe(false); + expect(() => + Formula.if(constant, true, value => Formula.add(value, 10)).invertIntegral(10) + ).toThrow(); + }); + test("Formula modifiers with variables mark formula as non-invertible", () => { expect( Formula.if(constant, true, value => Formula.add(value, variable)).isInvertible() @@ -587,53 +870,143 @@ describe("Conditionals", () => { }); describe("Custom Formulas", () => { - describe("Formula with just one input", () => { - let formula: GenericFormula; - beforeAll(() => { - formula = new Formula([10]); - }); - test("Is not invertible", () => expect(formula.isInvertible()).toBe(false)); - test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); - test("Evaluates correctly", () => expect(formula.evaluate()).compare_tolerance(10)); - test("Invert is pass-through", () => expect(formula.invert(20)).compare_tolerance(20)); - }); - - describe("Formula with non-one inputs without required other params", () => { - test("Zero inputs throws", () => expect(() => new Formula([])).toThrow()); - test("Two inputs throws", () => expect(() => new Formula([1, 2])).toThrow()); - test("Zero inputs and invert throws", () => - expect(() => new Formula([], undefined, value => value)).toThrow()); - test("Two inputs and invert throws", () => - expect(() => new Formula([1, 2], undefined, value => value)).toThrow()); - test("Zero inputs and evaluate and hasVariable throws", () => - expect(() => new Formula([], () => 10, undefined, true)).toThrow()); - test("Two inputs and evaluate and hasVariable throws", () => - expect(() => new Formula([1, 2], () => 10, undefined, true)).toThrow()); - }); - describe("Formula with evaluate", () => { test("Zero input evaluates correctly", () => - expect(new Formula([], () => 10).evaluate()).compare_tolerance(10)); + expect(new Formula({ inputs: [], evaluate: () => 10 }).evaluate()).compare_tolerance( + 10 + )); test("One input evaluates correctly", () => - expect(new Formula([1], value => value).evaluate()).compare_tolerance(1)); + expect( + new Formula({ inputs: [1], evaluate: value => value }).evaluate() + ).compare_tolerance(1)); test("Two inputs evaluates correctly", () => - expect(new Formula([1, 2], (v1, v2) => v1).evaluate()).compare_tolerance(1)); + expect( + new Formula({ inputs: [1, 2], evaluate: (v1, v2) => v1 }).evaluate() + ).compare_tolerance(1)); }); describe("Formula with invert", () => { test("Zero input inverts correctly", () => - expect(new Formula([], undefined, value => value).invert(10)).compare_tolerance(10)); + expect( + new Formula({ inputs: [], evaluate: () => 6, invert: value => value }).invert(10) + ).compare_tolerance(10)); test("One input inverts correctly", () => - expect(new Formula([1], undefined, (value, v1) => v1).invert(10)).compare_tolerance(1)); + expect( + new Formula({ inputs: [1], evaluate: () => 10, invert: (value, v1) => v1 }).invert( + 10 + ) + ).compare_tolerance(1)); test("Two inputs inverts correctly", () => expect( - new Formula([1, 2], undefined, (value, v1, v2) => v2).invert(10) + new Formula({ + inputs: [1, 2], + evaluate: () => 10, + invert: (value, v1, v2) => v2 + }).invert(10) ).compare_tolerance(2)); }); - test("Formula with hasVariable", () => { - const formula = new Formula([], undefined, value => value, true); - expect(formula.isInvertible()).toBe(true); - expect(formula.hasVariable()).toBe(true); + describe("Formula with integrate", () => { + test("Zero input integrates correctly", () => + expect( + new Formula({ + inputs: [], + evaluate: () => 10, + integrate: () => 20 + }).evaluateIntegral() + ).compare_tolerance(20)); + test("One input integrates correctly", () => + expect( + new Formula({ + inputs: [1], + evaluate: () => 10, + integrate: val => val ?? 20 + }).evaluateIntegral() + ).compare_tolerance(20)); + test("Two inputs integrates correctly", () => + expect( + new Formula({ + inputs: [1, 2], + evaluate: (v1, v2) => 10, + integrate: (v1, v2) => 3 + }).evaluateIntegral() + ).compare_tolerance(3)); + }); + + describe("Formula with invertIntegral", () => { + test("Zero input inverts integral correctly", () => + expect( + new Formula({ + inputs: [], + evaluate: () => 10, + invertIntegral: () => 1 + }).invertIntegral(8) + ).compare_tolerance(1)); + test("One input inverts integral correctly", () => + expect( + new Formula({ + inputs: [1], + evaluate: () => 10, + invertIntegral: val => 1 + }).invertIntegral(8) + ).compare_tolerance(1)); + test("Two inputs inverts integral correctly", () => + expect( + new Formula({ + inputs: [1, 2], + evaluate: (v1, v2) => 10, + invertIntegral: (v1, v2) => 1 + }).invertIntegral(8) + ).compare_tolerance(1)); + }); +}); + +describe("Buy Max", () => { + let resource: Resource; + beforeAll(() => { + resource = createResource(10); + }); + describe("With spending", () => { + test("Throws on formula with non-invertible integral", () => { + const { maxAffordable, cost } = calculateMaxAffordable( + Formula.neg(10), + resource, + false + ); + expect(() => maxAffordable.value).toThrow(); + expect(() => cost.value).toThrow(); + }); + // https://www.desmos.com/calculator/5vgletdc1p + test("Calculates max affordable and cost correctly", () => { + const variable = Formula.variable(10); + const { maxAffordable, cost } = calculateMaxAffordable( + Formula.pow(1.05, variable), + resource, + false + ); + expect(maxAffordable.value).compare_tolerance(47); + expect(cost.value).compare_tolerance(Decimal.pow(1.05, 47)); + }); + }); + describe("Without spending", () => { + test("Throws on non-invertible formula", () => { + const { maxAffordable, cost } = calculateMaxAffordable( + Formula.abs(10), + resource, + false + ); + expect(() => maxAffordable.value).toThrow(); + expect(() => cost.value).toThrow(); + }); + // https://www.desmos.com/calculator/5vgletdc1p + test("Calculates max affordable and cost correctly", () => { + const variable = Formula.variable(10); + const { maxAffordable, cost } = calculateMaxAffordable( + Formula.pow(1.05, variable), + resource + ); + expect(maxAffordable.value).compare_tolerance(7); + expect(cost.value).compare_tolerance(7.35); + }); }); });