From 8dc0c6c55cf24d5cb241103d1ba83d777758978d Mon Sep 17 00:00:00 2001
From: thepaperpilot <thepaperpilot@gmail.com>
Date: Sun, 2 Apr 2023 15:02:43 -0500
Subject: [PATCH] All tests pass now

---
 src/game/formulas/formulas.ts   | 28 ++++++-------
 src/game/formulas/operations.ts | 18 +-------
 tests/game/formulas.test.ts     | 74 ++++++++++++++++++++++++++-------
 3 files changed, 75 insertions(+), 45 deletions(-)

diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts
index 030e604..09f17b9 100644
--- a/src/game/formulas/formulas.ts
+++ b/src/game/formulas/formulas.ts
@@ -32,6 +32,10 @@ function integrateVariable(this: GenericFormula) {
     return Formula.pow(this, 2).div(2);
 }
 
+function integrateVariableInner(this: GenericFormula) {
+    return this;
+}
+
 /**
  * A class that can be used for cost/goal functions. It can be evaluated similar to a cost function, but also provides extra features for supported formulas. For example, a lot of math functions can be inverted.
  * Typically, the use of these extra features is to support cost/goal functions that have multiple levels purchased/completed at once efficiently.
@@ -81,6 +85,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
             internalVariables: 1,
             innermostVariable: variable,
             internalIntegrate: integrateVariable,
+            internalIntegrateInner: integrateVariableInner,
             applySubstitution: ops.passthrough as unknown as SubstitutionFunction<T>
         };
     }
@@ -119,7 +124,8 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         };
     }
 
-    private calculateConstantOfIntegration() {
+    /** Calculates C for the implementation of the integral formula for this formula. */
+    calculateConstantOfIntegration() {
         // Calculate C based on the knowledge that at x=1, the integral should be the average between f(0) and f(1)
         const integral = this.getIntegralFormula().evaluate(1);
         const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2);
@@ -189,10 +195,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
         if (!this.isIntegrable()) {
             throw new Error("Cannot evaluate integral of formula without integral");
         }
-        return Decimal.add(
-            this.getIntegralFormula().evaluate(variable),
-            this.calculateConstantOfIntegration()
-        );
+        return this.getIntegralFormula().evaluate(variable);
     }
 
     /**
@@ -212,7 +215,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
      * @param stack For nested formulas, a stack of operations that occur outside the complex operation.
      */
     getIntegralFormula(stack?: SubstitutionStack): GenericFormula {
-        if (this.integralFormula != null) {
+        if (this.integralFormula != null && stack == null) {
             return this.integralFormula;
         }
         if (stack == null) {
@@ -245,6 +248,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                     throw new Error("Cannot integrate formula without variable");
                 }
             }
+            return this.integralFormula;
         } else {
             // "Inner" part of the formula
             if (this.applySubstitution == null) {
@@ -255,25 +259,19 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
                 this.applySubstitution!.call(this, variable, ...this.inputs)
             );
             if (this.internalIntegrateInner) {
-                this.integralFormula = this.internalIntegrateInner.call(
-                    this,
-                    stack,
-                    ...this.inputs
-                );
+                return this.internalIntegrateInner.call(this, stack, ...this.inputs);
             } else if (this.internalIntegrate) {
-                this.integralFormula = this.internalIntegrate.call(this, stack, ...this.inputs);
+                return this.internalIntegrate.call(this, stack, ...this.inputs);
             } else if (
                 this.inputs.length === 1 &&
                 this.internalEvaluate == null &&
                 this.hasVariable()
             ) {
-                // eslint-disable-next-line @typescript-eslint/no-this-alias
-                this.integralFormula = this;
+                return this;
             } else {
                 throw new Error("Cannot integrate formula without variable");
             }
         }
