Make requirements support buying max

This commit is contained in:
thepaperpilot 2023-02-05 02:44:03 -06:00
parent 757cfaa1ab
commit 7eeb0318e2
3 changed files with 137 additions and 70 deletions

View file

@ -1992,7 +1992,7 @@ export default class Formula<T extends [FormulaSource] | FormulaSource[]> {
} }
/** /**
* Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers. Returns a ref of how many can be bought, as well as how much that will cost. * Utility for calculating the maximum amount of purchases possible with a given formula and resource. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers.
* @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 * @param spendResources Whether or not to count spent resources on each purchase or not
@ -2001,19 +2001,19 @@ export function calculateMaxAffordable(
formula: InvertibleFormula, formula: InvertibleFormula,
resource: Resource, resource: Resource,
spendResources?: true spendResources?: true
): { maxAffordable: ComputedRef<DecimalSource>; cost: ComputedRef<DecimalSource> }; ): ComputedRef<DecimalSource>;
export function calculateMaxAffordable( export function calculateMaxAffordable(
formula: InvertibleIntegralFormula, formula: InvertibleIntegralFormula,
resource: Resource, resource: Resource,
spendResources: Computable<boolean> spendResources: Computable<boolean>
): { maxAffordable: ComputedRef<DecimalSource>; cost: 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
) { ) {
const computedSpendResources = convertComputable(spendResources); const computedSpendResources = convertComputable(spendResources);
const maxAffordable = computed(() => { return computed(() => {
if (unref(computedSpendResources)) { if (unref(computedSpendResources)) {
if (!formula.isIntegrable() || !formula.isIntegralInvertible()) { if (!formula.isIntegrable() || !formula.isIntegralInvertible()) {
throw "Cannot calculate max affordable of formula with non-invertible integral"; throw "Cannot calculate max affordable of formula with non-invertible integral";
@ -2028,13 +2028,33 @@ export function calculateMaxAffordable(
return Decimal.floor((formula as InvertibleFormula).invert(resource.value)); return Decimal.floor((formula as InvertibleFormula).invert(resource.value));
} }
}); });
const cost = computed(() => { }
const newValue = maxAffordable.value.add(unref(formula.innermostVariable) ?? 0);
if (unref(computedSpendResources)) { /**
* Utility for calculating the cost of a formula for a given amount of purchases. If {@ref spendResources} is changed to false, the calculation will be much faster with higher numbers.
* @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
*/
export function calculateCost(
formula: InvertibleFormula,
amountToBuy: DecimalSource,
spendResources?: true
): DecimalSource;
export function calculateCost(
formula: InvertibleIntegralFormula,
amountToBuy: DecimalSource,
spendResources: boolean
): DecimalSource;
export function calculateCost(
formula: InvertibleFormula,
amountToBuy: DecimalSource,
spendResources = true
) {
const newValue = Decimal.add(amountToBuy, unref(formula.innermostVariable) ?? 0);
if (spendResources) {
return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral()); return Decimal.sub(formula.evaluateIntegral(newValue), formula.evaluateIntegral());
} else { } else {
return formula.evaluate(newValue); return formula.evaluate(newValue);
} }
});
return { maxAffordable, cost };
} }

View file

