Change formula typing to work better

This commit is contained in:
thepaperpilot 2023-04-22 17:48:44 -05:00
parent 80c66135b2
commit 97fcd28fe2
3 changed files with 307 additions and 246 deletions

View file

@ -22,11 +22,13 @@ import type {
} from "./types"; } from "./types";
export function hasVariable(value: FormulaSource): value is InvertibleFormula { export function hasVariable(value: FormulaSource): value is InvertibleFormula {
return value instanceof Formula && value.hasVariable(); return value instanceof InternalFormula && value.hasVariable();
} }
export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSource) { export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSource) {
return value instanceof Formula ? value.evaluate(variable) : unref(value); return value instanceof InternalFormula
? value.evaluate(variable)
: (unref(value) as DecimalSource);
} }
function integrateVariable(this: GenericFormula) { function integrateVariable(this: GenericFormula) {
@ -37,26 +39,27 @@ function integrateVariableInner(this: GenericFormula) {
return this; return this;
} }
/** // eslint-disable-next-line @typescript-eslint/no-unused-vars
* 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. export interface InternalFormula<T extends [FormulaSource] | FormulaSource[]> {
* Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently. invert?(value: DecimalSource): DecimalSource;
* @see {@link calculateMaxAffordable} evaluateIntegral?(variable?: DecimalSource): DecimalSource;
* @see {@link game/requirements.createCostRequirement} getIntegralFormula?(stack?: SubstitutionStack): GenericFormula;
*/ calculateConstantOfIntegration?(): Decimal;
export default class Formula<T extends [FormulaSource] | FormulaSource[]> { invertIntegral?(value: DecimalSource): DecimalSource;
}
export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[]> {
readonly inputs: T; readonly inputs: T;
private readonly internalEvaluate: EvaluateFunction<T> | undefined; protected readonly internalEvaluate: EvaluateFunction<T> | undefined;
private readonly internalInvert: InvertFunction<T> | undefined; protected readonly internalInvert: InvertFunction<T> | undefined;
private readonly internalIntegrate: IntegrateFunction<T> | undefined; protected readonly internalIntegrate: IntegrateFunction<T> | undefined;
private readonly internalIntegrateInner: IntegrateFunction<T> | undefined; protected readonly internalIntegrateInner: IntegrateFunction<T> | undefined;
private readonly applySubstitution: SubstitutionFunction<T> | undefined; protected readonly applySubstitution: SubstitutionFunction<T> | undefined;
private readonly internalVariables: number; protected readonly internalVariables: number;
public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined; public readonly innermostVariable: ProcessedComputable<DecimalSource> | undefined;
private integralFormula: GenericFormula | undefined;
constructor(options: FormulaOptions<T>) { constructor(options: FormulaOptions<T>) {
let readonlyProperties; let readonlyProperties;
if ("inputs" in options) { if ("inputs" in options) {
@ -112,12 +115,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
private setupFormula(options: GeneralFormulaOptions<T>): InternalFormulaProperties<T> { private setupFormula(options: GeneralFormulaOptions<T>): InternalFormulaProperties<T> {
const { inputs, evaluate, invert, integrate, integrateInner, applySubstitution } = options; const { inputs, evaluate, invert, integrate, integrateInner, applySubstitution } = options;
const numVariables = inputs.reduce<number>( const numVariables = inputs.reduce<number>(
(acc, input) => acc + (input instanceof Formula ? input.internalVariables : 0), (acc, input) => acc + (input instanceof InternalFormula ? input.internalVariables : 0),
0 0
); );
const variable = inputs.find(input => input instanceof Formula && input.hasVariable()) as const variable = inputs.find(
| GenericFormula input => input instanceof InternalFormula && input.hasVariable()
| undefined; ) as GenericFormula | undefined;
const innermostVariable = numVariables === 1 ? variable?.innermostVariable : undefined; const innermostVariable = numVariables === 1 ? variable?.innermostVariable : undefined;
@ -133,14 +136,6 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
}; };
} }
/** Calculates C for the implementation of the integral formula for this formula. */
calculateConstantOfIntegration() {
// Calculate C based on the knowledge that at x=1, the integral should be the average between f(0) and f(1)
const integral = this.getIntegralFormula().evaluate(1);
const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2);
return Decimal.sub(actualCost, integral);
}
/** Type predicate that this formula can be inverted. */ /** Type predicate that this formula can be inverted. */
isInvertible(): this is InvertibleFormula { isInvertible(): this is InvertibleFormula {
return this.hasVariable() && (this.internalInvert != null || this.internalEvaluate == null); return this.hasVariable() && (this.internalInvert != null || this.internalEvaluate == null);
@ -181,108 +176,6 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
); );
} }
/**
* Takes a potential result of the formula, and calculates what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula is invertible.
* @param value The result of the formula
* @see {@link isInvertible}
*/
invert(value: DecimalSource): DecimalSource {
if (this.internalInvert && this.hasVariable()) {
return this.internalInvert.call(this, value, ...this.inputs);
} else if (this.inputs.length === 1 && this.hasVariable()) {
return value;
}
throw new Error("Cannot invert non-invertible formula");
}
/**
* Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable. The formula can only have one "complex" operation (anything besides +,-,*,/).
* @param variable Optionally override the value of the variable while evaluating
* @see {@link isIntegrable}
*/
evaluateIntegral(variable?: DecimalSource): DecimalSource {
if (!this.isIntegrable()) {
throw new Error("Cannot evaluate integral of formula without integral");
}
return this.getIntegralFormula().evaluate(variable);
}
/**
* Given the potential result of the formula's integral (and the constant of integration), calculate what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula's integral is invertible.
* @param value The result of the integral.
* @see {@link isIntegralInvertible}
*/
invertIntegral(value: DecimalSource): DecimalSource {
if (!this.isIntegrable() || !this.getIntegralFormula().isInvertible()) {
throw new Error("Cannot invert integral of formula without invertible integral");
}
return this.getIntegralFormula().invert(value);
}
/**
* Get a formula that will evaluate to the integral of this formula. May also be invertible.
* @param stack For nested formulas, a stack of operations that occur outside the complex operation.
*/
getIntegralFormula(stack?: SubstitutionStack): GenericFormula {
if (this.integralFormula != null && stack == null) {
return this.integralFormula;
}
if (stack == null) {
// "Outer" part of the formula
if (this.applySubstitution == null) {
// We're the complex operation of this formula
stack = [];
if (this.internalIntegrate == null) {
throw new Error("Cannot integrate formula with non-integrable operation");
}
let value = this.internalIntegrate.call(this, stack, ...this.inputs);
stack.forEach(func => (value = func(value)));
this.integralFormula = value;
} else {
// Continue digging into the formula
if (this.internalIntegrate) {
this.integralFormula = this.internalIntegrate.call(
this,
undefined,
...this.inputs
);
} else if (
this.inputs.length === 1 &&
this.internalEvaluate == null &&
this.hasVariable()
) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
this.integralFormula = this;
} else {
throw new Error("Cannot integrate formula without variable");
}
}
return this.integralFormula;
} else {
// "Inner" part of the formula
if (this.applySubstitution == null) {
throw new Error("Cannot have two complex operations in an integrable formula");
}
stack.push((variable: GenericFormula) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.applySubstitution!.call(this, variable, ...this.inputs)
);
if (this.internalIntegrateInner) {
return this.internalIntegrateInner.call(this, stack, ...this.inputs);
} else if (this.internalIntegrate) {
return this.internalIntegrate.call(this, stack, ...this.inputs);
} else if (
this.inputs.length === 1 &&
this.internalEvaluate == null &&
this.hasVariable()
) {
return this;
} else {
throw new Error("Cannot integrate formula without variable");
}
}
}
/** /**
* Compares if two formulas are equivalent to each other. Note that function contexts can lead to false negatives. * Compares if two formulas are equivalent to each other. Note that function contexts can lead to false negatives.
* @param other The formula to compare to this one. * @param other The formula to compare to this one.
@ -291,11 +184,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
return ( return (
this.inputs.length === other.inputs.length && this.inputs.length === other.inputs.length &&
this.inputs.every((input, i) => this.inputs.every((input, i) =>
input instanceof Formula && other.inputs[i] instanceof Formula input instanceof InternalFormula && other.inputs[i] instanceof InternalFormula
? input.equals(other.inputs[i]) ? input.equals(other.inputs[i] as GenericFormula)
: !(input instanceof Formula) && : !(input instanceof InternalFormula) &&
!(other.inputs[i] instanceof Formula) && !(other.inputs[i] instanceof InternalFormula) &&
Decimal.eq(unref(input), unref(other.inputs[i])) Decimal.eq(unref(input), unref(other.inputs[i] as DecimalSource))
) && ) &&
this.internalEvaluate === other.internalEvaluate && this.internalEvaluate === other.internalEvaluate &&
this.internalInvert === other.internalInvert && this.internalInvert === other.internalInvert &&
@ -311,7 +204,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
public static constant( public static constant(
value: ProcessedComputable<DecimalSource> value: ProcessedComputable<DecimalSource>
): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula { ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula {
return new Formula({ inputs: [value] }) as InvertibleFormula; return new Formula({ inputs: [value] });
} }
/** /**
@ -321,7 +214,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
public static variable( public static variable(
value: ProcessedComputable<DecimalSource> value: ProcessedComputable<DecimalSource>
): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula { ): InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula {
return new Formula({ variable: value }) as InvertibleFormula; return new Formula({ variable: value });
} }
// TODO add integration support to step-wise functions // TODO add integration support to step-wise functions
@ -338,7 +231,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
formulaModifier: ( formulaModifier: (
value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula
) => GenericFormula ) => GenericFormula
): GenericFormula { ) {
const lhsRef = ref<DecimalSource>(0); const lhsRef = ref<DecimalSource>(0);
const formula = formulaModifier(Formula.variable(lhsRef)); const formula = formulaModifier(Formula.variable(lhsRef));
const processedStart = convertComputable(start); const processedStart = convertComputable(start);
@ -350,7 +243,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
return Decimal.add(formula.evaluate(), unref(processedStart)); return Decimal.add(formula.evaluate(), unref(processedStart));
} }
function invertStep(value: DecimalSource, lhs: FormulaSource) { function invertStep(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) { if (hasVariable(lhs) && formula.isInvertible()) {
if (Decimal.gt(value, unref(processedStart))) { if (Decimal.gt(value, unref(processedStart))) {
value = Decimal.add( value = Decimal.add(
formula.invert(Decimal.sub(value, unref(processedStart))), formula.invert(Decimal.sub(value, unref(processedStart))),
@ -384,7 +277,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
elseFormulaModifier?: ( elseFormulaModifier?: (
value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula value: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula
) => GenericFormula ) => GenericFormula
): GenericFormula { ) {
const lhsRef = ref<DecimalSource>(0); const lhsRef = ref<DecimalSource>(0);
const variable = Formula.variable(lhsRef); const variable = Formula.variable(lhsRef);
const formula = formulaModifier(variable); const formula = formulaModifier(variable);
@ -402,7 +295,11 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
} }
function invertStep(value: DecimalSource, lhs: FormulaSource) { function invertStep(value: DecimalSource, lhs: FormulaSource) {
if (!hasVariable(lhs)) { if (
!hasVariable(lhs) ||
!formula.isInvertible() ||
(elseFormula != null && !elseFormula.isInvertible())
) {
throw new Error("Could not invert due to no input being a variable"); throw new Error("Could not invert due to no input being a variable");
} }
if (unref(processedCondition)) { if (unref(processedCondition)) {
@ -432,7 +329,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
return Formula.if(value, condition, formulaModifier, elseFormulaModifier); return Formula.if(value, condition, formulaModifier, elseFormulaModifier);
} }
public static abs(value: FormulaSource): GenericFormula { public static abs(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.abs }); return new Formula({ inputs: [value], evaluate: Decimal.abs });
} }
@ -447,33 +344,36 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
integrate: ops.integrateNeg integrate: ops.integrateNeg
}); });
} }
public static negate = Formula.neg; public static negate = InternalFormula.neg;
public static negated = Formula.neg; public static negated = InternalFormula.neg;
public static sign(value: FormulaSource): GenericFormula { public static sign(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.sign }); return new Formula({ inputs: [value], evaluate: Decimal.sign });
} }
public static sgn = Formula.sign; public static sgn = InternalFormula.sign;
public static round(value: FormulaSource): GenericFormula { public static round(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.round }); return new Formula({ inputs: [value], evaluate: Decimal.round });
} }
public static floor(value: FormulaSource): GenericFormula { public static floor(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.floor }); return new Formula({ inputs: [value], evaluate: Decimal.floor });
} }
public static ceil(value: FormulaSource): GenericFormula { public static ceil(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.ceil }); return new Formula({ inputs: [value], evaluate: Decimal.ceil });
} }
public static trunc(value: FormulaSource): GenericFormula { public static trunc(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.trunc }); return new Formula({ inputs: [value], evaluate: Decimal.trunc });
} }
public static add<T extends GenericFormula>(value: T, other: FormulaSource): T; public static add<T extends GenericFormula>(value: T, other: FormulaSource): T;
public static add<T extends GenericFormula>(value: FormulaSource, other: T): T; public static add<T extends GenericFormula>(value: FormulaSource, other: T): T;
public static add(value: FormulaSource, other: FormulaSource): GenericFormula; public static add(
value: FormulaSource,
other: FormulaSource
): InternalFormula<[FormulaSource, FormulaSource]>;
public static add(value: FormulaSource, other: FormulaSource) { public static add(value: FormulaSource, other: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value, other], inputs: [value, other],
@ -484,11 +384,14 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
applySubstitution: ops.passthrough applySubstitution: ops.passthrough
}); });
} }
public static plus = Formula.add; public static plus = InternalFormula.add;
public static sub<T extends GenericFormula>(value: T, other: FormulaSource): T; public static sub<T extends GenericFormula>(value: T, other: FormulaSource): T;
public static sub<T extends GenericFormula>(value: FormulaSource, other: T): T; public static sub<T extends GenericFormula>(value: FormulaSource, other: T): T;
public static sub(value: FormulaSource, other: FormulaSource): GenericFormula; public static sub(
value: FormulaSource,
other: FormulaSource
): Formula<[FormulaSource, FormulaSource]>;
public static sub(value: FormulaSource, other: FormulaSource) { public static sub(value: FormulaSource, other: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value, other], inputs: [value, other],
@ -499,12 +402,15 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
applySubstitution: ops.passthrough applySubstitution: ops.passthrough
}); });
} }
public static subtract = Formula.sub; public static subtract = InternalFormula.sub;
public static minus = Formula.sub; public static minus = InternalFormula.sub;
public static mul<T extends GenericFormula>(value: T, other: FormulaSource): T; public static mul<T extends GenericFormula>(value: T, other: FormulaSource): T;
public static mul<T extends GenericFormula>(value: FormulaSource, other: T): T; public static mul<T extends GenericFormula>(value: FormulaSource, other: T): T;
public static mul(value: FormulaSource, other: FormulaSource): GenericFormula; public static mul(
value: FormulaSource,
other: FormulaSource
): Formula<[FormulaSource, FormulaSource]>;
public static mul(value: FormulaSource, other: FormulaSource) { public static mul(value: FormulaSource, other: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value, other], inputs: [value, other],
@ -514,12 +420,15 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
applySubstitution: ops.applySubstitutionMul applySubstitution: ops.applySubstitutionMul
}); });
} }
public static multiply = Formula.mul; public static multiply = InternalFormula.mul;
public static times = Formula.mul; public static times = InternalFormula.mul;
public static div<T extends GenericFormula>(value: T, other: FormulaSource): T; public static div<T extends GenericFormula>(value: T, other: FormulaSource): T;
public static div<T extends GenericFormula>(value: FormulaSource, other: T): T; public static div<T extends GenericFormula>(value: FormulaSource, other: T): T;
public static div(value: FormulaSource, other: FormulaSource): GenericFormula; public static div(
value: FormulaSource,
other: FormulaSource
): Formula<[FormulaSource, FormulaSource]>;
public static div(value: FormulaSource, other: FormulaSource) { public static div(value: FormulaSource, other: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value, other], inputs: [value, other],
@ -529,12 +438,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
applySubstitution: ops.applySubstitutionDiv applySubstitution: ops.applySubstitutionDiv
}); });
} }
public static divide = Formula.div; public static divide = InternalFormula.div;
public static divideBy = Formula.div; public static divideBy = InternalFormula.div;
public static dividedBy = Formula.div; public static dividedBy = InternalFormula.div;
public static recip<T extends GenericFormula>(value: T): T; public static recip<T extends GenericFormula>(value: T): T;
public static recip(value: FormulaSource): GenericFormula; public static recip(value: FormulaSource): Formula<[FormulaSource]>;
public static recip(value: FormulaSource) { public static recip(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -543,8 +452,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
integrate: ops.integrateRecip integrate: ops.integrateRecip
}); });
} }
public static reciprocal = Formula.recip; public static reciprocal = InternalFormula.recip;
public static reciprocate = Formula.recip; public static reciprocate = InternalFormula.recip;
// TODO these functions should ostensibly be integrable, and the integrals should be invertible // TODO these functions should ostensibly be integrable, and the integrals should be invertible
public static max = ops.createPassthroughBinaryFormula(Decimal.max); public static max = ops.createPassthroughBinaryFormula(Decimal.max);
@ -554,11 +463,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
public static clampMin = ops.createPassthroughBinaryFormula(Decimal.clampMin); public static clampMin = ops.createPassthroughBinaryFormula(Decimal.clampMin);
public static clampMax = ops.createPassthroughBinaryFormula(Decimal.clampMax); public static clampMax = ops.createPassthroughBinaryFormula(Decimal.clampMax);
public static clamp( public static clamp(value: FormulaSource, min: FormulaSource, max: FormulaSource) {
value: FormulaSource,
min: FormulaSource,
max: FormulaSource
): GenericFormula {
return new Formula({ return new Formula({
inputs: [value, min, max], inputs: [value, min, max],
evaluate: Decimal.clamp, evaluate: Decimal.clamp,
@ -566,16 +471,16 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
}); });
} }
public static pLog10(value: FormulaSource): GenericFormula { public static pLog10(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.pLog10 }); return new Formula({ inputs: [value], evaluate: Decimal.pLog10 });
} }
public static absLog10(value: FormulaSource): GenericFormula { public static absLog10(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.absLog10 }); return new Formula({ inputs: [value], evaluate: Decimal.absLog10 });
} }
public static log10<T extends GenericFormula>(value: T): T; public static log10<T extends GenericFormula>(value: T): T;
public static log10(value: FormulaSource): GenericFormula; public static log10(value: FormulaSource): Formula<[FormulaSource]>;
public static log10(value: FormulaSource) { public static log10(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -587,7 +492,10 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
public static log<T extends GenericFormula>(value: T, base: FormulaSource): T; public static log<T extends GenericFormula>(value: T, base: FormulaSource): T;
public static log<T extends GenericFormula>(value: FormulaSource, base: T): T; public static log<T extends GenericFormula>(value: FormulaSource, base: T): T;
public static log(value: FormulaSource, base: FormulaSource): GenericFormula; public static log(
value: FormulaSource,
base: FormulaSource
): Formula<[FormulaSource, FormulaSource]>;
public static log(value: FormulaSource, base: FormulaSource) { public static log(value: FormulaSource, base: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value, base], inputs: [value, base],
@ -596,10 +504,10 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
integrate: ops.integrateLog integrate: ops.integrateLog
}); });
} }
public static logarithm = Formula.log; public static logarithm = InternalFormula.log;
public static log2<T extends GenericFormula>(value: T): T; public static log2<T extends GenericFormula>(value: T): T;
public static log2(value: FormulaSource): GenericFormula; public static log2(value: FormulaSource): Formula<[FormulaSource]>;
public static log2(value: FormulaSource) { public static log2(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -610,7 +518,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static ln<T extends GenericFormula>(value: T): T; public static ln<T extends GenericFormula>(value: T): T;
public static ln(value: FormulaSource): GenericFormula; public static ln(value: FormulaSource): Formula<[FormulaSource]>;
public static ln(value: FormulaSource) { public static ln(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -622,7 +530,10 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
public static pow<T extends GenericFormula>(value: T, other: FormulaSource): T; public static pow<T extends GenericFormula>(value: T, other: FormulaSource): T;
public static pow<T extends GenericFormula>(value: FormulaSource, other: T): T; public static pow<T extends GenericFormula>(value: FormulaSource, other: T): T;
public static pow(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow(
value: FormulaSource,
other: FormulaSource
): Formula<[FormulaSource, FormulaSource]>;
public static pow(value: FormulaSource, other: FormulaSource) { public static pow(value: FormulaSource, other: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value, other], inputs: [value, other],
@ -645,7 +556,10 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
public static pow_base<T extends GenericFormula>(value: T, other: FormulaSource): T; public static pow_base<T extends GenericFormula>(value: T, other: FormulaSource): T;
public static pow_base<T extends GenericFormula>(value: FormulaSource, other: T): T; public static pow_base<T extends GenericFormula>(value: FormulaSource, other: T): T;
public static pow_base(value: FormulaSource, other: FormulaSource): GenericFormula; public static pow_base(
value: FormulaSource,
other: FormulaSource
): Formula<[FormulaSource, FormulaSource]>;
public static pow_base(value: FormulaSource, other: FormulaSource) { public static pow_base(value: FormulaSource, other: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value, other], inputs: [value, other],
@ -657,7 +571,10 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
public static root<T extends GenericFormula>(value: T, other: FormulaSource): T; public static root<T extends GenericFormula>(value: T, other: FormulaSource): T;
public static root<T extends GenericFormula>(value: FormulaSource, other: T): T; public static root<T extends GenericFormula>(value: FormulaSource, other: T): T;
public static root(value: FormulaSource, other: FormulaSource): GenericFormula; public static root(
value: FormulaSource,
other: FormulaSource
): Formula<[FormulaSource, FormulaSource]>;
public static root(value: FormulaSource, other: FormulaSource) { public static root(value: FormulaSource, other: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value, other], inputs: [value, other],
@ -680,7 +597,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static exp<T extends GenericFormula>(value: T): T; public static exp<T extends GenericFormula>(value: T): T;
public static exp(value: FormulaSource): GenericFormula; public static exp(value: FormulaSource): Formula<[FormulaSource]>;
public static exp(value: FormulaSource) { public static exp(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -691,25 +608,25 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static sqr<T extends GenericFormula>(value: T): T; public static sqr<T extends GenericFormula>(value: T): T;
public static sqr(value: FormulaSource): GenericFormula; public static sqr(value: FormulaSource): Formula<[FormulaSource, FormulaSource]>;
public static sqr(value: FormulaSource) { public static sqr(value: FormulaSource) {
return Formula.pow(value, 2); return Formula.pow(value, 2);
} }
public static sqrt<T extends GenericFormula>(value: T): T; public static sqrt<T extends GenericFormula>(value: T): T;
public static sqrt(value: FormulaSource): GenericFormula; public static sqrt(value: FormulaSource): Formula<[FormulaSource, FormulaSource]>;
public static sqrt(value: FormulaSource) { public static sqrt(value: FormulaSource) {
return Formula.root(value, 2); return Formula.root(value, 2);
} }
public static cube<T extends GenericFormula>(value: T): T; public static cube<T extends GenericFormula>(value: T): T;
public static cube(value: FormulaSource): GenericFormula; public static cube(value: FormulaSource): Formula<[FormulaSource, FormulaSource]>;
public static cube(value: FormulaSource) { public static cube(value: FormulaSource) {
return Formula.pow(value, 3); return Formula.pow(value, 3);
} }
public static cbrt<T extends GenericFormula>(value: T): T; public static cbrt<T extends GenericFormula>(value: T): T;
public static cbrt(value: FormulaSource): GenericFormula; public static cbrt(value: FormulaSource): Formula<[FormulaSource, FormulaSource]>;
public static cbrt(value: FormulaSource) { public static cbrt(value: FormulaSource) {
return Formula.root(value, 3); return Formula.root(value, 3);
} }
@ -718,12 +635,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
value: T, value: T,
height?: FormulaSource, height?: FormulaSource,
payload?: FormulaSource payload?: FormulaSource
): Omit<T, "integrate">; ): InvertibleFormula;
public static tetrate( public static tetrate(
value: FormulaSource, value: FormulaSource,
height?: FormulaSource, height?: FormulaSource,
payload?: FormulaSource payload?: FormulaSource
): GenericFormula; ): Formula<[FormulaSource, FormulaSource, FormulaSource]>;
public static tetrate( public static tetrate(
value: FormulaSource, value: FormulaSource,
height: FormulaSource = 2, height: FormulaSource = 2,
@ -740,12 +657,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
value: T, value: T,
height?: FormulaSource, height?: FormulaSource,
payload?: FormulaSource payload?: FormulaSource
): Omit<T, "integrate">; ): InvertibleFormula;
public static iteratedexp( public static iteratedexp(
value: FormulaSource, value: FormulaSource,
height?: FormulaSource, height?: FormulaSource,
payload?: FormulaSource payload?: FormulaSource
): GenericFormula; ): Formula<[FormulaSource, FormulaSource, FormulaSource]>;
public static iteratedexp( public static iteratedexp(
value: FormulaSource, value: FormulaSource,
height: FormulaSource = 2, height: FormulaSource = 2,
@ -762,15 +679,15 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
value: FormulaSource, value: FormulaSource,
base: FormulaSource = 10, base: FormulaSource = 10,
times: FormulaSource = 1 times: FormulaSource = 1
): GenericFormula { ) {
return new Formula({ inputs: [value, base, times], evaluate: ops.iteratedLog }); return new Formula({ inputs: [value, base, times], evaluate: ops.iteratedLog });
} }
public static slog<T extends GenericFormula>( public static slog<T extends GenericFormula>(value: T, base?: FormulaSource): InvertibleFormula;
value: T, public static slog(
value: FormulaSource,
base?: FormulaSource base?: FormulaSource
): Omit<T, "integrate">; ): Formula<[FormulaSource, FormulaSource]>;
public static slog(value: FormulaSource, base?: FormulaSource): GenericFormula;
public static slog(value: FormulaSource, base: FormulaSource = 10) { public static slog(value: FormulaSource, base: FormulaSource = 10) {
return new Formula({ inputs: [value, base], evaluate: ops.slog, invert: ops.invertSlog }); return new Formula({ inputs: [value, base], evaluate: ops.slog, invert: ops.invertSlog });
} }
@ -783,12 +700,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
value: T, value: T,
diff: FormulaSource, diff: FormulaSource,
base?: FormulaSource base?: FormulaSource
): Omit<T, "integrate">; ): InvertibleFormula;
public static layeradd( public static layeradd(
value: FormulaSource, value: FormulaSource,
diff: FormulaSource, diff: FormulaSource,
base?: FormulaSource base?: FormulaSource
): GenericFormula; ): Formula<[FormulaSource, FormulaSource, FormulaSource]>;
public static layeradd(value: FormulaSource, diff: FormulaSource, base: FormulaSource = 10) { public static layeradd(value: FormulaSource, diff: FormulaSource, base: FormulaSource = 10) {
return new Formula({ return new Formula({
inputs: [value, diff, base], inputs: [value, diff, base],
@ -797,8 +714,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
}); });
} }
public static lambertw<T extends GenericFormula>(value: T): Omit<T, "integrate">; public static lambertw<T extends GenericFormula>(value: T): InvertibleFormula;
public static lambertw(value: FormulaSource): GenericFormula; public static lambertw(value: FormulaSource): Formula<[FormulaSource]>;
public static lambertw(value: FormulaSource) { public static lambertw(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -807,8 +724,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
}); });
} }
public static ssqrt<T extends GenericFormula>(value: T): Omit<T, "integrate">; public static ssqrt<T extends GenericFormula>(value: T): InvertibleFormula;
public static ssqrt(value: FormulaSource): GenericFormula; public static ssqrt(value: FormulaSource): Formula<[FormulaSource]>;
public static ssqrt(value: FormulaSource) { public static ssqrt(value: FormulaSource) {
return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: ops.invertSsqrt }); return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: ops.invertSsqrt });
} }
@ -817,12 +734,12 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
value: FormulaSource, value: FormulaSource,
height: FormulaSource = 2, height: FormulaSource = 2,
payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1)
): GenericFormula { ) {
return new Formula({ inputs: [value, height, payload], evaluate: ops.pentate }); return new Formula({ inputs: [value, height, payload], evaluate: ops.pentate });
} }
public static sin<T extends GenericFormula>(value: T): T; public static sin<T extends GenericFormula>(value: T): T;
public static sin(value: FormulaSource): GenericFormula; public static sin(value: FormulaSource): Formula<[FormulaSource]>;
public static sin(value: FormulaSource) { public static sin(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -833,7 +750,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static cos<T extends GenericFormula>(value: T): T; public static cos<T extends GenericFormula>(value: T): T;
public static cos(value: FormulaSource): GenericFormula; public static cos(value: FormulaSource): Formula<[FormulaSource]>;
public static cos(value: FormulaSource) { public static cos(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -844,7 +761,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static tan<T extends GenericFormula>(value: T): T; public static tan<T extends GenericFormula>(value: T): T;
public static tan(value: FormulaSource): GenericFormula; public static tan(value: FormulaSource): Formula<[FormulaSource]>;
public static tan(value: FormulaSource) { public static tan(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -855,7 +772,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static asin<T extends GenericFormula>(value: T): T; public static asin<T extends GenericFormula>(value: T): T;
public static asin(value: FormulaSource): GenericFormula; public static asin(value: FormulaSource): Formula<[FormulaSource]>;
public static asin(value: FormulaSource) { public static asin(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -866,7 +783,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static acos<T extends GenericFormula>(value: T): T; public static acos<T extends GenericFormula>(value: T): T;
public static acos(value: FormulaSource): GenericFormula; public static acos(value: FormulaSource): Formula<[FormulaSource]>;
public static acos(value: FormulaSource) { public static acos(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -877,7 +794,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static atan<T extends GenericFormula>(value: T): T; public static atan<T extends GenericFormula>(value: T): T;
public static atan(value: FormulaSource): GenericFormula; public static atan(value: FormulaSource): Formula<[FormulaSource]>;
public static atan(value: FormulaSource) { public static atan(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -888,7 +805,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static sinh<T extends GenericFormula>(value: T): T; public static sinh<T extends GenericFormula>(value: T): T;
public static sinh(value: FormulaSource): GenericFormula; public static sinh(value: FormulaSource): Formula<[FormulaSource]>;
public static sinh(value: FormulaSource) { public static sinh(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -899,7 +816,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static cosh<T extends GenericFormula>(value: T): T; public static cosh<T extends GenericFormula>(value: T): T;
public static cosh(value: FormulaSource): GenericFormula; public static cosh(value: FormulaSource): Formula<[FormulaSource]>;
public static cosh(value: FormulaSource) { public static cosh(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -910,7 +827,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static tanh<T extends GenericFormula>(value: T): T; public static tanh<T extends GenericFormula>(value: T): T;
public static tanh(value: FormulaSource): GenericFormula; public static tanh(value: FormulaSource): Formula<[FormulaSource]>;
public static tanh(value: FormulaSource) { public static tanh(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -921,7 +838,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static asinh<T extends GenericFormula>(value: T): T; public static asinh<T extends GenericFormula>(value: T): T;
public static asinh(value: FormulaSource): GenericFormula; public static asinh(value: FormulaSource): Formula<[FormulaSource]>;
public static asinh(value: FormulaSource) { public static asinh(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -932,7 +849,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static acosh<T extends GenericFormula>(value: T): T; public static acosh<T extends GenericFormula>(value: T): T;
public static acosh(value: FormulaSource): GenericFormula; public static acosh(value: FormulaSource): Formula<[FormulaSource]>;
public static acosh(value: FormulaSource) { public static acosh(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -943,7 +860,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
public static atanh<T extends GenericFormula>(value: T): T; public static atanh<T extends GenericFormula>(value: T): T;
public static atanh(value: FormulaSource): GenericFormula; public static atanh(value: FormulaSource): Formula<[FormulaSource]>;
public static atanh(value: FormulaSource) { public static atanh(value: FormulaSource) {
return new Formula({ return new Formula({
inputs: [value], inputs: [value],
@ -1189,7 +1106,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
this: T, this: T,
height?: FormulaSource, height?: FormulaSource,
payload?: FormulaSource payload?: FormulaSource
): Omit<T, "integrate">; ): InvertibleFormula;
public tetrate( public tetrate(
this: FormulaSource, this: FormulaSource,
height?: FormulaSource, height?: FormulaSource,
@ -1207,7 +1124,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
this: T, this: T,
height?: FormulaSource, height?: FormulaSource,
payload?: FormulaSource payload?: FormulaSource
): Omit<T, "integrate">; ): InvertibleFormula;
public iteratedexp( public iteratedexp(
this: FormulaSource, this: FormulaSource,
height?: FormulaSource, height?: FormulaSource,
@ -1225,7 +1142,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
return Formula.iteratedlog(this, base, times); return Formula.iteratedlog(this, base, times);
} }
public slog<T extends GenericFormula>(this: T, base?: FormulaSource): Omit<T, "integrate">; public slog<T extends GenericFormula>(this: T, base?: FormulaSource): InvertibleFormula;
public slog(this: FormulaSource, base?: FormulaSource): GenericFormula; public slog(this: FormulaSource, base?: FormulaSource): GenericFormula;
public slog(this: FormulaSource, base: FormulaSource = 10) { public slog(this: FormulaSource, base: FormulaSource = 10) {
return Formula.slog(this, base); return Formula.slog(this, base);
@ -1235,23 +1152,23 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
return Formula.layeradd10(this, diff); return Formula.layeradd10(this, diff);
} }
public layeradd<T extends GenericFormula>( public layeradd<T extends InvertibleFormula>(
this: T, this: T,
diff: FormulaSource, diff: FormulaSource,
base?: FormulaSource base?: FormulaSource
): Omit<T, "integrate">; ): InvertibleFormula;
public layeradd(this: FormulaSource, diff: FormulaSource, base?: FormulaSource): GenericFormula; public layeradd(this: FormulaSource, diff: FormulaSource, base?: FormulaSource): GenericFormula;
public layeradd(this: FormulaSource, diff: FormulaSource, base: FormulaSource) { public layeradd(this: FormulaSource, diff: FormulaSource, base: FormulaSource) {
return Formula.layeradd(this, diff, base); return Formula.layeradd(this, diff, base);
} }
public lambertw<T extends GenericFormula>(this: T): Omit<T, "integrate">; public lambertw<T extends InvertibleFormula>(this: T): InvertibleFormula;
public lambertw(this: FormulaSource): GenericFormula; public lambertw(this: FormulaSource): GenericFormula;
public lambertw(this: FormulaSource) { public lambertw(this: FormulaSource) {
return Formula.lambertw(this); return Formula.lambertw(this);
} }
public ssqrt<T extends GenericFormula>(this: T): Omit<T, "integrate">; public ssqrt<T extends InvertibleFormula>(this: T): InvertibleFormula;
public ssqrt(this: FormulaSource): GenericFormula; public ssqrt(this: FormulaSource): GenericFormula;
public ssqrt(this: FormulaSource) { public ssqrt(this: FormulaSource) {
return Formula.ssqrt(this); return Formula.ssqrt(this);
@ -1337,6 +1254,127 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
} }
/**
* 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.
* @see {@link calculateMaxAffordable}
* @see {@link /game/requirements.createCostRequirement}
*/
export default class Formula<
T extends [FormulaSource] | FormulaSource[]
> extends InternalFormula<T> {
private integralFormula: GenericFormula | undefined;
/**
* Takes a potential result of the formula, and calculates what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula is invertible.
* @param value The result of the formula
* @see {@link isInvertible}
*/
invert(value: DecimalSource): DecimalSource {
if (this.internalInvert && this.hasVariable()) {
return this.internalInvert.call(this, value, ...this.inputs);
} else if (this.inputs.length === 1 && this.hasVariable()) {
return value;
}
throw new Error("Cannot invert non-invertible formula");
}
/**
* Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable. The formula can only have one "complex" operation (anything besides +,-,*,/).
* @param variable Optionally override the value of the variable while evaluating
* @see {@link isIntegrable}
*/
evaluateIntegral(variable?: DecimalSource): DecimalSource {
if (!this.isIntegrable()) {
throw new Error("Cannot evaluate integral of formula without integral");
}
return this.getIntegralFormula().evaluate(variable);
}
/**
* Given the potential result of the formula's integral (and the constant of integration), calculate what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula's integral is invertible.
* @param value The result of the integral.
* @see {@link isIntegralInvertible}
*/
invertIntegral(value: DecimalSource): DecimalSource {
if (!this.isIntegrable() || !this.getIntegralFormula().isInvertible()) {
throw new Error("Cannot invert integral of formula without invertible integral");
}
return (this.getIntegralFormula() as InvertibleFormula).invert(value);
}
/** Calculates C for the implementation of the integral formula for this formula. */
calculateConstantOfIntegration() {
// Calculate C based on the knowledge that at x=1, the integral should be the average between f(0) and f(1)
const integral = this.getIntegralFormula().evaluate(1);
const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2);
return Decimal.sub(actualCost, integral);
}
/**
* Get a formula that will evaluate to the integral of this formula. May also be invertible.
* @param stack For nested formulas, a stack of operations that occur outside the complex operation.
*/
getIntegralFormula(stack?: SubstitutionStack): GenericFormula {
if (this.integralFormula != null && stack == null) {
return this.integralFormula;
}
if (stack == null) {
// "Outer" part of the formula
if (this.applySubstitution == null) {
// We're the complex operation of this formula
stack = [];
if (this.internalIntegrate == null) {
throw new Error("Cannot integrate formula with non-integrable operation");
}
let value = this.internalIntegrate.call(this, stack, ...this.inputs);
stack.forEach(func => (value = func(value)));
this.integralFormula = value;
} else {
// Continue digging into the formula
if (this.internalIntegrate) {
this.integralFormula = this.internalIntegrate.call(
this,
undefined,
...this.inputs
);
} else if (
this.inputs.length === 1 &&
this.internalEvaluate == null &&
this.hasVariable()
) {
this.integralFormula = this;
} else {
throw new Error("Cannot integrate formula without variable");
}
}
return this.integralFormula;
} else {
// "Inner" part of the formula
if (this.applySubstitution == null) {
throw new Error("Cannot have two complex operations in an integrable formula");
}
stack.push((variable: GenericFormula) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.applySubstitution!.call(this, variable, ...this.inputs)
);
if (this.internalIntegrateInner) {
return this.internalIntegrateInner.call(this, stack, ...this.inputs);
} else if (this.internalIntegrate) {
return this.internalIntegrate.call(this, stack, ...this.inputs);
} else if (
this.inputs.length === 1 &&
this.internalEvaluate == null &&
this.hasVariable()
) {
return this;
} else {
throw new Error("Cannot integrate formula without variable");
}
}
}
}
/** /**
* Utility for recursively searching through a formula for the cause of non-invertibility. * 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 * @param formula The formula to search for a non-invertible formula within
@ -1360,7 +1398,7 @@ export function findNonInvertible(formula: GenericFormula): GenericFormula | nul
* @param formula The formula to print * @param formula The formula to print
*/ */
export function printFormula(formula: FormulaSource): string { export function printFormula(formula: FormulaSource): string {
if (formula instanceof Formula) { if (formula instanceof InternalFormula) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
return formula.internalEvaluate == null return formula.internalEvaluate == null
@ -1472,6 +1510,11 @@ export function calculateCost(
) { ) {
let newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0); let newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0);
if (spendResources) { if (spendResources) {
if (!formula.isIntegrable()) {
throw new Error(
"Cannot calculate cost with spending resources of non-integrable formula"
);
}
const targetValue = newValue; const targetValue = newValue;
newValue = newValue newValue = newValue
.sub(summedPurchases ?? 10) .sub(summedPurchases ?? 10)

View file

@ -1,32 +1,38 @@
import Formula from "game/formulas/formulas"; import { InternalFormula } from "game/formulas/formulas";
import { DecimalSource } from "util/bignum"; import { DecimalSource } from "util/bignum";
import { ProcessedComputable } from "util/computed"; import { ProcessedComputable } from "util/computed";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type GenericFormula = Formula<any>; type GenericFormula = InternalFormula<any>;
type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula; type FormulaSource = ProcessedComputable<DecimalSource> | GenericFormula;
type InvertibleFormula = GenericFormula & { type InvertibleFormula = GenericFormula & {
invert: (value: DecimalSource) => DecimalSource; invert: NonNullable<GenericFormula["invert"]>;
}; };
type IntegrableFormula = GenericFormula & { type IntegrableFormula = InvertibleFormula & {
evaluateIntegral: (variable?: DecimalSource) => DecimalSource; evaluateIntegral: NonNullable<GenericFormula["evaluateIntegral"]>;
getIntegralFormula: NonNullable<GenericFormula["getIntegralFormula"]>;
calculateConstantOfIntegration: NonNullable<GenericFormula["calculateConstantOfIntegration"]>;
}; };
type InvertibleIntegralFormula = GenericFormula & { type InvertibleIntegralFormula = IntegrableFormula & {
invertIntegral: (value: DecimalSource) => DecimalSource; invertIntegral: NonNullable<GenericFormula["invertIntegral"]>;
}; };
type EvaluateFunction<T> = ( type EvaluateFunction<T> = (
this: Formula<T>, this: InternalFormula<T>,
...inputs: GuardedFormulasToDecimals<T> ...inputs: GuardedFormulasToDecimals<T>
) => DecimalSource; ) => DecimalSource;
type InvertFunction<T> = (this: Formula<T>, value: DecimalSource, ...inputs: T) => DecimalSource; type InvertFunction<T> = (
this: InternalFormula<T>,
value: DecimalSource,
...inputs: T
) => DecimalSource;
type IntegrateFunction<T> = ( type IntegrateFunction<T> = (
this: Formula<T>, this: InternalFormula<T>,
stack: SubstitutionStack | undefined, stack: SubstitutionStack | undefined,
...inputs: T ...inputs: T
) => GenericFormula; ) => GenericFormula;
type SubstitutionFunction<T> = ( type SubstitutionFunction<T> = (
this: Formula<T>, this: InternalFormula<T>,
variable: GenericFormula, variable: GenericFormula,
...inputs: T ...inputs: T
) => GenericFormula; ) => GenericFormula;

View file

@ -4,11 +4,12 @@ import Formula, {
calculateMaxAffordable, calculateMaxAffordable,
unrefFormulaSource unrefFormulaSource
} from "game/formulas/formulas"; } from "game/formulas/formulas";
import type { GenericFormula, InvertibleFormula } from "game/formulas/types"; import type { GenericFormula, IntegrableFormula, InvertibleFormula } from "game/formulas/types";
import Decimal, { DecimalSource } from "util/bignum"; import Decimal, { DecimalSource } from "util/bignum";
import { beforeAll, describe, expect, test } from "vitest"; import { beforeAll, describe, expect, test } from "vitest";
import { ref } from "vue"; import { ref } from "vue";
import "../utils"; import "../utils";
import { InvertibleIntegralFormula } from "game/formulas/types";
type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
@ -224,9 +225,15 @@ describe("Creating Formulas", () => {
expect(formula.hasVariable()).toBe(false)); expect(formula.hasVariable()).toBe(false));
test("Evaluates correctly", () => test("Evaluates correctly", () =>
expect(formula.evaluate()).compare_tolerance(expectedValue)); expect(formula.evaluate()).compare_tolerance(expectedValue));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
test("Invert throws", () => expect(() => formula.invert(25)).toThrow()); test("Invert throws", () => expect(() => formula.invert(25)).toThrow());
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
test("Integrate throws", () => expect(() => formula.evaluateIntegral()).toThrow()); test("Integrate throws", () => expect(() => formula.evaluateIntegral()).toThrow());
test("Invert integral throws", () => test("Invert integral throws", () =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
expect(() => formula.invertIntegral(25)).toThrow()); expect(() => formula.invertIntegral(25)).toThrow());
}); });
} }
@ -250,6 +257,8 @@ describe("Creating Formulas", () => {
test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false)); test("Is not marked as having a variable", () => expect(formula.hasVariable()).toBe(false));
test("Is not invertible", () => expect(formula.isInvertible()).toBe(false)); test("Is not invertible", () => expect(formula.isInvertible()).toBe(false));
test(`Formula throws if trying to invert`, () => test(`Formula throws if trying to invert`, () =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
expect(() => formula.invert(10)).toThrow()); expect(() => formula.invert(10)).toThrow());
test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false)); test("Is not integrable", () => expect(formula.isIntegrable()).toBe(false));
test("Has a non-invertible integral", () => test("Has a non-invertible integral", () =>
@ -361,7 +370,7 @@ describe("Variables", () => {
}); });
describe("Inverting", () => { describe("Inverting", () => {
let variable: GenericFormula; let variable: IntegrableFormula;
let constant: GenericFormula; let constant: GenericFormula;
beforeAll(() => { beforeAll(() => {
variable = Formula.variable(10); variable = Formula.variable(10);
@ -437,8 +446,8 @@ describe("Inverting", () => {
}); });
describe("Inverting calculates the value of the variable", () => { describe("Inverting calculates the value of the variable", () => {
let variable: GenericFormula; let variable: IntegrableFormula;
let constant: GenericFormula; let constant: IntegrableFormula;
beforeAll(() => { beforeAll(() => {
variable = Formula.variable(2); variable = Formula.variable(2);
constant = Formula.constant(3); constant = Formula.constant(3);
@ -448,7 +457,8 @@ describe("Inverting", () => {
test(`${name}(var, const).invert()`, () => { test(`${name}(var, const).invert()`, () => {
const formula = Formula[name](variable, constant); const formula = Formula[name](variable, constant);
const result = formula.evaluate(); const result = formula.evaluate();
expect(formula.invert(result)).compare_tolerance(2); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(formula.invert!(result)).compare_tolerance(2);
}); });
if (name !== "layeradd") { if (name !== "layeradd") {
test(`${name}(const, var).invert()`, () => { test(`${name}(const, var).invert()`, () => {
@ -489,8 +499,8 @@ describe("Inverting", () => {
}); });
describe("Integrating", () => { describe("Integrating", () => {
let variable: GenericFormula; let variable: IntegrableFormula;
let constant: GenericFormula; let constant: IntegrableFormula;
beforeAll(() => { beforeAll(() => {
variable = Formula.variable(ref(10)); variable = Formula.variable(ref(10));
constant = Formula.constant(10); constant = Formula.constant(10);
@ -502,7 +512,7 @@ describe("Integrating", () => {
expect(variable.evaluateIntegral(20)).compare_tolerance(Decimal.pow(20, 2).div(2))); expect(variable.evaluateIntegral(20)).compare_tolerance(Decimal.pow(20, 2).div(2)));
describe("Integrable functions marked as such", () => { describe("Integrable functions marked as such", () => {
function checkFormula(formula: GenericFormula) { function checkFormula(formula: IntegrableFormula) {
expect(formula.isIntegrable()).toBe(true); expect(formula.isIntegrable()).toBe(true);
expect(() => formula.evaluateIntegral()).to.not.throw(); expect(() => formula.evaluateIntegral()).to.not.throw();
} }
@ -612,8 +622,8 @@ describe("Integrating", () => {
}); });
describe("Inverting integrals", () => { describe("Inverting integrals", () => {
let variable: GenericFormula; let variable: InvertibleIntegralFormula;
let constant: GenericFormula; let constant: InvertibleIntegralFormula;
beforeAll(() => { beforeAll(() => {
variable = Formula.variable(10); variable = Formula.variable(10);
constant = Formula.constant(10); constant = Formula.constant(10);
@ -625,7 +635,7 @@ describe("Inverting integrals", () => {
)); ));
describe("Invertible Integral functions marked as such", () => { describe("Invertible Integral functions marked as such", () => {
function checkFormula(formula: GenericFormula) { function checkFormula(formula: InvertibleIntegralFormula) {
expect(formula.isIntegralInvertible()).toBe(true); expect(formula.isIntegralInvertible()).toBe(true);
expect(() => formula.invertIntegral(10)).to.not.throw(); expect(() => formula.invertIntegral(10)).to.not.throw();
} }
@ -918,7 +928,7 @@ describe("Conditionals", () => {
}); });
describe("Custom Formulas", () => { describe("Custom Formulas", () => {
let variable: GenericFormula; let variable: InvertibleIntegralFormula;
beforeAll(() => { beforeAll(() => {
variable = Formula.variable(1); variable = Formula.variable(1);
}); });
@ -1020,7 +1030,7 @@ describe("Custom Formulas", () => {
}); });
describe("Formula as input", () => { describe("Formula as input", () => {
let customFormula: GenericFormula; let customFormula: InvertibleIntegralFormula;
beforeAll(() => { beforeAll(() => {
customFormula = new Formula({ customFormula = new Formula({
inputs: [variable], inputs: [variable],
@ -1045,6 +1055,8 @@ describe("Buy Max", () => {
}); });
describe("Without spending", () => { describe("Without spending", () => {
test("Throws on formula with non-invertible integral", () => { test("Throws on formula with non-invertible integral", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false); const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
expect(() => maxAffordable.value).toThrow(); expect(() => maxAffordable.value).toThrow();
}); });