-        return this.integralFormula;
     }
 
     /**
diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts
index becaec3..c6cbd05 100644
--- a/src/game/formulas/operations.ts
+++ b/src/game/formulas/operations.ts
@@ -326,24 +326,10 @@ export function invertPow10(value: DecimalSource, lhs: FormulaSource) {
     throw new Error("Could not invert due to no input being a variable");
 }
 
-function internalIntegratePow10(lhs: DecimalSource) {
-    return Decimal.pow10(lhs).div(Decimal.ln(lhs));
-}
-
-function internalInvertIntegralPow10(value: DecimalSource, lhs: FormulaSource) {
-    if (hasVariable(lhs)) {
-        return lhs.invert(ln10.times(value).ln().div(ln10));
-    }
-    throw new Error("Could not invert due to no input being a variable");
-}
-
 export function integratePow10(stack: SubstitutionStack, lhs: FormulaSource) {
     if (hasVariable(lhs)) {
-        return new Formula({
-            inputs: [lhs.getIntegralFormula(stack)],
-            evaluate: internalIntegratePow10,
-            invert: internalInvertIntegralPow10
-        });
+        const x = lhs.getIntegralFormula(stack);
+        return Formula.pow10(x).div(Formula.ln(10));
     }
     throw new Error("Could not integrate due to no input being a variable");
 }
diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts
index ccaa485..9c77d86 100644
--- a/tests/game/formulas.test.ts
+++ b/tests/game/formulas.test.ts
@@ -2,10 +2,11 @@ import { createResource, Resource } from "features/resources/resource";
 import Formula, {
     calculateCost,
     calculateMaxAffordable,
+    printFormula,
     unrefFormulaSource
 } from "game/formulas/formulas";
 import type { GenericFormula, InvertibleFormula } from "game/formulas/types";
-import Decimal, { DecimalSource } from "util/bignum";
+import Decimal, { DecimalSource, format } from "util/bignum";
 import { beforeAll, describe, expect, test } from "vitest";
 import { ref } from "vue";
 import "../utils";
@@ -572,7 +573,13 @@ describe("Integrating", () => {
         // Check if the calculated cost is within 10% of the actual cost,
         // because this is an approximation
         expect(
-            Decimal.sub(actualCost, formula.evaluateIntegral()).abs().div(actualCost).toNumber()
+            Decimal.sub(
+                actualCost,
+                Decimal.add(formula.evaluateIntegral(), formula.calculateConstantOfIntegration())
+            )
+                .abs()
+                .div(actualCost)
+                .toNumber()
         ).toBeLessThan(0.1);
     });
 
@@ -668,7 +675,7 @@ describe("Inverting integrals", () => {
 
     test("Inverting integral of nested formulas", () => {
         const formula = Formula.add(variable, constant).times(constant).pow(2).times(30);
-        expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10, 0.01);
+        expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10);
     });
 
     test("Inverting integral of nested complex formulas", () => {
@@ -929,18 +936,18 @@ describe("Custom Formulas", () => {
             expect(
                 new Formula({
                     inputs: [variable],
-                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    evaluate: v1 => Decimal.add(v1, 10),
                     integrate: (stack, v1) => Formula.add(v1, 10)
                 }).evaluateIntegral()
-            ).compare_tolerance(20));
+            ).compare_tolerance(11));
         test("Two inputs integrates correctly", () =>
             expect(
                 new Formula({
                     inputs: [variable, 10],
-                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    evaluate: (v1, v2) => Decimal.add(v1, v2),
                     integrate: (stack, v1, v2) => Formula.add(v1, v2)
                 }).evaluateIntegral()
-            ).compare_tolerance(20));
+            ).compare_tolerance(11));
     });
 
     describe("Formula with invertIntegral", () => {
@@ -956,7 +963,7 @@ describe("Custom Formulas", () => {
             expect(
                 new Formula({
                     inputs: [variable],
-                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    evaluate: v1 => Decimal.add(v1, 10),
                     integrate: (stack, v1) => Formula.add(v1, 10)
                 }).invertIntegral(20)
             ).compare_tolerance(10));
@@ -964,7 +971,7 @@ describe("Custom Formulas", () => {
             expect(
                 new Formula({
                     inputs: [variable, 10],
-                    evaluate: v1 => Decimal.add(v1, 19.5),
+                    evaluate: (v1, v2) => Decimal.add(v1, v2),
                     integrate: (stack, v1, v2) => Formula.add(v1, v2)
                 }).invertIntegral(20)
             ).compare_tolerance(10));
@@ -1001,14 +1008,25 @@ describe("Buy Max", () => {
             const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
             expect(() => maxAffordable.value).toThrow();
         });
-        // https://www.desmos.com/calculator/7ffthe7wi8
-        test("Calculates max affordable and cost correctly", () => {
-            const variable = Formula.variable(0);
+        test("Calculates max affordable and cost correctly with 0 purchases", () => {
+            const purchases = ref(0);
+            const variable = Formula.variable(purchases);
             const formula = Formula.pow(1.05, variable).times(100);
             const maxAffordable = calculateMaxAffordable(formula, resource);
-            expect(maxAffordable.value).compare_tolerance(7);
+            let actualAffordable = 0;
+            let summedCost = Decimal.dZero;
+            while (true) {
+                const nextCost = formula.evaluate(actualAffordable);
+                if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
+                    actualAffordable++;
+                    summedCost = Decimal.add(summedCost, nextCost);
+                } else {
+                    break;
+                }
+            }
+            expect(maxAffordable.value).compare_tolerance(actualAffordable);
 
-            const actualCost = new Array(7)
+            const actualCost = new Array(actualAffordable)
                 .fill(null)
                 .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0));
             const calculatedCost = calculateCost(formula, maxAffordable.value);
@@ -1018,5 +1036,33 @@ describe("Buy Max", () => {
                 Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
             ).toBeLessThan(0.1);
         });
+        test("Calculates max affordable and cost correctly with 1 purchase", () => {
+            const purchases = ref(1);
+            const variable = Formula.variable(purchases);
+            const formula = Formula.pow(1.05, variable).times(100);
+            const maxAffordable = calculateMaxAffordable(formula, resource);
+            let actualAffordable = 0;
+            let summedCost = Decimal.dZero;
+            while (true) {
+                const nextCost = formula.evaluate(Decimal.add(actualAffordable, 1));
+                if (Decimal.add(summedCost, nextCost).lte(resource.value)) {
+                    actualAffordable++;
+                    summedCost = Decimal.add(summedCost, nextCost);
+                } else {
+                    break;
+                }
+            }
+            expect(maxAffordable.value).compare_tolerance(actualAffordable);
+
+            const actualCost = new Array(actualAffordable)
+                .fill(null)
+                .reduce((acc, _, i) => acc.add(formula.evaluate(i + 1)), new Decimal(0));
+            const calculatedCost = calculateCost(formula, maxAffordable.value);
+            // Check if the calculated cost is within 10% of the actual cost,
+            // because this is an approximation
+            expect(
+                Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
+            ).toBeLessThan(0.1);
+        });
     });
 });