@ -11,6 +11,7 @@ import {
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { joinJSX, renderJSX } from "util/vue"; import { joinJSX, renderJSX } from "util/vue";
import { computed, unref } from "vue"; import { computed, unref } from "vue";
import Formula, { calculateCost, calculateMaxAffordable, GenericFormula } from "./formulas";
/** /**
* An object that can be used to describe a requirement to perform some purchase or other action. * An object that can be used to describe a requirement to perform some purchase or other action.
@ -18,36 +19,33 @@ import { computed, unref } from "vue";
*/ */
export interface Requirement { export interface Requirement {
/** The display for this specific requirement. This is used for displays multiple requirements condensed. Required if {@link visibility} can be {@link Visibility.Visible}. */ /** The display for this specific requirement. This is used for displays multiple requirements condensed. Required if {@link visibility} can be {@link Visibility.Visible}. */
partialDisplay?: JSXFunction; partialDisplay?: (amount?: DecimalSource) => JSX.Element;
/** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */ /** The display for this specific requirement. Required if {@link visibility} can be {@link Visibility.Visible}. */
display?: JSXFunction; display?: (amount?: DecimalSource) => JSX.Element;
visibility: ProcessedComputable<Visibility.Visible | Visibility.None>; visibility: ProcessedComputable<Visibility.Visible | Visibility.None>;
requirementMet: ProcessedComputable<boolean>; requirementMet: ProcessedComputable<DecimalSource | boolean>;
requiresPay: ProcessedComputable<boolean>; requiresPay: ProcessedComputable<boolean>;
pay?: VoidFunction; buyMax?: ProcessedComputable<boolean>;
pay?: (amount?: DecimalSource) => void;
} }
export type Requirements = Requirement | Requirement[]; export type Requirements = Requirement | Requirement[];
export interface CostRequirementOptions { export interface CostRequirementOptions {
resource: Resource; resource: Resource;
cost: Computable<DecimalSource>; cost: Computable<DecimalSource> | GenericFormula;
visibility?: Computable<Visibility.Visible | Visibility.None>; visibility?: Computable<Visibility.Visible | Visibility.None>;
requiresPay?: ProcessedComputable<boolean>; requiresPay?: Computable<boolean>;
pay?: VoidFunction; buyMax?: Computable<boolean>;
spendResources?: Computable<boolean>;
pay?: (amount?: DecimalSource) => void;
} }
export function createCostRequirement<T extends CostRequirementOptions>( export function createCostRequirement<T extends CostRequirementOptions>(optionsFunc: () => T) {
optionsFunc: () => T
): Requirement {
return createLazyProxy(() => { return createLazyProxy(() => {
const req = optionsFunc() as T & Partial<Requirement>; const req = optionsFunc() as T & Partial<Requirement>;
req.requirementMet = computed(() => req.partialDisplay = amount => (
Decimal.gte(req.resource.value, unref(req.cost as ProcessedComputable<DecimalSource>))
);
req.partialDisplay = jsx(() => (
<span <span
style={ style={
unref(req.requirementMet as ProcessedComputable<boolean>) unref(req.requirementMet as ProcessedComputable<boolean>)
@ -57,33 +55,81 @@ export function createCostRequirement<T extends CostRequirementOptions>(
> >
{displayResource( {displayResource(
req.resource, req.resource,
unref(req.cost as ProcessedComputable<DecimalSource>) req.cost instanceof Formula
? calculateCost(
req.cost,
amount ?? 1,
unref(
req.spendResources as ProcessedComputable<boolean> | undefined
) ?? true
)
: unref(req.cost as ProcessedComputable<DecimalSource>)
)}{" "} )}{" "}
{req.resource.displayName} {req.resource.displayName}
</span> </span>
)); );
req.display = jsx(() => ( req.display = amount => (
<div> <div>
{unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "} {unref(req.requiresPay as ProcessedComputable<boolean>) ? "Costs: " : "Requires: "}
{displayResource( {displayResource(
req.resource, req.resource,
unref(req.cost as ProcessedComputable<DecimalSource>) req.cost instanceof Formula
? calculateCost(
req.cost,
amount ?? 1,
unref(
req.spendResources as ProcessedComputable<boolean> | undefined
) ?? true
)
: unref(req.cost as ProcessedComputable<DecimalSource>)
)}{" "} )}{" "}
{req.resource.displayName} {req.resource.displayName}
</div> </div>
)); );
processComputable(req as T, "visibility"); processComputable(req as T, "visibility");
setDefault(req, "visibility", Visibility.Visible); setDefault(req, "visibility", Visibility.Visible);
processComputable(req as T, "cost"); processComputable(req as T, "cost");
processComputable(req as T, "requiresPay"); processComputable(req as T, "requiresPay");
processComputable(req as T, "spendResources");
setDefault(req, "requiresPay", true); setDefault(req, "requiresPay", true);
setDefault(req, "pay", function () { setDefault(req, "pay", function (amount?: DecimalSource) {
req.resource.value = Decimal.sub( const cost =
req.cost instanceof Formula
? calculateCost(
req.cost,
amount ?? 1,
unref(req.spendResources as ProcessedComputable<boolean> | undefined) ??
true
)
: unref(req.cost as ProcessedComputable<DecimalSource>);
req.resource.value = Decimal.sub(req.resource.value, cost).max(0);
});
processComputable(req as T, "buyMax");
if (
"buyMax" in req &&
req.buyMax !== false &&
req.cost instanceof Formula &&
req.cost.isInvertible()
) {
req.requirementMet = calculateMaxAffordable(
req.cost,
req.resource,
unref(req.spendResources as ProcessedComputable<boolean> | undefined) ?? true
);
} else {
req.requirementMet = computed(() => {
if (req.cost instanceof Formula) {
return Decimal.gte(req.resource.value, req.cost.evaluate());
} else {
return Decimal.gte(
req.resource.value, req.resource.value,
unref(req.cost as ProcessedComputable<DecimalSource>) unref(req.cost as ProcessedComputable<DecimalSource>)
).max(0); );
}
}); });
}
return req as Requirement; return req as Requirement;
}); });
@ -112,14 +158,26 @@ export function createBooleanRequirement(
})); }));
} }
export function requirementsMet(requirements: Requirements) { export function requirementsMet(requirements: Requirements): boolean {
if (isArray(requirements)) { if (isArray(requirements)) {
return requirements.every(r => unref(r.requirementMet)); return requirements.every(requirementsMet);
} }
return unref(requirements.requirementMet); const reqsMet = unref(requirements.requirementMet);
return typeof reqsMet === "boolean" ? reqsMet : Decimal.gt(reqsMet, 0);
} }
export function displayRequirements(requirements: Requirements) { export function maxRequirementsMet(requirements: Requirements): DecimalSource {
if (isArray(requirements)) {
return requirements.map(maxRequirementsMet).reduce(Decimal.min);
}
const reqsMet = unref(requirements.requirementMet);
if (typeof reqsMet === "boolean") {
return reqsMet ? Infinity : 0;
}
return reqsMet;
}
export function displayRequirements(requirements: Requirements, amount: DecimalSource = 1) {
if (isArray(requirements)) { if (isArray(requirements)) {
requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible); requirements = requirements.filter(r => unref(r.visibility) === Visibility.Visible);
if (requirements.length === 1) { if (requirements.length === 1) {
@ -136,7 +194,7 @@ export function displayRequirements(requirements: Requirements) {
<div> <div>
Costs:{" "} Costs:{" "}
{joinJSX( {joinJSX(
withCosts.map(r => r.partialDisplay!()), withCosts.map(r => r.partialDisplay!(amount)),
<>, </> <>, </>
)} )}
</div> </div>
@ -145,7 +203,7 @@ export function displayRequirements(requirements: Requirements) {
<div> <div>
Requires:{" "} Requires:{" "}
{joinJSX( {joinJSX(
withoutCosts.map(r => r.partialDisplay!()), withoutCosts.map(r => r.partialDisplay!(amount)),
<>, </> <>, </>
)} )}
</div> </div>
@ -156,10 +214,11 @@ export function displayRequirements(requirements: Requirements) {
return requirements.display?.() ?? <></>; return requirements.display?.() ?? <></>;
} }
export function payRequirements(requirements: Requirements) { export function payRequirements(requirements: Requirements, buyMax = false) {
const amount = buyMax ? maxRequirementsMet(requirements) : 1;
if (isArray(requirements)) { if (isArray(requirements)) {
requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.()); requirements.filter(r => unref(r.requiresPay)).forEach(r => r.pay?.(amount));
} else if (unref(requirements.requiresPay)) { } else if (unref(requirements.requiresPay)) {
requirements.pay?.(); requirements.pay?.(amount);
} }
} }

View file

@ -1,5 +1,6 @@
import { createResource, Resource } from "features/resources/resource"; import { createResource, Resource } from "features/resources/resource";
import Formula, { import Formula, {
calculateCost,
calculateMaxAffordable, calculateMaxAffordable,
GenericFormula, GenericFormula,
InvertibleFormula, InvertibleFormula,
@ -983,45 +984,32 @@ describe("Buy Max", () => {
}); });
describe("With spending", () => { describe("With spending", () => {
test("Throws on formula with non-invertible integral", () => { test("Throws on formula with non-invertible integral", () => {
const { maxAffordable, cost } = calculateMaxAffordable( const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false);
Formula.neg(10),
resource,
false
);
expect(() => maxAffordable.value).toThrow(); expect(() => maxAffordable.value).toThrow();
expect(() => cost.value).toThrow();
}); });
// https://www.desmos.com/calculator/5vgletdc1p // https://www.desmos.com/calculator/5vgletdc1p
test("Calculates max affordable and cost correctly", () => { test("Calculates max affordable and cost correctly", () => {
const variable = Formula.variable(10); const variable = Formula.variable(10);
const { maxAffordable, cost } = calculateMaxAffordable( const formula = Formula.pow(1.05, variable);
Formula.pow(1.05, variable), const maxAffordable = calculateMaxAffordable(formula, resource, false);
resource,
false
);
expect(maxAffordable.value).compare_tolerance(47); expect(maxAffordable.value).compare_tolerance(47);
expect(cost.value).compare_tolerance(Decimal.pow(1.05, 47)); expect(calculateCost(formula, maxAffordable.value, false)).compare_tolerance(
Decimal.pow(1.05, 47)
);
}); });
}); });
describe("Without spending", () => { describe("Without spending", () => {
test("Throws on non-invertible formula", () => { test("Throws on non-invertible formula", () => {
const { maxAffordable, cost } = calculateMaxAffordable( const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource, false);
Formula.abs(10),
resource,
false
);
expect(() => maxAffordable.value).toThrow(); expect(() => maxAffordable.value).toThrow();
expect(() => cost.value).toThrow();
}); });
// https://www.desmos.com/calculator/5vgletdc1p // https://www.desmos.com/calculator/5vgletdc1p
test("Calculates max affordable and cost correctly", () => { test("Calculates max affordable and cost correctly", () => {
const variable = Formula.variable(10); const variable = Formula.variable(10);
const { maxAffordable, cost } = calculateMaxAffordable( const formula = Formula.pow(1.05, variable);
Formula.pow(1.05, variable), const maxAffordable = calculateMaxAffordable(formula, resource);
resource
);
expect(maxAffordable.value).compare_tolerance(7); expect(maxAffordable.value).compare_tolerance(7);
expect(cost.value).compare_tolerance(7.35); expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(7.35);
}); });
}); });
}); });