diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 09f17b9..12ea545 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -1359,39 +1359,66 @@ export function printFormula(formula: FormulaSource): string { * @param formula The formula to use for calculating buy max from * @param resource The resource used when purchasing (is only read from) * @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards fewer purchases + * @param summedPurchases How many of the most expensive purchases should be manually summed for better accuracy. If unspecified uses 10 when spending resources and 0 when not */ export function calculateMaxAffordable( formula: InvertibleFormula, resource: Resource, - spendResources?: true + spendResources?: true, + summedPurchases?: number ): ComputedRef; export function calculateMaxAffordable( formula: InvertibleIntegralFormula, resource: Resource, - spendResources: Computable + spendResources: Computable, + summedPurchases?: number ): ComputedRef; export function calculateMaxAffordable( formula: InvertibleFormula, resource: Resource, - spendResources: Computable = true + spendResources: Computable = true, + summedPurchases?: number ) { const computedSpendResources = convertComputable(spendResources); return computed(() => { + let affordable; if (unref(computedSpendResources)) { if (!formula.isIntegrable() || !formula.isIntegralInvertible()) { throw new Error( "Cannot calculate max affordable of formula with non-invertible integral" ); } - return Decimal.floor( + affordable = Decimal.floor( formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral())) ).sub(unref(formula.innermostVariable) ?? 0); + if (summedPurchases == null) { + summedPurchases = 10; + } } else { if (!formula.isInvertible()) { throw new Error("Cannot calculate max affordable of non-invertible formula"); } - return Decimal.floor(formula.invert(resource.value)); + affordable = Decimal.floor(formula.invert(resource.value)); + if (summedPurchases == null) { + summedPurchases = 0; + } } + if (summedPurchases > 0) { + affordable = affordable.sub(summedPurchases).clampMin(0); + let summedCost = calculateCost(formula, affordable, true, 0); + while (true) { + const nextCost = formula.evaluate( + affordable.add(unref(formula.innermostVariable) ?? 0) + ); + if (Decimal.add(summedCost, nextCost).lt(resource.value)) { + affordable = affordable.add(1); + summedCost = Decimal.add(summedCost, nextCost); + } else { + break; + } + } + } + return affordable; }); } @@ -1400,26 +1427,56 @@ export function calculateMaxAffordable( * @param formula The formula to use for calculating buy max from * @param amountToBuy The amount of purchases to calculate the cost for * @param spendResources Whether or not to count spent resources on each purchase or not. If true, costs will be approximated for performance, skewing towards higher cost + * @param summedPurchases How many purchases to manually sum for improved accuracy. If not specified, defaults to 10 when spending resources and 0 when not */ export function calculateCost( formula: InvertibleFormula, amountToBuy: DecimalSource, - spendResources?: true + spendResources?: true, + summedPurchases?: number ): DecimalSource; export function calculateCost( formula: InvertibleIntegralFormula, amountToBuy: DecimalSource, - spendResources: boolean + spendResources: boolean, + summedPurchases?: number ): DecimalSource; export function calculateCost( formula: InvertibleFormula, amountToBuy: DecimalSource, - spendResources = true + spendResources = true, + summedPurchases?: number ) { - const newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0); + let newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0); if (spendResources) { - return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral()); + const targetValue = newValue; + newValue = newValue + .sub(summedPurchases ?? 10) + .clampMin(unref(formula.innermostVariable) ?? 0); + let cost = Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral()); + if (targetValue.gt(1e308)) { + // Too large of a number for summedPurchases to make a difference, + // just get the cost and multiply by summed purchases + return cost.add(Decimal.sub(targetValue, newValue).times(formula.evaluate(newValue))); + } + for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) { + cost = cost.add(formula.evaluate(i)); + } + return cost; } else { - return formula.evaluate(newValue); + const targetValue = newValue; + newValue = newValue + .sub(summedPurchases ?? 0) + .clampMin(unref(formula.innermostVariable) ?? 0); + let cost = formula.evaluate(newValue); + if (targetValue.gt(1e308)) { + // Too large of a number for summedPurchases to make a difference, + // just get the cost and multiply by summed purchases + return Decimal.sub(targetValue, newValue).add(1).times(cost); + } + for (let i = newValue.toNumber(); i < targetValue.toNumber(); i++) { + cost = Decimal.add(cost, formula.evaluate(i)); + } + return cost; } } diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index fbaa378..ed5adb2 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -1022,21 +1022,20 @@ describe("Custom Formulas", () => { describe("Buy Max", () => { let resource: Resource; beforeAll(() => { - resource = createResource(ref(1000)); + resource = createResource(ref(100000)); }); describe("Without spending", () => { test("Throws on formula with non-invertible integral", () => { const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false); expect(() => maxAffordable.value).toThrow(); }); - // https://www.desmos.com/calculator/7ffthe7wi8 test("Calculates max affordable and cost correctly", () => { const variable = Formula.variable(0); const formula = Formula.pow(1.05, variable).times(100); const maxAffordable = calculateMaxAffordable(formula, resource, false); - expect(maxAffordable.value).compare_tolerance(47); + expect(maxAffordable.value).compare_tolerance(141); expect(calculateCost(formula, maxAffordable.value, false)).compare_tolerance( - Decimal.pow(1.05, 47).times(100) + Decimal.pow(1.05, 141).times(100) ); }); }); @@ -1045,11 +1044,11 @@ describe("Buy Max", () => { const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource); expect(() => maxAffordable.value).toThrow(); }); - test("Calculates max affordable and cost correctly with 0 purchases", () => { + test("Estimates 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); + const maxAffordable = calculateMaxAffordable(formula, resource, true, 0); let actualAffordable = 0; let summedCost = Decimal.dZero; while (true) { @@ -1073,11 +1072,11 @@ describe("Buy Max", () => { Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber() ).toBeLessThan(0.1); }); - test("Calculates max affordable and cost correctly with 1 purchase", () => { + test("Estimates max affordable and cost 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); + const maxAffordable = calculateMaxAffordable(formula, resource, true, 0); let actualAffordable = 0; let summedCost = Decimal.dZero; while (true) { @@ -1101,5 +1100,71 @@ describe("Buy Max", () => { Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber() ).toBeLessThan(0.1); }); + test("Estimates max affordable and cost more accurately with summing last purchases", () => { + 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); + // Since we're summing the last few purchases, this has a tighter deviation allowed + expect( + Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber() + ).toBeLessThan(0.02); + }); + test("Handles summing purchases when making few purchases", () => { + const purchases = ref(90); + 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, purchases.value)); + 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 + purchases.value)), + new Decimal(0) + ); + const calculatedCost = calculateCost(formula, maxAffordable.value); + // Since we're summing all the purchases this should be equivalent + expect(calculatedCost).compare_tolerance(actualCost); + }); + test("Handles summing purchases when over e308 purchases", () => { + resource.value = "1ee308"; + const purchases = ref(0); + const variable = Formula.variable(purchases); + const formula = variable; + const maxAffordable = calculateMaxAffordable(formula, resource); + const calculatedCost = calculateCost(formula, maxAffordable.value); + expect(Decimal.isNaN(calculatedCost)).toBe(false); + expect(Decimal.isFinite(calculatedCost)).toBe(true); + resource.value = 100000; + }); }); });