Add summedPurchases param for buy max utilities

This commit is contained in:
thepaperpilot 2023-04-02 16:41:39 -05:00
parent fb360c72c5
commit 7e7a36bb78
2 changed files with 141 additions and 19 deletions

View file

@ -1359,39 +1359,66 @@ export function printFormula(formula: FormulaSource): string {
* @param formula The formula to use for calculating buy max from * @param formula The formula to use for calculating buy max from
* @param resource The resource used when purchasing (is only read 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 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( export function calculateMaxAffordable(
formula: InvertibleFormula, formula: InvertibleFormula,
resource: Resource, resource: Resource,
spendResources?: true spendResources?: true,
summedPurchases?: number
): ComputedRef<DecimalSource>; ): ComputedRef<DecimalSource>;
export function calculateMaxAffordable( export function calculateMaxAffordable(
formula: InvertibleIntegralFormula, formula: InvertibleIntegralFormula,
resource: Resource, resource: Resource,
spendResources: Computable<boolean> spendResources: Computable<boolean>,
summedPurchases?: number
): ComputedRef<DecimalSource>; ): ComputedRef<DecimalSource>;
export function calculateMaxAffordable( export function calculateMaxAffordable(
formula: InvertibleFormula, formula: InvertibleFormula,
resource: Resource, resource: Resource,
spendResources: Computable<boolean> = true spendResources: Computable<boolean> = true,
summedPurchases?: number
) { ) {
const computedSpendResources = convertComputable(spendResources); const computedSpendResources = convertComputable(spendResources);
return computed(() => { return computed(() => {
let affordable;
if (unref(computedSpendResources)) { if (unref(computedSpendResources)) {
if (!formula.isIntegrable() || !formula.isIntegralInvertible()) { if (!formula.isIntegrable() || !formula.isIntegralInvertible()) {
throw new Error( throw new Error(
"Cannot calculate max affordable of formula with non-invertible integral" "Cannot calculate max affordable of formula with non-invertible integral"
); );
} }
return Decimal.floor( affordable = Decimal.floor(
formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral())) formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral()))
).sub(unref(formula.innermostVariable) ?? 0); ).sub(unref(formula.innermostVariable) ?? 0);
if (summedPurchases == null) {
summedPurchases = 10;
}
} else { } else {
if (!formula.isInvertible()) { if (!formula.isInvertible()) {
throw new Error("Cannot calculate max affordable of non-invertible formula"); 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 formula The formula to use for calculating buy max from
* @param amountToBuy The amount of purchases to calculate the cost for * @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 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( export function calculateCost(
formula: InvertibleFormula, formula: InvertibleFormula,
amountToBuy: DecimalSource, amountToBuy: DecimalSource,
spendResources?: true spendResources?: true,
summedPurchases?: number
): DecimalSource; ): DecimalSource;
export function calculateCost( export function calculateCost(
formula: InvertibleIntegralFormula, formula: InvertibleIntegralFormula,
amountToBuy: DecimalSource, amountToBuy: DecimalSource,
spendResources: boolean spendResources: boolean,
summedPurchases?: number
): DecimalSource; ): DecimalSource;
export function calculateCost( export function calculateCost(
formula: InvertibleFormula, formula: InvertibleFormula,
amountToBuy: DecimalSource, 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) { 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 { } 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;
} }
} }

View file

@ -1022,21 +1022,20 @@ describe("Custom Formulas", () => {
describe("Buy Max", () => { describe("Buy Max", () => {
let resource: Resource; let resource: Resource;
beforeAll(() => { beforeAll(() => {
resource = createResource(ref(1000)); resource = createResource(ref(100000));
}); });
describe("Without spending", () => { describe("Without spending", () => {
test("Throws on formula with non-invertible integral", () => { test("Throws on formula with non-invertible integral", () => {
const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false); const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
expect(() => maxAffordable.value).toThrow(); expect(() => maxAffordable.value).toThrow();
}); });
// https://www.desmos.com/calculator/7ffthe7wi8
test("Calculates max affordable and cost correctly", () => { test("Calculates max affordable and cost correctly", () => {
const variable = Formula.variable(0); const variable = Formula.variable(0);
const formula = Formula.pow(1.05, variable).times(100); const formula = Formula.pow(1.05, variable).times(100);
const maxAffordable = calculateMaxAffordable(formula, resource, false); 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( 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); const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource);
expect(() => maxAffordable.value).toThrow(); 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 purchases = ref(0);
const variable = Formula.variable(purchases); const variable = Formula.variable(purchases);
const formula = Formula.pow(1.05, variable).times(100); 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 actualAffordable = 0;
let summedCost = Decimal.dZero; let summedCost = Decimal.dZero;
while (true) { while (true) {
@ -1073,11 +1072,11 @@ describe("Buy Max", () => {
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber() Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
).toBeLessThan(0.1); ).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 purchases = ref(1);
const variable = Formula.variable(purchases); const variable = Formula.variable(purchases);
const formula = Formula.pow(1.05, variable).times(100); 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 actualAffordable = 0;
let summedCost = Decimal.dZero; let summedCost = Decimal.dZero;
while (true) { while (true) {
@ -1101,5 +1100,71 @@ describe("Buy Max", () => {
Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber() Decimal.sub(actualCost, calculatedCost).abs().div(actualCost).toNumber()
).toBeLessThan(0.1); ).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;
});
}); });
}); });