forked from profectus/Profectus
Add summedPurchases param for buy max utilities
This commit is contained in:
parent
fb360c72c5
commit
7e7a36bb78
2 changed files with 141 additions and 19 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue