From 6115b6687dfe325afb746b21fce5044845d1f027 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sat, 1 Apr 2023 23:42:12 -0500 Subject: [PATCH] Fixing more tests with integral rework --- src/data/projEntry.tsx | 7 +- src/game/formulas/formulas.ts | 162 +++++++-------- src/game/formulas/operations.ts | 340 ++++++++++++++------------------ src/game/formulas/types.d.ts | 4 +- tests/game/formulas.test.ts | 97 ++++----- tests/utils.ts | 6 +- 6 files changed, 264 insertions(+), 352 deletions(-) diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index df86d6a..55e8500 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -4,7 +4,11 @@ import { createResource, trackBest, trackOOMPS, trackTotal } from "features/reso import type { GenericTree } from "features/trees/tree"; import { branchedResetPropagation, createTree } from "features/trees/tree"; import { globalBus } from "game/events"; -import Formula, { calculateCost, calculateMaxAffordable } from "game/formulas/formulas"; +import Formula, { + calculateCost, + calculateMaxAffordable, + findNonInvertible +} from "game/formulas/formulas"; import type { BaseLayer, GenericLayer } from "game/layers"; import { createLayer } from "game/layers"; import type { Player } from "game/player"; @@ -18,6 +22,7 @@ import prestige from "./layers/prestige"; window.Formula = Formula; window.calculateMaxAffordable = calculateMaxAffordable; window.calculateCost = calculateCost; +window.findNonInvertible = findNonInvertible; window.unref = unref; window.ref = ref; window.createResource = createResource; diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 305ad5b..8161903 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -1,7 +1,8 @@ import { Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; -import { computed, ComputedRef, Ref, ref, unref } from "vue"; +import { computed, ComputedRef, ref, unref } from "vue"; +import * as ops from "./operations"; import type { EvaluateFunction, FormulaOptions, @@ -18,7 +19,6 @@ import type { SubstitutionFunction, SubstitutionStack } from "./types"; -import * as ops from "./operations"; export function hasVariable(value: FormulaSource): value is InvertibleFormula { return value instanceof Formula && value.hasVariable(); @@ -32,13 +32,6 @@ function integrateVariable(this: GenericFormula) { return Formula.pow(this, 2).div(2); } -function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) { - if (variable == null && this.innermostVariable == null) { - throw new Error("Cannot integrate non-existent variable"); - } - return variable ?? unref(this.innermostVariable); -} - /** * A class that can be used for cost/goal functions. It can be evaluated similar to a cost function, but also provides extra features for supported formulas. For example, a lot of math functions can be inverted. * Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently. @@ -53,7 +46,7 @@ export default class Formula { private readonly internalIntegrate: IntegrateFunction | undefined; private readonly internalIntegrateInner: IntegrateFunction | undefined; private readonly applySubstitution: SubstitutionFunction | undefined; - private readonly internalHasVariable: boolean; + private readonly internalVariables: number; public readonly innermostVariable: ProcessedComputable | undefined; @@ -69,7 +62,7 @@ export default class Formula { readonlyProperties = this.setupFormula(options); } this.inputs = readonlyProperties.inputs; - this.internalHasVariable = readonlyProperties.internalHasVariable; + this.internalVariables = readonlyProperties.internalVariables; this.innermostVariable = readonlyProperties.innermostVariable; this.internalEvaluate = readonlyProperties.internalEvaluate; this.internalInvert = readonlyProperties.internalInvert; @@ -85,10 +78,9 @@ export default class Formula { }): InternalFormulaProperties { return { inputs: [variable] as T, - internalHasVariable: true, + internalVariables: 1, innermostVariable: variable, - internalIntegrate: integrateVariable as unknown as IntegrateFunction, - internalIntegrateInner: integrateVariableInner as unknown as IntegrateFunction, + internalIntegrate: integrateVariable, applySubstitution: ops.passthrough as unknown as SubstitutionFunction }; } @@ -99,68 +91,49 @@ export default class Formula { } return { inputs: inputs as T, - internalHasVariable: false + internalVariables: 0 }; } private setupFormula(options: GeneralFormulaOptions): InternalFormulaProperties { - const { - inputs, - evaluate, - invert, - integrate, - integrateInner, - applySubstitution, - hasVariable - } = options; - if (invert == null && hasVariable) { - throw new Error( - "A formula cannot be marked as having a variable if it is not invertible" - ); - } - - const numVariables = inputs.filter( - input => input instanceof Formula && input.hasVariable() - ).length; + const { inputs, evaluate, invert, integrate, integrateInner, applySubstitution } = options; + const numVariables = inputs.reduce( + (acc, input) => acc + (input instanceof Formula ? input.internalVariables : 0), + 0 + ); const variable = inputs.find(input => input instanceof Formula && input.hasVariable()) as | GenericFormula | undefined; - const internalHasVariable = - numVariables === 1 || (numVariables === 0 && hasVariable === true); - const innermostVariable = internalHasVariable ? variable?.innermostVariable : undefined; - const internalInvert = internalHasVariable && variable?.isInvertible() ? invert : undefined; + const innermostVariable = numVariables === 1 ? variable?.innermostVariable : undefined; return { inputs, internalEvaluate: evaluate, - internalInvert, + internalInvert: invert, internalIntegrate: integrate, internalIntegrateInner: integrateInner, applySubstitution, innermostVariable, - internalHasVariable + internalVariables: numVariables }; } private calculateConstantOfIntegration() { - // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase + // Calculate C based on the knowledge that at x=1, the integral should be the average between f(0) and f(1) const integral = this.getIntegralFormula().evaluate(1); - const actualCost = this.evaluate(0); + const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2); return Decimal.sub(actualCost, integral); } /** Type predicate that this formula can be inverted. */ isInvertible(): this is InvertibleFormula { - return ( - this.internalHasVariable && - (this.internalInvert != null || this.internalEvaluate == null) - ); + return this.hasVariable() && (this.internalInvert != null || this.internalEvaluate == null); } /** Type predicate that this formula can be integrated. */ isIntegrable(): this is IntegrableFormula { - return this.internalHasVariable && this.internalIntegrate != null; + return this.hasVariable() && this.internalIntegrate != null; } /** Type predicate that this formula has an integral function that can be inverted. */ @@ -173,7 +146,7 @@ export default class Formula { /** Whether or not this formula has a singular variable inside it, which can be accessed via {@link innermostVariable}. */ hasVariable(): boolean { - return this.internalHasVariable; + return this.internalVariables === 1; } /** @@ -188,7 +161,7 @@ export default class Formula { unrefFormulaSource(input, variable) ) as GuardedFormulasToDecimals) ) ?? - (this.internalHasVariable ? variable : null) ?? + (this.hasVariable() ? variable : null) ?? unrefFormulaSource(this.inputs[0]) ); } @@ -199,9 +172,9 @@ export default class Formula { * @see {@link isInvertible} */ invert(value: DecimalSource): DecimalSource { - if (this.internalInvert) { + if (this.internalInvert && this.hasVariable()) { return this.internalInvert.call(this, value, ...this.inputs); - } else if (this.inputs.length === 1 && this.internalHasVariable) { + } else if (this.inputs.length === 1 && this.hasVariable()) { return value; } throw new Error("Cannot invert non-invertible formula"); @@ -228,7 +201,7 @@ export default class Formula { * @see {@link isIntegralInvertible} */ invertIntegral(value: DecimalSource): DecimalSource { - if (this.integralFormula?.isInvertible()) { + if (!this.isIntegrable() || !this.getIntegralFormula().isInvertible()) { throw new Error("Cannot invert integral of formula without invertible integral"); } return this.getIntegralFormula().invert(value); @@ -236,24 +209,12 @@ export default class Formula { /** * Get a formula that will evaluate to the integral of this formula. May also be invertible. - * @param variable The variable that will be used to evaluate this integral at a given x value - * @param stack For nested formulas, a stack of operations that occur outside the complex operation + * @param stack For nested formulas, a stack of operations that occur outside the complex operation. */ - getIntegralFormula( - variable?: ProcessedComputable, - stack?: SubstitutionStack - ): GenericFormula { - if (variable == null && this.integralFormula != null) { + getIntegralFormula(stack?: SubstitutionStack): GenericFormula { + if (this.integralFormula != null) { return this.integralFormula; } - let formula; - const variablePresent = variable != null; - if (variable == null) { - variable = this.innermostVariable; - if (variable == null) { - throw new Error("Cannot integrate formula without variable"); - } - } if (stack == null) { // "Outer" part of the formula if (this.applySubstitution == null) { @@ -262,21 +223,24 @@ export default class Formula { if (this.internalIntegrate == null) { throw new Error("Cannot integrate formula with non-integrable operation"); } - let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs); + let value = this.internalIntegrate.call(this, stack, ...this.inputs); stack.forEach(func => (value = func(value))); - formula = value; + this.integralFormula = value; } else { // Continue digging into the formula if (this.internalIntegrate) { - formula = this.internalIntegrate.call( + this.integralFormula = this.internalIntegrate.call( this, - variable, undefined, ...this.inputs ); - } else if (this.inputs.length === 1 && this.internalHasVariable) { + } else if ( + this.inputs.length === 1 && + this.internalEvaluate == null && + this.hasVariable() + ) { // eslint-disable-next-line @typescript-eslint/no-this-alias - formula = this; + this.integralFormula = this; } else { throw new Error("Cannot integrate formula without variable"); } @@ -291,20 +255,25 @@ export default class Formula { this.applySubstitution!.call(this, variable, ...this.inputs) ); if (this.internalIntegrateInner) { - formula = this.internalIntegrateInner.call(this, variable, stack, ...this.inputs); + this.integralFormula = this.internalIntegrateInner.call( + this, + stack, + ...this.inputs + ); } else if (this.internalIntegrate) { - formula = this.internalIntegrate.call(this, variable, stack, ...this.inputs); - } else if (this.inputs.length === 1 && this.internalHasVariable) { + this.integralFormula = this.internalIntegrate.call(this, stack, ...this.inputs); + } else if ( + this.inputs.length === 1 && + this.internalEvaluate == null && + this.hasVariable() + ) { // eslint-disable-next-line @typescript-eslint/no-this-alias - formula = this; + this.integralFormula = this; } else { throw new Error("Cannot integrate formula without variable"); } } - if (!variablePresent) { - this.integralFormula = formula; - } - return formula; + return this.integralFormula; } /** @@ -324,7 +293,7 @@ export default class Formula { this.internalEvaluate === other.internalEvaluate && this.internalInvert === other.internalInvert && this.internalIntegrate === other.internalIntegrate && - this.internalHasVariable === other.internalHasVariable + this.internalVariables === other.internalVariables ); } @@ -556,17 +525,8 @@ export default class Formula { public static reciprocal = Formula.recip; public static reciprocate = Formula.recip; - public static max(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula({ - inputs: [value, other], - evaluate: Decimal.max, - invert: ops.passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource - }); - } - + // TODO these functions should ostensibly be integrable, and the integrals should be invertible + public static max = ops.createPassthroughBinaryFormula(Decimal.max); public static min = ops.createPassthroughBinaryFormula(Decimal.min); public static minabs = ops.createPassthroughBinaryFormula(Decimal.minabs); public static maxabs = ops.createPassthroughBinaryFormula(Decimal.maxabs); @@ -1356,6 +1316,24 @@ export default class Formula { } } +/** + * Utility for recursively searching through a formula for the cause of non-invertibility. + * @param formula The formula to search for a non-invertible formula within + */ +export function findNonInvertible(formula: GenericFormula): GenericFormula | null { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (formula.internalInvert == null && formula.internalEvaluate != null) { + return formula; + } + for (const input of formula.inputs) { + if (hasVariable(input)) { + return findNonInvertible(input); + } + } + return null; +} + /** * Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers. * @param formula The formula to use for calculating buy max from diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts index 4512f55..becaec3 100644 --- a/src/game/formulas/operations.ts +++ b/src/game/formulas/operations.ts @@ -1,8 +1,9 @@ import Decimal, { DecimalSource } from "util/bignum"; -import { Ref } from "vue"; import Formula, { hasVariable, unrefFormulaSource } from "./formulas"; import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types"; +const ln10 = Decimal.ln(10); + export function passthrough(value: T): T { return value; } @@ -14,13 +15,9 @@ export function invertNeg(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateNeg( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateNeg(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - return Formula.neg(lhs.getIntegralFormula(variable, stack)); + return Formula.neg(lhs.getIntegralFormula(stack)); } throw new Error("Could not integrate due to no input being a variable"); } @@ -38,33 +35,27 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateAdd( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateAdd(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0).add(x); + const x = lhs.getIntegralFormula(stack); + return Formula.times(rhs, lhs.innermostVariable ?? 0).add(x); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); - return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).add(x); + const x = rhs.getIntegralFormula(stack); + return Formula.times(lhs, rhs.innermostVariable ?? 0).add(x); } throw new Error("Could not integrate due to no input being a variable"); } export function integrateInnerAdd( - variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.add(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.add(x, lhs); } throw new Error("Could not integrate due to no input being a variable"); @@ -79,33 +70,27 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateSub( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateSub(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.sub(x, Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0)); + const x = lhs.getIntegralFormula(stack); + return Formula.sub(x, Formula.times(rhs, lhs.innermostVariable ?? 0)); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); - return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).sub(x); + const x = rhs.getIntegralFormula(stack); + return Formula.times(lhs, rhs.innermostVariable ?? 0).sub(x); } throw new Error("Could not integrate due to no input being a variable"); } export function integrateInnerSub( - variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.sub(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.sub(x, lhs); } throw new Error("Could not integrate due to no input being a variable"); @@ -120,17 +105,12 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateMul( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateMul(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.times(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.times(x, lhs); } throw new Error("Could not integrate due to no input being a variable"); @@ -158,17 +138,12 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateDiv( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateDiv(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.div(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.div(lhs, x); } throw new Error("Could not integrate due to no input being a variable"); @@ -194,13 +169,9 @@ export function invertRecip(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateRecip( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateRecip(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.ln(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -213,14 +184,25 @@ export function invertLog10(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateLog10( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +function internalIntegrateLog10(lhs: DecimalSource) { + return Decimal.ln(lhs).sub(1).times(lhs).div(ln10); +} + +function internalInvertIntegralLog10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x).div(Formula.ln(10)); + const numerator = ln10.times(value); + return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); + } + throw new Error("Could not invert due to no input being a variable"); +} + +export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return new Formula({ + inputs: [lhs.getIntegralFormula(stack)], + evaluate: internalIntegrateLog10, + invert: internalInvertIntegralLog10 + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -234,15 +216,25 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateLog( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +function internalIntegrateLog(lhs: DecimalSource, rhs: DecimalSource) { + return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(rhs)); +} + +function internalInvertIntegralLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x).div(Formula.ln(rhs)); + const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value); + return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); + } + throw new Error("Could not invert due to no input being a variable"); +} + +export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return new Formula({ + inputs: [lhs.getIntegralFormula(stack), rhs], + evaluate: internalIntegrateLog, + invert: internalInvertIntegralLog + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -254,14 +246,25 @@ export function invertLog2(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateLog2( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +function internalIntegrateLog2(lhs: DecimalSource) { + return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(2)); +} + +function internalInvertIntegralLog2(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x).div(Formula.ln(2)); + const numerator = Decimal.ln(2).times(value); + return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); + } + throw new Error("Could not invert due to no input being a variable"); +} + +export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return new Formula({ + inputs: [lhs.getIntegralFormula(stack)], + evaluate: internalIntegrateLog2, + invert: internalInvertIntegralLog2 + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -273,14 +276,24 @@ export function invertLn(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateLn( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +function internalIntegrateLn(lhs: DecimalSource) { + return Decimal.ln(lhs).sub(1).times(lhs); +} + +function internalInvertIntegralLn(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x); + return lhs.invert(Decimal.div(value, Decimal.div(value, Math.E).lambertw())); + } + throw new Error("Could not invert due to no input being a variable"); +} + +export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return new Formula({ + inputs: [lhs.getIntegralFormula(stack)], + evaluate: internalIntegrateLn, + invert: internalInvertIntegralLn + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -294,18 +307,13 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integratePow( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integratePow(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); const pow = Formula.add(rhs, 1); return Formula.pow(x, pow).div(pow); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.pow(lhs, x).div(Formula.ln(lhs)); } throw new Error("Could not integrate due to no input being a variable"); @@ -318,14 +326,24 @@ export function invertPow10(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integratePow10( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +function internalIntegratePow10(lhs: DecimalSource) { + return Decimal.pow10(lhs).div(Decimal.ln(lhs)); +} + +function internalInvertIntegralPow10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x).div(Decimal.ln(10)); + return lhs.invert(ln10.times(value).ln().div(ln10)); + } + throw new Error("Could not invert due to no input being a variable"); +} + +export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return new Formula({ + inputs: [lhs.getIntegralFormula(stack)], + evaluate: internalIntegratePow10, + invert: internalInvertIntegralPow10 + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -339,17 +357,12 @@ export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: For throw new Error("Could not invert due to no input being a variable"); } -export function integratePowBase( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integratePowBase(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.pow(rhs, x).div(Formula.ln(rhs)); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); const denominator = Formula.add(lhs, 1); return Formula.pow(x, denominator).div(denominator); } @@ -365,14 +378,9 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul throw new Error("Could not invert due to no input being a variable"); } -export function integrateRoot( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateRoot(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.pow(x, Formula.recip(rhs).add(1)).times(rhs).div(Formula.add(rhs, 1)); } throw new Error("Could not integrate due to no input being a variable"); @@ -385,13 +393,9 @@ export function invertExp(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateExp( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateExp(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.exp(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -520,13 +524,9 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateSin( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateSin(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.cos(x).neg(); } throw new Error("Could not integrate due to no input being a variable"); @@ -539,13 +539,9 @@ export function invertCos(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateCos( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateCos(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.sin(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -558,13 +554,9 @@ export function invertTan(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateTan( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateTan(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.cos(x).ln().neg(); } throw new Error("Could not integrate due to no input being a variable"); @@ -577,13 +569,9 @@ export function invertAsin(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAsin( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAsin(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.asin(x) .times(x) .add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2)))); @@ -598,13 +586,9 @@ export function invertAcos(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAcos( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAcos(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.acos(x) .times(x) .sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2)))); @@ -619,13 +603,9 @@ export function invertAtan(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAtan( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAtan(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.atan(x) .times(x) .sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2)); @@ -640,13 +620,9 @@ export function invertSinh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateSinh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateSinh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.cosh(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -659,13 +635,9 @@ export function invertCosh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateCosh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateCosh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.sinh(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -678,13 +650,9 @@ export function invertTanh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateTanh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateTanh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.cosh(x).ln(); } throw new Error("Could not integrate due to no input being a variable"); @@ -697,13 +665,9 @@ export function invertAsinh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAsinh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAsinh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.asinh(x).times(x).sub(Formula.pow(x, 2).add(1).sqrt()); } throw new Error("Could not integrate due to no input being a variable"); @@ -716,13 +680,9 @@ export function invertAcosh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAcosh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAcosh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.acosh(x) .times(x) .sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt())); @@ -737,13 +697,9 @@ export function invertAtanh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAtanh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAtanh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.atanh(x) .times(x) .add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2)); diff --git a/src/game/formulas/types.d.ts b/src/game/formulas/types.d.ts index 3d3bc83..96d7757 100644 --- a/src/game/formulas/types.d.ts +++ b/src/game/formulas/types.d.ts @@ -22,7 +22,6 @@ type EvaluateFunction = ( type InvertFunction = (this: Formula, value: DecimalSource, ...inputs: T) => DecimalSource; type IntegrateFunction = ( this: Formula, - variable: Ref, stack: SubstitutionStack | undefined, ...inputs: T ) => GenericFormula; @@ -43,7 +42,6 @@ type GeneralFormulaOptions = { integrate?: IntegrateFunction; integrateInner?: IntegrateFunction; applySubstitution?: SubstitutionFunction; - hasVariable?: boolean; }; type FormulaOptions = | VariableFormulaOptions @@ -52,7 +50,7 @@ type FormulaOptions = type InternalFormulaProperties = { inputs: T; - internalHasVariable: boolean; + internalVariables: number; internalEvaluate?: EvaluateFunction; internalInvert?: InvertFunction; internalIntegrate?: IntegrateFunction; diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index ce0120b..ccaa485 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -96,21 +96,21 @@ const invertibleIntegralZeroPramFunctionNames = [ "sqr", "sqrt", "cube", - "cbrt" -] as const; -const nonInvertibleIntegralZeroPramFunctionNames = [ - ...nonIntegrableZeroParamFunctionNames, + "cbrt", "neg", "exp", "sin", "cos", "tan", + "sinh", + "cosh", + "tanh" +] as const; +const nonInvertibleIntegralZeroPramFunctionNames = [ + ...nonIntegrableZeroParamFunctionNames, "asin", "acos", "atan", - "sinh", - "cosh", - "tanh", "asinh", "acosh", "atanh" @@ -493,8 +493,8 @@ describe("Integrating", () => { test("variable.evaluateIntegral() calculates correctly", () => expect(variable.evaluateIntegral()).compare_tolerance(Decimal.pow(10, 2).div(2))); - test("evaluateIntegral(variable) overrides variable value", () => - expect(variable.add(10).evaluateIntegral(20)).compare_tolerance(400)); + test("variable.evaluateIntegral(variable) overrides variable value", () => + expect(variable.evaluateIntegral(20)).compare_tolerance(Decimal.pow(20, 2).div(2))); describe("Integrable functions marked as such", () => { function checkFormula(formula: GenericFormula) { @@ -668,32 +668,13 @@ describe("Inverting integrals", () => { test("Inverting integral of nested formulas", () => { const formula = Formula.add(variable, constant).times(constant).pow(2).times(30); - expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10); + expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10, 0.01); }); test("Inverting integral of nested complex formulas", () => { const formula = Formula.pow(1.05, variable).times(100).pow(0.5); expect(() => formula.invertIntegral(100)).toThrow(); }); - - describe("Inverting integral pass-throughs", () => { - test("max", () => - expect(Formula.max(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("min", () => - expect(Formula.min(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("minabs", () => - expect(Formula.minabs(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("maxabs", () => - expect(Formula.maxabs(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("clampMax", () => - expect(Formula.clampMax(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("clampMin", () => - expect(Formula.clampMin(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("clamp", () => - expect( - Formula.clamp(variable, constant, constant).invertIntegral(10) - ).compare_tolerance(10)); - }); }); describe("Step-wise", () => { @@ -914,8 +895,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [], evaluate: () => 6, - invert: value => value, - hasVariable: true + invert: value => value }).invert(10) ).toThrow()); test("One input inverts correctly", () => @@ -923,8 +903,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable], evaluate: () => 10, - invert: (value, v1) => v1.evaluate(), - hasVariable: true + invert: (value, v1) => v1.evaluate() }).invert(10) ).compare_tolerance(1)); test("Two inputs inverts correctly", () => @@ -932,37 +911,36 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable, 2], evaluate: () => 10, - invert: (value, v1, v2) => v2, - hasVariable: true + invert: (value, v1, v2) => v2 }).invert(10) ).compare_tolerance(2)); }); describe("Formula with integrate", () => { - test("Zero input integrates correctly", () => - expect( + test("Zero input cannot integrate", () => + expect(() => new Formula({ inputs: [], - evaluate: () => 10, - integrate: variable => variable + evaluate: () => 0, + integrate: stack => variable }).evaluateIntegral() - ).compare_tolerance(20)); + ).toThrow()); test("One input integrates correctly", () => expect( new Formula({ inputs: [variable], - evaluate: () => 10, - integrate: (variable, stack, v1) => Formula.add(variable, v1) + evaluate: v1 => Decimal.add(v1, 19.5), + integrate: (stack, v1) => Formula.add(v1, 10) }).evaluateIntegral() ).compare_tolerance(20)); test("Two inputs integrates correctly", () => expect( new Formula({ - inputs: [variable, 2], - evaluate: (v1, v2) => 10, - integrate: (variable, v1, v2) => variable + inputs: [variable, 10], + evaluate: v1 => Decimal.add(v1, 19.5), + integrate: (stack, v1, v2) => Formula.add(v1, v2) }).evaluateIntegral() - ).compare_tolerance(3)); + ).compare_tolerance(20)); }); describe("Formula with invertIntegral", () => { @@ -970,29 +948,26 @@ describe("Custom Formulas", () => { expect(() => new Formula({ inputs: [], - evaluate: () => 10, - integrate: variable => variable, - hasVariable: true - }).invertIntegral(8) + evaluate: () => 0, + integrate: stack => variable + }).invertIntegral(20) ).toThrow()); test("One input inverts integral correctly", () => expect( new Formula({ inputs: [variable], - evaluate: () => 10, - integrate: (variable, stack, v1) => variable, - hasVariable: true - }).invertIntegral(8) - ).compare_tolerance(1)); + evaluate: v1 => Decimal.add(v1, 19.5), + integrate: (stack, v1) => Formula.add(v1, 10) + }).invertIntegral(20) + ).compare_tolerance(10)); test("Two inputs inverts integral correctly", () => expect( new Formula({ - inputs: [variable, 2], - evaluate: (v1, v2) => 10, - integrate: (variable, v1, v2) => variable, - hasVariable: true - }).invertIntegral(8) - ).compare_tolerance(1)); + inputs: [variable, 10], + evaluate: v1 => Decimal.add(v1, 19.5), + integrate: (stack, v1, v2) => Formula.add(v1, v2) + }).invertIntegral(20) + ).compare_tolerance(10)); }); describe.todo("Formula as input"); diff --git a/tests/utils.ts b/tests/utils.ts index 0e58e19..a1e6084 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -2,7 +2,7 @@ import Decimal, { DecimalSource, format } from "util/bignum"; import { expect } from "vitest"; interface CustomMatchers { - compare_tolerance(expected: DecimalSource): R; + compare_tolerance(expected: DecimalSource, tolerance?: number): R; } declare global { @@ -16,7 +16,7 @@ declare global { } expect.extend({ - compare_tolerance(received: DecimalSource, expected: DecimalSource) { + compare_tolerance(received: DecimalSource, expected: DecimalSource, tolerance?: number) { const { isNot } = this; let pass = false; if (!Decimal.isFinite(expected)) { @@ -24,7 +24,7 @@ expect.extend({ } else if (Decimal.isNaN(expected)) { pass = Decimal.isNaN(received); } else { - pass = Decimal.eq_tolerance(received, expected); + pass = Decimal.eq_tolerance(received, expected, tolerance); } return { // do not alter your "pass" based on isNot. Vitest does it for you