From 3489c83cc46fe8f8389a732706e29c3488e74076 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 27 Feb 2023 22:44:30 -0600 Subject: [PATCH 01/36] Add util for creating formula from a modifier --- src/data/common.tsx | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/data/common.tsx b/src/data/common.tsx index b0becca..ec3317e 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -8,7 +8,7 @@ import { GenericMilestone } from "features/milestones/milestone"; import { displayResource, Resource } from "features/resources/resource"; import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree"; import { createTreeNode } from "features/trees/tree"; -import { GenericFormula } from "game/formulas"; +import Formula, { FormulaSource, GenericFormula, InvertibleFormula } from "game/formulas"; import type { Modifier } from "game/modifiers"; import type { Persistent } from "game/persistence"; import { DefaultValue, persistent } from "game/persistence"; @@ -490,3 +490,24 @@ export function createFormulaPreview( return formatSmall(formula.evaluate()); }); } + +export function modifierToFormula( + modifier: WithRequired, + base: T +): T; +export function modifierToFormula(modifier: Modifier, base: FormulaSource): GenericFormula; +export function modifierToFormula(modifier: Modifier, base: FormulaSource) { + return new Formula({ + inputs: [base], + evaluate: val => modifier.apply(val), + invert: + "revert" in modifier && modifier.revert != null + ? (val, lhs) => { + if (lhs instanceof Formula && lhs.hasVariable()) { + return lhs.invert(modifier.revert!(val)); + } + throw "Could not invert due to no input being a variable"; + } + : undefined + }); +} From 1b8755dbfc891866bf4120abfaf733ac7613fbe2 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 27 Feb 2023 23:47:31 -0600 Subject: [PATCH 02/36] Write up changelog thus far --- CHANGELOG.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b982041..b2afda6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **BREAKING** New requirements system + - Replaces many features' existing requirements with new generic form +- Formulas, which can be used to calculate buy max for you +- Action feature +- ETA util +- createCollapsibleMilestones util +- deleteLowerSaves util +- Minimized layers can now display a component +- submitOnBlur property to Text fields +- showPopups property to Milestones +- Mouse/touch events to more onClick listeners +- Example hotkey to starting layer +- Schema for projInfo.json +### Changes +- **BREAKING** Buyables renamed to Repeatables + - Renamed purchaseLimit to limit + - Renamed buyMax to maximize + - Added initialAmount property +- **BREAKING** Persistent refs no longer have redundancies in save object + - Requires referencing persistent refs either through a proxy or by wrapping in `noPersist()` +- **BREAKING** Visibility properties can now take booleans + - Removed showIf util +- Tweaked settings display +- setupPassiveGeneration will no longer lower the resource +- displayResource now floors resource amounts +- Tweaked modifier displays, incl showing negative modifiers in red +- Hotkeys now appear on key graphic +- Mofifier sections now accept computable strings for title and subtitle +- Updated b_e +### Fixed +- NaN detection stopped working + - Now specifically only checks persistent refs +- trackTotal would increase the total when loading the save +- PWAs wouldn't show updates +- Board feature no longer working at all +- Some discord links didn't open in new tab +- Adjacent grid cells wouldn't merge +- When fixing old saves, the modVersion would not be updated +- Default layer would display `Dev Speed: 0x` when paused +- Fixed hotkeys not working with shift + numbers +- Fixed console errors about deleted persistent refs not being included in the layer object +- Modifiers wouldn't display small numbers +- Conversions' addSoftcap wouldn't affect currentAt or nextAt +- MainDisplay not respecting style and classes props +- Tabs could sometimes not update correctly +- offlineTime not capping properly +- Tooltips being user-selectable +- Workflows not working with submodules +- Various minor typing issues +### Documented +- requirements.tsx +- formulas.tsx +- repeatables.tsx +### Tests +- requirements +- formulas + +Contributors: thepaperpilot, escapee, adsaf, ducdat + ## [0.5.2] - 2022-08-22 ### Added - onLoad event From 8939ad133a4127d6004beb9201db18d7d2507fce Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Wed, 15 Mar 2023 16:30:29 -0500 Subject: [PATCH 03/36] Update vitest --- package-lock.json | 196 ++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 96 insertions(+), 102 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7082e7c..31bffe8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "jsdom": "^20.0.0", "prettier": "^2.5.1", "typescript": "^4.7.4", - "vitest": "^0.28.5", + "vitest": "^0.29.3", "vue-tsc": "^0.38.1" }, "engines": { @@ -2718,23 +2718,23 @@ } }, "node_modules/@vitest/expect": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.28.5.tgz", - "integrity": "sha512-gqTZwoUTwepwGIatnw4UKpQfnoyV0Z9Czn9+Lo2/jLIt4/AXLTn+oVZxlQ7Ng8bzcNkR+3DqLJ08kNr8jRmdNQ==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.29.3.tgz", + "integrity": "sha512-z/0JqBqqrdtrT/wzxNrWC76EpkOHdl+SvuNGxWulLaoluygntYyG5wJul5u/rQs5875zfFz/F+JaDf90SkLUIg==", "dev": true, "dependencies": { - "@vitest/spy": "0.28.5", - "@vitest/utils": "0.28.5", + "@vitest/spy": "0.29.3", + "@vitest/utils": "0.29.3", "chai": "^4.3.7" } }, "node_modules/@vitest/runner": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.28.5.tgz", - "integrity": "sha512-NKkHtLB+FGjpp5KmneQjTcPLWPTDfB7ie+MmF1PnUBf/tGe2OjGxWyB62ySYZ25EYp9krR5Bw0YPLS/VWh1QiA==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.29.3.tgz", + "integrity": "sha512-XLi8ctbvOWhUWmuvBUSIBf8POEDH4zCh6bOuVxm/KGfARpgmVF1ku+vVNvyq85va+7qXxtl+MFmzyXQ2xzhAvw==", "dev": true, "dependencies": { - "@vitest/utils": "0.28.5", + "@vitest/utils": "0.29.3", "p-limit": "^4.0.0", "pathe": "^1.1.0" } @@ -2767,24 +2767,23 @@ } }, "node_modules/@vitest/spy": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.28.5.tgz", - "integrity": "sha512-7if6rsHQr9zbmvxN7h+gGh2L9eIIErgf8nSKYDlg07HHimCxp4H6I/X/DPXktVPPLQfiZ1Cw2cbDIx9fSqDjGw==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.29.3.tgz", + "integrity": "sha512-LLpCb1oOCOZcBm0/Oxbr1DQTuKLRBsSIHyLYof7z4QVE8/v8NcZKdORjMUq645fcfX55+nLXwU/1AQ+c2rND+w==", "dev": true, "dependencies": { "tinyspy": "^1.0.2" } }, "node_modules/@vitest/utils": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.28.5.tgz", - "integrity": "sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.29.3.tgz", + "integrity": "sha512-hg4Ff8AM1GtUnLpUJlNMxrf9f4lZr/xRJjh3uJ0QFP+vjaW82HAxKrmeBmLnhc8Os2eRf+f+VBu4ts7TafPPkA==", "dev": true, "dependencies": { "cli-truncate": "^3.1.0", "diff": "^5.1.0", "loupe": "^2.3.6", - "picocolors": "^1.0.0", "pretty-format": "^27.5.1" } }, @@ -5819,15 +5818,15 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/mlly": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.1.0.tgz", - "integrity": "sha512-cwzBrBfwGC1gYJyfcy8TcZU1f+dbH/T+TuOhtYP2wLv/Fb51/uV7HJQfBPtEupZ2ORLRU1EKFS/QfS3eo9+kBQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", + "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", "dev": true, "dependencies": { - "acorn": "^8.8.1", - "pathe": "^1.0.0", - "pkg-types": "^1.0.1", - "ufo": "^1.0.1" + "acorn": "^8.8.2", + "pathe": "^1.1.0", + "pkg-types": "^1.0.2", + "ufo": "^1.1.1" } }, "node_modules/ms": { @@ -6083,14 +6082,14 @@ } }, "node_modules/pkg-types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.1.tgz", - "integrity": "sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz", + "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==", "dev": true, "dependencies": { "jsonc-parser": "^3.2.0", - "mlly": "^1.0.0", - "pathe": "^1.0.0" + "mlly": "^1.1.1", + "pathe": "^1.1.0" } }, "node_modules/postcss": { @@ -7019,9 +7018,9 @@ } }, "node_modules/ufo": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz", - "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", + "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==", "dev": true }, "node_modules/unbox-primitive": { @@ -7202,9 +7201,9 @@ } }, "node_modules/vite-node": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.5.tgz", - "integrity": "sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.29.3.tgz", + "integrity": "sha512-QYzYSA4Yt2IiduEjYbccfZQfxKp+T1Do8/HEpSX/G5WIECTFKJADwLs9c94aQH4o0A+UtCKU61lj1m5KvbxxQA==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -7212,8 +7211,6 @@ "mlly": "^1.1.0", "pathe": "^1.1.0", "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "source-map-support": "^0.5.21", "vite": "^3.0.0 || ^4.0.0" }, "bin": { @@ -7280,9 +7277,9 @@ } }, "node_modules/vite-node/node_modules/rollup": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.15.0.tgz", - "integrity": "sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg==", + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.19.1.tgz", + "integrity": "sha512-lAbrdN7neYCg/8WaoWn/ckzCtz+jr70GFfYdlf50OF7387HTg+wiuiqJRFYawwSPpqfqDNYqK7smY/ks2iAudg==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -7296,9 +7293,9 @@ } }, "node_modules/vite-node/node_modules/vite": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz", - "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.4.tgz", + "integrity": "sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==", "dev": true, "dependencies": { "esbuild": "^0.16.14", @@ -7380,18 +7377,18 @@ } }, "node_modules/vitest": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.28.5.tgz", - "integrity": "sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.29.3.tgz", + "integrity": "sha512-muMsbXnZsrzDGiyqf/09BKQsGeUxxlyLeLK/sFFM4EXdURPQRv8y7dco32DXaRORYP0bvyN19C835dT23mL0ow==", "dev": true, "dependencies": { "@types/chai": "^4.3.4", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.28.5", - "@vitest/runner": "0.28.5", - "@vitest/spy": "0.28.5", - "@vitest/utils": "0.28.5", + "@vitest/expect": "0.29.3", + "@vitest/runner": "0.29.3", + "@vitest/spy": "0.29.3", + "@vitest/utils": "0.29.3", "acorn": "^8.8.1", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -7407,7 +7404,7 @@ "tinypool": "^0.3.1", "tinyspy": "^1.0.2", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.28.5", + "vite-node": "0.29.3", "why-is-node-running": "^2.2.2" }, "bin": { @@ -9914,23 +9911,23 @@ } }, "@vitest/expect": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.28.5.tgz", - "integrity": "sha512-gqTZwoUTwepwGIatnw4UKpQfnoyV0Z9Czn9+Lo2/jLIt4/AXLTn+oVZxlQ7Ng8bzcNkR+3DqLJ08kNr8jRmdNQ==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.29.3.tgz", + "integrity": "sha512-z/0JqBqqrdtrT/wzxNrWC76EpkOHdl+SvuNGxWulLaoluygntYyG5wJul5u/rQs5875zfFz/F+JaDf90SkLUIg==", "dev": true, "requires": { - "@vitest/spy": "0.28.5", - "@vitest/utils": "0.28.5", + "@vitest/spy": "0.29.3", + "@vitest/utils": "0.29.3", "chai": "^4.3.7" } }, "@vitest/runner": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.28.5.tgz", - "integrity": "sha512-NKkHtLB+FGjpp5KmneQjTcPLWPTDfB7ie+MmF1PnUBf/tGe2OjGxWyB62ySYZ25EYp9krR5Bw0YPLS/VWh1QiA==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.29.3.tgz", + "integrity": "sha512-XLi8ctbvOWhUWmuvBUSIBf8POEDH4zCh6bOuVxm/KGfARpgmVF1ku+vVNvyq85va+7qXxtl+MFmzyXQ2xzhAvw==", "dev": true, "requires": { - "@vitest/utils": "0.28.5", + "@vitest/utils": "0.29.3", "p-limit": "^4.0.0", "pathe": "^1.1.0" }, @@ -9953,24 +9950,23 @@ } }, "@vitest/spy": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.28.5.tgz", - "integrity": "sha512-7if6rsHQr9zbmvxN7h+gGh2L9eIIErgf8nSKYDlg07HHimCxp4H6I/X/DPXktVPPLQfiZ1Cw2cbDIx9fSqDjGw==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.29.3.tgz", + "integrity": "sha512-LLpCb1oOCOZcBm0/Oxbr1DQTuKLRBsSIHyLYof7z4QVE8/v8NcZKdORjMUq645fcfX55+nLXwU/1AQ+c2rND+w==", "dev": true, "requires": { "tinyspy": "^1.0.2" } }, "@vitest/utils": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.28.5.tgz", - "integrity": "sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.29.3.tgz", + "integrity": "sha512-hg4Ff8AM1GtUnLpUJlNMxrf9f4lZr/xRJjh3uJ0QFP+vjaW82HAxKrmeBmLnhc8Os2eRf+f+VBu4ts7TafPPkA==", "dev": true, "requires": { "cli-truncate": "^3.1.0", "diff": "^5.1.0", "loupe": "^2.3.6", - "picocolors": "^1.0.0", "pretty-format": "^27.5.1" } }, @@ -12129,15 +12125,15 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "mlly": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.1.0.tgz", - "integrity": "sha512-cwzBrBfwGC1gYJyfcy8TcZU1f+dbH/T+TuOhtYP2wLv/Fb51/uV7HJQfBPtEupZ2ORLRU1EKFS/QfS3eo9+kBQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz", + "integrity": "sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==", "dev": true, "requires": { - "acorn": "^8.8.1", - "pathe": "^1.0.0", - "pkg-types": "^1.0.1", - "ufo": "^1.0.1" + "acorn": "^8.8.2", + "pathe": "^1.1.0", + "pkg-types": "^1.0.2", + "ufo": "^1.1.1" } }, "ms": { @@ -12327,14 +12323,14 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, "pkg-types": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.1.tgz", - "integrity": "sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.2.tgz", + "integrity": "sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==", "dev": true, "requires": { "jsonc-parser": "^3.2.0", - "mlly": "^1.0.0", - "pathe": "^1.0.0" + "mlly": "^1.1.1", + "pathe": "^1.1.0" } }, "postcss": { @@ -12981,9 +12977,9 @@ "dev": true }, "ufo": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.0.1.tgz", - "integrity": "sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", + "integrity": "sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==", "dev": true }, "unbox-primitive": { @@ -13101,9 +13097,9 @@ } }, "vite-node": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.28.5.tgz", - "integrity": "sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.29.3.tgz", + "integrity": "sha512-QYzYSA4Yt2IiduEjYbccfZQfxKp+T1Do8/HEpSX/G5WIECTFKJADwLs9c94aQH4o0A+UtCKU61lj1m5KvbxxQA==", "dev": true, "requires": { "cac": "^6.7.14", @@ -13111,8 +13107,6 @@ "mlly": "^1.1.0", "pathe": "^1.1.0", "picocolors": "^1.0.0", - "source-map": "^0.6.1", - "source-map-support": "^0.5.21", "vite": "^3.0.0 || ^4.0.0" }, "dependencies": { @@ -13154,18 +13148,18 @@ } }, "rollup": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.15.0.tgz", - "integrity": "sha512-F9hrCAhnp5/zx/7HYmftvsNBkMfLfk/dXUh73hPSM2E3CRgap65orDNJbLetoiUFwSAk6iHPLvBrZ5iHYvzqsg==", + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.19.1.tgz", + "integrity": "sha512-lAbrdN7neYCg/8WaoWn/ckzCtz+jr70GFfYdlf50OF7387HTg+wiuiqJRFYawwSPpqfqDNYqK7smY/ks2iAudg==", "dev": true, "requires": { "fsevents": "~2.3.2" } }, "vite": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz", - "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.4.tgz", + "integrity": "sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==", "dev": true, "requires": { "esbuild": "^0.16.14", @@ -13202,18 +13196,18 @@ } }, "vitest": { - "version": "0.28.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.28.5.tgz", - "integrity": "sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==", + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.29.3.tgz", + "integrity": "sha512-muMsbXnZsrzDGiyqf/09BKQsGeUxxlyLeLK/sFFM4EXdURPQRv8y7dco32DXaRORYP0bvyN19C835dT23mL0ow==", "dev": true, "requires": { "@types/chai": "^4.3.4", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.28.5", - "@vitest/runner": "0.28.5", - "@vitest/spy": "0.28.5", - "@vitest/utils": "0.28.5", + "@vitest/expect": "0.29.3", + "@vitest/runner": "0.29.3", + "@vitest/spy": "0.29.3", + "@vitest/utils": "0.29.3", "acorn": "^8.8.1", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -13229,7 +13223,7 @@ "tinypool": "^0.3.1", "tinyspy": "^1.0.2", "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.28.5", + "vite-node": "0.29.3", "why-is-node-running": "^2.2.2" }, "dependencies": { diff --git a/package.json b/package.json index 8403380..0bf023d 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "jsdom": "^20.0.0", "prettier": "^2.5.1", "typescript": "^4.7.4", - "vitest": "^0.28.5", + "vitest": "^0.29.3", "vue-tsc": "^0.38.1" }, "engines": { From 5afb691b309292bd01a498b2e4a7f3c3143142b4 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Tue, 21 Mar 2023 00:15:28 -0500 Subject: [PATCH 04/36] Fix more tests --- src/game/formulas.ts | 4 +- src/game/requirements.tsx | 5 +- tests/game/formulas.test.ts | 145 +++++++++++--------------------- tests/game/requirements.test.ts | 43 +++++----- 4 files changed, 78 insertions(+), 119 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index e812de3..3c0b379 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -477,9 +477,7 @@ function invertTetrate( payload: FormulaSource ) { if (hasVariable(base)) { - return base.invert( - Decimal.slog(value, Decimal.minabs(1e308, unrefFormulaSource(height)).toNumber()) - ); + return base.invert(Decimal.ssqrt(value)); } // Other params can't be inverted ATM throw "Could not invert due to no input being a variable"; diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 9412e22..5372e36 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -82,7 +82,7 @@ export interface CostRequirementOptions { * When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions. * @see {Formula} */ - spendResources?: Computable; + spendResources: Computable; /** * Pass-through to {@link Requirement.pay}. May be required for maximizing support. * @see {@link cost} for restrictions on maximizing support. @@ -148,8 +148,9 @@ export function createCostRequirement( setDefault(req, "visibility", Visibility.Visible); processComputable(req as T, "cost"); processComputable(req as T, "requiresPay"); - processComputable(req as T, "spendResources"); setDefault(req, "requiresPay", true); + processComputable(req as T, "spendResources"); + setDefault(req, "spendResources", false); setDefault(req, "pay", function (amount?: DecimalSource) { const cost = req.cost instanceof Formula diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index ddc0bee..43d876f 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -40,7 +40,10 @@ const invertibleZeroParamFunctionNames = [ "tanh", "asinh", "acosh", - "atanh" + "atanh", + "slog", + "tetrate", + "iteratedexp" ] as const; const nonInvertibleZeroParamFunctionNames = [ "abs", @@ -122,7 +125,7 @@ const invertibleOneParamFunctionNames = [ "log", "pow", "root", - "slog" + "layeradd" ] as const; const nonInvertibleOneParamFunctionNames = ["layeradd10"] as const; const integrableOneParamFunctionNames = ["add", "sub", "mul", "div", "log", "pow", "root"] as const; @@ -130,12 +133,8 @@ const nonIntegrableOneParamFunctionNames = [...nonInvertibleOneParamFunctionName const invertibleIntegralOneParamFunctionNames = integrableOneParamFunctionNames; const nonInvertibleIntegralOneParamFunctionNames = nonIntegrableOneParamFunctionNames; -const invertibleTwoParamFunctionNames = ["tetrate", "layeradd", "iteratedexp"] as const; const nonInvertibleTwoParamFunctionNames = ["iteratedlog", "pentate"] as const; -const nonIntegrableTwoParamFunctionNames = [ - ...invertibleTwoParamFunctionNames, - ...nonInvertibleZeroParamFunctionNames -]; +const nonIntegrableTwoParamFunctionNames = nonInvertibleTwoParamFunctionNames; const nonInvertibleIntegralTwoParamFunctionNames = nonIntegrableTwoParamFunctionNames; describe("Formula Equality Checking", () => { @@ -201,16 +200,6 @@ describe("Formula Equality Checking", () => { }); } ); - - [...invertibleTwoParamFunctionNames, ...nonInvertibleTwoParamFunctionNames].forEach( - name => { - test(name, () => { - const instanceFormula = formula[name](1, 1); - const staticFormula = Formula[name](formula, 1, 1); - expect(instanceFormula.equals(staticFormula)).toBe(true); - }); - } - ); }); }); @@ -331,13 +320,7 @@ describe("Creating Formulas", () => { ); }); describe("2-param", () => { - ( - [ - ...invertibleTwoParamFunctionNames, - ...nonInvertibleTwoParamFunctionNames, - "clamp" - ] as const - ).forEach(names => + ([...nonInvertibleTwoParamFunctionNames, "clamp"] as const).forEach(names => describe(names, () => { checkFormula(names, [0, 0, 0] as const); testValues.forEach(i => @@ -403,24 +386,6 @@ describe("Inverting", () => { checkFormula(Formula[name](variable, variable), false)); }); }); - invertibleTwoParamFunctionNames.forEach(name => { - describe(name, () => { - test(`${name}(var, const, const) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](variable, constant, constant))); - test(`${name}(const, var, const) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](constant, variable, constant))); - test(`${name}(const, const, var) is marked as invertible and having a variable`, () => - checkFormula(Formula[name](constant, constant, variable))); - test(`${name}(var, var, const) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, variable, constant), false)); - test(`${name}(var, const, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, constant, variable), false)); - test(`${name}(const, var, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](constant, variable, variable), false)); - test(`${name}(var, var, var) is marked as not invertible and not having a variable`, () => - checkFormula(Formula[name](variable, variable, variable), false)); - }); - }); }); describe("Non-invertible formulas marked as such", () => { @@ -479,30 +444,13 @@ describe("Inverting", () => { const result = formula.evaluate(); expect(formula.invert(result)).compare_tolerance(2); }); - test(`${name}(const, var).invert()`, () => { - const formula = Formula[name](constant, variable); - const result = formula.evaluate(); - expect(formula.invert(result)).compare_tolerance(2); - }); - }) - ); - invertibleTwoParamFunctionNames.forEach(name => - describe(name, () => { - test(`${name}(var, const, const).invert()`, () => { - const formula = Formula[name](variable, constant, constant); - const result = formula.evaluate(); - expect(formula.invert(result)).compare_tolerance(2); - }); - test(`${name}(const, var, const).invert()`, () => { - const formula = Formula[name](constant, variable, constant); - const result = formula.evaluate(); - expect(formula.invert(result)).compare_tolerance(2); - }); - test(`${name}(const, const, var).invert()`, () => { - const formula = Formula[name](constant, constant, variable); - const result = formula.evaluate(); - expect(formula.invert(result)).compare_tolerance(2); - }); + if (name !== "layeradd") { + test(`${name}(const, var).invert()`, () => { + const formula = Formula[name](constant, variable); + const result = formula.evaluate(); + expect(formula.invert(result)).compare_tolerance(2); + }); + } }) ); }); @@ -530,7 +478,7 @@ describe("Inverting", () => { test("Inverting with non-invertible sections", () => { const formula = Formula.add(variable, constant.ceil()); expect(formula.isInvertible()).toBe(true); - expect(formula.invert(10)).compare_tolerance(7); + expect(formula.invert(10)).compare_tolerance(0); }); }); @@ -562,8 +510,10 @@ describe("Integrating", () => { describe(name, () => { test(`${name}(var, const) is marked as integrable`, () => checkFormula(Formula[name](variable, constant))); - test(`${name}(const, var) is marked as integrable`, () => - checkFormula(Formula[name](constant, variable))); + if (name !== "log" && name !== "root") { + test(`${name}(const, var) is marked as integrable`, () => + checkFormula(Formula[name](constant, variable))); + } test(`${name}(var, var) is marked as not integrable`, () => expect(Formula[name](variable, variable).isIntegrable()).toBe(false)); }); @@ -645,8 +595,10 @@ describe("Inverting integrals", () => { describe(name, () => { test(`${name}(var, const) is marked as having an invertible integral`, () => checkFormula(Formula[name](variable, constant))); - test(`${name}(const, var) is marked as having an invertible integral`, () => - checkFormula(Formula[name](constant, variable))); + if (name !== "log" && name !== "root") { + test(`${name}(const, var) is marked as having an invertible integral`, () => + checkFormula(Formula[name](constant, variable))); + } test(`${name}(var, var) is marked as not having an invertible integral`, () => { const formula = Formula[name](variable, variable); expect(formula.isIntegralInvertible()).toBe(false); @@ -871,6 +823,11 @@ describe("Conditionals", () => { }); describe("Custom Formulas", () => { + let variable: GenericFormula; + beforeAll(() => { + variable = Formula.variable(1); + }); + describe("Formula with evaluate", () => { test("Zero input evaluates correctly", () => expect(new Formula({ inputs: [], evaluate: () => 10 }).evaluate()).compare_tolerance( @@ -887,28 +844,28 @@ describe("Custom Formulas", () => { }); describe("Formula with invert", () => { - test("Zero input inverts correctly", () => - expect( + test("Zero input does not invert", () => + expect(() => new Formula({ inputs: [], evaluate: () => 6, invert: value => value, hasVariable: true }).invert(10) - ).compare_tolerance(10)); + ).toThrow()); test("One input inverts correctly", () => expect( new Formula({ - inputs: [1], + inputs: [variable], evaluate: () => 10, - invert: (value, v1) => v1, + invert: (value, v1) => v1.evaluate(), hasVariable: true }).invert(10) ).compare_tolerance(1)); test("Two inputs inverts correctly", () => expect( new Formula({ - inputs: [1, 2], + inputs: [variable, 2], evaluate: () => 10, invert: (value, v1, v2) => v2, hasVariable: true @@ -918,7 +875,7 @@ describe("Custom Formulas", () => { describe("Formula with integrate", () => { test("Zero input integrates correctly", () => - expect( + expect(() => new Formula({ inputs: [], evaluate: () => 10, @@ -928,7 +885,7 @@ describe("Custom Formulas", () => { test("One input integrates correctly", () => expect( new Formula({ - inputs: [1], + inputs: [variable], evaluate: () => 10, integrate: (val, v1) => val ?? 20 }).evaluateIntegral() @@ -936,7 +893,7 @@ describe("Custom Formulas", () => { test("Two inputs integrates correctly", () => expect( new Formula({ - inputs: [1, 2], + inputs: [variable, 2], evaluate: (v1, v2) => 10, integrate: (v1, v2) => 3 }).evaluateIntegral() @@ -944,19 +901,19 @@ describe("Custom Formulas", () => { }); describe("Formula with invertIntegral", () => { - test("Zero input inverts integral correctly", () => - expect( + test("Zero input does not invert integral", () => + expect(() => new Formula({ inputs: [], evaluate: () => 10, invertIntegral: () => 1, hasVariable: true }).invertIntegral(8) - ).compare_tolerance(1)); + ).toThrow()); test("One input inverts integral correctly", () => expect( new Formula({ - inputs: [1], + inputs: [variable], evaluate: () => 10, invertIntegral: (val, v1) => 1, hasVariable: true @@ -965,7 +922,7 @@ describe("Custom Formulas", () => { test("Two inputs inverts integral correctly", () => expect( new Formula({ - inputs: [1, 2], + inputs: [variable, 2], evaluate: (v1, v2) => 10, invertIntegral: (v1, v2) => 1, hasVariable: true @@ -977,36 +934,36 @@ describe("Custom Formulas", () => { describe("Buy Max", () => { let resource: Resource; beforeAll(() => { - resource = createResource(ref(10)); + resource = createResource(ref(1000)); }); - describe("With spending", () => { + 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/5vgletdc1p test("Calculates max affordable and cost correctly", () => { - const variable = Formula.variable(10); - const formula = Formula.pow(1.05, variable); + 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(calculateCost(formula, maxAffordable.value, false)).compare_tolerance( - Decimal.pow(1.05, 47) + Decimal.pow(1.05, 47).times(100) ); }); }); - describe("Without spending", () => { + describe("With spending", () => { test("Throws on non-invertible formula", () => { const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource); expect(() => maxAffordable.value).toThrow(); }); // https://www.desmos.com/calculator/5vgletdc1p test("Calculates max affordable and cost correctly", () => { - const variable = Formula.variable(10); - const formula = Formula.pow(1.05, variable); + const variable = Formula.variable(0); + const formula = Formula.pow(1.05, variable).times(100); const maxAffordable = calculateMaxAffordable(formula, resource); expect(maxAffordable.value).compare_tolerance(7); - expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(7.35); + expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(735); }); }); }); diff --git a/tests/game/requirements.test.ts b/tests/game/requirements.test.ts index 57e574b..55474b9 100644 --- a/tests/game/requirements.test.ts +++ b/tests/game/requirements.test.ts @@ -2,6 +2,7 @@ import { Visibility } from "features/feature"; import { createResource, Resource } from "features/resources/resource"; import Formula from "game/formulas"; import { + CostRequirement, createBooleanRequirement, createCostRequirement, createVisibilityRequirement, @@ -17,19 +18,18 @@ import "../utils"; describe("Creating cost requirement", () => { describe("Minimal requirement", () => { let resource: Resource; - let requirement: Requirement; + let requirement: CostRequirement; beforeAll(() => { resource = createResource(ref(10)); requirement = createCostRequirement(() => ({ resource, - cost: 10 + cost: 10, + spendResources: false })); }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - test("resource pass-through", () => expect((requirement as any).resource).toBe(resource)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - test("cost pass-through", () => expect((requirement as any).cost).toBe(10)); + test("resource pass-through", () => expect(requirement.resource).toBe(resource)); + test("cost pass-through", () => expect(requirement.cost).toBe(10)); test("partialDisplay exists", () => expect(typeof requirement.partialDisplay).toBe("function")); @@ -41,23 +41,22 @@ describe("Creating cost requirement", () => { }); test("is visible", () => expect(requirement.visibility).toBe(Visibility.Visible)); test("requires pay", () => expect(requirement.requiresPay).toBe(true)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - test("spends resources", () => expect((requirement as any).spendResources).toBe(true)); + test("does not spend resources", () => expect(requirement.spendResources).toBe(false)); test("cannot maximize", () => expect(unref(requirement.canMaximize)).toBe(false)); }); describe("Fully customized", () => { let resource: Resource; - let requirement: Requirement; + let requirement: CostRequirement; beforeAll(() => { resource = createResource(ref(10)); requirement = createCostRequirement(() => ({ resource, - cost: 10, + cost: Formula.variable(resource).times(10), visibility: Visibility.None, requiresPay: false, maximize: true, - spendResources: false, + spendResources: true, // eslint-disable-next-line @typescript-eslint/no-empty-function pay() {} })); @@ -69,9 +68,7 @@ describe("Creating cost requirement", () => { requirement.pay.length === 1); test("is not visible", () => expect(requirement.visibility).toBe(Visibility.None)); test("does not require pay", () => expect(requirement.requiresPay).toBe(false)); - test("does not spend resources", () => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((requirement as any).spendResources).toBe(false)); + test("spends resources", () => expect(requirement.spendResources).toBe(true)); test("can maximize", () => expect(unref(requirement.canMaximize)).toBe(true)); }); @@ -79,7 +76,8 @@ describe("Creating cost requirement", () => { const resource = createResource(ref(10)); const requirement = createCostRequirement(() => ({ resource, - cost: 10 + cost: 10, + spendResources: false })); expect(unref(requirement.requirementMet)).toBe(true); }); @@ -88,7 +86,8 @@ describe("Creating cost requirement", () => { const resource = createResource(ref(10)); const requirement = createCostRequirement(() => ({ resource, - cost: 100 + cost: 100, + spendResources: false })); expect(unref(requirement.requirementMet)).toBe(false); }); @@ -148,7 +147,8 @@ describe("Checking maximum levels of requirements met", () => { createBooleanRequirement(true), createCostRequirement(() => ({ resource: createResource(ref(10)), - cost: Formula.variable(0) + cost: Formula.variable(0), + spendResources: false })) ]; expect(maxRequirementsMet(requirements)).compare_tolerance(0); @@ -159,7 +159,8 @@ describe("Checking maximum levels of requirements met", () => { createBooleanRequirement(true), createCostRequirement(() => ({ resource: createResource(ref(10)), - cost: Formula.variable(0) + cost: Formula.variable(0), + spendResources: false })) ]; expect(maxRequirementsMet(requirements)).compare_tolerance(10); @@ -171,11 +172,13 @@ test("Paying requirements", () => { const noPayment = createCostRequirement(() => ({ resource, cost: 10, - requiresPay: false + requiresPay: false, + spendResources: false })); const payment = createCostRequirement(() => ({ resource, - cost: 10 + cost: 10, + spendResources: false })); payRequirements([noPayment, payment]); expect(resource.value).compare_tolerance(90); From 3078584043798ed794d7d65dce028ce5a565ed35 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 11:43:44 -0500 Subject: [PATCH 05/36] Rewrite integration to handle nested formulas properly And more clearly defines which formulas are supported --- src/game/formulas.ts | 472 ++++++++++++++++++++++++++++-------- tests/game/formulas.test.ts | 52 +++- 2 files changed, 416 insertions(+), 108 deletions(-) diff --git a/src/game/formulas.ts b/src/game/formulas.ts index 3c0b379..7fd489b 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas.ts @@ -1,7 +1,7 @@ import { Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; -import { computed, ComputedRef, ref, Ref, unref } from "vue"; +import { computed, ComputedRef, ref, unref } from "vue"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type GenericFormula = Formula; @@ -16,6 +16,8 @@ export type InvertibleIntegralFormula = GenericFormula & { invertIntegral: (value: DecimalSource) => DecimalSource; }; +export type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined; + export type FormulaOptions = | { variable: ProcessedComputable; @@ -34,6 +36,18 @@ export type FormulaOptions = integrate?: ( this: Formula, variable: DecimalSource | undefined, + stack: SubstitutionStack | undefined, + ...inputs: T + ) => DecimalSource; + integrateInner?: ( + this: Formula, + variable: DecimalSource | undefined, + stack: SubstitutionStack | undefined, + ...inputs: T + ) => DecimalSource; + applySubstitution?: ( + this: Formula, + variable: DecimalSource, ...inputs: T ) => DecimalSource; invertIntegral?: (this: Formula, value: DecimalSource, ...inputs: T) => DecimalSource; @@ -60,6 +74,17 @@ function passthrough(value: DecimalSource) { return value; } +function integrateVariable(variable: DecimalSource) { + return Decimal.pow(variable, 2).div(2); +} + +function integrateVariableInner(this: GenericFormula, variable: DecimalSource | undefined) { + if (variable == null && this.innermostVariable == null) { + throw "Cannot integrate non-existent variable"; + } + return variable ?? unref(this.innermostVariable); +} + function invertNeg(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.neg(value)); @@ -67,8 +92,19 @@ function invertNeg(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateNeg(variable: DecimalSource | undefined, lhs: FormulaSource) { - return Decimal.pow(unrefFormulaSource(lhs, variable), 2).div(2).neg(); +function integrateNeg( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + return Decimal.neg(lhs.evaluateIntegral(variable, stack)); + } + throw "Could not integrate due to no input being a variable"; +} + +function applySubstitutionNeg(value: DecimalSource) { + return Decimal.neg(value); } function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -80,17 +116,40 @@ function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateAdd(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateAdd( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.pow(x, 2) - .div(2) - .add(Decimal.times(unrefFormulaSource(rhs), x)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.times( + unrefFormulaSource(rhs), + variable ?? unref(lhs.innermostVariable) ?? 0 + ).add(x); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); - return Decimal.pow(x, 2) - .div(2) - .add(Decimal.times(unrefFormulaSource(lhs), x)); + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.times( + unrefFormulaSource(lhs), + variable ?? unref(rhs.innermostVariable) ?? 0 + ).add(x); + } + throw "Could not integrate due to no input being a variable"; +} + +function integrateInnerAdd( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.add(x, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.add(x, unrefFormulaSource(lhs)); } throw "Could not integrate due to no input being a variable"; } @@ -115,15 +174,40 @@ function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateSub(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateSub( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.pow(x, 2) - .div(2) - .add(Decimal.times(unrefFormulaSource(rhs), x).neg()); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sub( + x, + Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0) + ); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); - return Decimal.sub(unrefFormulaSource(lhs), Decimal.div(x, 2)).times(x); + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.times( + unrefFormulaSource(lhs), + variable ?? unref(rhs.innermostVariable) ?? 0 + ).sub(x); + } + throw "Could not integrate due to no input being a variable"; +} + +function integrateInnerSub( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sub(x, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.sub(x, unrefFormulaSource(lhs)); } throw "Could not integrate due to no input being a variable"; } @@ -148,17 +232,31 @@ function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateMul(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateMul( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(rhs)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.times(x, unrefFormulaSource(rhs)); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); - return Decimal.pow(x, 2).div(2).times(unrefFormulaSource(lhs)); + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.times(x, unrefFormulaSource(lhs)); } throw "Could not integrate due to no input being a variable"; } +function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.div(value, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + return Decimal.div(value, unrefFormulaSource(lhs)); + } + throw "Could not apply substitution due to no input being a variable"; +} + function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { const b = unrefFormulaSource(rhs); @@ -179,17 +277,31 @@ function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateDiv(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateDiv( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(rhs))); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.div(x, unrefFormulaSource(rhs)); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); - return Decimal.pow(x, 2).div(Decimal.times(2, unrefFormulaSource(lhs))); + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.div(unrefFormulaSource(lhs), x); } throw "Could not integrate due to no input being a variable"; } +function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.mul(value, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + return Decimal.mul(value, unrefFormulaSource(lhs)); + } + throw "Could not apply substitution due to no input being a variable"; +} + function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { const b = unrefFormulaSource(rhs); @@ -208,9 +320,13 @@ function invertRecip(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateRecip(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateRecip( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.ln(x); } throw "Could not integrate due to no input being a variable"; @@ -230,10 +346,14 @@ function invertLog10(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateLog10(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateLog10( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(10))); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); } throw "Could not integrate due to no input being a variable"; } @@ -256,13 +376,18 @@ function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integrateLog(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integrateLog( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.times( - x, - Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(unrefFormulaSource(rhs))) - ); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x) + .sub(1) + .times(x) + .div(Decimal.ln(unrefFormulaSource(rhs))); } throw "Could not integrate due to no input being a variable"; } @@ -282,10 +407,14 @@ function invertLog2(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateLog2(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateLog2( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.times(x, Decimal.sub(Decimal.ln(x), 1).div(Decimal.ln(2))); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2)); } throw "Could not integrate due to no input being a variable"; } @@ -304,10 +433,14 @@ function invertLn(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateLn(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateLn( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); - return Decimal.times(x, Decimal.ln(x).sub(1)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x); } throw "Could not integrate due to no input being a variable"; } @@ -328,13 +461,18 @@ function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) throw "Could not invert due to no input being a variable"; } -function integratePow(variable: DecimalSource | undefined, lhs: FormulaSource, rhs: FormulaSource) { +function integratePow( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); const pow = Decimal.add(unrefFormulaSource(rhs), 1); return Decimal.pow(x, pow).div(pow); } else if (hasVariable(rhs)) { - const x = unrefFormulaSource(rhs, variable); + const x = rhs.evaluateIntegral(variable, stack); const b = unrefFormulaSource(lhs); return Decimal.pow(b, x).div(Decimal.ln(b)); } @@ -359,9 +497,13 @@ function invertPow10(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integratePow10(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integratePow10( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); } throw "Could not integrate due to no input being a variable"; @@ -387,15 +529,18 @@ function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSou function integratePowBase( variable: DecimalSource | undefined, + stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs, variable); - return Decimal.pow(b, unrefFormulaSource(lhs)).div(Decimal.ln(b)); + const x = lhs.evaluateIntegral(variable, stack); + const b = unrefFormulaSource(rhs); + return Decimal.pow(b, x).div(Decimal.ln(b)); } else if (hasVariable(rhs)) { - const denominator = Decimal.add(unrefFormulaSource(lhs, variable), 1); - return Decimal.pow(unrefFormulaSource(rhs), denominator).div(denominator); + const x = rhs.evaluateIntegral(variable, stack); + const denominator = Decimal.add(unrefFormulaSource(lhs), 1); + return Decimal.pow(x, denominator).div(denominator); } throw "Could not integrate due to no input being a variable"; } @@ -422,14 +567,14 @@ function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource function integrateRoot( variable: DecimalSource | undefined, + stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return Decimal.pow(unrefFormulaSource(lhs, variable), Decimal.recip(b).add(1)) - .times(b) - .div(Decimal.add(b, 1)); + const x = lhs.evaluateIntegral(variable, stack); + const a = unrefFormulaSource(rhs); + return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1)); } throw "Could not integrate due to no input being a variable"; } @@ -454,9 +599,14 @@ function invertExp(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateExp(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateExp( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.exp(unrefFormulaSource(lhs, variable)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.exp(x); } throw "Could not integrate due to no input being a variable"; } @@ -580,9 +730,14 @@ function invertSin(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateSin(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateSin( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.cos(unrefFormulaSource(lhs, variable)).neg(); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.cos(x).neg(); } throw "Could not integrate due to no input being a variable"; } @@ -594,9 +749,14 @@ function invertCos(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateCos(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateCos( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.sin(unrefFormulaSource(lhs, variable)); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sin(x); } throw "Could not integrate due to no input being a variable"; } @@ -608,9 +768,14 @@ function invertTan(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateTan(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateTan( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.cos(unrefFormulaSource(lhs, variable)).ln().neg(); + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.cos(x).ln().neg(); } throw "Could not integrate due to no input being a variable"; } @@ -622,9 +787,13 @@ function invertAsin(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAsin(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAsin( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.asin(x) .times(x) .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); @@ -639,9 +808,13 @@ function invertAcos(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAcos(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAcos( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.acos(x) .times(x) .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); @@ -656,9 +829,13 @@ function invertAtan(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAtan(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAtan( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.atan(x) .times(x) .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2)); @@ -673,9 +850,13 @@ function invertSinh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateSinh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateSinh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.cosh(x); } throw "Could not integrate due to no input being a variable"; @@ -688,9 +869,13 @@ function invertCosh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateCosh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateCosh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.sinh(x); } throw "Could not integrate due to no input being a variable"; @@ -703,9 +888,13 @@ function invertTanh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateTanh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateTanh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.cosh(x).ln(); } throw "Could not integrate due to no input being a variable"; @@ -718,9 +907,13 @@ function invertAsinh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAsinh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAsinh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt()); } throw "Could not integrate due to no input being a variable"; @@ -733,9 +926,13 @@ function invertAcosh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAcosh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAcosh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.acosh(x) .times(x) .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt())); @@ -750,9 +947,13 @@ function invertAtanh(value: DecimalSource, lhs: FormulaSource) { throw "Could not invert due to no input being a variable"; } -function integrateAtanh(variable: DecimalSource | undefined, lhs: FormulaSource) { +function integrateAtanh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { if (hasVariable(lhs)) { - const x = unrefFormulaSource(lhs, variable); + const x = lhs.evaluateIntegral(variable, stack); return Decimal.atanh(x) .times(x) .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2)); @@ -776,7 +977,21 @@ export default class Formula { | ((value: DecimalSource, ...inputs: T) => DecimalSource) | undefined; private readonly internalIntegrate: - | ((variable: DecimalSource | undefined, ...inputs: T) => DecimalSource) + | (( + variable: DecimalSource | undefined, + stack: SubstitutionStack | undefined, + ...inputs: T + ) => DecimalSource) + | undefined; + private readonly internalIntegrateInner: + | (( + variable: DecimalSource | undefined, + stack: SubstitutionStack | undefined, + ...inputs: T + ) => DecimalSource) + | undefined; + private readonly applySubstitution: + | ((variable: DecimalSource, ...inputs: T) => DecimalSource) | undefined; private readonly internalInvertIntegral: | ((value: DecimalSource, ...inputs: T) => DecimalSource) @@ -791,6 +1006,11 @@ export default class Formula { this.inputs = [options.variable] as T; this.internalHasVariable = true; this.innermostVariable = options.variable; + this.internalIntegrate = + integrateVariable as unknown as Formula["internalIntegrate"]; + this.internalIntegrateInner = + integrateVariableInner as unknown as Formula["internalIntegrateInner"]; + this.applySubstitution = passthrough as unknown as Formula["applySubstitution"]; return; } // Constant case @@ -803,7 +1023,16 @@ export default class Formula { return; } - const { inputs, evaluate, invert, integrate, invertIntegral, hasVariable } = options; + const { + inputs, + evaluate, + invert, + integrate, + integrateInner, + applySubstitution, + invertIntegral, + hasVariable + } = options; if (invert == null && invertIntegral == null && hasVariable) { throw "A formula cannot be marked as having a variable if it is not invertible"; } @@ -811,6 +1040,8 @@ export default class Formula { this.inputs = inputs; this.internalEvaluate = evaluate; this.internalIntegrate = integrate; + this.internalIntegrateInner = integrateInner; + this.applySubstitution = applySubstitution; const numVariables = inputs.filter( input => input instanceof Formula && input.hasVariable() @@ -874,7 +1105,7 @@ export default class Formula { unrefFormulaSource(input, variable) ) as GuardedFormulasToDecimals) ) ?? - variable ?? + (this.internalHasVariable ? variable : null) ?? unrefFormulaSource(this.inputs[0]) ); } @@ -894,17 +1125,57 @@ export default class Formula { } /** - * Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable + * Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable. The formula can only have one "complex" operation (anything besides +,-,*,/). * @param variable Optionally override the value of the variable while evaluating + * @param stack The list of callbacks to run to handle simple operations inside the complex operation. Used in nested formulas * @see {@link isIntegrable} */ - evaluateIntegral(variable?: DecimalSource): DecimalSource { - if (this.internalIntegrate) { - return this.internalIntegrate.call(this, variable, ...this.inputs); - } else if (this.inputs.length === 1 && this.internalHasVariable) { - return variable ?? unrefFormulaSource(this.inputs[0]); + evaluateIntegral(variable?: DecimalSource, stack?: SubstitutionStack): DecimalSource { + if (stack == null) { + // "Outer" part of the formula + if (this.applySubstitution == null) { + // We're the complex operation of this formula + stack = []; + if (this.internalIntegrate == null) { + throw "Cannot integrate formula with non-existent operation"; + } + let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs); + stack.forEach(func => (value = func(value))); + return value; + } else { + // Continue digging into the formula + if (this.internalIntegrate) { + return this.internalIntegrate.call(this, variable, undefined, ...this.inputs); + } else if (this.inputs.length === 1 && this.internalHasVariable) { + return integrateVariable(variable ?? unrefFormulaSource(this.inputs[0])); + } + throw "Cannot integrate formula without variable"; + } + } else { + // "Inner" part of the formula + if (this.applySubstitution == null) { + throw "Cannot have two complex operations in an integrable formula"; + } + stack.push((variable: DecimalSource) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.applySubstitution!.call(this, variable, ...this.inputs) + ); + if (this.internalIntegrateInner) { + return this.internalIntegrateInner.call(this, variable, stack, ...this.inputs); + } else if (this.internalIntegrate) { + return this.internalIntegrate.call(this, variable, stack, ...this.inputs); + } else if (this.inputs.length === 1 && this.internalHasVariable) { + return variable ?? unrefFormulaSource(this.inputs[0]); + } + throw "Cannot integrate formula without variable"; } - throw "Cannot integrate formula without variable"; + } + + calculateConstantOfIntegration() { + // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase + const integral = this.evaluateIntegral(1); + const actualCost = this.evaluate(0); + return Decimal.sub(actualCost, integral); } /** @@ -1070,6 +1341,7 @@ export default class Formula { inputs: [value], evaluate: Decimal.neg, invert: invertNeg, + applySubstitution: applySubstitutionNeg, integrate: integrateNeg }); } @@ -1116,6 +1388,8 @@ export default class Formula { evaluate: Decimal.add, invert: invertAdd, integrate: integrateAdd, + integrateInner: integrateInnerAdd, + applySubstitution: passthrough, invertIntegral: invertIntegrateAdd }); } @@ -1135,6 +1409,8 @@ export default class Formula { evaluate: Decimal.sub, invert: invertSub, integrate: integrateSub, + integrateInner: integrateInnerSub, + applySubstitution: passthrough, invertIntegral: invertIntegrateSub }); } @@ -1160,6 +1436,7 @@ export default class Formula { evaluate: Decimal.mul, invert: invertMul, integrate: integrateMul, + applySubstitution: applySubstitutionMul, invertIntegral: invertIntegrateMul }); } @@ -1185,6 +1462,7 @@ export default class Formula { evaluate: Decimal.div, invert: invertDiv, integrate: integrateDiv, + applySubstitution: applySubstitutionDiv, invertIntegral: invertIntegrateDiv }); } @@ -2180,7 +2458,7 @@ export default class Formula { * 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 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. If true, costs will be approximated for performance, skewing towards fewer purchases */ export function calculateMaxAffordable( formula: InvertibleFormula, @@ -2219,7 +2497,7 @@ export function calculateMaxAffordable( * 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 + * @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 */ export function calculateCost( formula: InvertibleFormula, diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 43d876f..b45112d 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -8,7 +8,7 @@ import Formula, { } from "game/formulas"; import Decimal, { DecimalSource } from "util/bignum"; import { beforeAll, describe, expect, test } from "vitest"; -import { ref } from "vue"; +import { ref, unref } from "vue"; import "../utils"; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; @@ -486,7 +486,7 @@ describe("Integrating", () => { let variable: GenericFormula; let constant: GenericFormula; beforeAll(() => { - variable = Formula.variable(10); + variable = Formula.variable(ref(10)); constant = Formula.constant(10); }); @@ -564,8 +564,24 @@ describe("Integrating", () => { describe.todo("Integrable formulas integrate correctly"); test("Integrating nested formulas", () => { - const formula = Formula.add(variable, constant).times(constant); - expect(formula.evaluateIntegral()).compare_tolerance(1500); + const formula = Formula.add(variable, constant).times(constant).pow(2).times(30); + const actualCost = new Array(10) + .fill(null) + .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0)); + const calculatedCost = Decimal.add( + formula.evaluateIntegral(), + formula.calculateConstantOfIntegration() + ); + // 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); + }); + + test("Integrating nested complex formulas", () => { + const formula = Formula.pow(1.05, variable).times(100).pow(0.5); + expect(() => formula.evaluateIntegral()).toThrow(); }); }); @@ -652,8 +668,13 @@ describe("Inverting integrals", () => { describe.todo("Invertible Integral formulas invert correctly"); test("Inverting integral of nested formulas", () => { - const formula = Formula.add(variable, constant).times(constant); - expect(formula.invertIntegral(1500)).compare_tolerance(10); + const formula = Formula.add(variable, constant).times(constant).pow(2).times(30); + expect(formula.invertIntegral(7000000)).compare_tolerance(10); + }); + + test("Inverting integral of nested complex formulas", () => { + const formula = Formula.pow(1.05, variable).times(100).pow(0.5); + expect(() => formula.invertIntegral(100)).toThrow(); }); describe("Inverting integral pass-throughs", () => { @@ -875,7 +896,7 @@ describe("Custom Formulas", () => { describe("Formula with integrate", () => { test("Zero input integrates correctly", () => - expect(() => + expect( new Formula({ inputs: [], evaluate: () => 10, @@ -887,7 +908,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable], evaluate: () => 10, - integrate: (val, v1) => val ?? 20 + integrate: (val, stack, v1) => val ?? 20 }).evaluateIntegral() ).compare_tolerance(20)); test("Two inputs integrates correctly", () => @@ -941,7 +962,7 @@ describe("Buy Max", () => { const maxAffordable = calculateMaxAffordable(Formula.neg(10), resource, false); expect(() => maxAffordable.value).toThrow(); }); - // https://www.desmos.com/calculator/5vgletdc1p + // 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); @@ -957,13 +978,22 @@ describe("Buy Max", () => { const maxAffordable = calculateMaxAffordable(Formula.abs(10), resource); expect(() => maxAffordable.value).toThrow(); }); - // https://www.desmos.com/calculator/5vgletdc1p + // 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); expect(maxAffordable.value).compare_tolerance(7); - expect(calculateCost(formula, maxAffordable.value)).compare_tolerance(735); + + const actualCost = new Array(7) + .fill(null) + .reduce((acc, _, i) => acc.add(formula.evaluate(i)), 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); }); }); }); From bb0f83e75ab3c051a1ddf4e344d7e7c18d71c049 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 11:47:39 -0500 Subject: [PATCH 06/36] Make tests faster --- tests/game/formulas.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index b45112d..9937d30 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -8,12 +8,12 @@ import Formula, { } from "game/formulas"; import Decimal, { DecimalSource } from "util/bignum"; import { beforeAll, describe, expect, test } from "vitest"; -import { ref, unref } from "vue"; +import { ref } from "vue"; import "../utils"; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; -const testValues = ["-1e400", 0, 0.25] as const; +const testValues = [-1, "0", Decimal.dOne] as const; const invertibleZeroParamFunctionNames = [ "neg", From 6e4c61155a21e126069313187a96aabf5d5cbe4f Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 19:04:07 -0500 Subject: [PATCH 07/36] Re-organize formulas for better readability/maintainability --- src/data/common.tsx | 3 +- src/game/{ => formulas}/formulas.ts | 1361 ++++----------------------- src/game/formulas/operations.ts | 904 ++++++++++++++++++ src/game/formulas/types.d.ts | 79 ++ src/game/requirements.tsx | 8 +- tests/game/formulas.test.ts | 5 +- tests/game/requirements.test.ts | 2 +- 7 files changed, 1175 insertions(+), 1187 deletions(-) rename src/game/{ => formulas}/formulas.ts (56%) create mode 100644 src/game/formulas/operations.ts create mode 100644 src/game/formulas/types.d.ts diff --git a/src/data/common.tsx b/src/data/common.tsx index ec3317e..b1d5398 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -8,7 +8,8 @@ import { GenericMilestone } from "features/milestones/milestone"; import { displayResource, Resource } from "features/resources/resource"; import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree"; import { createTreeNode } from "features/trees/tree"; -import Formula, { FormulaSource, GenericFormula, InvertibleFormula } from "game/formulas"; +import Formula from "game/formulas/formulas"; +import type { FormulaSource, GenericFormula } from "game/formulas/types"; import type { Modifier } from "game/modifiers"; import type { Persistent } from "game/persistence"; import { DefaultValue, persistent } from "game/persistence"; diff --git a/src/game/formulas.ts b/src/game/formulas/formulas.ts similarity index 56% rename from src/game/formulas.ts rename to src/game/formulas/formulas.ts index 7fd489b..885adee 100644 --- a/src/game/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -2,965 +2,44 @@ import { Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; import { computed, ComputedRef, ref, unref } from "vue"; +import type { + EvaluateFunction, + FormulaOptions, + FormulaSource, + GeneralFormulaOptions, + GenericFormula, + GuardedFormulasToDecimals, + IntegrableFormula, + IntegrateFunction, + InternalFormulaProperties, + InvertFunction, + InvertibleFormula, + InvertibleIntegralFormula, + InvertIntegralFunction, + SubstitutionFunction, + SubstitutionStack +} from "./types"; +import * as ops from "./operations"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type GenericFormula = Formula; -export type FormulaSource = ProcessedComputable | GenericFormula; -export type InvertibleFormula = GenericFormula & { - invert: (value: DecimalSource) => DecimalSource; -}; -export type IntegrableFormula = GenericFormula & { - evaluateIntegral: (variable?: DecimalSource) => DecimalSource; -}; -export type InvertibleIntegralFormula = GenericFormula & { - invertIntegral: (value: DecimalSource) => DecimalSource; -}; - -export type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined; - -export type FormulaOptions = - | { - variable: ProcessedComputable; - } - | { - inputs: [FormulaSource]; - } - | { - inputs: T; - evaluate: (this: Formula, ...inputs: GuardedFormulasToDecimals) => DecimalSource; - invert?: ( - this: Formula, - value: DecimalSource, - ...inputs: [...T, ...unknown[]] - ) => DecimalSource; - integrate?: ( - this: Formula, - variable: DecimalSource | undefined, - stack: SubstitutionStack | undefined, - ...inputs: T - ) => DecimalSource; - integrateInner?: ( - this: Formula, - variable: DecimalSource | undefined, - stack: SubstitutionStack | undefined, - ...inputs: T - ) => DecimalSource; - applySubstitution?: ( - this: Formula, - variable: DecimalSource, - ...inputs: T - ) => DecimalSource; - invertIntegral?: (this: Formula, value: DecimalSource, ...inputs: T) => DecimalSource; - hasVariable?: boolean; - }; - -function hasVariable(value: FormulaSource): value is InvertibleFormula { +export function hasVariable(value: FormulaSource): value is InvertibleFormula { return value instanceof Formula && value.hasVariable(); } -// It's really hard to type mapped tuples, but these classes seem to manage -type FormulasToDecimals = { - [K in keyof T]: DecimalSource; -}; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type TupleGuard = T extends any[] ? FormulasToDecimals : never; -type GuardedFormulasToDecimals = TupleGuard; - export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSource) { return value instanceof Formula ? value.evaluate(variable) : unref(value); } -function passthrough(value: DecimalSource) { - return value; -} - function integrateVariable(variable: DecimalSource) { return Decimal.pow(variable, 2).div(2); } -function integrateVariableInner(this: GenericFormula, variable: DecimalSource | undefined) { +function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) { if (variable == null && this.innermostVariable == null) { throw "Cannot integrate non-existent variable"; } return variable ?? unref(this.innermostVariable); } -function invertNeg(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.neg(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateNeg( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - return Decimal.neg(lhs.evaluateIntegral(variable, stack)); - } - throw "Could not integrate due to no input being a variable"; -} - -function applySubstitutionNeg(value: DecimalSource) { - return Decimal.neg(value); -} - -function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.sub(value, unrefFormulaSource(rhs))); - } else if (hasVariable(rhs)) { - return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs))); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateAdd( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.times( - unrefFormulaSource(rhs), - variable ?? unref(lhs.innermostVariable) ?? 0 - ).add(x); - } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.times( - unrefFormulaSource(lhs), - variable ?? unref(rhs.innermostVariable) ?? 0 - ).add(x); - } - throw "Could not integrate due to no input being a variable"; -} - -function integrateInnerAdd( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.add(x, unrefFormulaSource(rhs)); - } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.add(x, unrefFormulaSource(lhs)); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); - } - throw "Could not invert due to no input being a variable"; -} - -function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs))); - } else if (hasVariable(rhs)) { - return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateSub( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.sub( - x, - Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0) - ); - } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.times( - unrefFormulaSource(lhs), - variable ?? unref(rhs.innermostVariable) ?? 0 - ).sub(x); - } - throw "Could not integrate due to no input being a variable"; -} - -function integrateInnerSub( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.sub(x, unrefFormulaSource(rhs)); - } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.sub(x, unrefFormulaSource(lhs)); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); - } - throw "Could not invert due to no input being a variable"; -} - -function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs))); - } else if (hasVariable(rhs)) { - return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs))); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateMul( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.times(x, unrefFormulaSource(rhs)); - } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.times(x, unrefFormulaSource(lhs)); - } - throw "Could not integrate due to no input being a variable"; -} - -function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return Decimal.div(value, unrefFormulaSource(rhs)); - } else if (hasVariable(rhs)) { - return Decimal.div(value, unrefFormulaSource(lhs)); - } - throw "Could not apply substitution due to no input being a variable"; -} - -function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); - } - throw "Could not invert due to no input being a variable"; -} - -function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs))); - } else if (hasVariable(rhs)) { - return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateDiv( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.div(x, unrefFormulaSource(rhs)); - } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.div(unrefFormulaSource(lhs), x); - } - throw "Could not integrate due to no input being a variable"; -} - -function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return Decimal.mul(value, unrefFormulaSource(rhs)); - } else if (hasVariable(rhs)) { - return Decimal.mul(value, unrefFormulaSource(lhs)); - } - throw "Could not apply substitution due to no input being a variable"; -} - -function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); - } - throw "Could not invert due to no input being a variable"; -} - -function invertRecip(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.recip(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateRecip( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegrateRecip(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.exp(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function invertLog10(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.pow10(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateLog10( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert( - Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1)) - ); - } - throw "Could not invert due to no input being a variable"; -} - -function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value)); - } else if (hasVariable(rhs)) { - return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateLog( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x) - .sub(1) - .times(x) - .div(Decimal.ln(unrefFormulaSource(rhs))); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value); - return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); - } - throw "Could not invert due to no input being a variable"; -} - -function invertLog2(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.pow(2, value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateLog2( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2)); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1))); - } - throw "Could not invert due to no input being a variable"; -} - -function invertLn(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.exp(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateLn( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x).sub(1).times(x); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1))); - } - throw "Could not invert due to no input being a variable"; -} - -function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs))); - } else if (hasVariable(rhs)) { - return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs)))); - } - throw "Could not invert due to no input being a variable"; -} - -function integratePow( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - const pow = Decimal.add(unrefFormulaSource(rhs), 1); - return Decimal.pow(x, pow).div(pow); - } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - const b = unrefFormulaSource(lhs); - return Decimal.pow(b, x).div(Decimal.ln(b)); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.negate(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); - } else if (hasVariable(rhs)) { - const denominator = Decimal.ln(unrefFormulaSource(lhs)); - return rhs.invert(Decimal.times(denominator, value).ln().div(denominator)); - } - throw "Could not invert due to no input being a variable"; -} - -function invertPow10(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.root(value, 10)); - } - throw "Could not invert due to no input being a variable"; -} - -function integratePow10( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert( - Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp() - ); - } - throw "Could not invert due to no input being a variable"; -} - -function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs))); - } else if (hasVariable(rhs)) { - return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integratePowBase( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - const b = unrefFormulaSource(rhs); - return Decimal.pow(b, x).div(Decimal.ln(b)); - } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - const denominator = Decimal.add(unrefFormulaSource(lhs), 1); - return Decimal.pow(x, denominator).div(denominator); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegratePowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.ln(b).times(value).ln().div(Decimal.ln(b))); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); - } - throw "Could not invert due to no input being a variable"; -} - -function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs)))); - } else if (hasVariable(rhs)) { - return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value))); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateRoot( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - const a = unrefFormulaSource(rhs); - return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1)); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert( - Decimal.add(b, 1) - .times(value) - .div(b) - .pow(Decimal.div(b, Decimal.add(b, 1))) - ); - } - throw "Could not invert due to no input being a variable"; -} - -function invertExp(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.ln(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateExp( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.exp(x); - } - throw "Could not integrate due to no input being a variable"; -} - -function tetrate( - value: DecimalSource, - height: DecimalSource = 2, - payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1) -) { - const heightNumber = Decimal.minabs(height, 1e308).toNumber(); - return Decimal.tetrate(value, heightNumber, payload); -} - -function invertTetrate( - value: DecimalSource, - base: FormulaSource, - height: FormulaSource, - payload: FormulaSource -) { - if (hasVariable(base)) { - return base.invert(Decimal.ssqrt(value)); - } - // Other params can't be inverted ATM - throw "Could not invert due to no input being a variable"; -} - -function iteratedexp( - value: DecimalSource, - height: DecimalSource = 2, - payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1) -) { - const heightNumber = Decimal.minabs(height, 1e308).toNumber(); - return Decimal.iteratedexp(value, heightNumber, new Decimal(payload)); -} - -function invertIteratedExp( - value: DecimalSource, - lhs: FormulaSource, - height: FormulaSource, - payload: FormulaSource -) { - if (hasVariable(lhs)) { - return lhs.invert( - Decimal.iteratedlog( - value, - Math.E, - Decimal.minabs(1e308, unrefFormulaSource(height)).toNumber() - ) - ); - } - // Other params can't be inverted ATM - throw "Could not invert due to no input being a variable"; -} - -function iteratedLog(value: DecimalSource, lhs: DecimalSource = 10, times: DecimalSource = 2) { - const timesNumber = Decimal.minabs(times, 1e308).toNumber(); - return Decimal.iteratedlog(value, lhs, timesNumber); -} - -function slog(value: DecimalSource, lhs: DecimalSource = 10) { - const baseNumber = Decimal.minabs(lhs, 1e308).toNumber(); - return Decimal.slog(value, baseNumber); -} - -function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert( - Decimal.tetrate(value, Decimal.minabs(1e308, unrefFormulaSource(rhs)).toNumber()) - ); - } - // Other params can't be inverted ATM - throw "Could not invert due to no input being a variable"; -} - -function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) { - const diffNumber = Decimal.minabs(diff, 1e308).toNumber(); - return Decimal.layeradd(value, diffNumber, base); -} - -function invertLayeradd( - value: DecimalSource, - lhs: FormulaSource, - diff: FormulaSource, - base: FormulaSource -) { - if (hasVariable(lhs)) { - return lhs.invert( - Decimal.layeradd( - value, - Decimal.minabs(1e308, unrefFormulaSource(diff)).negate().toNumber() - ) - ); - } - // Other params can't be inverted ATM - throw "Could not invert due to no input being a variable"; -} - -function invertLambertw(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.pow(Math.E, value).times(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function invertSsqrt(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.tetrate(value, 2)); - } - throw "Could not invert due to no input being a variable"; -} - -function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) { - const heightNumber = Decimal.minabs(height, 1e308).toNumber(); - return Decimal.pentate(value, heightNumber, payload); -} - -function invertSin(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.asin(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateSin( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.cos(x).neg(); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertCos(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.acos(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateCos( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.sin(x); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertTan(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.atan(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateTan( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.cos(x).ln().neg(); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertAsin(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.sin(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateAsin( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.asin(x) - .times(x) - .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertAcos(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.cos(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateAcos( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.acos(x) - .times(x) - .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertAtan(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.tan(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateAtan( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.atan(x) - .times(x) - .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2)); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertSinh(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.asinh(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateSinh( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.cosh(x); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertCosh(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.acosh(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateCosh( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.sinh(x); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertTanh(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.atanh(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateTanh( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.cosh(x).ln(); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertAsinh(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.sinh(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateAsinh( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt()); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertAcosh(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.cosh(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateAcosh( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.acosh(x) - .times(x) - .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt())); - } - throw "Could not integrate due to no input being a variable"; -} - -function invertAtanh(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.tanh(value)); - } - throw "Could not invert due to no input being a variable"; -} - -function integrateAtanh( - variable: DecimalSource | undefined, - stack: SubstitutionStack, - lhs: FormulaSource -) { - if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.atanh(x) - .times(x) - .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2)); - } - throw "Could not integrate due to no input being a variable"; -} - /** * 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. @@ -970,59 +49,59 @@ function integrateAtanh( export default class Formula { readonly inputs: T; - private readonly internalEvaluate: - | ((...inputs: GuardedFormulasToDecimals) => DecimalSource) - | undefined; - private readonly internalInvert: - | ((value: DecimalSource, ...inputs: T) => DecimalSource) - | undefined; - private readonly internalIntegrate: - | (( - variable: DecimalSource | undefined, - stack: SubstitutionStack | undefined, - ...inputs: T - ) => DecimalSource) - | undefined; - private readonly internalIntegrateInner: - | (( - variable: DecimalSource | undefined, - stack: SubstitutionStack | undefined, - ...inputs: T - ) => DecimalSource) - | undefined; - private readonly applySubstitution: - | ((variable: DecimalSource, ...inputs: T) => DecimalSource) - | undefined; - private readonly internalInvertIntegral: - | ((value: DecimalSource, ...inputs: T) => DecimalSource) - | undefined; + private readonly internalEvaluate: EvaluateFunction | undefined; + private readonly internalInvert: InvertFunction | undefined; + private readonly internalIntegrate: IntegrateFunction | undefined; + private readonly internalIntegrateInner: IntegrateFunction | undefined; + private readonly applySubstitution: SubstitutionFunction | undefined; + private readonly internalInvertIntegral: InvertIntegralFunction | undefined; private readonly internalHasVariable: boolean; public readonly innermostVariable: ProcessedComputable | undefined; constructor(options: FormulaOptions) { - // Variable case + let readonlyProperties; if ("variable" in options) { - this.inputs = [options.variable] as T; - this.internalHasVariable = true; - this.innermostVariable = options.variable; - this.internalIntegrate = - integrateVariable as unknown as Formula["internalIntegrate"]; - this.internalIntegrateInner = - integrateVariableInner as unknown as Formula["internalIntegrateInner"]; - this.applySubstitution = passthrough as unknown as Formula["applySubstitution"]; - return; - } - // Constant case - if (!("evaluate" in options)) { - if (options.inputs.length !== 1) { - throw "Evaluate function is required if inputs is not length 1"; - } - this.inputs = options.inputs as T; - this.internalHasVariable = false; - return; + readonlyProperties = this.setupVariable(options); + } else if (!("evaluate" in options)) { + readonlyProperties = this.setupConstant(options); + } else { + readonlyProperties = this.setupFormula(options); } + this.inputs = readonlyProperties.inputs; + this.internalHasVariable = readonlyProperties.internalHasVariable; + this.innermostVariable = readonlyProperties.innermostVariable; + this.internalIntegrate = readonlyProperties.internalIntegrate; + this.internalIntegrateInner = readonlyProperties.internalIntegrateInner; + this.applySubstitution = readonlyProperties.applySubstitution; + } + private setupVariable({ + variable + }: { + variable: ProcessedComputable; + }): InternalFormulaProperties { + return { + inputs: [variable] as T, + internalHasVariable: true, + innermostVariable: variable, + internalIntegrate: integrateVariable as unknown as IntegrateFunction, + internalIntegrateInner: integrateVariableInner as unknown as IntegrateFunction, + applySubstitution: ops.passthrough as unknown as SubstitutionFunction + }; + } + + private setupConstant({ inputs }: { inputs: [FormulaSource] }): InternalFormulaProperties { + if (inputs.length !== 1) { + throw "Evaluate function is required if inputs is not length 1"; + } + return { + inputs: inputs as T, + internalHasVariable: false + }; + } + + private setupFormula(options: GeneralFormulaOptions): InternalFormulaProperties { const { inputs, evaluate, @@ -1037,12 +116,6 @@ export default class Formula { throw "A formula cannot be marked as having a variable if it is not invertible"; } - this.inputs = inputs; - this.internalEvaluate = evaluate; - this.internalIntegrate = integrate; - this.internalIntegrateInner = integrateInner; - this.applySubstitution = applySubstitution; - const numVariables = inputs.filter( input => input instanceof Formula && input.hasVariable() ).length; @@ -1050,21 +123,24 @@ export default class Formula { | GenericFormula | undefined; - this.internalHasVariable = + const internalHasVariable = numVariables === 1 || (numVariables === 0 && hasVariable === true); - if (this.internalHasVariable) { - this.innermostVariable = variable?.innermostVariable; - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.internalInvert = - this.internalHasVariable && variable?.isInvertible() ? invert : undefined; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.internalInvertIntegral = - this.internalHasVariable && variable?.isIntegralInvertible() - ? invertIntegral - : undefined; + const innermostVariable = internalHasVariable ? variable?.innermostVariable : undefined; + const internalInvert = internalHasVariable && variable?.isInvertible() ? invert : undefined; + const internalInvertIntegral = + internalHasVariable && variable?.isIntegralInvertible() ? invertIntegral : undefined; + + return { + inputs, + internalEvaluate: evaluate, + internalIntegrate: integrate, + internalIntegrateInner: integrateInner, + applySubstitution, + innermostVariable, + internalHasVariable, + internalInvert, + internalInvertIntegral + }; } /** Type predicate that this formula can be inverted. */ @@ -1340,9 +416,9 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.neg, - invert: invertNeg, - applySubstitution: applySubstitutionNeg, - integrate: integrateNeg + invert: ops.invertNeg, + applySubstitution: ops.applySubstitutionNeg, + integrate: ops.integrateNeg }); } public static negate(value: T): Omit; @@ -1386,11 +462,11 @@ export default class Formula { return new Formula({ inputs: [value, other], evaluate: Decimal.add, - invert: invertAdd, - integrate: integrateAdd, - integrateInner: integrateInnerAdd, - applySubstitution: passthrough, - invertIntegral: invertIntegrateAdd + invert: ops.invertAdd, + integrate: ops.integrateAdd, + integrateInner: ops.integrateInnerAdd, + applySubstitution: ops.passthrough, + invertIntegral: ops.invertIntegrateAdd }); } public static plus(value: T, other: FormulaSource): T; @@ -1407,11 +483,11 @@ export default class Formula { return new Formula({ inputs: [value, other], evaluate: Decimal.sub, - invert: invertSub, - integrate: integrateSub, - integrateInner: integrateInnerSub, - applySubstitution: passthrough, - invertIntegral: invertIntegrateSub + invert: ops.invertSub, + integrate: ops.integrateSub, + integrateInner: ops.integrateInnerSub, + applySubstitution: ops.passthrough, + invertIntegral: ops.invertIntegrateSub }); } public static subtract(value: T, other: FormulaSource): T; @@ -1434,10 +510,10 @@ export default class Formula { return new Formula({ inputs: [value, other], evaluate: Decimal.mul, - invert: invertMul, - integrate: integrateMul, - applySubstitution: applySubstitutionMul, - invertIntegral: invertIntegrateMul + invert: ops.invertMul, + integrate: ops.integrateMul, + applySubstitution: ops.applySubstitutionMul, + invertIntegral: ops.invertIntegrateMul }); } public static multiply(value: T, other: FormulaSource): T; @@ -1460,10 +536,10 @@ export default class Formula { return new Formula({ inputs: [value, other], evaluate: Decimal.div, - invert: invertDiv, - integrate: integrateDiv, - applySubstitution: applySubstitutionDiv, - invertIntegral: invertIntegrateDiv + invert: ops.invertDiv, + integrate: ops.integrateDiv, + applySubstitution: ops.applySubstitutionDiv, + invertIntegral: ops.invertIntegrateDiv }); } public static divide(value: T, other: FormulaSource): T; @@ -1479,9 +555,9 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.recip, - invert: invertRecip, - integrate: integrateRecip, - invertIntegral: invertIntegrateRecip + invert: ops.invertRecip, + integrate: ops.integrateRecip, + invertIntegral: ops.invertIntegrateRecip }); } public static reciprocal(value: T): T; @@ -1499,61 +575,22 @@ export default class Formula { return new Formula({ inputs: [value, other], evaluate: Decimal.max, - invert: passthrough as ( + invert: ops.passthrough as ( value: DecimalSource, ...inputs: [FormulaSource, FormulaSource] ) => DecimalSource, - invertIntegral: passthrough as ( + invertIntegral: ops.passthrough as ( value: DecimalSource, ...inputs: [FormulaSource, FormulaSource] ) => DecimalSource }); } - public static min(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula({ - inputs: [value, other], - evaluate: Decimal.min, - invert: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource, - invertIntegral: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource - }); - } - - public static minabs(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula({ - inputs: [value, other], - evaluate: Decimal.minabs, - invert: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource, - invertIntegral: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource - }); - } - - public static maxabs(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula({ - inputs: [value, other], - evaluate: Decimal.maxabs, - invert: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource, - invertIntegral: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource - }); - } + public static min = ops.createPassthroughBinaryFormula(Decimal.min); + public static minabs = ops.createPassthroughBinaryFormula(Decimal.minabs); + public static maxabs = ops.createPassthroughBinaryFormula(Decimal.maxabs); + public static clampMin = ops.createPassthroughBinaryFormula(Decimal.clampMin); + public static clampMax = ops.createPassthroughBinaryFormula(Decimal.clampMax); public static clamp( value: FormulaSource, @@ -1563,44 +600,12 @@ export default class Formula { return new Formula({ inputs: [value, min, max], evaluate: Decimal.clamp, - invert: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource, FormulaSource] - ) => DecimalSource, - invertIntegral: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource, FormulaSource] - ) => DecimalSource - }); - } - - public static clampMin(value: FormulaSource, min: FormulaSource): GenericFormula { - return new Formula({ - inputs: [value, min], - evaluate: Decimal.clampMin, - invert: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource, - invertIntegral: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource - }); - } - - public static clampMax(value: FormulaSource, max: FormulaSource): GenericFormula { - return new Formula({ - inputs: [value, max], - evaluate: Decimal.clampMax, - invert: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource, - invertIntegral: passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource + invert: ops.passthrough as InvertFunction< + [FormulaSource, FormulaSource, FormulaSource] + >, + invertIntegral: ops.passthrough as InvertFunction< + [FormulaSource, FormulaSource, FormulaSource] + > }); } @@ -1618,9 +623,9 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.log10, - invert: invertLog10, - integrate: integrateLog10, - invertIntegral: invertIntegrateLog10 + invert: ops.invertLog10, + integrate: ops.integrateLog10, + invertIntegral: ops.invertIntegrateLog10 }); } @@ -1631,9 +636,9 @@ export default class Formula { return new Formula({ inputs: [value, base], evaluate: Decimal.log, - invert: invertLog, - integrate: integrateLog, - invertIntegral: invertIntegrateLog + invert: ops.invertLog, + integrate: ops.integrateLog, + invertIntegral: ops.invertIntegrateLog }); } public static logarithm(value: T, base: FormulaSource): T; @@ -1649,9 +654,9 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.log2, - invert: invertLog2, - integrate: integrateLog2, - invertIntegral: invertIntegrateLog2 + invert: ops.invertLog2, + integrate: ops.integrateLog2, + invertIntegral: ops.invertIntegrateLog2 }); } @@ -1661,9 +666,9 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.ln, - invert: invertLn, - integrate: integrateLn, - invertIntegral: invertIntegrateLn + invert: ops.invertLn, + integrate: ops.integrateLn, + invertIntegral: ops.invertIntegrateLn }); } @@ -1674,9 +679,9 @@ export default class Formula { return new Formula({ inputs: [value, other], evaluate: Decimal.pow, - invert: invertPow, - integrate: integratePow, - invertIntegral: invertIntegratePow + invert: ops.invertPow, + integrate: ops.integratePow, + invertIntegral: ops.invertIntegratePow }); } @@ -1686,9 +691,9 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.pow10, - invert: invertPow10, - integrate: integratePow10, - invertIntegral: invertIntegratePow10 + invert: ops.invertPow10, + integrate: ops.integratePow10, + invertIntegral: ops.invertIntegratePow10 }); } @@ -1699,9 +704,9 @@ export default class Formula { return new Formula({ inputs: [value, other], evaluate: Decimal.pow_base, - invert: invertPowBase, - integrate: integratePowBase, - invertIntegral: invertIntegratePowBase + invert: ops.invertPowBase, + integrate: ops.integratePowBase, + invertIntegral: ops.invertIntegratePowBase }); } @@ -1712,9 +717,9 @@ export default class Formula { return new Formula({ inputs: [value, other], evaluate: Decimal.root, - invert: invertRoot, - integrate: integrateRoot, - invertIntegral: invertIntegrateRoot + invert: ops.invertRoot, + integrate: ops.integrateRoot, + invertIntegral: ops.invertIntegrateRoot }); } @@ -1736,8 +741,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.exp, - invert: invertExp, - integrate: integrateExp + invert: ops.invertExp, + integrate: ops.integrateExp }); } @@ -1782,8 +787,8 @@ export default class Formula { ) { return new Formula({ inputs: [value, height, payload], - evaluate: tetrate, - invert: invertTetrate + evaluate: ops.tetrate, + invert: ops.invertTetrate }); } @@ -1804,8 +809,8 @@ export default class Formula { ) { return new Formula({ inputs: [value, height, payload], - evaluate: iteratedexp, - invert: invertIteratedExp + evaluate: ops.iteratedexp, + invert: ops.invertIteratedExp }); } @@ -1814,7 +819,7 @@ export default class Formula { base: FormulaSource = 10, times: FormulaSource = 1 ): GenericFormula { - return new Formula({ inputs: [value, base, times], evaluate: iteratedLog }); + return new Formula({ inputs: [value, base, times], evaluate: ops.iteratedLog }); } public static slog( @@ -1823,7 +828,7 @@ export default class Formula { ): Omit; public static slog(value: FormulaSource, base?: FormulaSource): GenericFormula; public static slog(value: FormulaSource, base: FormulaSource = 10) { - return new Formula({ inputs: [value, base], evaluate: slog, invert: invertSlog }); + return new Formula({ inputs: [value, base], evaluate: ops.slog, invert: ops.invertSlog }); } public static layeradd10(value: FormulaSource, diff: FormulaSource) { @@ -1843,8 +848,8 @@ export default class Formula { public static layeradd(value: FormulaSource, diff: FormulaSource, base: FormulaSource = 10) { return new Formula({ inputs: [value, diff, base], - evaluate: layeradd, - invert: invertLayeradd + evaluate: ops.layeradd, + invert: ops.invertLayeradd }); } @@ -1853,7 +858,11 @@ export default class Formula { ): Omit; public static lambertw(value: FormulaSource): GenericFormula; public static lambertw(value: FormulaSource) { - return new Formula({ inputs: [value], evaluate: Decimal.lambertw, invert: invertLambertw }); + return new Formula({ + inputs: [value], + evaluate: Decimal.lambertw, + invert: ops.invertLambertw + }); } public static ssqrt( @@ -1861,7 +870,7 @@ export default class Formula { ): Omit; public static ssqrt(value: FormulaSource): GenericFormula; public static ssqrt(value: FormulaSource) { - return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: invertSsqrt }); + return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: ops.invertSsqrt }); } public static pentate( @@ -1869,7 +878,7 @@ export default class Formula { height: FormulaSource = 2, payload: FormulaSource = Decimal.fromComponents_noNormalize(1, 0, 1) ): GenericFormula { - return new Formula({ inputs: [value, height, payload], evaluate: pentate }); + return new Formula({ inputs: [value, height, payload], evaluate: ops.pentate }); } public static sin(value: T): Omit; @@ -1878,8 +887,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.sin, - invert: invertAsin, - integrate: integrateSin + invert: ops.invertAsin, + integrate: ops.integrateSin }); } @@ -1889,8 +898,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.cos, - invert: invertAcos, - integrate: integrateCos + invert: ops.invertAcos, + integrate: ops.integrateCos }); } @@ -1900,8 +909,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.tan, - invert: invertAtan, - integrate: integrateTan + invert: ops.invertAtan, + integrate: ops.integrateTan }); } @@ -1911,8 +920,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.asin, - invert: invertSin, - integrate: integrateAsin + invert: ops.invertSin, + integrate: ops.integrateAsin }); } @@ -1922,8 +931,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.acos, - invert: invertCos, - integrate: integrateAcos + invert: ops.invertCos, + integrate: ops.integrateAcos }); } @@ -1933,8 +942,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.atan, - invert: invertTan, - integrate: integrateAtan + invert: ops.invertTan, + integrate: ops.integrateAtan }); } @@ -1944,8 +953,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.sinh, - invert: invertAsinh, - integrate: integrateSinh + invert: ops.invertAsinh, + integrate: ops.integrateSinh }); } @@ -1955,8 +964,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.cosh, - invert: invertAcosh, - integrate: integrateCosh + invert: ops.invertAcosh, + integrate: ops.integrateCosh }); } @@ -1966,8 +975,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.tanh, - invert: invertAtanh, - integrate: integrateTanh + invert: ops.invertAtanh, + integrate: ops.integrateTanh }); } @@ -1977,8 +986,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.asinh, - invert: invertSinh, - integrate: integrateAsinh + invert: ops.invertSinh, + integrate: ops.integrateAsinh }); } @@ -1988,8 +997,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.acosh, - invert: invertCosh, - integrate: integrateAcosh + invert: ops.invertCosh, + integrate: ops.integrateAcosh }); } @@ -1999,8 +1008,8 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.atanh, - invert: invertTanh, - integrate: integrateAtanh + invert: ops.invertTanh, + integrate: ops.integrateAtanh }); } @@ -2488,7 +1497,7 @@ export function calculateMaxAffordable( if (!formula.isInvertible()) { throw "Cannot calculate max affordable of non-invertible formula"; } - return Decimal.floor((formula as InvertibleFormula).invert(resource.value)); + return Decimal.floor(formula.invert(resource.value)); } }); } diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts new file mode 100644 index 0000000..fb860d3 --- /dev/null +++ b/src/game/formulas/operations.ts @@ -0,0 +1,904 @@ +import Decimal, { DecimalSource } from "util/bignum"; +import { unref } from "vue"; +import Formula, { hasVariable, unrefFormulaSource } from "./formulas"; +import { FormulaSource, InvertFunction, SubstitutionStack } from "./types"; + +export function passthrough(value: DecimalSource) { + return value; +} + +export function invertNeg(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.neg(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateNeg( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + return Decimal.neg(lhs.evaluateIntegral(variable, stack)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function applySubstitutionNeg(value: DecimalSource) { + return Decimal.neg(value); +} + +export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.sub(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs))); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateAdd( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.times( + unrefFormulaSource(rhs), + variable ?? unref(lhs.innermostVariable) ?? 0 + ).add(x); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.times( + unrefFormulaSource(lhs), + variable ?? unref(rhs.innermostVariable) ?? 0 + ).add(x); + } + throw "Could not integrate due to no input being a variable"; +} + +export function integrateInnerAdd( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.add(x, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.add(x, unrefFormulaSource(lhs)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateSub( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sub( + x, + Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0) + ); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.times( + unrefFormulaSource(lhs), + variable ?? unref(rhs.innermostVariable) ?? 0 + ).sub(x); + } + throw "Could not integrate due to no input being a variable"; +} + +export function integrateInnerSub( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sub(x, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.sub(x, unrefFormulaSource(lhs)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs))); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateMul( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.times(x, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.times(x, unrefFormulaSource(lhs)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.div(value, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + return Decimal.div(value, unrefFormulaSource(lhs)); + } + throw "Could not apply substitution due to no input being a variable"; +} + +export function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateDiv( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.div(x, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + return Decimal.div(unrefFormulaSource(lhs), x); + } + throw "Could not integrate due to no input being a variable"; +} + +export function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return Decimal.mul(value, unrefFormulaSource(rhs)); + } else if (hasVariable(rhs)) { + return Decimal.mul(value, unrefFormulaSource(lhs)); + } + throw "Could not apply substitution due to no input being a variable"; +} + +export function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertRecip(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.recip(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateRecip( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegrateRecip(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertLog10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.pow10(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateLog10( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1)) + ); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value)); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateLog( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x) + .sub(1) + .times(x) + .div(Decimal.ln(unrefFormulaSource(rhs))); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value); + return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertLog2(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.pow(2, value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateLog2( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1))); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertLn(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateLn( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1))); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs)))); + } + throw "Could not invert due to no input being a variable"; +} + +export function integratePow( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + const pow = Decimal.add(unrefFormulaSource(rhs), 1); + return Decimal.pow(x, pow).div(pow); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + const b = unrefFormulaSource(lhs); + return Decimal.pow(b, x).div(Decimal.ln(b)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.negate(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); + } else if (hasVariable(rhs)) { + const denominator = Decimal.ln(unrefFormulaSource(lhs)); + return rhs.invert(Decimal.times(denominator, value).ln().div(denominator)); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertPow10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.root(value, 10)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integratePow10( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp() + ); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integratePowBase( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + const b = unrefFormulaSource(rhs); + return Decimal.pow(b, x).div(Decimal.ln(b)); + } else if (hasVariable(rhs)) { + const x = rhs.evaluateIntegral(variable, stack); + const denominator = Decimal.add(unrefFormulaSource(lhs), 1); + return Decimal.pow(x, denominator).div(denominator); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegratePowBase( + value: DecimalSource, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert(Decimal.ln(b).times(value).ln().div(Decimal.ln(b))); + } else if (hasVariable(rhs)) { + const b = unrefFormulaSource(lhs); + return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs)))); + } else if (hasVariable(rhs)) { + return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value))); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateRoot( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource, + rhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + const a = unrefFormulaSource(rhs); + return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + const b = unrefFormulaSource(rhs); + return lhs.invert( + Decimal.add(b, 1) + .times(value) + .div(b) + .pow(Decimal.div(b, Decimal.add(b, 1))) + ); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertExp(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.ln(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateExp( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.exp(x); + } + throw "Could not integrate due to no input being a variable"; +} + +export function tetrate( + value: DecimalSource, + height: DecimalSource = 2, + payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1) +) { + const heightNumber = Decimal.minabs(height, 1e308).toNumber(); + return Decimal.tetrate(value, heightNumber, payload); +} + +export function invertTetrate( + value: DecimalSource, + base: FormulaSource, + height: FormulaSource, + payload: FormulaSource +) { + if (hasVariable(base)) { + return base.invert(Decimal.ssqrt(value)); + } + // Other params can't be inverted ATM + throw "Could not invert due to no input being a variable"; +} + +export function iteratedexp( + value: DecimalSource, + height: DecimalSource = 2, + payload: DecimalSource = Decimal.fromComponents_noNormalize(1, 0, 1) +) { + const heightNumber = Decimal.minabs(height, 1e308).toNumber(); + return Decimal.iteratedexp(value, heightNumber, new Decimal(payload)); +} + +export function invertIteratedExp( + value: DecimalSource, + lhs: FormulaSource, + height: FormulaSource, + payload: FormulaSource +) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.iteratedlog( + value, + Math.E, + Decimal.minabs(1e308, unrefFormulaSource(height)).toNumber() + ) + ); + } + // Other params can't be inverted ATM + throw "Could not invert due to no input being a variable"; +} + +export function iteratedLog( + value: DecimalSource, + lhs: DecimalSource = 10, + times: DecimalSource = 2 +) { + const timesNumber = Decimal.minabs(times, 1e308).toNumber(); + return Decimal.iteratedlog(value, lhs, timesNumber); +} + +export function slog(value: DecimalSource, lhs: DecimalSource = 10) { + const baseNumber = Decimal.minabs(lhs, 1e308).toNumber(); + return Decimal.slog(value, baseNumber); +} + +export function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.tetrate(value, Decimal.minabs(1e308, unrefFormulaSource(rhs)).toNumber()) + ); + } + // Other params can't be inverted ATM + throw "Could not invert due to no input being a variable"; +} + +export function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) { + const diffNumber = Decimal.minabs(diff, 1e308).toNumber(); + return Decimal.layeradd(value, diffNumber, base); +} + +export function invertLayeradd( + value: DecimalSource, + lhs: FormulaSource, + diff: FormulaSource, + base: FormulaSource +) { + if (hasVariable(lhs)) { + return lhs.invert( + Decimal.layeradd( + value, + Decimal.minabs(1e308, unrefFormulaSource(diff)).negate().toNumber() + ) + ); + } + // Other params can't be inverted ATM + throw "Could not invert due to no input being a variable"; +} + +export function invertLambertw(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.pow(Math.E, value).times(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function invertSsqrt(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.tetrate(value, 2)); + } + throw "Could not invert due to no input being a variable"; +} + +export function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) { + const heightNumber = Decimal.minabs(height, 1e308).toNumber(); + return Decimal.pentate(value, heightNumber, payload); +} + +export function invertSin(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.asin(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateSin( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.cos(x).neg(); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertCos(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.acos(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateCos( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sin(x); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertTan(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.atan(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateTan( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.cos(x).ln().neg(); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertAsin(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.sin(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateAsin( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.asin(x) + .times(x) + .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertAcos(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.cos(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateAcos( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.acos(x) + .times(x) + .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertAtan(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.tan(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateAtan( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.atan(x) + .times(x) + .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertSinh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.asinh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateSinh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.cosh(x); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertCosh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.acosh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateCosh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.sinh(x); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertTanh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.atanh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateTanh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.cosh(x).ln(); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertAsinh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.sinh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateAsinh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt()); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertAcosh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.cosh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateAcosh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.acosh(x) + .times(x) + .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt())); + } + throw "Could not integrate due to no input being a variable"; +} + +export function invertAtanh(value: DecimalSource, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return lhs.invert(Decimal.tanh(value)); + } + throw "Could not invert due to no input being a variable"; +} + +export function integrateAtanh( + variable: DecimalSource | undefined, + stack: SubstitutionStack, + lhs: FormulaSource +) { + if (hasVariable(lhs)) { + const x = lhs.evaluateIntegral(variable, stack); + return Decimal.atanh(x) + .times(x) + .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2)); + } + throw "Could not integrate due to no input being a variable"; +} + +export function createPassthroughBinaryFormula( + operation: (a: DecimalSource, b: DecimalSource) => DecimalSource +) { + return (value: FormulaSource, other: FormulaSource) => + new Formula({ + inputs: [value, other], + evaluate: operation, + invert: passthrough as InvertFunction<[FormulaSource, FormulaSource]>, + invertIntegral: passthrough as InvertFunction<[FormulaSource, FormulaSource]> + }); +} diff --git a/src/game/formulas/types.d.ts b/src/game/formulas/types.d.ts new file mode 100644 index 0000000..baaf2bb --- /dev/null +++ b/src/game/formulas/types.d.ts @@ -0,0 +1,79 @@ +import Formula from "game/formulas/formulas"; +import { DecimalSource } from "util/bignum"; +import { ProcessedComputable } from "util/computed"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type GenericFormula = Formula; +type FormulaSource = ProcessedComputable | GenericFormula; +type InvertibleFormula = GenericFormula & { + invert: (value: DecimalSource) => DecimalSource; +}; +type IntegrableFormula = GenericFormula & { + evaluateIntegral: (variable?: DecimalSource) => DecimalSource; +}; +type InvertibleIntegralFormula = GenericFormula & { + invertIntegral: (value: DecimalSource) => DecimalSource; +}; + +type EvaluateFunction = ( + this: Formula, + ...inputs: GuardedFormulasToDecimals +) => DecimalSource; +type InvertFunction = (this: Formula, value: DecimalSource, ...inputs: T) => DecimalSource; +type IntegrateFunction = ( + this: Formula, + variable: DecimalSource | undefined, + stack: SubstitutionStack | undefined, + ...inputs: T +) => DecimalSource; +type SubstitutionFunction = ( + this: Formula, + variable: DecimalSource, + ...inputs: T +) => DecimalSource; +type InvertIntegralFunction = ( + this: Formula, + value: DecimalSource, + ...inputs: T +) => DecimalSource; + +type VariableFormulaOptions = { variable: ProcessedComputable }; +type ConstantFormulaOptions = { + inputs: [FormulaSource]; +}; +type GeneralFormulaOptions = { + inputs: T; + evaluate: EvaluateFunction; + invert?: InvertFunction; + integrate?: IntegrateFunction; + integrateInner?: IntegrateFunction; + applySubstitution?: SubstitutionFunction; + invertIntegral?: InvertIntegralFunction; + hasVariable?: boolean; +}; +type FormulaOptions = + | VariableFormulaOptions + | ConstantFormulaOptions + | GeneralFormulaOptions; + +type InternalFormulaProperties = { + inputs: T; + internalHasVariable: boolean; + internalEvaluate?: EvaluateFunction; + internalInvert?: InvertFunction; + internalIntegrate?: IntegrateFunction; + internalIntegrateInner?: IntegrateFunction; + internalInvertIntegral?: InvertIntegralFunction; + applySubstitution?: SubstitutionFunction; + innermostVariable?: ProcessedComputable; +}; + +type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined; + +// It's really hard to type mapped tuples, but these classes seem to manage +type FormulasToDecimals = { + [K in keyof T]: DecimalSource; +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TupleGuard = T extends any[] ? FormulasToDecimals : never; +type GuardedFormulasToDecimals = TupleGuard; diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 5372e36..8cd1cf7 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -11,12 +11,8 @@ import { import { createLazyProxy } from "util/proxies"; import { joinJSX, renderJSX } from "util/vue"; import { computed, unref } from "vue"; -import Formula, { - calculateCost, - calculateMaxAffordable, - GenericFormula, - InvertibleFormula -} from "./formulas"; +import Formula, { calculateCost, calculateMaxAffordable } from "./formulas/formulas"; +import type { GenericFormula, InvertibleFormula } from "./formulas/types"; import { DefaultValue, Persistent } from "./persistence"; /** diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 9937d30..a2b4012 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -2,10 +2,9 @@ import { createResource, Resource } from "features/resources/resource"; import Formula, { calculateCost, calculateMaxAffordable, - GenericFormula, - InvertibleFormula, unrefFormulaSource -} from "game/formulas"; +} from "game/formulas/formulas"; +import type { GenericFormula, InvertibleFormula } from "game/formulas/types"; import Decimal, { DecimalSource } from "util/bignum"; import { beforeAll, describe, expect, test } from "vitest"; import { ref } from "vue"; diff --git a/tests/game/requirements.test.ts b/tests/game/requirements.test.ts index 55474b9..13a13e9 100644 --- a/tests/game/requirements.test.ts +++ b/tests/game/requirements.test.ts @@ -1,6 +1,6 @@ import { Visibility } from "features/feature"; import { createResource, Resource } from "features/resources/resource"; -import Formula from "game/formulas"; +import Formula from "game/formulas/formulas"; import { CostRequirement, createBooleanRequirement, From f589184c9e95ef9a0e844d912c7758756bb83c5d Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 19:22:27 -0500 Subject: [PATCH 08/36] Simplify alias definitions --- src/game/formulas/formulas.ts | 170 ++++++---------------------------- 1 file changed, 28 insertions(+), 142 deletions(-) diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 885adee..6255981 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -421,23 +421,13 @@ export default class Formula { integrate: ops.integrateNeg }); } - public static negate(value: T): Omit; - public static negate(value: FormulaSource): GenericFormula; - public static negate(value: FormulaSource) { - return Formula.neg(value); - } - public static negated(value: T): Omit; - public static negated(value: FormulaSource): GenericFormula; - public static negated(value: FormulaSource) { - return Formula.neg(value); - } + public static negate = Formula.neg; + public static negated = Formula.neg; public static sign(value: FormulaSource): GenericFormula { return new Formula({ inputs: [value], evaluate: Decimal.sign }); } - public static sgn(value: FormulaSource) { - return Formula.sign(value); - } + public static sgn = Formula.sign; public static round(value: FormulaSource): GenericFormula { return new Formula({ inputs: [value], evaluate: Decimal.round }); @@ -469,12 +459,7 @@ export default class Formula { invertIntegral: ops.invertIntegrateAdd }); } - public static plus(value: T, other: FormulaSource): T; - public static plus(value: FormulaSource, other: T): T; - public static plus(value: FormulaSource, other: FormulaSource): GenericFormula; - public static plus(value: FormulaSource, other: FormulaSource) { - return Formula.add(value, other); - } + public static plus = Formula.add; public static sub(value: T, other: FormulaSource): T; public static sub(value: FormulaSource, other: T): T; @@ -490,18 +475,8 @@ export default class Formula { invertIntegral: ops.invertIntegrateSub }); } - public static subtract(value: T, other: FormulaSource): T; - public static subtract(value: FormulaSource, other: T): T; - public static subtract(value: FormulaSource, other: FormulaSource): GenericFormula; - public static subtract(value: FormulaSource, other: FormulaSource) { - return Formula.sub(value, other); - } - public static minus(value: T, other: FormulaSource): T; - public static minus(value: FormulaSource, other: T): T; - public static minus(value: FormulaSource, other: FormulaSource): GenericFormula; - public static minus(value: FormulaSource, other: FormulaSource) { - return Formula.sub(value, other); - } + public static subtract = Formula.sub; + public static minus = Formula.sub; public static mul(value: T, other: FormulaSource): T; public static mul(value: FormulaSource, other: T): T; @@ -516,18 +491,8 @@ export default class Formula { invertIntegral: ops.invertIntegrateMul }); } - public static multiply(value: T, other: FormulaSource): T; - public static multiply(value: FormulaSource, other: T): T; - public static multiply(value: FormulaSource, other: FormulaSource): GenericFormula; - public static multiply(value: FormulaSource, other: FormulaSource) { - return Formula.mul(value, other); - } - public static times(value: T, other: FormulaSource): T; - public static times(value: FormulaSource, other: T): T; - public static times(value: FormulaSource, other: FormulaSource): GenericFormula; - public static times(value: FormulaSource, other: FormulaSource) { - return Formula.mul(value, other); - } + public static multiply = Formula.mul; + public static times = Formula.mul; public static div(value: T, other: FormulaSource): T; public static div(value: FormulaSource, other: T): T; @@ -542,12 +507,9 @@ export default class Formula { invertIntegral: ops.invertIntegrateDiv }); } - public static divide(value: T, other: FormulaSource): T; - public static divide(value: FormulaSource, other: T): T; - public static divide(value: FormulaSource, other: FormulaSource): GenericFormula; - public static divide(value: FormulaSource, other: FormulaSource) { - return Formula.div(value, other); - } + public static divide = Formula.div; + public static divideBy = Formula.div; + public static dividedBy = Formula.div; public static recip(value: T): T; public static recip(value: FormulaSource): GenericFormula; @@ -560,16 +522,8 @@ export default class Formula { invertIntegral: ops.invertIntegrateRecip }); } - public static reciprocal(value: T): T; - public static reciprocal(value: FormulaSource): GenericFormula; - public static reciprocal(value: FormulaSource): GenericFormula { - return Formula.recip(value); - } - public static reciprocate(value: T): T; - public static reciprocate(value: FormulaSource): GenericFormula; - public static reciprocate(value: FormulaSource) { - return Formula.recip(value); - } + public static reciprocal = Formula.recip; + public static reciprocate = Formula.recip; public static max(value: FormulaSource, other: FormulaSource): GenericFormula { return new Formula({ @@ -641,12 +595,7 @@ export default class Formula { invertIntegral: ops.invertIntegrateLog }); } - public static logarithm(value: T, base: FormulaSource): T; - public static logarithm(value: FormulaSource, base: T): T; - public static logarithm(value: FormulaSource, base: FormulaSource): GenericFormula; - public static logarithm(value: FormulaSource, base: FormulaSource) { - return Formula.log(value, base); - } + public static logarithm = Formula.log; public static log2(value: T): T; public static log2(value: FormulaSource): GenericFormula; @@ -1048,23 +997,13 @@ export default class Formula { public neg(this: GenericFormula) { return Formula.neg(this); } - public negate(this: T): Omit; - public negate(this: GenericFormula): GenericFormula; - public negate(this: GenericFormula) { - return Formula.neg(this); - } - public negated(this: T): Omit; - public negated(this: GenericFormula): GenericFormula; - public negated(this: GenericFormula) { - return Formula.neg(this); - } + public negate = this.neg; + public negated = this.neg; public sign() { return Formula.sign(this); } - public sgn() { - return this.sign(); - } + public sgn = this.sign; public round() { return Formula.round(this); @@ -1088,12 +1027,7 @@ export default class Formula { public add(this: GenericFormula, value: FormulaSource) { return Formula.add(this, value); } - public plus(this: T, value: FormulaSource): T; - public plus(this: GenericFormula, value: T): T; - public plus(this: GenericFormula, value: FormulaSource): GenericFormula; - public plus(value: FormulaSource) { - return Formula.add(this, value); - } + public plus = this.add; public sub(this: T, value: FormulaSource): T; public sub(this: GenericFormula, value: T): T; @@ -1101,18 +1035,8 @@ export default class Formula { public sub(value: FormulaSource) { return Formula.sub(this, value); } - public subtract(this: T, value: FormulaSource): T; - public subtract(this: GenericFormula, value: T): T; - public subtract(this: GenericFormula, value: FormulaSource): GenericFormula; - public subtract(value: FormulaSource) { - return Formula.sub(this, value); - } - public minus(this: T, value: FormulaSource): T; - public minus(this: GenericFormula, value: T): T; - public minus(this: GenericFormula, value: FormulaSource): GenericFormula; - public minus(value: FormulaSource) { - return Formula.sub(this, value); - } + public subtract = this.sub; + public minus = this.sub; public mul(this: T, value: FormulaSource): T; public mul(this: GenericFormula, value: T): T; @@ -1120,18 +1044,8 @@ export default class Formula { public mul(value: FormulaSource) { return Formula.mul(this, value); } - public multiply(this: T, value: FormulaSource): T; - public multiply(this: GenericFormula, value: T): T; - public multiply(this: GenericFormula, value: FormulaSource): GenericFormula; - public multiply(value: FormulaSource) { - return Formula.mul(this, value); - } - public times(this: T, value: FormulaSource): T; - public times(this: GenericFormula, value: T): T; - public times(this: GenericFormula, value: FormulaSource): GenericFormula; - public times(value: FormulaSource) { - return Formula.mul(this, value); - } + public multiply = this.mul; + public times = this.mul; public div(this: T, value: FormulaSource): T; public div(this: GenericFormula, value: T): T; @@ -1139,40 +1053,17 @@ export default class Formula { public div(value: FormulaSource) { return Formula.div(this, value); } - public divide(this: T, value: FormulaSource): T; - public divide(this: GenericFormula, value: T): T; - public divide(this: GenericFormula, value: FormulaSource): GenericFormula; - public divide(value: FormulaSource) { - return Formula.div(this, value); - } - public divideBy(this: T, value: FormulaSource): T; - public divideBy(this: GenericFormula, value: T): T; - public divideBy(this: GenericFormula, value: FormulaSource): GenericFormula; - public divideBy(value: FormulaSource) { - return Formula.div(this, value); - } - public dividedBy(this: T, value: FormulaSource): T; - public dividedBy(this: GenericFormula, value: T): T; - public dividedBy(this: GenericFormula, value: FormulaSource): GenericFormula; - public dividedBy(value: FormulaSource) { - return Formula.div(this, value); - } + public divide = this.div; + public divideBy = this.div; + public dividedBy = this.div; public recip(this: T): T; public recip(this: FormulaSource): GenericFormula; public recip() { return Formula.recip(this); } - public reciprocal(this: T): T; - public reciprocal(this: FormulaSource): GenericFormula; - public reciprocal() { - return Formula.recip(this); - } - public reciprocate(this: T): T; - public reciprocate(this: FormulaSource): GenericFormula; - public reciprocate() { - return Formula.recip(this); - } + public reciprocal = this.recip; + public reciprocate = this.recip; public max(value: FormulaSource) { return Formula.max(this, value); @@ -1222,12 +1113,7 @@ export default class Formula { public log(value: FormulaSource) { return Formula.log(this, value); } - public logarithm(this: T, value: FormulaSource): T; - public logarithm(this: FormulaSource, value: T): T; - public logarithm(this: FormulaSource, value: FormulaSource): GenericFormula; - public logarithm(value: FormulaSource) { - return Formula.log(this, value); - } + public logarithm = this.log; public log2(this: T): T; public log2(this: FormulaSource): GenericFormula; From 3b4c098f9e9acf330393f805058cf46f9b935e80 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 19:59:45 -0500 Subject: [PATCH 09/36] Fix some formula tests --- tests/game/formulas.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index a2b4012..967080d 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -138,11 +138,13 @@ const nonInvertibleIntegralTwoParamFunctionNames = nonIntegrableTwoParamFunction describe("Formula Equality Checking", () => { describe("Equality Checks", () => { - test("Equals", () => Formula.add(1, 1).equals(Formula.add(1, 1))); - test("Not Equals due to inputs", () => Formula.add(1, 1).equals(Formula.add(1, 0))); - test("Not Equals due to functions", () => Formula.add(1, 1).equals(Formula.sub(1, 1))); + test("Equals", () => expect(Formula.add(1, 1).equals(Formula.add(1, 1))).toBe(true)); + test("Not Equals due to inputs", () => + expect(Formula.add(1, 1).equals(Formula.add(1, 0))).toBe(false)); + test("Not Equals due to functions", () => + expect(Formula.add(1, 1).equals(Formula.sub(1, 1))).toBe(false)); test("Not Equals due to hasVariable", () => - Formula.constant(1).equals(Formula.variable(1))); + expect(Formula.constant(1).equals(Formula.variable(1))).toBe(false)); }); describe("Formula aliases", () => { From f94c0c1f68a9e1a7be92ed7312136b8ba10f922b Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 20:00:06 -0500 Subject: [PATCH 10/36] Add some more tests for step-wise and conditional formulas --- tests/game/formulas.test.ts | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 967080d..13a6e53 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -759,6 +759,17 @@ describe("Step-wise", () => { ).compare_tolerance(10)); }); + describe("Pass-through at boundary", () => { + test("Evaluates correctly", () => + expect( + Formula.step(constant, 10, value => Formula.sqrt(value)).evaluate() + ).compare_tolerance(10)); + test("Inverts correctly with variable in input", () => + expect( + Formula.step(variable, 10, value => Formula.sqrt(value)).invert(10) + ).compare_tolerance(10)); + }); + describe("Evaluates correctly beyond start", () => { test("Evaluates correctly", () => expect( @@ -769,6 +780,23 @@ describe("Step-wise", () => { Formula.step(variable, 8, value => Formula.add(value, 2)).invert(12) ).compare_tolerance(10)); }); + + describe("Evaluates correctly when nested", () => { + test("Evaluates correctly", () => + expect( + Formula.add(variable, constant) + .step(10, value => Formula.mul(value, 2)) + .sub(10) + .evaluate() + ).compare_tolerance(20)); + test("Inverts correctly", () => + expect( + Formula.add(variable, constant) + .step(10, value => Formula.mul(value, 2)) + .sub(10) + .invert(30) + ).compare_tolerance(15)); + }); }); describe("Conditionals", () => { @@ -842,6 +870,23 @@ describe("Conditionals", () => { Formula.if(variable, true, value => Formula.add(value, 2)).invert(12) ).compare_tolerance(10)); }); + + describe("Evaluates correctly when nested", () => { + test("Evaluates correctly", () => + expect( + Formula.add(variable, constant) + .if(true, value => Formula.add(value, 2)) + .div(2) + .evaluate() + ).compare_tolerance(11)); + test("Inverts correctly", () => + expect( + Formula.add(variable, constant) + .if(true, value => Formula.add(value, 2)) + .div(2) + .invert(12) + ).compare_tolerance(12)); + }); }); describe("Custom Formulas", () => { From f47cc23eea29a1160d1ef037381be0522f9124f7 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 20:16:59 -0500 Subject: [PATCH 11/36] Add some test TODOs --- tests/game/formulas.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 13a6e53..4003051 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -996,6 +996,10 @@ describe("Custom Formulas", () => { }).invertIntegral(8) ).compare_tolerance(1)); }); + + describe.todo("Formula as input"); + + describe.todo("Determines invertibility etc. correctly"); }); describe("Buy Max", () => { From c3b5f2cdad930993dd3aae2a036e6cd48f0d3c63 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 20:22:03 -0500 Subject: [PATCH 12/36] Throw error objects instead of strings --- src/data/common.tsx | 4 +- src/data/projEntry.tsx | 10 +- src/features/challenges/challenge.tsx | 4 +- src/features/tabs/tabFamily.ts | 2 +- src/game/formulas/formulas.ts | 30 ++--- src/game/formulas/operations.ts | 154 +++++++++++++------------- src/game/persistence.ts | 2 +- src/lib/break_eternity.ts | 2 +- src/main.ts | 4 +- 9 files changed, 114 insertions(+), 98 deletions(-) diff --git a/src/data/common.tsx b/src/data/common.tsx index b1d5398..befb408 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -465,7 +465,7 @@ export function createFormulaPreview( const processedShowPreview = convertComputable(showPreview); const processedPreviewAmount = convertComputable(previewAmount); if (!formula.hasVariable()) { - throw "Cannot create formula preview if the formula does not have a variable"; + throw new Error("Cannot create formula preview if the formula does not have a variable"); } return computed(() => { if (unref(processedShowPreview)) { @@ -507,7 +507,7 @@ export function modifierToFormula(modifier: Modifier, base: FormulaSource) { if (lhs instanceof Formula && lhs.hasVariable()) { return lhs.invert(modifier.revert!(val)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } : undefined }); diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index e4640b6..df86d6a 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -4,6 +4,7 @@ import { createResource, trackBest, trackOOMPS, trackTotal } from "features/reso import type { GenericTree } from "features/trees/tree"; import { branchedResetPropagation, createTree } from "features/trees/tree"; import { globalBus } from "game/events"; +import Formula, { calculateCost, calculateMaxAffordable } from "game/formulas/formulas"; import type { BaseLayer, GenericLayer } from "game/layers"; import { createLayer } from "game/layers"; import type { Player } from "game/player"; @@ -11,9 +12,16 @@ import player from "game/player"; import type { DecimalSource } from "util/bignum"; import Decimal, { format, formatTime } from "util/bignum"; import { render } from "util/vue"; -import { computed, toRaw } from "vue"; +import { computed, ref, toRaw, unref } from "vue"; import prestige from "./layers/prestige"; +window.Formula = Formula; +window.calculateMaxAffordable = calculateMaxAffordable; +window.calculateCost = calculateCost; +window.unref = unref; +window.ref = ref; +window.createResource = createResource; + /** * @hidden */ diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index 58a804c..92dce42 100644 --- a/src/features/challenges/challenge.tsx +++ b/src/features/challenges/challenge.tsx @@ -113,7 +113,9 @@ export function createChallenge( "Cannot create challenge without a canComplete property or a resource and goal property", challenge ); - throw "Cannot create challenge without a canComplete property or a resource and goal property"; + throw new Error( + "Cannot create challenge without a canComplete property or a resource and goal property" + ); } challenge.id = getUniqueID("challenge-"); diff --git a/src/features/tabs/tabFamily.ts b/src/features/tabs/tabFamily.ts index 5f5c666..9652f3d 100644 --- a/src/features/tabs/tabFamily.ts +++ b/src/features/tabs/tabFamily.ts @@ -98,7 +98,7 @@ export function createTabFamily( ): TabFamily { if (Object.keys(tabs).length === 0) { console.warn("Cannot create tab family with 0 tabs"); - throw "Cannot create tab family with 0 tabs"; + throw new Error("Cannot create tab family with 0 tabs"); } const selected = persistent(Object.keys(tabs)[0], false); diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 6255981..30a2b20 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -35,7 +35,7 @@ function integrateVariable(variable: DecimalSource) { function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) { if (variable == null && this.innermostVariable == null) { - throw "Cannot integrate non-existent variable"; + throw new Error("Cannot integrate non-existent variable"); } return variable ?? unref(this.innermostVariable); } @@ -93,7 +93,7 @@ export default class Formula { private setupConstant({ inputs }: { inputs: [FormulaSource] }): InternalFormulaProperties { if (inputs.length !== 1) { - throw "Evaluate function is required if inputs is not length 1"; + throw new Error("Evaluate function is required if inputs is not length 1"); } return { inputs: inputs as T, @@ -113,7 +113,9 @@ export default class Formula { hasVariable } = options; if (invert == null && invertIntegral == null && hasVariable) { - throw "A formula cannot be marked as having a variable if it is not invertible"; + throw new Error( + "A formula cannot be marked as having a variable if it is not invertible" + ); } const numVariables = inputs.filter( @@ -197,7 +199,7 @@ export default class Formula { } else if (this.inputs.length === 1 && this.internalHasVariable) { return value; } - throw "Cannot invert non-invertible formula"; + throw new Error("Cannot invert non-invertible formula"); } /** @@ -213,7 +215,7 @@ export default class Formula { // We're the complex operation of this formula stack = []; if (this.internalIntegrate == null) { - throw "Cannot integrate formula with non-existent operation"; + throw new Error("Cannot integrate formula with non-existent operation"); } let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs); stack.forEach(func => (value = func(value))); @@ -225,12 +227,12 @@ export default class Formula { } else if (this.inputs.length === 1 && this.internalHasVariable) { return integrateVariable(variable ?? unrefFormulaSource(this.inputs[0])); } - throw "Cannot integrate formula without variable"; + throw new Error("Cannot integrate formula without variable"); } } else { // "Inner" part of the formula if (this.applySubstitution == null) { - throw "Cannot have two complex operations in an integrable formula"; + throw new Error("Cannot have two complex operations in an integrable formula"); } stack.push((variable: DecimalSource) => // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -243,7 +245,7 @@ export default class Formula { } else if (this.inputs.length === 1 && this.internalHasVariable) { return variable ?? unrefFormulaSource(this.inputs[0]); } - throw "Cannot integrate formula without variable"; + throw new Error("Cannot integrate formula without variable"); } } @@ -267,7 +269,7 @@ export default class Formula { } else if (this.inputs.length === 1 && this.internalHasVariable) { return value; } - throw "Cannot invert integral of formula without invertible integral"; + throw new Error("Cannot invert integral of formula without invertible integral"); } /** @@ -346,7 +348,7 @@ export default class Formula { } return lhs.invert(value); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } return new Formula({ inputs: [value], @@ -381,7 +383,7 @@ export default class Formula { } function invertStep(value: DecimalSource, lhs: FormulaSource) { if (!hasVariable(lhs)) { - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } if (unref(processedCondition)) { return lhs.invert(formula.invert(value)); @@ -1374,14 +1376,16 @@ export function calculateMaxAffordable( return computed(() => { if (unref(computedSpendResources)) { if (!formula.isIntegrable() || !formula.isIntegralInvertible()) { - throw "Cannot calculate max affordable of formula with non-invertible integral"; + throw new Error( + "Cannot calculate max affordable of formula with non-invertible integral" + ); } return Decimal.floor( formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral())) ).sub(unref(formula.innermostVariable) ?? 0); } else { if (!formula.isInvertible()) { - throw "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)); } diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts index fb860d3..f4dd1f0 100644 --- a/src/game/formulas/operations.ts +++ b/src/game/formulas/operations.ts @@ -11,7 +11,7 @@ export function invertNeg(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.neg(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateNeg( @@ -22,7 +22,7 @@ export function integrateNeg( if (hasVariable(lhs)) { return Decimal.neg(lhs.evaluateIntegral(variable, stack)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function applySubstitutionNeg(value: DecimalSource) { @@ -35,7 +35,7 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula } else if (hasVariable(rhs)) { return rhs.invert(Decimal.sub(value, unrefFormulaSource(lhs))); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateAdd( @@ -57,7 +57,7 @@ export function integrateAdd( variable ?? unref(rhs.innermostVariable) ?? 0 ).add(x); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function integrateInnerAdd( @@ -73,7 +73,7 @@ export function integrateInnerAdd( const x = rhs.evaluateIntegral(variable, stack); return Decimal.add(x, unrefFormulaSource(lhs)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -84,7 +84,7 @@ export function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs const b = unrefFormulaSource(lhs); return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -93,7 +93,7 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula } else if (hasVariable(rhs)) { return rhs.invert(Decimal.sub(unrefFormulaSource(lhs), value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateSub( @@ -115,7 +115,7 @@ export function integrateSub( variable ?? unref(rhs.innermostVariable) ?? 0 ).sub(x); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function integrateInnerSub( @@ -131,7 +131,7 @@ export function integrateInnerSub( const x = rhs.evaluateIntegral(variable, stack); return Decimal.sub(x, unrefFormulaSource(lhs)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -142,7 +142,7 @@ export function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs const b = unrefFormulaSource(lhs); return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -151,7 +151,7 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula } else if (hasVariable(rhs)) { return rhs.invert(Decimal.div(value, unrefFormulaSource(lhs))); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateMul( @@ -167,7 +167,7 @@ export function integrateMul( const x = rhs.evaluateIntegral(variable, stack); return Decimal.times(x, unrefFormulaSource(lhs)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -176,7 +176,7 @@ export function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, r } else if (hasVariable(rhs)) { return Decimal.div(value, unrefFormulaSource(lhs)); } - throw "Could not apply substitution due to no input being a variable"; + throw new Error("Could not apply substitution due to no input being a variable"); } export function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -187,7 +187,7 @@ export function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs const b = unrefFormulaSource(lhs); return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -196,7 +196,7 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula } else if (hasVariable(rhs)) { return rhs.invert(Decimal.div(unrefFormulaSource(lhs), value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateDiv( @@ -212,7 +212,7 @@ export function integrateDiv( const x = rhs.evaluateIntegral(variable, stack); return Decimal.div(unrefFormulaSource(lhs), x); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -221,7 +221,7 @@ export function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, r } else if (hasVariable(rhs)) { return Decimal.mul(value, unrefFormulaSource(lhs)); } - throw "Could not apply substitution due to no input being a variable"; + throw new Error("Could not apply substitution due to no input being a variable"); } export function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -232,14 +232,14 @@ export function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs const b = unrefFormulaSource(lhs); return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertRecip(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.recip(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateRecip( @@ -251,21 +251,21 @@ export function integrateRecip( const x = lhs.evaluateIntegral(variable, stack); return Decimal.ln(x); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegrateRecip(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.exp(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertLog10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow10(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateLog10( @@ -277,7 +277,7 @@ export function integrateLog10( const x = lhs.evaluateIntegral(variable, stack); return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) { @@ -286,7 +286,7 @@ export function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) { Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1)) ); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -295,7 +295,7 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula } else if (hasVariable(rhs)) { return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateLog( @@ -311,7 +311,7 @@ export function integrateLog( .times(x) .div(Decimal.ln(unrefFormulaSource(rhs))); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -319,14 +319,14 @@ export function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value); return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertLog2(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow(2, value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateLog2( @@ -338,21 +338,21 @@ export function integrateLog2( const x = lhs.evaluateIntegral(variable, stack); return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1))); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertLn(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.exp(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateLn( @@ -364,14 +364,14 @@ export function integrateLn( const x = lhs.evaluateIntegral(variable, stack); return Decimal.ln(x).sub(1).times(x); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1))); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -380,7 +380,7 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula } else if (hasVariable(rhs)) { return rhs.invert(Decimal.ln(value).div(Decimal.ln(unrefFormulaSource(lhs)))); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integratePow( @@ -398,7 +398,7 @@ export function integratePow( const b = unrefFormulaSource(lhs); return Decimal.pow(b, x).div(Decimal.ln(b)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -409,14 +409,14 @@ export function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs const denominator = Decimal.ln(unrefFormulaSource(lhs)); return rhs.invert(Decimal.times(denominator, value).ln().div(denominator)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertPow10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, 10)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integratePow10( @@ -428,7 +428,7 @@ export function integratePow10( const x = lhs.evaluateIntegral(variable, stack); return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) { @@ -437,7 +437,7 @@ export function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) { Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp() ); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -446,7 +446,7 @@ export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: For } else if (hasVariable(rhs)) { return rhs.invert(Decimal.root(unrefFormulaSource(lhs), value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integratePowBase( @@ -464,7 +464,7 @@ export function integratePowBase( const denominator = Decimal.add(unrefFormulaSource(lhs), 1); return Decimal.pow(x, denominator).div(denominator); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegratePowBase( @@ -479,7 +479,7 @@ export function invertIntegratePowBase( const b = unrefFormulaSource(lhs); return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -488,7 +488,7 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul } else if (hasVariable(rhs)) { return rhs.invert(Decimal.ln(unrefFormulaSource(lhs)).div(Decimal.ln(value))); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateRoot( @@ -502,7 +502,7 @@ export function integrateRoot( const a = unrefFormulaSource(rhs); return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -515,14 +515,14 @@ export function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rh .pow(Decimal.div(b, Decimal.add(b, 1))) ); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertExp(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.ln(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateExp( @@ -534,7 +534,7 @@ export function integrateExp( const x = lhs.evaluateIntegral(variable, stack); return Decimal.exp(x); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function tetrate( @@ -556,7 +556,7 @@ export function invertTetrate( return base.invert(Decimal.ssqrt(value)); } // Other params can't be inverted ATM - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function iteratedexp( @@ -584,7 +584,7 @@ export function invertIteratedExp( ); } // Other params can't be inverted ATM - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function iteratedLog( @@ -608,7 +608,7 @@ export function invertSlog(value: DecimalSource, lhs: FormulaSource, rhs: Formul ); } // Other params can't be inverted ATM - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function layeradd(value: DecimalSource, diff: DecimalSource, base: DecimalSource) { @@ -631,21 +631,21 @@ export function invertLayeradd( ); } // Other params can't be inverted ATM - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertLambertw(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow(Math.E, value).times(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function invertSsqrt(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.tetrate(value, 2)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function pentate(value: DecimalSource, height: DecimalSource, payload: DecimalSource) { @@ -657,7 +657,7 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.asin(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateSin( @@ -669,14 +669,14 @@ export function integrateSin( const x = lhs.evaluateIntegral(variable, stack); return Decimal.cos(x).neg(); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertCos(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.acos(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateCos( @@ -688,14 +688,14 @@ export function integrateCos( const x = lhs.evaluateIntegral(variable, stack); return Decimal.sin(x); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertTan(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.atan(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateTan( @@ -707,14 +707,14 @@ export function integrateTan( const x = lhs.evaluateIntegral(variable, stack); return Decimal.cos(x).ln().neg(); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertAsin(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.sin(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateAsin( @@ -728,14 +728,14 @@ export function integrateAsin( .times(x) .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertAcos(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.cos(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateAcos( @@ -749,14 +749,14 @@ export function integrateAcos( .times(x) .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertAtan(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.tan(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateAtan( @@ -770,14 +770,14 @@ export function integrateAtan( .times(x) .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertSinh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.asinh(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateSinh( @@ -789,14 +789,14 @@ export function integrateSinh( const x = lhs.evaluateIntegral(variable, stack); return Decimal.cosh(x); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertCosh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.acosh(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateCosh( @@ -808,14 +808,14 @@ export function integrateCosh( const x = lhs.evaluateIntegral(variable, stack); return Decimal.sinh(x); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertTanh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.atanh(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateTanh( @@ -827,14 +827,14 @@ export function integrateTanh( const x = lhs.evaluateIntegral(variable, stack); return Decimal.cosh(x).ln(); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertAsinh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.sinh(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateAsinh( @@ -846,14 +846,14 @@ export function integrateAsinh( const x = lhs.evaluateIntegral(variable, stack); return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt()); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertAcosh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.cosh(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateAcosh( @@ -867,14 +867,14 @@ export function integrateAcosh( .times(x) .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt())); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function invertAtanh(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.tanh(value)); } - throw "Could not invert due to no input being a variable"; + throw new Error("Could not invert due to no input being a variable"); } export function integrateAtanh( @@ -888,7 +888,7 @@ export function integrateAtanh( .times(x) .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2)); } - throw "Could not integrate due to no input being a variable"; + throw new Error("Could not integrate due to no input being a variable"); } export function createPassthroughBinaryFormula( diff --git a/src/game/persistence.ts b/src/game/persistence.ts index bf3afa9..b56f52f 100644 --- a/src/game/persistence.ts +++ b/src/game/persistence.ts @@ -115,7 +115,7 @@ function checkNaNAndWrite(persistent: Persistent, value: T) persistent[SaveDataPath]?.join("."), persistent ); - throw "Attempted to set NaN value. See above for details"; + throw new Error("Attempted to set NaN value. See above for details"); } persistent[PersistentState].value = value; } diff --git a/src/lib/break_eternity.ts b/src/lib/break_eternity.ts index 8d39f22..ec0203b 100644 --- a/src/lib/break_eternity.ts +++ b/src/lib/break_eternity.ts @@ -2788,7 +2788,7 @@ export default class Decimal { return FC_NN(this.sign, this.layer - 1, this.mag); } - throw "Unhandled behavior in lambertw()"; + throw new Error("Unhandled behavior in lambertw()"); } //The super square-root function - what number, tetrated to height 2, equals this? diff --git a/src/main.ts b/src/main.ts index 74893b2..38c9ac3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,7 +26,9 @@ declare global { document.title = projInfo.title; window.projInfo = projInfo; if (projInfo.id === "") { - throw "Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json"; + throw new Error( + "Project ID is empty! Please select a unique ID for this project in /src/data/projInfo.json" + ); } requestAnimationFrame(async () => { From d7e26583045bbb2dcac71956667543c793ed9672 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Thu, 23 Mar 2023 20:59:41 -0500 Subject: [PATCH 13/36] Fix some tests --- src/game/formulas/formulas.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 30a2b20..d6eabcd 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -71,8 +71,11 @@ export default class Formula { this.inputs = readonlyProperties.inputs; this.internalHasVariable = readonlyProperties.internalHasVariable; this.innermostVariable = readonlyProperties.innermostVariable; + this.internalEvaluate = readonlyProperties.internalEvaluate; + this.internalInvert = readonlyProperties.internalInvert; this.internalIntegrate = readonlyProperties.internalIntegrate; this.internalIntegrateInner = readonlyProperties.internalIntegrateInner; + this.internalInvertIntegral = readonlyProperties.internalInvertIntegral; this.applySubstitution = readonlyProperties.applySubstitution; } @@ -135,13 +138,13 @@ export default class Formula { return { inputs, internalEvaluate: evaluate, + internalInvert, internalIntegrate: integrate, internalIntegrateInner: integrateInner, + internalInvertIntegral, applySubstitution, innermostVariable, - internalHasVariable, - internalInvert, - internalInvertIntegral + internalHasVariable }; } From a91efffd5c1bc37513f88bb5b55344b2f5eab6cb Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sat, 1 Apr 2023 15:55:17 -0500 Subject: [PATCH 14/36] WIP integral rework --- src/game/formulas/formulas.ts | 268 ++++++++++----------- src/game/formulas/operations.ts | 399 +++++++++++--------------------- src/game/formulas/types.d.ts | 17 +- tests/game/formulas.test.ts | 30 ++- 4 files changed, 283 insertions(+), 431 deletions(-) diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index d6eabcd..305ad5b 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -1,7 +1,7 @@ import { Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; -import { computed, ComputedRef, ref, unref } from "vue"; +import { computed, ComputedRef, Ref, ref, unref } from "vue"; import type { EvaluateFunction, FormulaOptions, @@ -15,7 +15,6 @@ import type { InvertFunction, InvertibleFormula, InvertibleIntegralFormula, - InvertIntegralFunction, SubstitutionFunction, SubstitutionStack } from "./types"; @@ -29,8 +28,8 @@ export function unrefFormulaSource(value: FormulaSource, variable?: DecimalSourc return value instanceof Formula ? value.evaluate(variable) : unref(value); } -function integrateVariable(variable: DecimalSource) { - return Decimal.pow(variable, 2).div(2); +function integrateVariable(this: GenericFormula) { + return Formula.pow(this, 2).div(2); } function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) { @@ -54,11 +53,12 @@ export default class Formula { private readonly internalIntegrate: IntegrateFunction | undefined; private readonly internalIntegrateInner: IntegrateFunction | undefined; private readonly applySubstitution: SubstitutionFunction | undefined; - private readonly internalInvertIntegral: InvertIntegralFunction | undefined; private readonly internalHasVariable: boolean; public readonly innermostVariable: ProcessedComputable | undefined; + private integralFormula: GenericFormula | undefined; + constructor(options: FormulaOptions) { let readonlyProperties; if ("variable" in options) { @@ -75,7 +75,6 @@ export default class Formula { this.internalInvert = readonlyProperties.internalInvert; this.internalIntegrate = readonlyProperties.internalIntegrate; this.internalIntegrateInner = readonlyProperties.internalIntegrateInner; - this.internalInvertIntegral = readonlyProperties.internalInvertIntegral; this.applySubstitution = readonlyProperties.applySubstitution; } @@ -112,10 +111,9 @@ export default class Formula { integrate, integrateInner, applySubstitution, - invertIntegral, hasVariable } = options; - if (invert == null && invertIntegral == null && hasVariable) { + if (invert == null && hasVariable) { throw new Error( "A formula cannot be marked as having a variable if it is not invertible" ); @@ -132,8 +130,6 @@ export default class Formula { numVariables === 1 || (numVariables === 0 && hasVariable === true); const innermostVariable = internalHasVariable ? variable?.innermostVariable : undefined; const internalInvert = internalHasVariable && variable?.isInvertible() ? invert : undefined; - const internalInvertIntegral = - internalHasVariable && variable?.isIntegralInvertible() ? invertIntegral : undefined; return { inputs, @@ -141,13 +137,19 @@ export default class Formula { internalInvert, internalIntegrate: integrate, internalIntegrateInner: integrateInner, - internalInvertIntegral, applySubstitution, innermostVariable, internalHasVariable }; } + private calculateConstantOfIntegration() { + // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase + const integral = this.getIntegralFormula().evaluate(1); + const actualCost = this.evaluate(0); + return Decimal.sub(actualCost, integral); + } + /** Type predicate that this formula can be inverted. */ isInvertible(): this is InvertibleFormula { return ( @@ -163,10 +165,10 @@ export default class Formula { /** Type predicate that this formula has an integral function that can be inverted. */ isIntegralInvertible(): this is InvertibleIntegralFormula { - return ( - this.internalHasVariable && - (this.internalInvertIntegral != null || this.internalEvaluate == null) - ); + if (!this.isIntegrable()) { + return false; + } + return this.getIntegralFormula().isInvertible(); } /** Whether or not this formula has a singular variable inside it, which can be accessed via {@link innermostVariable}. */ @@ -208,71 +210,101 @@ export default class Formula { /** * Evaluate the result of the indefinite integral (sans the constant of integration). Only works if there's a single variable and the formula is integrable. The formula can only have one "complex" operation (anything besides +,-,*,/). * @param variable Optionally override the value of the variable while evaluating - * @param stack The list of callbacks to run to handle simple operations inside the complex operation. Used in nested formulas * @see {@link isIntegrable} */ - evaluateIntegral(variable?: DecimalSource, stack?: SubstitutionStack): DecimalSource { + evaluateIntegral(variable?: DecimalSource): DecimalSource { + if (!this.isIntegrable()) { + throw new Error("Cannot evaluate integral of formula without integral"); + } + return Decimal.add( + this.getIntegralFormula().evaluate(variable), + this.calculateConstantOfIntegration() + ); + } + + /** + * Given the potential result of the formula's integral (and the constant of integration), calculate what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula's integral is invertible. + * @param value The result of the integral. + * @see {@link isIntegralInvertible} + */ + invertIntegral(value: DecimalSource): DecimalSource { + if (this.integralFormula?.isInvertible()) { + throw new Error("Cannot invert integral of formula without invertible integral"); + } + return this.getIntegralFormula().invert(value); + } + + /** + * Get a formula that will evaluate to the integral of this formula. May also be invertible. + * @param variable The variable that will be used to evaluate this integral at a given x value + * @param stack For nested formulas, a stack of operations that occur outside the complex operation + */ + getIntegralFormula( + variable?: ProcessedComputable, + stack?: SubstitutionStack + ): GenericFormula { + if (variable == null && this.integralFormula != null) { + return this.integralFormula; + } + let formula; + const variablePresent = variable != null; + if (variable == null) { + variable = this.innermostVariable; + if (variable == null) { + throw new Error("Cannot integrate formula without variable"); + } + } if (stack == null) { // "Outer" part of the formula if (this.applySubstitution == null) { // We're the complex operation of this formula stack = []; if (this.internalIntegrate == null) { - throw new Error("Cannot integrate formula with non-existent operation"); + throw new Error("Cannot integrate formula with non-integrable operation"); } let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs); stack.forEach(func => (value = func(value))); - return value; + formula = value; } else { // Continue digging into the formula if (this.internalIntegrate) { - return this.internalIntegrate.call(this, variable, undefined, ...this.inputs); + formula = this.internalIntegrate.call( + this, + variable, + undefined, + ...this.inputs + ); } else if (this.inputs.length === 1 && this.internalHasVariable) { - return integrateVariable(variable ?? unrefFormulaSource(this.inputs[0])); + // eslint-disable-next-line @typescript-eslint/no-this-alias + formula = this; + } else { + throw new Error("Cannot integrate formula without variable"); } - throw new Error("Cannot integrate formula without variable"); } } else { // "Inner" part of the formula if (this.applySubstitution == null) { throw new Error("Cannot have two complex operations in an integrable formula"); } - stack.push((variable: DecimalSource) => + stack.push((variable: GenericFormula) => // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.applySubstitution!.call(this, variable, ...this.inputs) ); if (this.internalIntegrateInner) { - return this.internalIntegrateInner.call(this, variable, stack, ...this.inputs); + formula = this.internalIntegrateInner.call(this, variable, stack, ...this.inputs); } else if (this.internalIntegrate) { - return this.internalIntegrate.call(this, variable, stack, ...this.inputs); + formula = this.internalIntegrate.call(this, variable, stack, ...this.inputs); } else if (this.inputs.length === 1 && this.internalHasVariable) { - return variable ?? unrefFormulaSource(this.inputs[0]); + // eslint-disable-next-line @typescript-eslint/no-this-alias + formula = this; + } else { + throw new Error("Cannot integrate formula without variable"); } - throw new Error("Cannot integrate formula without variable"); } - } - - calculateConstantOfIntegration() { - // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase - const integral = this.evaluateIntegral(1); - const actualCost = this.evaluate(0); - return Decimal.sub(actualCost, integral); - } - - /** - * Given the potential result of the formula's integral (sand the constant of integration), calculate what value the variable inside the formula would have to be for that result to occur. Only works if there's a single variable and if the formula's integral is invertible. - * @param value The result of the integral. - * @see {@link isIntegralInvertible} - */ - invertIntegral(value: DecimalSource): DecimalSource { - // This is nearly completely non-functional - // Proper nesting will require somehow using integration by substitution or integration by parts - if (this.internalInvertIntegral) { - return this.internalInvertIntegral.call(this, value, ...this.inputs); - } else if (this.inputs.length === 1 && this.internalHasVariable) { - return value; + if (!variablePresent) { + this.integralFormula = formula; } - throw new Error("Cannot invert integral of formula without invertible integral"); + return formula; } /** @@ -292,7 +324,6 @@ export default class Formula { this.internalEvaluate === other.internalEvaluate && this.internalInvert === other.internalInvert && this.internalIntegrate === other.internalIntegrate && - this.internalInvertIntegral === other.internalInvertIntegral && this.internalHasVariable === other.internalHasVariable ); } @@ -415,7 +446,7 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.abs }); } - public static neg(value: T): Omit; + public static neg(value: T): T; public static neg(value: FormulaSource): GenericFormula; public static neg(value: FormulaSource) { return new Formula({ @@ -460,8 +491,7 @@ export default class Formula { invert: ops.invertAdd, integrate: ops.integrateAdd, integrateInner: ops.integrateInnerAdd, - applySubstitution: ops.passthrough, - invertIntegral: ops.invertIntegrateAdd + applySubstitution: ops.passthrough }); } public static plus = Formula.add; @@ -476,8 +506,7 @@ export default class Formula { invert: ops.invertSub, integrate: ops.integrateSub, integrateInner: ops.integrateInnerSub, - applySubstitution: ops.passthrough, - invertIntegral: ops.invertIntegrateSub + applySubstitution: ops.passthrough }); } public static subtract = Formula.sub; @@ -492,8 +521,7 @@ export default class Formula { evaluate: Decimal.mul, invert: ops.invertMul, integrate: ops.integrateMul, - applySubstitution: ops.applySubstitutionMul, - invertIntegral: ops.invertIntegrateMul + applySubstitution: ops.applySubstitutionMul }); } public static multiply = Formula.mul; @@ -508,8 +536,7 @@ export default class Formula { evaluate: Decimal.div, invert: ops.invertDiv, integrate: ops.integrateDiv, - applySubstitution: ops.applySubstitutionDiv, - invertIntegral: ops.invertIntegrateDiv + applySubstitution: ops.applySubstitutionDiv }); } public static divide = Formula.div; @@ -523,8 +550,7 @@ export default class Formula { inputs: [value], evaluate: Decimal.recip, invert: ops.invertRecip, - integrate: ops.integrateRecip, - invertIntegral: ops.invertIntegrateRecip + integrate: ops.integrateRecip }); } public static reciprocal = Formula.recip; @@ -537,10 +563,6 @@ export default class Formula { invert: ops.passthrough as ( value: DecimalSource, ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource, - invertIntegral: ops.passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] ) => DecimalSource }); } @@ -559,12 +581,7 @@ export default class Formula { return new Formula({ inputs: [value, min, max], evaluate: Decimal.clamp, - invert: ops.passthrough as InvertFunction< - [FormulaSource, FormulaSource, FormulaSource] - >, - invertIntegral: ops.passthrough as InvertFunction< - [FormulaSource, FormulaSource, FormulaSource] - > + invert: ops.passthrough as InvertFunction<[FormulaSource, FormulaSource, FormulaSource]> }); } @@ -583,8 +600,7 @@ export default class Formula { inputs: [value], evaluate: Decimal.log10, invert: ops.invertLog10, - integrate: ops.integrateLog10, - invertIntegral: ops.invertIntegrateLog10 + integrate: ops.integrateLog10 }); } @@ -596,8 +612,7 @@ export default class Formula { inputs: [value, base], evaluate: Decimal.log, invert: ops.invertLog, - integrate: ops.integrateLog, - invertIntegral: ops.invertIntegrateLog + integrate: ops.integrateLog }); } public static logarithm = Formula.log; @@ -609,8 +624,7 @@ export default class Formula { inputs: [value], evaluate: Decimal.log2, invert: ops.invertLog2, - integrate: ops.integrateLog2, - invertIntegral: ops.invertIntegrateLog2 + integrate: ops.integrateLog2 }); } @@ -621,8 +635,7 @@ export default class Formula { inputs: [value], evaluate: Decimal.ln, invert: ops.invertLn, - integrate: ops.integrateLn, - invertIntegral: ops.invertIntegrateLn + integrate: ops.integrateLn }); } @@ -634,8 +647,7 @@ export default class Formula { inputs: [value, other], evaluate: Decimal.pow, invert: ops.invertPow, - integrate: ops.integratePow, - invertIntegral: ops.invertIntegratePow + integrate: ops.integratePow }); } @@ -646,8 +658,7 @@ export default class Formula { inputs: [value], evaluate: Decimal.pow10, invert: ops.invertPow10, - integrate: ops.integratePow10, - invertIntegral: ops.invertIntegratePow10 + integrate: ops.integratePow10 }); } @@ -659,8 +670,7 @@ export default class Formula { inputs: [value, other], evaluate: Decimal.pow_base, invert: ops.invertPowBase, - integrate: ops.integratePowBase, - invertIntegral: ops.invertIntegratePowBase + integrate: ops.integratePowBase }); } @@ -672,8 +682,7 @@ export default class Formula { inputs: [value, other], evaluate: Decimal.root, invert: ops.invertRoot, - integrate: ops.integrateRoot, - invertIntegral: ops.invertIntegrateRoot + integrate: ops.integrateRoot }); } @@ -689,7 +698,7 @@ export default class Formula { return new Formula({ inputs: [value], evaluate: Decimal.lngamma }); } - public static exp(value: T): Omit; + public static exp(value: T): T; public static exp(value: FormulaSource): GenericFormula; public static exp(value: FormulaSource) { return new Formula({ @@ -728,7 +737,7 @@ export default class Formula { value: T, height?: FormulaSource, payload?: FormulaSource - ): Omit; + ): Omit; public static tetrate( value: FormulaSource, height?: FormulaSource, @@ -750,7 +759,7 @@ export default class Formula { value: T, height?: FormulaSource, payload?: FormulaSource - ): Omit; + ): Omit; public static iteratedexp( value: FormulaSource, height?: FormulaSource, @@ -779,7 +788,7 @@ export default class Formula { public static slog( value: T, base?: FormulaSource - ): Omit; + ): Omit; public static slog(value: FormulaSource, base?: FormulaSource): GenericFormula; public static slog(value: FormulaSource, base: FormulaSource = 10) { return new Formula({ inputs: [value, base], evaluate: ops.slog, invert: ops.invertSlog }); @@ -793,7 +802,7 @@ export default class Formula { value: T, diff: FormulaSource, base?: FormulaSource - ): Omit; + ): Omit; public static layeradd( value: FormulaSource, diff: FormulaSource, @@ -807,9 +816,7 @@ export default class Formula { }); } - public static lambertw( - value: T - ): Omit; + public static lambertw(value: T): Omit; public static lambertw(value: FormulaSource): GenericFormula; public static lambertw(value: FormulaSource) { return new Formula({ @@ -819,9 +826,7 @@ export default class Formula { }); } - public static ssqrt( - value: T - ): Omit; + public static ssqrt(value: T): Omit; public static ssqrt(value: FormulaSource): GenericFormula; public static ssqrt(value: FormulaSource) { return new Formula({ inputs: [value], evaluate: Decimal.ssqrt, invert: ops.invertSsqrt }); @@ -835,7 +840,7 @@ export default class Formula { return new Formula({ inputs: [value, height, payload], evaluate: ops.pentate }); } - public static sin(value: T): Omit; + public static sin(value: T): T; public static sin(value: FormulaSource): GenericFormula; public static sin(value: FormulaSource) { return new Formula({ @@ -846,7 +851,7 @@ export default class Formula { }); } - public static cos(value: T): Omit; + public static cos(value: T): T; public static cos(value: FormulaSource): GenericFormula; public static cos(value: FormulaSource) { return new Formula({ @@ -857,7 +862,7 @@ export default class Formula { }); } - public static tan(value: T): Omit; + public static tan(value: T): T; public static tan(value: FormulaSource): GenericFormula; public static tan(value: FormulaSource) { return new Formula({ @@ -868,7 +873,7 @@ export default class Formula { }); } - public static asin(value: T): Omit; + public static asin(value: T): T; public static asin(value: FormulaSource): GenericFormula; public static asin(value: FormulaSource) { return new Formula({ @@ -879,7 +884,7 @@ export default class Formula { }); } - public static acos(value: T): Omit; + public static acos(value: T): T; public static acos(value: FormulaSource): GenericFormula; public static acos(value: FormulaSource) { return new Formula({ @@ -890,7 +895,7 @@ export default class Formula { }); } - public static atan(value: T): Omit; + public static atan(value: T): T; public static atan(value: FormulaSource): GenericFormula; public static atan(value: FormulaSource) { return new Formula({ @@ -901,7 +906,7 @@ export default class Formula { }); } - public static sinh(value: T): Omit; + public static sinh(value: T): T; public static sinh(value: FormulaSource): GenericFormula; public static sinh(value: FormulaSource) { return new Formula({ @@ -912,7 +917,7 @@ export default class Formula { }); } - public static cosh(value: T): Omit; + public static cosh(value: T): T; public static cosh(value: FormulaSource): GenericFormula; public static cosh(value: FormulaSource) { return new Formula({ @@ -923,7 +928,7 @@ export default class Formula { }); } - public static tanh(value: T): Omit; + public static tanh(value: T): T; public static tanh(value: FormulaSource): GenericFormula; public static tanh(value: FormulaSource) { return new Formula({ @@ -934,7 +939,7 @@ export default class Formula { }); } - public static asinh(value: T): Omit; + public static asinh(value: T): T; public static asinh(value: FormulaSource): GenericFormula; public static asinh(value: FormulaSource) { return new Formula({ @@ -945,7 +950,7 @@ export default class Formula { }); } - public static acosh(value: T): Omit; + public static acosh(value: T): T; public static acosh(value: FormulaSource): GenericFormula; public static acosh(value: FormulaSource) { return new Formula({ @@ -956,7 +961,7 @@ export default class Formula { }); } - public static atanh(value: T): Omit; + public static atanh(value: T): T; public static atanh(value: FormulaSource): GenericFormula; public static atanh(value: FormulaSource) { return new Formula({ @@ -997,7 +1002,7 @@ export default class Formula { return Formula.abs(this); } - public neg(this: T): Omit; + public neg(this: T): T; public neg(this: GenericFormula): GenericFormula; public neg(this: GenericFormula) { return Formula.neg(this); @@ -1170,7 +1175,7 @@ export default class Formula { return Formula.lngamma(this); } - public exp(this: T): Omit; + public exp(this: T): T; public exp(this: FormulaSource): GenericFormula; public exp(this: FormulaSource) { return Formula.exp(this); @@ -1203,7 +1208,7 @@ export default class Formula { this: T, height?: FormulaSource, payload?: FormulaSource - ): Omit; + ): Omit; public tetrate( this: FormulaSource, height?: FormulaSource, @@ -1221,7 +1226,7 @@ export default class Formula { this: T, height?: FormulaSource, payload?: FormulaSource - ): Omit; + ): Omit; public iteratedexp( this: FormulaSource, height?: FormulaSource, @@ -1239,10 +1244,7 @@ export default class Formula { return Formula.iteratedlog(this, base, times); } - public slog( - this: T, - base?: FormulaSource - ): Omit; + public slog(this: T, base?: FormulaSource): Omit; public slog(this: FormulaSource, base?: FormulaSource): GenericFormula; public slog(this: FormulaSource, base: FormulaSource = 10) { return Formula.slog(this, base); @@ -1256,19 +1258,19 @@ export default class Formula { this: T, diff: FormulaSource, base?: FormulaSource - ): Omit; + ): Omit; public layeradd(this: FormulaSource, diff: FormulaSource, base?: FormulaSource): GenericFormula; public layeradd(this: FormulaSource, diff: FormulaSource, base: FormulaSource) { return Formula.layeradd(this, diff, base); } - public lambertw(this: T): Omit; + public lambertw(this: T): Omit; public lambertw(this: FormulaSource): GenericFormula; public lambertw(this: FormulaSource) { return Formula.lambertw(this); } - public ssqrt(this: T): Omit; + public ssqrt(this: T): Omit; public ssqrt(this: FormulaSource): GenericFormula; public ssqrt(this: FormulaSource) { return Formula.ssqrt(this); @@ -1281,73 +1283,73 @@ export default class Formula { return Formula.pentate(this, height, payload); } - public sin(this: T): Omit; + public sin(this: T): T; public sin(this: FormulaSource): GenericFormula; public sin(this: FormulaSource) { return Formula.sin(this); } - public cos(this: T): Omit; + public cos(this: T): T; public cos(this: FormulaSource): GenericFormula; public cos(this: FormulaSource) { return Formula.cos(this); } - public tan(this: T): Omit; + public tan(this: T): T; public tan(this: FormulaSource): GenericFormula; public tan(this: FormulaSource) { return Formula.tan(this); } - public asin(this: T): Omit; + public asin(this: T): T; public asin(this: FormulaSource): GenericFormula; public asin(this: FormulaSource) { return Formula.asin(this); } - public acos(this: T): Omit; + public acos(this: T): T; public acos(this: FormulaSource): GenericFormula; public acos(this: FormulaSource) { return Formula.acos(this); } - public atan(this: T): Omit; + public atan(this: T): T; public atan(this: FormulaSource): GenericFormula; public atan(this: FormulaSource) { return Formula.atan(this); } - public sinh(this: T): Omit; + public sinh(this: T): T; public sinh(this: FormulaSource): GenericFormula; public sinh(this: FormulaSource) { return Formula.sinh(this); } - public cosh(this: T): Omit; + public cosh(this: T): T; public cosh(this: FormulaSource): GenericFormula; public cosh(this: FormulaSource) { return Formula.cosh(this); } - public tanh(this: T): Omit; + public tanh(this: T): T; public tanh(this: FormulaSource): GenericFormula; public tanh(this: FormulaSource) { return Formula.tanh(this); } - public asinh(this: T): Omit; + public asinh(this: T): T; public asinh(this: FormulaSource): GenericFormula; public asinh(this: FormulaSource) { return Formula.asinh(this); } - public acosh(this: T): Omit; + public acosh(this: T): T; public acosh(this: FormulaSource): GenericFormula; public acosh(this: FormulaSource) { return Formula.acosh(this); } - public atanh(this: T): Omit; + public atanh(this: T): T; public atanh(this: FormulaSource): GenericFormula; public atanh(this: FormulaSource) { return Formula.atanh(this); diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts index f4dd1f0..4512f55 100644 --- a/src/game/formulas/operations.ts +++ b/src/game/formulas/operations.ts @@ -1,9 +1,9 @@ import Decimal, { DecimalSource } from "util/bignum"; -import { unref } from "vue"; +import { Ref } from "vue"; import Formula, { hasVariable, unrefFormulaSource } from "./formulas"; -import { FormulaSource, InvertFunction, SubstitutionStack } from "./types"; +import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types"; -export function passthrough(value: DecimalSource) { +export function passthrough(value: T): T { return value; } @@ -15,18 +15,18 @@ export function invertNeg(value: DecimalSource, lhs: FormulaSource) { } export function integrateNeg( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - return Decimal.neg(lhs.evaluateIntegral(variable, stack)); + return Formula.neg(lhs.getIntegralFormula(variable, stack)); } throw new Error("Could not integrate due to no input being a variable"); } -export function applySubstitutionNeg(value: DecimalSource) { - return Decimal.neg(value); +export function applySubstitutionNeg(value: GenericFormula) { + return Formula.neg(value); } export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { @@ -39,54 +39,37 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula } export function integrateAdd( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.times( - unrefFormulaSource(rhs), - variable ?? unref(lhs.innermostVariable) ?? 0 - ).add(x); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0).add(x); } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.times( - unrefFormulaSource(lhs), - variable ?? unref(rhs.innermostVariable) ?? 0 - ).add(x); + const x = rhs.getIntegralFormula(variable, stack); + return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).add(x); } throw new Error("Could not integrate due to no input being a variable"); } export function integrateInnerAdd( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.add(x, unrefFormulaSource(rhs)); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.add(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.add(x, unrefFormulaSource(lhs)); + const x = rhs.getIntegralFormula(variable, stack); + return Formula.add(x, lhs); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegrateAdd(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sub(b)); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.add(value, unrefFormulaSource(rhs))); @@ -97,54 +80,37 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula } export function integrateSub( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.sub( - x, - Decimal.times(unrefFormulaSource(rhs), variable ?? unref(lhs.innermostVariable) ?? 0) - ); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.sub(x, Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0)); } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.times( - unrefFormulaSource(lhs), - variable ?? unref(rhs.innermostVariable) ?? 0 - ).sub(x); + const x = rhs.getIntegralFormula(variable, stack); + return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).sub(x); } throw new Error("Could not integrate due to no input being a variable"); } export function integrateInnerSub( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.sub(x, unrefFormulaSource(rhs)); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.sub(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.sub(x, unrefFormulaSource(lhs)); + const x = rhs.getIntegralFormula(variable, stack); + return Formula.sub(x, lhs); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegrateSub(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.pow(b, 2).add(Decimal.times(value, 2)).sqrt().sub(b)); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.div(value, unrefFormulaSource(rhs))); @@ -155,41 +121,34 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula } export function integrateMul( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.times(x, unrefFormulaSource(rhs)); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.times(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.times(x, unrefFormulaSource(lhs)); + const x = rhs.getIntegralFormula(variable, stack); + return Formula.times(x, lhs); } throw new Error("Could not integrate due to no input being a variable"); } -export function applySubstitutionMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { +export function applySubstitutionMul( + value: GenericFormula, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.div(value, unrefFormulaSource(rhs)); + return Formula.div(value, rhs); } else if (hasVariable(rhs)) { - return Decimal.div(value, unrefFormulaSource(lhs)); + return Formula.div(value, lhs); } throw new Error("Could not apply substitution due to no input being a variable"); } -export function invertIntegrateMul(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).div(Decimal.sqrt(b))); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.mul(value, unrefFormulaSource(rhs))); @@ -200,41 +159,34 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula } export function integrateDiv( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.div(x, unrefFormulaSource(rhs)); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.div(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - return Decimal.div(unrefFormulaSource(lhs), x); + const x = rhs.getIntegralFormula(variable, stack); + return Formula.div(lhs, x); } throw new Error("Could not integrate due to no input being a variable"); } -export function applySubstitutionDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { +export function applySubstitutionDiv( + value: GenericFormula, + lhs: FormulaSource, + rhs: FormulaSource +) { if (hasVariable(lhs)) { - return Decimal.mul(value, unrefFormulaSource(rhs)); + return Formula.mul(value, rhs); } else if (hasVariable(rhs)) { - return Decimal.mul(value, unrefFormulaSource(lhs)); + return Formula.mul(value, lhs); } throw new Error("Could not apply substitution due to no input being a variable"); } -export function invertIntegrateDiv(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.sqrt(value).times(Decimal.sqrt(2)).times(Decimal.sqrt(b))); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertRecip(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.recip(value)); @@ -243,24 +195,17 @@ export function invertRecip(value: DecimalSource, lhs: FormulaSource) { } export function integrateRecip( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.ln(x); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegrateRecip(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.exp(value)); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertLog10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow10(value)); @@ -269,26 +214,17 @@ export function invertLog10(value: DecimalSource, lhs: FormulaSource) { } export function integrateLog10( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.ln(x).sub(1).times(x).div(Formula.ln(10)); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegrateLog10(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert( - Decimal.exp(Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1)) - ); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow(unrefFormulaSource(rhs), value)); @@ -299,29 +235,18 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula } export function integrateLog( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x) - .sub(1) - .times(x) - .div(Decimal.ln(unrefFormulaSource(rhs))); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.ln(x).sub(1).times(x).div(Formula.ln(rhs)); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegrateLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value); - return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertLog2(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.pow(2, value)); @@ -330,24 +255,17 @@ export function invertLog2(value: DecimalSource, lhs: FormulaSource) { } export function integrateLog2( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(2)); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.ln(x).sub(1).times(x).div(Formula.ln(2)); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegrateLog2(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.exp(Decimal.ln(2).times(value).div(Math.E).lambertw().add(1))); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertLn(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.exp(value)); @@ -356,24 +274,17 @@ export function invertLn(value: DecimalSource, lhs: FormulaSource) { } export function integrateLn( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x).sub(1).times(x); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.ln(x).sub(1).times(x); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegrateLn(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert(Decimal.exp(Decimal.div(value, Math.E).lambertw().add(1))); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, unrefFormulaSource(rhs))); @@ -384,34 +295,22 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula } export function integratePow( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - const pow = Decimal.add(unrefFormulaSource(rhs), 1); - return Decimal.pow(x, pow).div(pow); + const x = lhs.getIntegralFormula(variable, stack); + const pow = Formula.add(rhs, 1); + return Formula.pow(x, pow).div(pow); } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - const b = unrefFormulaSource(lhs); - return Decimal.pow(b, x).div(Decimal.ln(b)); + const x = rhs.getIntegralFormula(variable, stack); + return Formula.pow(lhs, x).div(Formula.ln(lhs)); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegratePow(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.negate(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); - } else if (hasVariable(rhs)) { - const denominator = Decimal.ln(unrefFormulaSource(lhs)); - return rhs.invert(Decimal.times(denominator, value).ln().div(denominator)); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertPow10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, 10)); @@ -420,26 +319,17 @@ export function invertPow10(value: DecimalSource, lhs: FormulaSource) { } export function integratePow10( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.ln(x).sub(1).times(x).div(Decimal.ln(10)); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.ln(x).sub(1).times(x).div(Decimal.ln(10)); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegratePow10(value: DecimalSource, lhs: FormulaSource) { - if (hasVariable(lhs)) { - return lhs.invert( - Decimal.ln(2).add(Decimal.ln(5)).times(value).div(Math.E).lambertw().add(1).exp() - ); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.ln(value).div(unrefFormulaSource(rhs))); @@ -450,38 +340,22 @@ export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: For } export function integratePowBase( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - const b = unrefFormulaSource(rhs); - return Decimal.pow(b, x).div(Decimal.ln(b)); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.pow(rhs, x).div(Formula.ln(rhs)); } else if (hasVariable(rhs)) { - const x = rhs.evaluateIntegral(variable, stack); - const denominator = Decimal.add(unrefFormulaSource(lhs), 1); - return Decimal.pow(x, denominator).div(denominator); + const x = rhs.getIntegralFormula(variable, stack); + const denominator = Formula.add(lhs, 1); + return Formula.pow(x, denominator).div(denominator); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegratePowBase( - value: DecimalSource, - lhs: FormulaSource, - rhs: FormulaSource -) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert(Decimal.ln(b).times(value).ln().div(Decimal.ln(b))); - } else if (hasVariable(rhs)) { - const b = unrefFormulaSource(lhs); - return rhs.invert(Decimal.neg(b).sub(1).negate().times(value).root(Decimal.add(b, 1))); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.root(value, Decimal.recip(unrefFormulaSource(rhs)))); @@ -492,32 +366,18 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul } export function integrateRoot( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - const a = unrefFormulaSource(rhs); - return Decimal.pow(x, Decimal.recip(a).add(1)).times(a).div(Decimal.add(a, 1)); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.pow(x, Formula.recip(rhs).add(1)).times(rhs).div(Formula.add(rhs, 1)); } throw new Error("Could not integrate due to no input being a variable"); } -export function invertIntegrateRoot(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { - if (hasVariable(lhs)) { - const b = unrefFormulaSource(rhs); - return lhs.invert( - Decimal.add(b, 1) - .times(value) - .div(b) - .pow(Decimal.div(b, Decimal.add(b, 1))) - ); - } - throw new Error("Could not invert due to no input being a variable"); -} - export function invertExp(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { return lhs.invert(Decimal.ln(value)); @@ -526,13 +386,13 @@ export function invertExp(value: DecimalSource, lhs: FormulaSource) { } export function integrateExp( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.exp(x); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.exp(x); } throw new Error("Could not integrate due to no input being a variable"); } @@ -661,13 +521,13 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) { } export function integrateSin( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.cos(x).neg(); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.cos(x).neg(); } throw new Error("Could not integrate due to no input being a variable"); } @@ -680,13 +540,13 @@ export function invertCos(value: DecimalSource, lhs: FormulaSource) { } export function integrateCos( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.sin(x); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.sin(x); } throw new Error("Could not integrate due to no input being a variable"); } @@ -699,13 +559,13 @@ export function invertTan(value: DecimalSource, lhs: FormulaSource) { } export function integrateTan( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.cos(x).ln().neg(); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.cos(x).ln().neg(); } throw new Error("Could not integrate due to no input being a variable"); } @@ -718,15 +578,15 @@ export function invertAsin(value: DecimalSource, lhs: FormulaSource) { } export function integrateAsin( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.asin(x) + const x = lhs.getIntegralFormula(variable, stack); + return Formula.asin(x) .times(x) - .add(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); + .add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2)))); } throw new Error("Could not integrate due to no input being a variable"); } @@ -739,15 +599,15 @@ export function invertAcos(value: DecimalSource, lhs: FormulaSource) { } export function integrateAcos( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.acos(x) + const x = lhs.getIntegralFormula(variable, stack); + return Formula.acos(x) .times(x) - .sub(Decimal.sqrt(Decimal.sub(1, Decimal.pow(x, 2)))); + .sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2)))); } throw new Error("Could not integrate due to no input being a variable"); } @@ -760,15 +620,15 @@ export function invertAtan(value: DecimalSource, lhs: FormulaSource) { } export function integrateAtan( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.atan(x) + const x = lhs.getIntegralFormula(variable, stack); + return Formula.atan(x) .times(x) - .sub(Decimal.ln(Decimal.pow(x, 2).add(1)).div(2)); + .sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2)); } throw new Error("Could not integrate due to no input being a variable"); } @@ -781,13 +641,13 @@ export function invertSinh(value: DecimalSource, lhs: FormulaSource) { } export function integrateSinh( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.cosh(x); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.cosh(x); } throw new Error("Could not integrate due to no input being a variable"); } @@ -800,13 +660,13 @@ export function invertCosh(value: DecimalSource, lhs: FormulaSource) { } export function integrateCosh( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.sinh(x); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.sinh(x); } throw new Error("Could not integrate due to no input being a variable"); } @@ -819,13 +679,13 @@ export function invertTanh(value: DecimalSource, lhs: FormulaSource) { } export function integrateTanh( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.cosh(x).ln(); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.cosh(x).ln(); } throw new Error("Could not integrate due to no input being a variable"); } @@ -838,13 +698,13 @@ export function invertAsinh(value: DecimalSource, lhs: FormulaSource) { } export function integrateAsinh( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.asinh(x).times(x).sub(Decimal.pow(x, 2).add(1).sqrt()); + const x = lhs.getIntegralFormula(variable, stack); + return Formula.asinh(x).times(x).sub(Formula.pow(x, 2).add(1).sqrt()); } throw new Error("Could not integrate due to no input being a variable"); } @@ -857,15 +717,15 @@ export function invertAcosh(value: DecimalSource, lhs: FormulaSource) { } export function integrateAcosh( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.acosh(x) + const x = lhs.getIntegralFormula(variable, stack); + return Formula.acosh(x) .times(x) - .sub(Decimal.add(x, 1).sqrt().times(Decimal.sub(x, 1).sqrt())); + .sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt())); } throw new Error("Could not integrate due to no input being a variable"); } @@ -878,15 +738,15 @@ export function invertAtanh(value: DecimalSource, lhs: FormulaSource) { } export function integrateAtanh( - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack, lhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.evaluateIntegral(variable, stack); - return Decimal.atanh(x) + const x = lhs.getIntegralFormula(variable, stack); + return Formula.atanh(x) .times(x) - .add(Decimal.sub(1, Decimal.pow(x, 2)).ln().div(2)); + .add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2)); } throw new Error("Could not integrate due to no input being a variable"); } @@ -898,7 +758,6 @@ export function createPassthroughBinaryFormula( new Formula({ inputs: [value, other], evaluate: operation, - invert: passthrough as InvertFunction<[FormulaSource, FormulaSource]>, - invertIntegral: passthrough as InvertFunction<[FormulaSource, FormulaSource]> + invert: passthrough as InvertFunction<[FormulaSource, FormulaSource]> }); } diff --git a/src/game/formulas/types.d.ts b/src/game/formulas/types.d.ts index baaf2bb..3d3bc83 100644 --- a/src/game/formulas/types.d.ts +++ b/src/game/formulas/types.d.ts @@ -22,20 +22,15 @@ type EvaluateFunction = ( type InvertFunction = (this: Formula, value: DecimalSource, ...inputs: T) => DecimalSource; type IntegrateFunction = ( this: Formula, - variable: DecimalSource | undefined, + variable: Ref, stack: SubstitutionStack | undefined, ...inputs: T -) => DecimalSource; +) => GenericFormula; type SubstitutionFunction = ( this: Formula, - variable: DecimalSource, + variable: GenericFormula, ...inputs: T -) => DecimalSource; -type InvertIntegralFunction = ( - this: Formula, - value: DecimalSource, - ...inputs: T -) => DecimalSource; +) => GenericFormula; type VariableFormulaOptions = { variable: ProcessedComputable }; type ConstantFormulaOptions = { @@ -48,7 +43,6 @@ type GeneralFormulaOptions = { integrate?: IntegrateFunction; integrateInner?: IntegrateFunction; applySubstitution?: SubstitutionFunction; - invertIntegral?: InvertIntegralFunction; hasVariable?: boolean; }; type FormulaOptions = @@ -63,12 +57,11 @@ type InternalFormulaProperties = { internalInvert?: InvertFunction; internalIntegrate?: IntegrateFunction; internalIntegrateInner?: IntegrateFunction; - internalInvertIntegral?: InvertIntegralFunction; applySubstitution?: SubstitutionFunction; innermostVariable?: ProcessedComputable; }; -type SubstitutionStack = ((value: DecimalSource) => DecimalSource)[] | undefined; +type SubstitutionStack = ((value: GenericFormula) => GenericFormula)[] | undefined; // It's really hard to type mapped tuples, but these classes seem to manage type FormulasToDecimals = { diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 4003051..ce0120b 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -491,8 +491,8 @@ describe("Integrating", () => { constant = Formula.constant(10); }); - test("evaluateIntegral() returns variable's value", () => - expect(variable.evaluate()).compare_tolerance(10)); + test("variable.evaluateIntegral() calculates correctly", () => + expect(variable.evaluateIntegral()).compare_tolerance(Decimal.pow(10, 2).div(2))); test("evaluateIntegral(variable) overrides variable value", () => expect(variable.add(10).evaluateIntegral(20)).compare_tolerance(400)); @@ -569,14 +569,10 @@ describe("Integrating", () => { const actualCost = new Array(10) .fill(null) .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0)); - const calculatedCost = Decimal.add( - formula.evaluateIntegral(), - formula.calculateConstantOfIntegration() - ); // 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() + Decimal.sub(actualCost, formula.evaluateIntegral()).abs().div(actualCost).toNumber() ).toBeLessThan(0.1); }); @@ -594,8 +590,10 @@ describe("Inverting integrals", () => { constant = Formula.constant(10); }); - test("variable.invertIntegral() is pass-through", () => - expect(variable.invertIntegral(20)).compare_tolerance(20)); + test("variable.invertIntegral() calculates correctly", () => + expect(variable.invertIntegral(20)).compare_tolerance( + Decimal.sqrt(20).times(Decimal.sqrt(2)) + )); describe("Invertible Integral functions marked as such", () => { function checkFormula(formula: GenericFormula) { @@ -670,7 +668,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(7000000)).compare_tolerance(10); + expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10); }); test("Inverting integral of nested complex formulas", () => { @@ -946,7 +944,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [], evaluate: () => 10, - integrate: () => 20 + integrate: variable => variable }).evaluateIntegral() ).compare_tolerance(20)); test("One input integrates correctly", () => @@ -954,7 +952,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable], evaluate: () => 10, - integrate: (val, stack, v1) => val ?? 20 + integrate: (variable, stack, v1) => Formula.add(variable, v1) }).evaluateIntegral() ).compare_tolerance(20)); test("Two inputs integrates correctly", () => @@ -962,7 +960,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable, 2], evaluate: (v1, v2) => 10, - integrate: (v1, v2) => 3 + integrate: (variable, v1, v2) => variable }).evaluateIntegral() ).compare_tolerance(3)); }); @@ -973,7 +971,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [], evaluate: () => 10, - invertIntegral: () => 1, + integrate: variable => variable, hasVariable: true }).invertIntegral(8) ).toThrow()); @@ -982,7 +980,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable], evaluate: () => 10, - invertIntegral: (val, v1) => 1, + integrate: (variable, stack, v1) => variable, hasVariable: true }).invertIntegral(8) ).compare_tolerance(1)); @@ -991,7 +989,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable, 2], evaluate: (v1, v2) => 10, - invertIntegral: (v1, v2) => 1, + integrate: (variable, v1, v2) => variable, hasVariable: true }).invertIntegral(8) ).compare_tolerance(1)); From 6115b6687dfe325afb746b21fce5044845d1f027 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sat, 1 Apr 2023 23:42:12 -0500 Subject: [PATCH 15/36] Fixing more tests with integral rework --- src/data/projEntry.tsx | 7 +- src/game/formulas/formulas.ts | 162 +++++++-------- src/game/formulas/operations.ts | 340 ++++++++++++++------------------ src/game/formulas/types.d.ts | 4 +- tests/game/formulas.test.ts | 97 ++++----- tests/utils.ts | 6 +- 6 files changed, 264 insertions(+), 352 deletions(-) diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index df86d6a..55e8500 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -4,7 +4,11 @@ import { createResource, trackBest, trackOOMPS, trackTotal } from "features/reso import type { GenericTree } from "features/trees/tree"; import { branchedResetPropagation, createTree } from "features/trees/tree"; import { globalBus } from "game/events"; -import Formula, { calculateCost, calculateMaxAffordable } from "game/formulas/formulas"; +import Formula, { + calculateCost, + calculateMaxAffordable, + findNonInvertible +} from "game/formulas/formulas"; import type { BaseLayer, GenericLayer } from "game/layers"; import { createLayer } from "game/layers"; import type { Player } from "game/player"; @@ -18,6 +22,7 @@ import prestige from "./layers/prestige"; window.Formula = Formula; window.calculateMaxAffordable = calculateMaxAffordable; window.calculateCost = calculateCost; +window.findNonInvertible = findNonInvertible; window.unref = unref; window.ref = ref; window.createResource = createResource; diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 305ad5b..8161903 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -1,7 +1,8 @@ import { Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; -import { computed, ComputedRef, Ref, ref, unref } from "vue"; +import { computed, ComputedRef, ref, unref } from "vue"; +import * as ops from "./operations"; import type { EvaluateFunction, FormulaOptions, @@ -18,7 +19,6 @@ import type { SubstitutionFunction, SubstitutionStack } from "./types"; -import * as ops from "./operations"; export function hasVariable(value: FormulaSource): value is InvertibleFormula { return value instanceof Formula && value.hasVariable(); @@ -32,13 +32,6 @@ function integrateVariable(this: GenericFormula) { return Formula.pow(this, 2).div(2); } -function integrateVariableInner(this: GenericFormula, variable?: DecimalSource) { - if (variable == null && this.innermostVariable == null) { - throw new Error("Cannot integrate non-existent variable"); - } - return variable ?? unref(this.innermostVariable); -} - /** * 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. @@ -53,7 +46,7 @@ export default class Formula { private readonly internalIntegrate: IntegrateFunction | undefined; private readonly internalIntegrateInner: IntegrateFunction | undefined; private readonly applySubstitution: SubstitutionFunction | undefined; - private readonly internalHasVariable: boolean; + private readonly internalVariables: number; public readonly innermostVariable: ProcessedComputable | undefined; @@ -69,7 +62,7 @@ export default class Formula { readonlyProperties = this.setupFormula(options); } this.inputs = readonlyProperties.inputs; - this.internalHasVariable = readonlyProperties.internalHasVariable; + this.internalVariables = readonlyProperties.internalVariables; this.innermostVariable = readonlyProperties.innermostVariable; this.internalEvaluate = readonlyProperties.internalEvaluate; this.internalInvert = readonlyProperties.internalInvert; @@ -85,10 +78,9 @@ export default class Formula { }): InternalFormulaProperties { return { inputs: [variable] as T, - internalHasVariable: true, + internalVariables: 1, innermostVariable: variable, - internalIntegrate: integrateVariable as unknown as IntegrateFunction, - internalIntegrateInner: integrateVariableInner as unknown as IntegrateFunction, + internalIntegrate: integrateVariable, applySubstitution: ops.passthrough as unknown as SubstitutionFunction }; } @@ -99,68 +91,49 @@ export default class Formula { } return { inputs: inputs as T, - internalHasVariable: false + internalVariables: 0 }; } private setupFormula(options: GeneralFormulaOptions): InternalFormulaProperties { - const { - inputs, - evaluate, - invert, - integrate, - integrateInner, - applySubstitution, - hasVariable - } = options; - if (invert == null && hasVariable) { - throw new Error( - "A formula cannot be marked as having a variable if it is not invertible" - ); - } - - const numVariables = inputs.filter( - input => input instanceof Formula && input.hasVariable() - ).length; + const { inputs, evaluate, invert, integrate, integrateInner, applySubstitution } = options; + const numVariables = inputs.reduce( + (acc, input) => acc + (input instanceof Formula ? input.internalVariables : 0), + 0 + ); const variable = inputs.find(input => input instanceof Formula && input.hasVariable()) as | GenericFormula | undefined; - const internalHasVariable = - numVariables === 1 || (numVariables === 0 && hasVariable === true); - const innermostVariable = internalHasVariable ? variable?.innermostVariable : undefined; - const internalInvert = internalHasVariable && variable?.isInvertible() ? invert : undefined; + const innermostVariable = numVariables === 1 ? variable?.innermostVariable : undefined; return { inputs, internalEvaluate: evaluate, - internalInvert, + internalInvert: invert, internalIntegrate: integrate, internalIntegrateInner: integrateInner, applySubstitution, innermostVariable, - internalHasVariable + internalVariables: numVariables }; } private calculateConstantOfIntegration() { - // Calculate C based on the knowledge that at 1 purchase, the total sum would be the cost of that one purchase + // 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 = this.evaluate(0); + const actualCost = Decimal.add(this.evaluate(0), this.evaluate(1)).div(2); return Decimal.sub(actualCost, integral); } /** Type predicate that this formula can be inverted. */ isInvertible(): this is InvertibleFormula { - return ( - this.internalHasVariable && - (this.internalInvert != null || this.internalEvaluate == null) - ); + return this.hasVariable() && (this.internalInvert != null || this.internalEvaluate == null); } /** Type predicate that this formula can be integrated. */ isIntegrable(): this is IntegrableFormula { - return this.internalHasVariable && this.internalIntegrate != null; + return this.hasVariable() && this.internalIntegrate != null; } /** Type predicate that this formula has an integral function that can be inverted. */ @@ -173,7 +146,7 @@ export default class Formula { /** Whether or not this formula has a singular variable inside it, which can be accessed via {@link innermostVariable}. */ hasVariable(): boolean { - return this.internalHasVariable; + return this.internalVariables === 1; } /** @@ -188,7 +161,7 @@ export default class Formula { unrefFormulaSource(input, variable) ) as GuardedFormulasToDecimals) ) ?? - (this.internalHasVariable ? variable : null) ?? + (this.hasVariable() ? variable : null) ?? unrefFormulaSource(this.inputs[0]) ); } @@ -199,9 +172,9 @@ export default class Formula { * @see {@link isInvertible} */ invert(value: DecimalSource): DecimalSource { - if (this.internalInvert) { + if (this.internalInvert && this.hasVariable()) { return this.internalInvert.call(this, value, ...this.inputs); - } else if (this.inputs.length === 1 && this.internalHasVariable) { + } else if (this.inputs.length === 1 && this.hasVariable()) { return value; } throw new Error("Cannot invert non-invertible formula"); @@ -228,7 +201,7 @@ export default class Formula { * @see {@link isIntegralInvertible} */ invertIntegral(value: DecimalSource): DecimalSource { - if (this.integralFormula?.isInvertible()) { + if (!this.isIntegrable() || !this.getIntegralFormula().isInvertible()) { throw new Error("Cannot invert integral of formula without invertible integral"); } return this.getIntegralFormula().invert(value); @@ -236,24 +209,12 @@ export default class Formula { /** * Get a formula that will evaluate to the integral of this formula. May also be invertible. - * @param variable The variable that will be used to evaluate this integral at a given x value - * @param stack For nested formulas, a stack of operations that occur outside the complex operation + * @param stack For nested formulas, a stack of operations that occur outside the complex operation. */ - getIntegralFormula( - variable?: ProcessedComputable, - stack?: SubstitutionStack - ): GenericFormula { - if (variable == null && this.integralFormula != null) { + getIntegralFormula(stack?: SubstitutionStack): GenericFormula { + if (this.integralFormula != null) { return this.integralFormula; } - let formula; - const variablePresent = variable != null; - if (variable == null) { - variable = this.innermostVariable; - if (variable == null) { - throw new Error("Cannot integrate formula without variable"); - } - } if (stack == null) { // "Outer" part of the formula if (this.applySubstitution == null) { @@ -262,21 +223,24 @@ export default class Formula { if (this.internalIntegrate == null) { throw new Error("Cannot integrate formula with non-integrable operation"); } - let value = this.internalIntegrate.call(this, variable, stack, ...this.inputs); + let value = this.internalIntegrate.call(this, stack, ...this.inputs); stack.forEach(func => (value = func(value))); - formula = value; + this.integralFormula = value; } else { // Continue digging into the formula if (this.internalIntegrate) { - formula = this.internalIntegrate.call( + this.integralFormula = this.internalIntegrate.call( this, - variable, undefined, ...this.inputs ); - } else if (this.inputs.length === 1 && this.internalHasVariable) { + } else if ( + this.inputs.length === 1 && + this.internalEvaluate == null && + this.hasVariable() + ) { // eslint-disable-next-line @typescript-eslint/no-this-alias - formula = this; + this.integralFormula = this; } else { throw new Error("Cannot integrate formula without variable"); } @@ -291,20 +255,25 @@ export default class Formula { this.applySubstitution!.call(this, variable, ...this.inputs) ); if (this.internalIntegrateInner) { - formula = this.internalIntegrateInner.call(this, variable, stack, ...this.inputs); + this.integralFormula = this.internalIntegrateInner.call( + this, + stack, + ...this.inputs + ); } else if (this.internalIntegrate) { - formula = this.internalIntegrate.call(this, variable, stack, ...this.inputs); - } else if (this.inputs.length === 1 && this.internalHasVariable) { + this.integralFormula = 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 - formula = this; + this.integralFormula = this; } else { throw new Error("Cannot integrate formula without variable"); } } - if (!variablePresent) { - this.integralFormula = formula; - } - return formula; + return this.integralFormula; } /** @@ -324,7 +293,7 @@ export default class Formula { this.internalEvaluate === other.internalEvaluate && this.internalInvert === other.internalInvert && this.internalIntegrate === other.internalIntegrate && - this.internalHasVariable === other.internalHasVariable + this.internalVariables === other.internalVariables ); } @@ -556,17 +525,8 @@ export default class Formula { public static reciprocal = Formula.recip; public static reciprocate = Formula.recip; - public static max(value: FormulaSource, other: FormulaSource): GenericFormula { - return new Formula({ - inputs: [value, other], - evaluate: Decimal.max, - invert: ops.passthrough as ( - value: DecimalSource, - ...inputs: [FormulaSource, FormulaSource] - ) => DecimalSource - }); - } - + // TODO these functions should ostensibly be integrable, and the integrals should be invertible + public static max = ops.createPassthroughBinaryFormula(Decimal.max); public static min = ops.createPassthroughBinaryFormula(Decimal.min); public static minabs = ops.createPassthroughBinaryFormula(Decimal.minabs); public static maxabs = ops.createPassthroughBinaryFormula(Decimal.maxabs); @@ -1356,6 +1316,24 @@ export default class Formula { } } +/** + * Utility for recursively searching through a formula for the cause of non-invertibility. + * @param formula The formula to search for a non-invertible formula within + */ +export function findNonInvertible(formula: GenericFormula): GenericFormula | null { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (formula.internalInvert == null && formula.internalEvaluate != null) { + return formula; + } + for (const input of formula.inputs) { + if (hasVariable(input)) { + return findNonInvertible(input); + } + } + return null; +} + /** * 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 diff --git a/src/game/formulas/operations.ts b/src/game/formulas/operations.ts index 4512f55..becaec3 100644 --- a/src/game/formulas/operations.ts +++ b/src/game/formulas/operations.ts @@ -1,8 +1,9 @@ import Decimal, { DecimalSource } from "util/bignum"; -import { Ref } from "vue"; import Formula, { hasVariable, unrefFormulaSource } from "./formulas"; import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types"; +const ln10 = Decimal.ln(10); + export function passthrough(value: T): T { return value; } @@ -14,13 +15,9 @@ export function invertNeg(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateNeg( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateNeg(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - return Formula.neg(lhs.getIntegralFormula(variable, stack)); + return Formula.neg(lhs.getIntegralFormula(stack)); } throw new Error("Could not integrate due to no input being a variable"); } @@ -38,33 +35,27 @@ export function invertAdd(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateAdd( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateAdd(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0).add(x); + const x = lhs.getIntegralFormula(stack); + return Formula.times(rhs, lhs.innermostVariable ?? 0).add(x); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); - return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).add(x); + const x = rhs.getIntegralFormula(stack); + return Formula.times(lhs, rhs.innermostVariable ?? 0).add(x); } throw new Error("Could not integrate due to no input being a variable"); } export function integrateInnerAdd( - variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.add(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.add(x, lhs); } throw new Error("Could not integrate due to no input being a variable"); @@ -79,33 +70,27 @@ export function invertSub(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateSub( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateSub(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.sub(x, Formula.times(rhs, variable ?? lhs.innermostVariable ?? 0)); + const x = lhs.getIntegralFormula(stack); + return Formula.sub(x, Formula.times(rhs, lhs.innermostVariable ?? 0)); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); - return Formula.times(lhs, variable ?? rhs.innermostVariable ?? 0).sub(x); + const x = rhs.getIntegralFormula(stack); + return Formula.times(lhs, rhs.innermostVariable ?? 0).sub(x); } throw new Error("Could not integrate due to no input being a variable"); } export function integrateInnerSub( - variable: Ref, stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource ) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.sub(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.sub(x, lhs); } throw new Error("Could not integrate due to no input being a variable"); @@ -120,17 +105,12 @@ export function invertMul(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateMul( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateMul(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.times(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.times(x, lhs); } throw new Error("Could not integrate due to no input being a variable"); @@ -158,17 +138,12 @@ export function invertDiv(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateDiv( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateDiv(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.div(x, rhs); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.div(lhs, x); } throw new Error("Could not integrate due to no input being a variable"); @@ -194,13 +169,9 @@ export function invertRecip(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateRecip( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateRecip(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.ln(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -213,14 +184,25 @@ export function invertLog10(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateLog10( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +function internalIntegrateLog10(lhs: DecimalSource) { + return Decimal.ln(lhs).sub(1).times(lhs).div(ln10); +} + +function internalInvertIntegralLog10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x).div(Formula.ln(10)); + const numerator = ln10.times(value); + return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); + } + throw new Error("Could not invert due to no input being a variable"); +} + +export function integrateLog10(stack: SubstitutionStack, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return new Formula({ + inputs: [lhs.getIntegralFormula(stack)], + evaluate: internalIntegrateLog10, + invert: internalInvertIntegralLog10 + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -234,15 +216,25 @@ export function invertLog(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integrateLog( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +function internalIntegrateLog(lhs: DecimalSource, rhs: DecimalSource) { + return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(rhs)); +} + +function internalInvertIntegralLog(value: DecimalSource, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x).div(Formula.ln(rhs)); + const numerator = Decimal.ln(unrefFormulaSource(rhs)).times(value); + return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); + } + throw new Error("Could not invert due to no input being a variable"); +} + +export function integrateLog(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { + if (hasVariable(lhs)) { + return new Formula({ + inputs: [lhs.getIntegralFormula(stack), rhs], + evaluate: internalIntegrateLog, + invert: internalInvertIntegralLog + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -254,14 +246,25 @@ export function invertLog2(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateLog2( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +function internalIntegrateLog2(lhs: DecimalSource) { + return Decimal.ln(lhs).sub(1).times(lhs).div(Decimal.ln(2)); +} + +function internalInvertIntegralLog2(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x).div(Formula.ln(2)); + const numerator = Decimal.ln(2).times(value); + return lhs.invert(numerator.div(numerator.div(Math.E).lambertw())); + } + throw new Error("Could not invert due to no input being a variable"); +} + +export function integrateLog2(stack: SubstitutionStack, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return new Formula({ + inputs: [lhs.getIntegralFormula(stack)], + evaluate: internalIntegrateLog2, + invert: internalInvertIntegralLog2 + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -273,14 +276,24 @@ export function invertLn(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateLn( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +function internalIntegrateLn(lhs: DecimalSource) { + return Decimal.ln(lhs).sub(1).times(lhs); +} + +function internalInvertIntegralLn(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x); + return lhs.invert(Decimal.div(value, Decimal.div(value, Math.E).lambertw())); + } + throw new Error("Could not invert due to no input being a variable"); +} + +export function integrateLn(stack: SubstitutionStack, lhs: FormulaSource) { + if (hasVariable(lhs)) { + return new Formula({ + inputs: [lhs.getIntegralFormula(stack)], + evaluate: internalIntegrateLn, + invert: internalInvertIntegralLn + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -294,18 +307,13 @@ export function invertPow(value: DecimalSource, lhs: FormulaSource, rhs: Formula throw new Error("Could not invert due to no input being a variable"); } -export function integratePow( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integratePow(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); const pow = Formula.add(rhs, 1); return Formula.pow(x, pow).div(pow); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); return Formula.pow(lhs, x).div(Formula.ln(lhs)); } throw new Error("Could not integrate due to no input being a variable"); @@ -318,14 +326,24 @@ export function invertPow10(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integratePow10( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +function internalIntegratePow10(lhs: DecimalSource) { + return Decimal.pow10(lhs).div(Decimal.ln(lhs)); +} + +function internalInvertIntegralPow10(value: DecimalSource, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); - return Formula.ln(x).sub(1).times(x).div(Decimal.ln(10)); + 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 + }); } throw new Error("Could not integrate due to no input being a variable"); } @@ -339,17 +357,12 @@ export function invertPowBase(value: DecimalSource, lhs: FormulaSource, rhs: For throw new Error("Could not invert due to no input being a variable"); } -export function integratePowBase( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integratePowBase(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.pow(rhs, x).div(Formula.ln(rhs)); } else if (hasVariable(rhs)) { - const x = rhs.getIntegralFormula(variable, stack); + const x = rhs.getIntegralFormula(stack); const denominator = Formula.add(lhs, 1); return Formula.pow(x, denominator).div(denominator); } @@ -365,14 +378,9 @@ export function invertRoot(value: DecimalSource, lhs: FormulaSource, rhs: Formul throw new Error("Could not invert due to no input being a variable"); } -export function integrateRoot( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource, - rhs: FormulaSource -) { +export function integrateRoot(stack: SubstitutionStack, lhs: FormulaSource, rhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.pow(x, Formula.recip(rhs).add(1)).times(rhs).div(Formula.add(rhs, 1)); } throw new Error("Could not integrate due to no input being a variable"); @@ -385,13 +393,9 @@ export function invertExp(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateExp( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateExp(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.exp(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -520,13 +524,9 @@ export function invertSin(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateSin( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateSin(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.cos(x).neg(); } throw new Error("Could not integrate due to no input being a variable"); @@ -539,13 +539,9 @@ export function invertCos(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateCos( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateCos(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.sin(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -558,13 +554,9 @@ export function invertTan(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateTan( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateTan(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.cos(x).ln().neg(); } throw new Error("Could not integrate due to no input being a variable"); @@ -577,13 +569,9 @@ export function invertAsin(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAsin( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAsin(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.asin(x) .times(x) .add(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2)))); @@ -598,13 +586,9 @@ export function invertAcos(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAcos( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAcos(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.acos(x) .times(x) .sub(Formula.sqrt(Formula.sub(1, Formula.pow(x, 2)))); @@ -619,13 +603,9 @@ export function invertAtan(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAtan( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAtan(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.atan(x) .times(x) .sub(Formula.ln(Formula.pow(x, 2).add(1)).div(2)); @@ -640,13 +620,9 @@ export function invertSinh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateSinh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateSinh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.cosh(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -659,13 +635,9 @@ export function invertCosh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateCosh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateCosh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.sinh(x); } throw new Error("Could not integrate due to no input being a variable"); @@ -678,13 +650,9 @@ export function invertTanh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateTanh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateTanh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.cosh(x).ln(); } throw new Error("Could not integrate due to no input being a variable"); @@ -697,13 +665,9 @@ export function invertAsinh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAsinh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAsinh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.asinh(x).times(x).sub(Formula.pow(x, 2).add(1).sqrt()); } throw new Error("Could not integrate due to no input being a variable"); @@ -716,13 +680,9 @@ export function invertAcosh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAcosh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAcosh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.acosh(x) .times(x) .sub(Formula.add(x, 1).sqrt().times(Formula.sub(x, 1).sqrt())); @@ -737,13 +697,9 @@ export function invertAtanh(value: DecimalSource, lhs: FormulaSource) { throw new Error("Could not invert due to no input being a variable"); } -export function integrateAtanh( - variable: Ref, - stack: SubstitutionStack, - lhs: FormulaSource -) { +export function integrateAtanh(stack: SubstitutionStack, lhs: FormulaSource) { if (hasVariable(lhs)) { - const x = lhs.getIntegralFormula(variable, stack); + const x = lhs.getIntegralFormula(stack); return Formula.atanh(x) .times(x) .add(Formula.sub(1, Formula.pow(x, 2)).ln().div(2)); diff --git a/src/game/formulas/types.d.ts b/src/game/formulas/types.d.ts index 3d3bc83..96d7757 100644 --- a/src/game/formulas/types.d.ts +++ b/src/game/formulas/types.d.ts @@ -22,7 +22,6 @@ type EvaluateFunction = ( type InvertFunction = (this: Formula, value: DecimalSource, ...inputs: T) => DecimalSource; type IntegrateFunction = ( this: Formula, - variable: Ref, stack: SubstitutionStack | undefined, ...inputs: T ) => GenericFormula; @@ -43,7 +42,6 @@ type GeneralFormulaOptions = { integrate?: IntegrateFunction; integrateInner?: IntegrateFunction; applySubstitution?: SubstitutionFunction; - hasVariable?: boolean; }; type FormulaOptions = | VariableFormulaOptions @@ -52,7 +50,7 @@ type FormulaOptions = type InternalFormulaProperties = { inputs: T; - internalHasVariable: boolean; + internalVariables: number; internalEvaluate?: EvaluateFunction; internalInvert?: InvertFunction; internalIntegrate?: IntegrateFunction; diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index ce0120b..ccaa485 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -96,21 +96,21 @@ const invertibleIntegralZeroPramFunctionNames = [ "sqr", "sqrt", "cube", - "cbrt" -] as const; -const nonInvertibleIntegralZeroPramFunctionNames = [ - ...nonIntegrableZeroParamFunctionNames, + "cbrt", "neg", "exp", "sin", "cos", "tan", + "sinh", + "cosh", + "tanh" +] as const; +const nonInvertibleIntegralZeroPramFunctionNames = [ + ...nonIntegrableZeroParamFunctionNames, "asin", "acos", "atan", - "sinh", - "cosh", - "tanh", "asinh", "acosh", "atanh" @@ -493,8 +493,8 @@ describe("Integrating", () => { test("variable.evaluateIntegral() calculates correctly", () => expect(variable.evaluateIntegral()).compare_tolerance(Decimal.pow(10, 2).div(2))); - test("evaluateIntegral(variable) overrides variable value", () => - expect(variable.add(10).evaluateIntegral(20)).compare_tolerance(400)); + test("variable.evaluateIntegral(variable) overrides variable value", () => + expect(variable.evaluateIntegral(20)).compare_tolerance(Decimal.pow(20, 2).div(2))); describe("Integrable functions marked as such", () => { function checkFormula(formula: GenericFormula) { @@ -668,32 +668,13 @@ 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); + expect(formula.invertIntegral(formula.evaluateIntegral())).compare_tolerance(10, 0.01); }); test("Inverting integral of nested complex formulas", () => { const formula = Formula.pow(1.05, variable).times(100).pow(0.5); expect(() => formula.invertIntegral(100)).toThrow(); }); - - describe("Inverting integral pass-throughs", () => { - test("max", () => - expect(Formula.max(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("min", () => - expect(Formula.min(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("minabs", () => - expect(Formula.minabs(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("maxabs", () => - expect(Formula.maxabs(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("clampMax", () => - expect(Formula.clampMax(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("clampMin", () => - expect(Formula.clampMin(variable, constant).invertIntegral(10)).compare_tolerance(10)); - test("clamp", () => - expect( - Formula.clamp(variable, constant, constant).invertIntegral(10) - ).compare_tolerance(10)); - }); }); describe("Step-wise", () => { @@ -914,8 +895,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [], evaluate: () => 6, - invert: value => value, - hasVariable: true + invert: value => value }).invert(10) ).toThrow()); test("One input inverts correctly", () => @@ -923,8 +903,7 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable], evaluate: () => 10, - invert: (value, v1) => v1.evaluate(), - hasVariable: true + invert: (value, v1) => v1.evaluate() }).invert(10) ).compare_tolerance(1)); test("Two inputs inverts correctly", () => @@ -932,37 +911,36 @@ describe("Custom Formulas", () => { new Formula({ inputs: [variable, 2], evaluate: () => 10, - invert: (value, v1, v2) => v2, - hasVariable: true + invert: (value, v1, v2) => v2 }).invert(10) ).compare_tolerance(2)); }); describe("Formula with integrate", () => { - test("Zero input integrates correctly", () => - expect( + test("Zero input cannot integrate", () => + expect(() => new Formula({ inputs: [], - evaluate: () => 10, - integrate: variable => variable + evaluate: () => 0, + integrate: stack => variable }).evaluateIntegral() - ).compare_tolerance(20)); + ).toThrow()); test("One input integrates correctly", () => expect( new Formula({ inputs: [variable], - evaluate: () => 10, - integrate: (variable, stack, v1) => Formula.add(variable, v1) + evaluate: v1 => Decimal.add(v1, 19.5), + integrate: (stack, v1) => Formula.add(v1, 10) }).evaluateIntegral() ).compare_tolerance(20)); test("Two inputs integrates correctly", () => expect( new Formula({ - inputs: [variable, 2], - evaluate: (v1, v2) => 10, - integrate: (variable, v1, v2) => variable + inputs: [variable, 10], + evaluate: v1 => Decimal.add(v1, 19.5), + integrate: (stack, v1, v2) => Formula.add(v1, v2) }).evaluateIntegral() - ).compare_tolerance(3)); + ).compare_tolerance(20)); }); describe("Formula with invertIntegral", () => { @@ -970,29 +948,26 @@ describe("Custom Formulas", () => { expect(() => new Formula({ inputs: [], - evaluate: () => 10, - integrate: variable => variable, - hasVariable: true - }).invertIntegral(8) + evaluate: () => 0, + integrate: stack => variable + }).invertIntegral(20) ).toThrow()); test("One input inverts integral correctly", () => expect( new Formula({ inputs: [variable], - evaluate: () => 10, - integrate: (variable, stack, v1) => variable, - hasVariable: true - }).invertIntegral(8) - ).compare_tolerance(1)); + evaluate: v1 => Decimal.add(v1, 19.5), + integrate: (stack, v1) => Formula.add(v1, 10) + }).invertIntegral(20) + ).compare_tolerance(10)); test("Two inputs inverts integral correctly", () => expect( new Formula({ - inputs: [variable, 2], - evaluate: (v1, v2) => 10, - integrate: (variable, v1, v2) => variable, - hasVariable: true - }).invertIntegral(8) - ).compare_tolerance(1)); + inputs: [variable, 10], + evaluate: v1 => Decimal.add(v1, 19.5), + integrate: (stack, v1, v2) => Formula.add(v1, v2) + }).invertIntegral(20) + ).compare_tolerance(10)); }); describe.todo("Formula as input"); diff --git a/tests/utils.ts b/tests/utils.ts index 0e58e19..a1e6084 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -2,7 +2,7 @@ import Decimal, { DecimalSource, format } from "util/bignum"; import { expect } from "vitest"; interface CustomMatchers { - compare_tolerance(expected: DecimalSource): R; + compare_tolerance(expected: DecimalSource, tolerance?: number): R; } declare global { @@ -16,7 +16,7 @@ declare global { } expect.extend({ - compare_tolerance(received: DecimalSource, expected: DecimalSource) { + compare_tolerance(received: DecimalSource, expected: DecimalSource, tolerance?: number) { const { isNot } = this; let pass = false; if (!Decimal.isFinite(expected)) { @@ -24,7 +24,7 @@ expect.extend({ } else if (Decimal.isNaN(expected)) { pass = Decimal.isNaN(received); } else { - pass = Decimal.eq_tolerance(received, expected); + pass = Decimal.eq_tolerance(received, expected, tolerance); } return { // do not alter your "pass" based on isNot. Vitest does it for you From 6f9b73d0e87b96fc7314c3ddd5e8d96b88fdb606 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 00:48:48 -0500 Subject: [PATCH 16/36] Add printFormula util --- src/data/projEntry.tsx | 4 +++- src/game/formulas/formulas.ts | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index 55e8500..5c8d456 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -7,7 +7,8 @@ import { globalBus } from "game/events"; import Formula, { calculateCost, calculateMaxAffordable, - findNonInvertible + findNonInvertible, + printFormula } from "game/formulas/formulas"; import type { BaseLayer, GenericLayer } from "game/layers"; import { createLayer } from "game/layers"; @@ -23,6 +24,7 @@ window.Formula = Formula; window.calculateMaxAffordable = calculateMaxAffordable; window.calculateCost = calculateCost; window.findNonInvertible = findNonInvertible; +window.printFormula = printFormula; window.unref = unref; window.ref = ref; window.createResource = createResource; diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 8161903..030e604 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -1,5 +1,5 @@ import { Resource } from "features/resources/resource"; -import Decimal, { DecimalSource } from "util/bignum"; +import Decimal, { DecimalSource, format } from "util/bignum"; import { Computable, convertComputable, ProcessedComputable } from "util/computed"; import { computed, ComputedRef, ref, unref } from "vue"; import * as ops from "./operations"; @@ -1334,6 +1334,28 @@ export function findNonInvertible(formula: GenericFormula): GenericFormula | nul return null; } +/** + * Stringifies a formula so it's more easy to read in the console + * @param formula The formula to print + */ +export function printFormula(formula: FormulaSource): string { + if (formula instanceof Formula) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return formula.internalEvaluate == null + ? formula.hasVariable() + ? "x" + : formula.inputs[0] ?? 0 + : // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + formula.internalEvaluate.name + + "(" + + formula.inputs.map(printFormula).join(", ") + + ")"; + } + return format(unref(formula)); +} + /** * 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 From 8dc0c6c55cf24d5cb241103d1ba83d777758978d Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 15:02:43 -0500 Subject: [PATCH 17/36] 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 { internalVariables: 1, innermostVariable: variable, internalIntegrate: integrateVariable, + internalIntegrateInner: integrateVariableInner, applySubstitution: ops.passthrough as unknown as SubstitutionFunction }; } @@ -119,7 +124,8 @@ export default class Formula { }; } - 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 { 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 { * @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 { 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 { 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); + }); }); }); From 0c1410a1c12e3e029c99863e5b37b4121ed36558 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 15:04:31 -0500 Subject: [PATCH 18/36] Remove a handful of long-running tests --- tests/game/formulas.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 9c77d86..95370f6 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -263,6 +263,11 @@ describe("Creating Formulas", () => { functionName: T, args: Readonly> ) { + if ((functionName === "slog" || functionName === "layeradd") && args[0] === -1) { + // These cases in particular take a long time, so skip them + // We still have plenty of coverage + return; + } let testName = functionName + "("; for (let i = 0; i < args.length; i++) { if (i !== 0) { From fb360c72c5836f90b341128c2c3a19a630b23309 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 15:16:47 -0500 Subject: [PATCH 19/36] Add some more tests --- tests/game/formulas.test.ts | 40 +++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/tests/game/formulas.test.ts b/tests/game/formulas.test.ts index 95370f6..fbaa378 100644 --- a/tests/game/formulas.test.ts +++ b/tests/game/formulas.test.ts @@ -571,7 +571,7 @@ describe("Integrating", () => { describe.todo("Integrable formulas integrate correctly"); test("Integrating nested formulas", () => { - const formula = Formula.add(variable, constant).times(constant).pow(2).times(30); + const formula = Formula.add(variable, constant).times(constant).pow(2).times(30).add(10); const actualCost = new Array(10) .fill(null) .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0)); @@ -588,6 +588,24 @@ describe("Integrating", () => { ).toBeLessThan(0.1); }); + test("Integrating nested formulas with overidden variable", () => { + const formula = Formula.add(variable, constant).times(constant).pow(2).times(30).add(10); + const actualCost = new Array(20) + .fill(null) + .reduce((acc, _, i) => acc.add(formula.evaluate(i)), new Decimal(0)); + // Check if the calculated cost is within 10% of the actual cost, + // because this is an approximation + expect( + Decimal.sub( + actualCost, + Decimal.add(formula.evaluateIntegral(20), formula.calculateConstantOfIntegration()) + ) + .abs() + .div(actualCost) + .toNumber() + ).toBeLessThan(0.1); + }); + test("Integrating nested complex formulas", () => { const formula = Formula.pow(1.05, variable).times(100).pow(0.5); expect(() => formula.evaluateIntegral()).toThrow(); @@ -982,9 +1000,23 @@ describe("Custom Formulas", () => { ).compare_tolerance(10)); }); - describe.todo("Formula as input"); - - describe.todo("Determines invertibility etc. correctly"); + describe("Formula as input", () => { + let customFormula: GenericFormula; + beforeAll(() => { + customFormula = new Formula({ + inputs: [variable], + evaluate: v1 => v1, + invert: value => value, + integrate: (stack, v1) => v1.getIntegralFormula(stack) + }); + }); + test("Evaluate correctly", () => + expect(customFormula.add(10).evaluate()).compare_tolerance(11)); + test("Invert correctly", () => + expect(customFormula.add(10).invert(20)).compare_tolerance(10)); + test("Integrate correctly", () => + expect(customFormula.add(10).evaluateIntegral(10)).compare_tolerance(20)); + }); }); describe("Buy Max", () => { From 7e7a36bb7821a939234a042ba0c9bb1957747dde Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 16:41:39 -0500 Subject: [PATCH 20/36] Add summedPurchases param for buy max utilities --- src/game/formulas/formulas.ts | 79 +++++++++++++++++++++++++++++----- tests/game/formulas.test.ts | 81 +++++++++++++++++++++++++++++++---- 2 files changed, 141 insertions(+), 19 deletions(-) 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; + }); }); }); From eff5852b04a4d9051e665e07a374d711dba0f451 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 16:42:03 -0500 Subject: [PATCH 21/36] Remove some debugging code --- src/data/projEntry.tsx | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index 5c8d456..e4640b6 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -4,12 +4,6 @@ import { createResource, trackBest, trackOOMPS, trackTotal } from "features/reso import type { GenericTree } from "features/trees/tree"; import { branchedResetPropagation, createTree } from "features/trees/tree"; import { globalBus } from "game/events"; -import Formula, { - calculateCost, - calculateMaxAffordable, - findNonInvertible, - printFormula -} from "game/formulas/formulas"; import type { BaseLayer, GenericLayer } from "game/layers"; import { createLayer } from "game/layers"; import type { Player } from "game/player"; @@ -17,18 +11,9 @@ import player from "game/player"; import type { DecimalSource } from "util/bignum"; import Decimal, { format, formatTime } from "util/bignum"; import { render } from "util/vue"; -import { computed, ref, toRaw, unref } from "vue"; +import { computed, toRaw } from "vue"; import prestige from "./layers/prestige"; -window.Formula = Formula; -window.calculateMaxAffordable = calculateMaxAffordable; -window.calculateCost = calculateCost; -window.findNonInvertible = findNonInvertible; -window.printFormula = printFormula; -window.unref = unref; -window.ref = ref; -window.createResource = createResource; - /** * @hidden */ From cc1a2998e073df08d6e18330c3b0a0708ff2c3d4 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 19:22:23 -0500 Subject: [PATCH 22/36] Make challenges use the requirements system --- src/features/challenges/challenge.tsx | 67 ++++++++------------------- src/features/repeatable.tsx | 21 +++++---- src/game/requirements.tsx | 2 +- 3 files changed, 32 insertions(+), 58 deletions(-) diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index 92dce42..34f850c 100644 --- a/src/features/challenges/challenge.tsx +++ b/src/features/challenges/challenge.tsx @@ -12,10 +12,10 @@ import { Visibility } from "features/feature"; import type { GenericReset } from "features/reset"; -import type { Resource } from "features/resources/resource"; import { globalBus } from "game/events"; import type { Persistent } from "game/persistence"; import { persistent } from "game/persistence"; +import { maxRequirementsMet, Requirements } from "game/requirements"; import settings, { registerSettingField } from "game/settings"; import type { DecimalSource } from "util/bignum"; import Decimal from "util/bignum"; @@ -36,11 +36,10 @@ export interface ChallengeOptions { visibility?: Computable; canStart?: Computable; reset?: GenericReset; - canComplete?: Computable; + requirements: Requirements; + maximize?: Computable; completionLimit?: Computable; mark?: Computable; - resource?: Resource; - goal?: Computable; classes?: Computable>; style?: Computable; display?: Computable< @@ -60,6 +59,7 @@ export interface ChallengeOptions { export interface BaseChallenge { id: string; + canComplete: Ref; completions: Persistent; completed: Ref; maxed: Ref; @@ -76,10 +76,10 @@ export type Challenge = Replace< { visibility: GetComputableTypeWithDefault; canStart: GetComputableTypeWithDefault; - canComplete: GetComputableTypeWithDefault>; + requirements: GetComputableType; + maximize: GetComputableType; completionLimit: GetComputableTypeWithDefault; mark: GetComputableTypeWithDefault>; - goal: GetComputableType; classes: GetComputableType; style: GetComputableType; display: GetComputableType; @@ -91,7 +91,6 @@ export type GenericChallenge = Replace< { visibility: ProcessedComputable; canStart: ProcessedComputable; - canComplete: ProcessedComputable; completionLimit: ProcessedComputable; mark: ProcessedComputable; } @@ -105,19 +104,6 @@ export function createChallenge( return createLazyProxy(() => { const challenge = optionsFunc(); - if ( - challenge.canComplete == null && - (challenge.resource == null || challenge.goal == null) - ) { - console.warn( - "Cannot create challenge without a canComplete property or a resource and goal property", - challenge - ); - throw new Error( - "Cannot create challenge without a canComplete property or a resource and goal property" - ); - } - challenge.id = getUniqueID("challenge-"); challenge.type = ChallengeType; challenge[Component] = ChallengeComponent; @@ -137,13 +123,10 @@ export function createChallenge( const genericChallenge = challenge as GenericChallenge; if (genericChallenge.active.value) { if ( - unref(genericChallenge.canComplete) !== false && + Decimal.gt(unref(genericChallenge.canComplete), 0) && !genericChallenge.maxed.value ) { - let completions: boolean | DecimalSource = unref(genericChallenge.canComplete); - if (typeof completions === "boolean") { - completions = 1; - } + const completions = unref(genericChallenge.canComplete); genericChallenge.completions.value = Decimal.min( Decimal.add(genericChallenge.completions.value, completions), unref(genericChallenge.completionLimit) @@ -163,18 +146,20 @@ export function createChallenge( genericChallenge.onEnter?.(); } }; + challenge.canComplete = computed(() => + Decimal.max( + maxRequirementsMet((challenge as GenericChallenge).requirements), + unref((challenge as GenericChallenge).maximize) ? Decimal.dInf : 1 + ) + ); challenge.complete = function (remainInChallenge?: boolean) { const genericChallenge = challenge as GenericChallenge; - let completions: boolean | DecimalSource = unref(genericChallenge.canComplete); + const completions = unref(genericChallenge.canComplete); if ( genericChallenge.active.value && - completions !== false && - (completions === true || Decimal.neq(0, completions)) && + Decimal.gt(completions, 0) && !genericChallenge.maxed.value ) { - if (typeof completions === "boolean") { - completions = 1; - } genericChallenge.completions.value = Decimal.min( Decimal.add(genericChallenge.completions.value, completions), unref(genericChallenge.completionLimit) @@ -196,19 +181,6 @@ export function createChallenge( } return unref(visibility); }); - if (challenge.canComplete == null) { - challenge.canComplete = computed(() => { - const genericChallenge = challenge as GenericChallenge; - if ( - !genericChallenge.active.value || - genericChallenge.resource == null || - genericChallenge.goal == null - ) { - return false; - } - return Decimal.gte(genericChallenge.resource.value, unref(genericChallenge.goal)); - }); - } if (challenge.mark == null) { challenge.mark = computed( () => @@ -219,11 +191,10 @@ export function createChallenge( processComputable(challenge as T, "canStart"); setDefault(challenge, "canStart", true); - processComputable(challenge as T, "canComplete"); + processComputable(challenge as T, "maximize"); processComputable(challenge as T, "completionLimit"); setDefault(challenge, "completionLimit", 1); processComputable(challenge as T, "mark"); - processComputable(challenge as T, "goal"); processComputable(challenge as T, "classes"); processComputable(challenge as T, "style"); processComputable(challenge as T, "display"); @@ -278,9 +249,9 @@ export function setupAutoComplete( ): WatchStopHandle { const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive; return watch( - [challenge.canComplete as Ref, isActive as Ref], + [challenge.canComplete as Ref, isActive as Ref], ([canComplete, isActive]) => { - if (canComplete && isActive) { + if (Decimal.gt(canComplete, 0) && isActive) { challenge.complete(!exitOnComplete); } } diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index 96bea11..b442068 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -78,6 +78,11 @@ export interface BaseRepeatable { maxed: Ref; /** Whether or not this repeatable can be clicked. */ canClick: ProcessedComputable; + /** + * How much amount can be increased by, or 1 if unclickable. + * Capped at 1 if {@link RepeatableOptions.maximize} is false. + **/ + amountToIncrease: Ref; /** A function that gets called when this repeatable is clicked. */ onClick: (event?: MouseEvent | TouchEvent) => void; /** A symbol that helps identify features of the same type. */ @@ -170,6 +175,11 @@ export function createRepeatable( } return currClasses; }); + repeatable.amountToIncrease = computed(() => + unref((repeatable as GenericRepeatable).maximize) + ? maxRequirementsMet(repeatable.requirements) + : 1 + ); repeatable.canClick = computed(() => requirementsMet(repeatable.requirements)); const onClick = repeatable.onClick; repeatable.onClick = function (this: GenericRepeatable, event?: MouseEvent | TouchEvent) { @@ -177,12 +187,7 @@ export function createRepeatable( if (!unref(genericRepeatable.canClick)) { return; } - payRequirements( - repeatable.requirements, - unref(genericRepeatable.maximize) - ? maxRequirementsMet(genericRepeatable.requirements) - : 1 - ); + payRequirements(repeatable.requirements, unref(repeatable.amountToIncrease)); genericRepeatable.amount.value = Decimal.add(genericRepeatable.amount.value, 1); onClick?.(event); }; @@ -233,9 +238,7 @@ export function createRepeatable(
{displayRequirements( genericRepeatable.requirements, - unref(genericRepeatable.maximize) - ? maxRequirementsMet(genericRepeatable.requirements) - : 1 + unref(repeatable.amountToIncrease) )} )} diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 8cd1cf7..9e775de 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -239,7 +239,7 @@ export function maxRequirementsMet(requirements: Requirements): DecimalSource { } const reqsMet = unref(requirements.requirementMet); if (typeof reqsMet === "boolean") { - return reqsMet ? Infinity : 0; + return reqsMet ? Decimal.dInf : 0; } else if (Decimal.gt(reqsMet, 1) && unref(requirements.canMaximize) !== true) { return 1; } From 3e0aafcf02350becbf1d717525af93db5d041f3d Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 20:30:08 -0500 Subject: [PATCH 23/36] Make conversions use formulas --- src/features/conversion.ts | 296 ++-------------------------------- src/game/formulas/formulas.ts | 1 + 2 files changed, 15 insertions(+), 282 deletions(-) diff --git a/src/features/conversion.ts b/src/features/conversion.ts index 97637fa..f5146ca 100644 --- a/src/features/conversion.ts +++ b/src/features/conversion.ts @@ -1,11 +1,10 @@ import type { OptionsFunc, Replace } from "features/feature"; import { setDefault } from "features/feature"; import type { Resource } from "features/resources/resource"; +import { InvertibleFormula } from "game/formulas/types"; import type { BaseLayer } from "game/layers"; -import type { Modifier } from "game/modifiers"; import type { DecimalSource } from "util/bignum"; import Decimal from "util/bignum"; -import type { WithRequired } from "util/common"; import type { Computable, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed"; import { convertComputable, processComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; @@ -15,9 +14,10 @@ import { computed, unref } from "vue"; /** An object that configures a {@link Conversion}. */ export interface ConversionOptions { /** - * The scaling function that is used to determine the rate of conversion from one {@link features/resources/resource.Resource} to the other. + * The formula used to determine how much {@link gainResource} should be earned by this converting. + * When evaluating, the variable will always be overidden to the amount of {@link baseResource}. */ - scaling: ScalingFunction; + formula: InvertibleFormula; /** * How much of the output resource the conversion can currently convert for. * Typically this will be set for you in a conversion constructor. @@ -53,10 +53,6 @@ export interface ConversionOptions { * Defaults to true. */ buyMax?: Computable; - /** - * Whether or not to round up the cost to generate a given amount of the output resource. - */ - roundUpCost?: Computable; /** * The function that performs the actual conversion from {@link baseResource} to {@link gainResource}. * Typically this will be set for you in a conversion constructor. @@ -73,20 +69,6 @@ export interface ConversionOptions { * This will not be called whenever using currentGain without calling convert (e.g. passive generation) */ onConvert?: (amountGained: DecimalSource) => void; - /** - * An additional modifier that will be applied to the gain amounts. - * Must be reversible in order to correctly calculate {@link nextAt}. - * @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers. - */ - gainModifier?: WithRequired; - /** - * A modifier that will be applied to the cost amounts. - * That is to say, this modifier will be applied to the amount of baseResource before going into the scaling function. - * A cost modifier of x0.5 would give gain amounts equal to the player having half the baseResource they actually have. - * Must be reversible in order to correctly calculate {@link nextAt}. - * @see {@link game/modifiers.createSequentialModifier} if you want to apply multiple modifiers. - */ - costModifier?: WithRequired; } /** @@ -109,7 +91,6 @@ export type Conversion = Replace< nextAt: GetComputableTypeWithDefault>; buyMax: GetComputableTypeWithDefault; spend: undefined extends T["spend"] ? (amountGained: DecimalSource) => void : T["spend"]; - roundUpCost: GetComputableTypeWithDefault; } >; @@ -123,7 +104,6 @@ export type GenericConversion = Replace< nextAt: ProcessedComputable; buyMax: ProcessedComputable; spend: (amountGained: DecimalSource) => void; - roundUpCost: ProcessedComputable; } >; @@ -142,11 +122,7 @@ export function createConversion( if (conversion.currentGain == null) { conversion.currentGain = computed(() => { - let gain = conversion.gainModifier - ? conversion.gainModifier.apply( - conversion.scaling.currentGain(conversion as GenericConversion) - ) - : conversion.scaling.currentGain(conversion as GenericConversion); + let gain = conversion.formula.evaluate(conversion.baseResource.value); gain = Decimal.floor(gain).max(0); if (unref(conversion.buyMax) === false) { @@ -160,17 +136,16 @@ export function createConversion( } if (conversion.currentAt == null) { conversion.currentAt = computed(() => { - let current = conversion.scaling.currentAt(conversion as GenericConversion); - if (unref((conversion as GenericConversion).roundUpCost)) - current = Decimal.ceil(current); - return current; + return conversion.formula.invert( + Decimal.floor(unref((conversion as GenericConversion).currentGain)) + ); }); } if (conversion.nextAt == null) { conversion.nextAt = computed(() => { - let next = conversion.scaling.nextAt(conversion as GenericConversion); - if (unref((conversion as GenericConversion).roundUpCost)) next = Decimal.ceil(next); - return next; + return conversion.formula.invert( + Decimal.floor(unref((conversion as GenericConversion).currentGain)).add(1) + ); }); } @@ -198,177 +173,11 @@ export function createConversion( processComputable(conversion as T, "nextAt"); processComputable(conversion as T, "buyMax"); setDefault(conversion, "buyMax", true); - processComputable(conversion as T, "roundUpCost"); - setDefault(conversion, "roundUpCost", true); return conversion as unknown as Conversion; }); } -/** - * A collection of functions that allow a conversion to scale the amount of resources gained based on the input resource. - * This typically shouldn't be created directly. Instead use one of the scaling function constructors. - * @see {@link createLinearScaling}. - * @see {@link createPolynomialScaling}. - */ -export interface ScalingFunction { - /** - * Calculates the amount of the output resource a conversion should be able to currently produce. - * This should be based off of `conversion.baseResource.value`. - * The conversion is responsible for applying the gainModifier, so this function should be un-modified. - * It does not need to be clamped or rounded. - */ - currentGain: (conversion: GenericConversion) => DecimalSource; - /** - * Calculates the amount of the input resource that is required for the current value of `conversion.currentGain`. - * Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate. - * The conversion is responsible for rounding up the amount as appropriate. - * The returned value should not be below 0. - */ - currentAt: (conversion: GenericConversion) => DecimalSource; - /** - * Calculates the amount of the input resource that would be required for the current value of `conversion.currentGain` to increase. - * Note that `conversion.currentGain` has been modified by `conversion.gainModifier`, so you will need to revert that as appropriate. - * The conversion is responsible for rounding up the amount as appropriate. - * The returned value should not be below 0. - */ - nextAt: (conversion: GenericConversion) => DecimalSource; -} - -/** - * Creates a scaling function based off the formula `(baseResource - base) * coefficient`. - * If the baseResource value is less than base then the currentGain will be 0. - * @param base The base variable in the scaling formula. - * @param coefficient The coefficient variable in the scaling formula. - * @example - * A scaling function created via `createLinearScaling(10, 0.5)` would produce the following values: - * | Base Resource | Current Gain | - * | ------------- | ------------ | - * | 10 | 1 | - * | 12 | 2 | - * | 20 | 6 | - */ -export function createLinearScaling( - base: Computable, - coefficient: Computable -): ScalingFunction { - const processedBase = convertComputable(base); - const processedCoefficient = convertComputable(coefficient); - return { - currentGain(conversion) { - let baseAmount: DecimalSource = unref(conversion.baseResource.value); - if (conversion.costModifier) { - baseAmount = conversion.costModifier.apply(baseAmount); - } - if (Decimal.lt(baseAmount, unref(processedBase))) { - return 0; - } - - return Decimal.sub(baseAmount, unref(processedBase)) - .sub(1) - .times(unref(processedCoefficient)) - .add(1); - }, - currentAt(conversion) { - let current: DecimalSource = unref(conversion.currentGain); - if (conversion.gainModifier) { - current = conversion.gainModifier.revert(current); - } - current = Decimal.max(0, current) - .sub(1) - .div(unref(processedCoefficient)) - .add(unref(processedBase)); - if (conversion.costModifier) { - current = conversion.costModifier.revert(current); - } - return current; - }, - nextAt(conversion) { - let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1).floor(); - if (conversion.gainModifier) { - next = conversion.gainModifier.revert(next); - } - next = Decimal.max(0, next) - .sub(1) - .div(unref(processedCoefficient)) - .add(unref(processedBase)) - .max(unref(processedBase)); - if (conversion.costModifier) { - next = conversion.costModifier.revert(next); - } - return next; - } - }; -} - -/** - * Creates a scaling function based off the formula `(baseResource / base) ^ exponent`. - * If the baseResource value is less than base then the currentGain will be 0. - * @param base The base variable in the scaling formula. - * @param exponent The exponent variable in the scaling formula. - * @example - * A scaling function created via `createPolynomialScaling(10, 0.5)` would produce the following values: - * | Base Resource | Current Gain | - * | ------------- | ------------ | - * | 10 | 1 | - * | 40 | 2 | - * | 250 | 5 | - */ -export function createPolynomialScaling( - base: Computable, - exponent: Computable -): ScalingFunction { - const processedBase = convertComputable(base); - const processedExponent = convertComputable(exponent); - return { - currentGain(conversion) { - let baseAmount: DecimalSource = unref(conversion.baseResource.value); - if (conversion.costModifier) { - baseAmount = conversion.costModifier.apply(baseAmount); - } - if (Decimal.lt(baseAmount, unref(processedBase))) { - return 0; - } - - const gain = Decimal.div(baseAmount, unref(processedBase)).pow( - unref(processedExponent) - ); - - if (gain.isNan()) { - return new Decimal(0); - } - return gain; - }, - currentAt(conversion) { - let current: DecimalSource = unref(conversion.currentGain); - if (conversion.gainModifier) { - current = conversion.gainModifier.revert(current); - } - current = Decimal.max(0, current) - .root(unref(processedExponent)) - .times(unref(processedBase)); - if (conversion.costModifier) { - current = conversion.costModifier.revert(current); - } - return current; - }, - nextAt(conversion) { - let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1).floor(); - if (conversion.gainModifier) { - next = conversion.gainModifier.revert(next); - } - next = Decimal.max(0, next) - .root(unref(processedExponent)) - .times(unref(processedBase)) - .max(unref(processedBase)); - if (conversion.costModifier) { - next = conversion.costModifier.revert(next); - } - return next; - } - }; -} - /** * Creates a conversion that simply adds to the gainResource amount upon converting. * This is similar to the behavior of "normal" layers in The Modding Tree. @@ -396,13 +205,8 @@ export function createIndependentConversion( if (conversion.currentGain == null) { conversion.currentGain = computed(() => { - let gain = conversion.gainModifier - ? conversion.gainModifier.apply( - conversion.scaling.currentGain(conversion as GenericConversion) - ) - : conversion.scaling.currentGain(conversion as GenericConversion); + let gain = conversion.formula.evaluate(conversion.baseResource.value); gain = Decimal.floor(gain).max(conversion.gainResource.value); - if (unref(conversion.buyMax) === false) { gain = gain.min(Decimal.add(conversion.gainResource.value, 1)); } @@ -412,7 +216,7 @@ export function createIndependentConversion( if (conversion.actualGain == null) { conversion.actualGain = computed(() => { let gain = Decimal.sub( - Decimal.floor(conversion.scaling.currentGain(conversion as GenericConversion)), + conversion.formula.evaluate(conversion.baseResource.value), conversion.gainResource.value ).max(0); @@ -424,11 +228,7 @@ export function createIndependentConversion( } setDefault(conversion, "convert", function () { const amountGained = unref((conversion as GenericConversion).actualGain); - conversion.gainResource.value = conversion.gainModifier - ? conversion.gainModifier.apply( - unref((conversion as GenericConversion).currentGain) - ) - : unref((conversion as GenericConversion).currentGain); + conversion.gainResource.value = unref((conversion as GenericConversion).currentGain); (conversion as GenericConversion).spend(amountGained); conversion.onConvert?.(amountGained); }); @@ -466,71 +266,3 @@ export function setupPassiveGeneration( } }); } - -/** - * Given a value, this function finds the amount above a certain value and raises it to a power. - * If the power is <1, this will effectively make the value scale slower after the cap. - * @param value The raw value. - * @param cap The value after which the softcap should be applied. - * @param power The power to raise value above the cap to. - * @example - * A softcap added via `addSoftcap(scaling, 100, 0.5)` would produce the following values: - * | Raw Value | Softcapped Value | - * | --------- | ---------------- | - * | 1 | 1 | - * | 100 | 100 | - * | 125 | 105 | - * | 200 | 110 | - */ -export function softcap( - value: DecimalSource, - cap: DecimalSource, - power: DecimalSource = 0.5 -): DecimalSource { - if (Decimal.lte(value, cap)) { - return value; - } else { - return Decimal.pow(value, power).times(Decimal.pow(cap, Decimal.sub(1, power))); - } -} - -/** - * Creates a scaling function based off an existing scaling function, with a softcap applied to it. - * The softcap will take any value above a certain value and raise it to a power. - * If the power is <1, this will effectively make the value scale slower after the cap. - * @param scaling The raw scaling function. - * @param cap The value after which the softcap should be applied. - * @param power The power to raise value about the cap to. - * @see {@link softcap}. - */ -export function addSoftcap( - scaling: ScalingFunction, - cap: ProcessedComputable, - power: ProcessedComputable = 0.5 -): ScalingFunction { - return { - ...scaling, - currentAt: conversion => - softcap(scaling.currentAt(conversion), unref(cap), Decimal.recip(unref(power))), - nextAt: conversion => - softcap(scaling.nextAt(conversion), unref(cap), Decimal.recip(unref(power))), - currentGain: conversion => - softcap(scaling.currentGain(conversion), unref(cap), unref(power)) - }; -} - -/** - * Creates a scaling function off an existing function, with a hardcap applied to it. - * The harcap will ensure that the currentGain will stop at a given cap. - * @param scaling The raw scaling function. - * @param cap The maximum value the scaling function can output. - */ -export function addHardcap( - scaling: ScalingFunction, - cap: ProcessedComputable -): ScalingFunction { - return { - ...scaling, - currentGain: conversion => Decimal.min(scaling.currentGain(conversion), unref(cap)) - }; -} diff --git a/src/game/formulas/formulas.ts b/src/game/formulas/formulas.ts index 12ea545..9806580 100644 --- a/src/game/formulas/formulas.ts +++ b/src/game/formulas/formulas.ts @@ -315,6 +315,7 @@ export default class Formula { return new Formula({ variable: value }) as InvertibleFormula; } + // TODO add integration support to step-wise functions /** * Creates a step-wise formula. After {@ref start} the formula will have an additional modifier. * This function assumes the incoming {@ref value} will be continuous and monotonically increasing. From 98cb997056f4de177004092038bd3b519a39b2cf Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 21:47:31 -0500 Subject: [PATCH 24/36] Make conversions pass the variable to the formula --- src/features/conversion.ts | 41 ++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/features/conversion.ts b/src/features/conversion.ts index f5146ca..9eeb792 100644 --- a/src/features/conversion.ts +++ b/src/features/conversion.ts @@ -1,7 +1,12 @@ import type { OptionsFunc, Replace } from "features/feature"; import { setDefault } from "features/feature"; import type { Resource } from "features/resources/resource"; -import { InvertibleFormula } from "game/formulas/types"; +import Formula from "game/formulas/formulas"; +import { + IntegrableFormula, + InvertibleFormula, + InvertibleIntegralFormula +} from "game/formulas/types"; import type { BaseLayer } from "game/layers"; import type { DecimalSource } from "util/bignum"; import Decimal from "util/bignum"; @@ -15,9 +20,11 @@ import { computed, unref } from "vue"; export interface ConversionOptions { /** * The formula used to determine how much {@link gainResource} should be earned by this converting. - * When evaluating, the variable will always be overidden to the amount of {@link baseResource}. + * The passed value will be a Formula representing the {@link baseResource} variable. */ - formula: InvertibleFormula; + formula: ( + variable: InvertibleFormula & IntegrableFormula & InvertibleIntegralFormula + ) => InvertibleFormula; /** * How much of the output resource the conversion can currently convert for. * Typically this will be set for you in a conversion constructor. @@ -98,6 +105,7 @@ export type Conversion = Replace< export type GenericConversion = Replace< Conversion, { + formula: InvertibleFormula; currentGain: ProcessedComputable; actualGain: ProcessedComputable; currentAt: ProcessedComputable; @@ -120,9 +128,14 @@ export function createConversion( return createLazyProxy(() => { const conversion = optionsFunc(); + (conversion as GenericConversion).formula = conversion.formula( + Formula.variable(conversion.baseResource) + ); if (conversion.currentGain == null) { conversion.currentGain = computed(() => { - let gain = conversion.formula.evaluate(conversion.baseResource.value); + let gain = (conversion as GenericConversion).formula.evaluate( + conversion.baseResource.value + ); gain = Decimal.floor(gain).max(0); if (unref(conversion.buyMax) === false) { @@ -136,14 +149,14 @@ export function createConversion( } if (conversion.currentAt == null) { conversion.currentAt = computed(() => { - return conversion.formula.invert( + return (conversion as GenericConversion).formula.invert( Decimal.floor(unref((conversion as GenericConversion).currentGain)) ); }); } if (conversion.nextAt == null) { conversion.nextAt = computed(() => { - return conversion.formula.invert( + return (conversion as GenericConversion).formula.invert( Decimal.floor(unref((conversion as GenericConversion).currentGain)).add(1) ); }); @@ -205,7 +218,9 @@ export function createIndependentConversion( if (conversion.currentGain == null) { conversion.currentGain = computed(() => { - let gain = conversion.formula.evaluate(conversion.baseResource.value); + let gain = (conversion as unknown as GenericConversion).formula.evaluate( + conversion.baseResource.value + ); gain = Decimal.floor(gain).max(conversion.gainResource.value); if (unref(conversion.buyMax) === false) { gain = gain.min(Decimal.add(conversion.gainResource.value, 1)); @@ -216,7 +231,9 @@ export function createIndependentConversion( if (conversion.actualGain == null) { conversion.actualGain = computed(() => { let gain = Decimal.sub( - conversion.formula.evaluate(conversion.baseResource.value), + (conversion as unknown as GenericConversion).formula.evaluate( + conversion.baseResource.value + ), conversion.gainResource.value ).max(0); @@ -227,9 +244,11 @@ export function createIndependentConversion( }); } setDefault(conversion, "convert", function () { - const amountGained = unref((conversion as GenericConversion).actualGain); - conversion.gainResource.value = unref((conversion as GenericConversion).currentGain); - (conversion as GenericConversion).spend(amountGained); + const amountGained = unref((conversion as unknown as GenericConversion).actualGain); + conversion.gainResource.value = unref( + (conversion as unknown as GenericConversion).currentGain + ); + (conversion as unknown as GenericConversion).spend(amountGained); conversion.onConvert?.(amountGained); }); From 804d48ae80efd8c4ec7b85bda37b2dbca263cbfe Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 21:56:43 -0500 Subject: [PATCH 25/36] Fix cost requirement requiring spendResources Also fixed it defaulting to false instead of true --- src/game/requirements.tsx | 48 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/game/requirements.tsx b/src/game/requirements.tsx index 9e775de..acb7f7b 100644 --- a/src/game/requirements.tsx +++ b/src/game/requirements.tsx @@ -1,5 +1,12 @@ import { isArray } from "@vue/shared"; -import { CoercableComponent, isVisible, jsx, setDefault, Visibility } from "features/feature"; +import { + CoercableComponent, + isVisible, + jsx, + Replace, + setDefault, + Visibility +} from "features/feature"; import { displayResource, Resource } from "features/resources/resource"; import Decimal, { DecimalSource } from "lib/break_eternity"; import { @@ -78,7 +85,7 @@ export interface CostRequirementOptions { * When calculating multiple levels to be handled at once, whether it should consider resources used for each level as spent. Setting this to false causes calculations to be faster with larger numbers and supports more math functions. * @see {Formula} */ - spendResources: Computable; + spendResources?: Computable; /** * Pass-through to {@link Requirement.pay}. May be required for maximizing support. * @see {@link cost} for restrictions on maximizing support. @@ -86,7 +93,15 @@ export interface CostRequirementOptions { pay?: (amount?: DecimalSource) => void; } -export type CostRequirement = Requirement & CostRequirementOptions; +export type CostRequirement = Replace< + Requirement & CostRequirementOptions, + { + cost: ProcessedComputable | GenericFormula; + visibility: ProcessedComputable; + requiresPay: ProcessedComputable; + spendResources: ProcessedComputable; + } +>; /** * Lazily creates a requirement with the given options, that is based on meeting an amount of a resource. @@ -109,13 +124,7 @@ export function createCostRequirement( {displayResource( req.resource, req.cost instanceof Formula - ? calculateCost( - req.cost, - amount ?? 1, - unref( - req.spendResources as ProcessedComputable | undefined - ) ?? true - ) + ? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean) : unref(req.cost as ProcessedComputable) )}{" "} {req.resource.displayName} @@ -127,13 +136,7 @@ export function createCostRequirement( {displayResource( req.resource, req.cost instanceof Formula - ? calculateCost( - req.cost, - amount ?? 1, - unref( - req.spendResources as ProcessedComputable | undefined - ) ?? true - ) + ? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean) : unref(req.cost as ProcessedComputable) )}{" "} {req.resource.displayName} @@ -146,16 +149,11 @@ export function createCostRequirement( processComputable(req as T, "requiresPay"); setDefault(req, "requiresPay", true); processComputable(req as T, "spendResources"); - setDefault(req, "spendResources", false); + setDefault(req, "spendResources", true); setDefault(req, "pay", function (amount?: DecimalSource) { const cost = req.cost instanceof Formula - ? calculateCost( - req.cost, - amount ?? 1, - unref(req.spendResources as ProcessedComputable | undefined) ?? - true - ) + ? calculateCost(req.cost, amount ?? 1, unref(req.spendResources) as boolean) : unref(req.cost as ProcessedComputable); req.resource.value = Decimal.sub(req.resource.value, cost).max(0); }); @@ -166,7 +164,7 @@ export function createCostRequirement( req.requirementMet = calculateMaxAffordable( req.cost as InvertibleFormula, req.resource, - unref(req.spendResources as ProcessedComputable | undefined) ?? true + unref(req.spendResources) as boolean ); } else { req.requirementMet = computed(() => { From f717fe8db286c0048ea4dd6b9424da70c58c25f0 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 21:42:17 -0500 Subject: [PATCH 26/36] Make every component use GenericComponent some projects seem to require this and others not? --- src/features/achievements/achievement.tsx | 5 +++-- src/features/bars/bar.ts | 4 ++-- src/features/boards/board.ts | 6 +++--- src/features/challenges/challenge.tsx | 12 +++++++++--- src/features/clickables/clickable.ts | 4 ++-- src/features/grids/grid.ts | 12 +++++++++--- src/features/infoboxes/infobox.ts | 12 +++++++++--- src/features/links/links.ts | 6 +++--- src/features/milestones/milestone.tsx | 4 ++-- src/features/repeatable.tsx | 12 +++++++++--- src/features/tabs/tab.ts | 12 +++++++++--- src/features/tabs/tabFamily.ts | 16 +++++++++++----- src/features/trees/tree.ts | 16 +++++++++++----- src/features/upgrades/upgrade.ts | 4 ++-- 14 files changed, 84 insertions(+), 41 deletions(-) diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 7daaf7e..4776fd1 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -3,6 +3,7 @@ import { CoercableComponent, Component, GatherProps, + GenericComponent, getUniqueID, isVisible, OptionsFunc, @@ -48,7 +49,7 @@ export interface BaseAchievement { earned: Persistent; complete: VoidFunction; type: typeof AchievementType; - [Component]: typeof AchievementComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -79,7 +80,7 @@ export function createAchievement( const achievement = optionsFunc?.() ?? ({} as ReturnType>); achievement.id = getUniqueID("achievement-"); achievement.type = AchievementType; - achievement[Component] = AchievementComponent; + achievement[Component] = AchievementComponent as GenericComponent; achievement.earned = earned; achievement.complete = function () { diff --git a/src/features/bars/bar.ts b/src/features/bars/bar.ts index f0436f4..bab5887 100644 --- a/src/features/bars/bar.ts +++ b/src/features/bars/bar.ts @@ -40,7 +40,7 @@ export interface BarOptions { export interface BaseBar { id: string; type: typeof BarType; - [Component]: typeof BarComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -77,7 +77,7 @@ export function createBar( const bar = optionsFunc(); bar.id = getUniqueID("bar-"); bar.type = BarType; - bar[Component] = BarComponent; + bar[Component] = BarComponent as GenericComponent; processComputable(bar as T, "visibility"); setDefault(bar, "visibility", Visibility.Visible); diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts index cc17d2e..528bc26 100644 --- a/src/features/boards/board.ts +++ b/src/features/boards/board.ts @@ -1,5 +1,5 @@ import BoardComponent from "features/boards/Board.vue"; -import type { OptionsFunc, Replace, StyleValue } from "features/feature"; +import type { GenericComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; import { Component, findFeatures, @@ -178,7 +178,7 @@ export interface BaseBoard { selectedAction: Ref; mousePosition: Ref<{ x: number; y: number } | null>; type: typeof BoardType; - [Component]: typeof BoardComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -221,7 +221,7 @@ export function createBoard( const board = optionsFunc(); board.id = getUniqueID("board-"); board.type = BoardType; - board[Component] = BoardComponent; + board[Component] = BoardComponent as GenericComponent; if (board.state) { deletePersistent(state); diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index 34f850c..2925595 100644 --- a/src/features/challenges/challenge.tsx +++ b/src/features/challenges/challenge.tsx @@ -1,7 +1,13 @@ import { isArray } from "@vue/shared"; import Toggle from "components/fields/Toggle.vue"; import ChallengeComponent from "features/challenges/Challenge.vue"; -import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; +import type { + CoercableComponent, + GenericComponent, + OptionsFunc, + Replace, + StyleValue +} from "features/feature"; import { Component, GatherProps, @@ -67,7 +73,7 @@ export interface BaseChallenge { toggle: VoidFunction; complete: (remainInChallenge?: boolean) => void; type: typeof ChallengeType; - [Component]: typeof ChallengeComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -106,7 +112,7 @@ export function createChallenge( challenge.id = getUniqueID("challenge-"); challenge.type = ChallengeType; - challenge[Component] = ChallengeComponent; + challenge[Component] = ChallengeComponent as GenericComponent; challenge.completions = completions; challenge.active = active; diff --git a/src/features/clickables/clickable.ts b/src/features/clickables/clickable.ts index c7c83e3..b44854b 100644 --- a/src/features/clickables/clickable.ts +++ b/src/features/clickables/clickable.ts @@ -42,7 +42,7 @@ export interface ClickableOptions { export interface BaseClickable { id: string; type: typeof ClickableType; - [Component]: typeof ClickableComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -73,7 +73,7 @@ export function createClickable( const clickable = optionsFunc?.() ?? ({} as ReturnType>); clickable.id = getUniqueID("clickable-"); clickable.type = ClickableType; - clickable[Component] = ClickableComponent; + clickable[Component] = ClickableComponent as GenericComponent; processComputable(clickable as T, "visibility"); setDefault(clickable, "visibility", Visibility.Visible); diff --git a/src/features/grids/grid.ts b/src/features/grids/grid.ts index 53fd7fb..b7899ae 100644 --- a/src/features/grids/grid.ts +++ b/src/features/grids/grid.ts @@ -1,4 +1,10 @@ -import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; +import type { + CoercableComponent, + GenericComponent, + OptionsFunc, + Replace, + StyleValue +} from "features/feature"; import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature"; import GridComponent from "features/grids/Grid.vue"; import type { Persistent, State } from "game/persistence"; @@ -206,7 +212,7 @@ export interface BaseGrid { cells: Record; cellState: Persistent>; type: typeof GridType; - [Component]: typeof GridComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -242,7 +248,7 @@ export function createGrid( return createLazyProxy(() => { const grid = optionsFunc(); grid.id = getUniqueID("grid-"); - grid[Component] = GridComponent; + grid[Component] = GridComponent as GenericComponent; grid.cellState = cellState; diff --git a/src/features/infoboxes/infobox.ts b/src/features/infoboxes/infobox.ts index 81d7bf6..ccd2bd3 100644 --- a/src/features/infoboxes/infobox.ts +++ b/src/features/infoboxes/infobox.ts @@ -1,4 +1,10 @@ -import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; +import type { + CoercableComponent, + GenericComponent, + OptionsFunc, + Replace, + StyleValue +} from "features/feature"; import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature"; import InfoboxComponent from "features/infoboxes/Infobox.vue"; import type { Persistent } from "game/persistence"; @@ -30,7 +36,7 @@ export interface BaseInfobox { id: string; collapsed: Persistent; type: typeof InfoboxType; - [Component]: typeof InfoboxComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -63,7 +69,7 @@ export function createInfobox( const infobox = optionsFunc(); infobox.id = getUniqueID("infobox-"); infobox.type = InfoboxType; - infobox[Component] = InfoboxComponent; + infobox[Component] = InfoboxComponent as GenericComponent; infobox.collapsed = collapsed; diff --git a/src/features/links/links.ts b/src/features/links/links.ts index 5ab8779..77ea0ba 100644 --- a/src/features/links/links.ts +++ b/src/features/links/links.ts @@ -1,4 +1,4 @@ -import type { OptionsFunc, Replace } from "features/feature"; +import type { GenericComponent, OptionsFunc, Replace } from "features/feature"; import { GatherProps, Component } from "features/feature"; import type { Position } from "game/layers"; import type { Computable, GetComputableType, ProcessedComputable } from "util/computed"; @@ -22,7 +22,7 @@ export interface LinksOptions { export interface BaseLinks { type: typeof LinksType; - [Component]: typeof LinksComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -46,7 +46,7 @@ export function createLinks( return createLazyProxy(() => { const links = optionsFunc(); links.type = LinksType; - links[Component] = LinksComponent; + links[Component] = LinksComponent as GenericComponent; processComputable(links as T, "links"); diff --git a/src/features/milestones/milestone.tsx b/src/features/milestones/milestone.tsx index 30cd243..0a973db 100644 --- a/src/features/milestones/milestone.tsx +++ b/src/features/milestones/milestone.tsx @@ -69,7 +69,7 @@ export interface BaseMilestone { earned: Persistent; complete: VoidFunction; type: typeof MilestoneType; - [Component]: typeof MilestoneComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -99,7 +99,7 @@ export function createMilestone( const milestone = optionsFunc?.() ?? ({} as ReturnType>); milestone.id = getUniqueID("milestone-"); milestone.type = MilestoneType; - milestone[Component] = MilestoneComponent; + milestone[Component] = MilestoneComponent as GenericComponent; milestone.earned = earned; milestone.complete = function () { diff --git a/src/features/repeatable.tsx b/src/features/repeatable.tsx index b442068..ec338eb 100644 --- a/src/features/repeatable.tsx +++ b/src/features/repeatable.tsx @@ -1,6 +1,12 @@ import { isArray } from "@vue/shared"; import ClickableComponent from "features/clickables/Clickable.vue"; -import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; +import type { + CoercableComponent, + GenericComponent, + OptionsFunc, + Replace, + StyleValue +} from "features/feature"; import { Component, GatherProps, getUniqueID, jsx, setDefault, Visibility } from "features/feature"; import { DefaultValue, Persistent, persistent } from "game/persistence"; import { @@ -88,7 +94,7 @@ export interface BaseRepeatable { /** A symbol that helps identify features of the same type. */ type: typeof RepeatableType; /** The Vue component used to render this feature. */ - [Component]: typeof ClickableComponent; + [Component]: GenericComponent; /** A function to gather the props the vue component requires for this feature. */ [GatherProps]: () => Record; } @@ -131,7 +137,7 @@ export function createRepeatable( repeatable.id = getUniqueID("repeatable-"); repeatable.type = RepeatableType; - repeatable[Component] = ClickableComponent; + repeatable[Component] = ClickableComponent as GenericComponent; repeatable.amount = amount; repeatable.amount[DefaultValue] = repeatable.initialAmount ?? 0; diff --git a/src/features/tabs/tab.ts b/src/features/tabs/tab.ts index 3f205fc..6917009 100644 --- a/src/features/tabs/tab.ts +++ b/src/features/tabs/tab.ts @@ -1,4 +1,10 @@ -import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; +import type { + CoercableComponent, + GenericComponent, + OptionsFunc, + Replace, + StyleValue +} from "features/feature"; import { Component, GatherProps, getUniqueID } from "features/feature"; import TabComponent from "features/tabs/Tab.vue"; import type { Computable, GetComputableType } from "util/computed"; @@ -15,7 +21,7 @@ export interface TabOptions { export interface BaseTab { id: string; type: typeof TabType; - [Component]: typeof TabComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -37,7 +43,7 @@ export function createTab( const tab = optionsFunc(); tab.id = getUniqueID("tab-"); tab.type = TabType; - tab[Component] = TabComponent; + tab[Component] = TabComponent as GenericComponent; tab[GatherProps] = function (this: GenericTab) { const { display } = this; diff --git a/src/features/tabs/tabFamily.ts b/src/features/tabs/tabFamily.ts index 9652f3d..32df1e7 100644 --- a/src/features/tabs/tabFamily.ts +++ b/src/features/tabs/tabFamily.ts @@ -1,4 +1,10 @@ -import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; +import type { + CoercableComponent, + GenericComponent, + OptionsFunc, + Replace, + StyleValue +} from "features/feature"; import { Component, GatherProps, @@ -37,7 +43,7 @@ export interface TabButtonOptions { export interface BaseTabButton { type: typeof TabButtonType; - [Component]: typeof TabButtonComponent; + [Component]: GenericComponent; } export type TabButton = Replace< @@ -73,7 +79,7 @@ export interface BaseTabFamily { activeTab: Ref; selected: Persistent; type: typeof TabFamilyType; - [Component]: typeof TabFamilyComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -107,13 +113,13 @@ export function createTabFamily( tabFamily.id = getUniqueID("tabFamily-"); tabFamily.type = TabFamilyType; - tabFamily[Component] = TabFamilyComponent; + tabFamily[Component] = TabFamilyComponent as GenericComponent; tabFamily.tabs = Object.keys(tabs).reduce>( (parsedTabs, tab) => { const tabButton: TabButtonOptions & Partial = tabs[tab](); tabButton.type = TabButtonType; - tabButton[Component] = TabButtonComponent; + tabButton[Component] = TabButtonComponent as GenericComponent; processComputable(tabButton as TabButtonOptions, "visibility"); setDefault(tabButton, "visibility", Visibility.Visible); diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts index bcbf333..a3efba7 100644 --- a/src/features/trees/tree.ts +++ b/src/features/trees/tree.ts @@ -1,4 +1,10 @@ -import type { CoercableComponent, OptionsFunc, Replace, StyleValue } from "features/feature"; +import type { + CoercableComponent, + GenericComponent, + OptionsFunc, + Replace, + StyleValue +} from "features/feature"; import { Component, GatherProps, getUniqueID, setDefault, Visibility } from "features/feature"; import type { Link } from "features/links/links"; import type { GenericReset } from "features/reset"; @@ -39,7 +45,7 @@ export interface TreeNodeOptions { export interface BaseTreeNode { id: string; type: typeof TreeNodeType; - [Component]: typeof TreeNodeComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -72,7 +78,7 @@ export function createTreeNode( const treeNode = optionsFunc?.() ?? ({} as ReturnType>); treeNode.id = getUniqueID("treeNode-"); treeNode.type = TreeNodeType; - treeNode[Component] = TreeNodeComponent; + treeNode[Component] = TreeNodeComponent as GenericComponent; processComputable(treeNode as T, "visibility"); setDefault(treeNode, "visibility", Visibility.Visible); @@ -157,7 +163,7 @@ export interface BaseTree { isResetting: Ref; resettingNode: Ref; type: typeof TreeType; - [Component]: typeof TreeComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -186,7 +192,7 @@ export function createTree( const tree = optionsFunc(); tree.id = getUniqueID("tree-"); tree.type = TreeType; - tree[Component] = TreeComponent; + tree[Component] = TreeComponent as GenericComponent; tree.isResetting = ref(false); tree.resettingNode = shallowRef(null); diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index 924e28f..5bcf5df 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -61,7 +61,7 @@ export interface BaseUpgrade { canPurchase: Ref; purchase: VoidFunction; type: typeof UpgradeType; - [Component]: typeof UpgradeComponent; + [Component]: GenericComponent; [GatherProps]: () => Record; } @@ -92,7 +92,7 @@ export function createUpgrade( const upgrade = optionsFunc(); upgrade.id = getUniqueID("upgrade-"); upgrade.type = UpgradeType; - upgrade[Component] = UpgradeComponent; + upgrade[Component] = UpgradeComponent as GenericComponent; upgrade.bought = bought; upgrade.canPurchase = computed(() => requirementsMet(upgrade.requirements)); From 572566c4c15197c291b6d3d3f2ae10deb9a41bbf Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 22:03:58 -0500 Subject: [PATCH 27/36] Updated prestige layer to new conversion --- src/data/layers/prestige.tsx | 7 +++---- src/features/conversion.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/data/layers/prestige.tsx b/src/data/layers/prestige.tsx index 4490ded..30fe99f 100644 --- a/src/data/layers/prestige.tsx +++ b/src/data/layers/prestige.tsx @@ -3,7 +3,7 @@ * @hidden */ import { main } from "data/projEntry"; -import { createCumulativeConversion, createPolynomialScaling } from "features/conversion"; +import { createCumulativeConversion } from "features/conversion"; import { jsx } from "features/feature"; import { createHotkey } from "features/hotkey"; import { createReset } from "features/reset"; @@ -23,10 +23,9 @@ const layer = createLayer(id, function (this: BaseLayer) { const points = createResource(0, "prestige points"); const conversion = createCumulativeConversion(() => ({ - scaling: createPolynomialScaling(10, 0.5), + formula: x => x.div(10).sqrt(), baseResource: main.points, - gainResource: points, - roundUpCost: true + gainResource: points })); const reset = createReset(() => ({ diff --git a/src/features/conversion.ts b/src/features/conversion.ts index 9eeb792..1ccf26b 100644 --- a/src/features/conversion.ts +++ b/src/features/conversion.ts @@ -92,6 +92,7 @@ export interface BaseConversion { export type Conversion = Replace< T & BaseConversion, { + formula: InvertibleFormula; currentGain: GetComputableTypeWithDefault>; actualGain: GetComputableTypeWithDefault>; currentAt: GetComputableTypeWithDefault>; @@ -105,7 +106,6 @@ export type Conversion = Replace< export type GenericConversion = Replace< Conversion, { - formula: InvertibleFormula; currentGain: ProcessedComputable; actualGain: ProcessedComputable; currentAt: ProcessedComputable; From 528afc6b590edf235e6c6f26433594bd32a6e8b4 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 22:09:13 -0500 Subject: [PATCH 28/36] Fix tooltips being pinnable causing SO --- src/features/tooltips/tooltip.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/features/tooltips/tooltip.ts b/src/features/tooltips/tooltip.ts index 13cb931..2cb977d 100644 --- a/src/features/tooltips/tooltip.ts +++ b/src/features/tooltips/tooltip.ts @@ -1,6 +1,6 @@ import type { CoercableComponent, Replace, StyleValue } from "features/feature"; import { Component, GatherProps, setDefault } from "features/feature"; -import { persistent } from "game/persistence"; +import { deletePersistent, Persistent, persistent } from "game/persistence"; import { Direction } from "util/common"; import type { Computable, @@ -71,18 +71,22 @@ export function addTooltip( processComputable(options as T, "yoffset"); if (options.pinnable) { - if ("pinned" in element) { - console.error( - "Cannot add pinnable tooltip to element that already has a property called 'pinned'" - ); - options.pinnable = false; - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (element as any).pinned = options.pinned = persistent(false, false); - } + options.pinned = persistent(false, false); } nextTick(() => { + if (options.pinnable) { + if ("pinned" in element) { + console.error( + "Cannot add pinnable tooltip to element that already has a property called 'pinned'" + ); + options.pinnable = false; + deletePersistent(options.pinned as Persistent); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (element as any).pinned = options.pinned; + } + } const elementComponent = element[Component]; element[Component] = TooltipComponent; const elementGatherProps = element[GatherProps].bind(element); From 165eba688e2fc67c2073f8378d340655d7acbd2f Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 22:25:18 -0500 Subject: [PATCH 29/36] Slightly improve resource imports --- src/features/resources/resource.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/features/resources/resource.ts b/src/features/resources/resource.ts index be42e7e..8e203d3 100644 --- a/src/features/resources/resource.ts +++ b/src/features/resources/resource.ts @@ -1,8 +1,6 @@ import { globalBus } from "game/events"; -import { NonPersistent, Persistent, State } from "game/persistence"; -import { persistent } from "game/persistence"; -import player from "game/player"; -import settings from "game/settings"; +import type { Persistent, State } from "game/persistence"; +import { NonPersistent, persistent } from "game/persistence"; import type { DecimalSource } from "util/bignum"; import Decimal, { format, formatWhole } from "util/bignum"; import type { ProcessedComputable } from "util/computed"; From e6c7ad62a79d991d96f1282d87be23b09692d490 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 23:33:49 -0500 Subject: [PATCH 30/36] Document challenge --- src/features/challenges/challenge.tsx | 69 +++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/src/features/challenges/challenge.tsx b/src/features/challenges/challenge.tsx index 2925595..3a1ba48 100644 --- a/src/features/challenges/challenge.tsx +++ b/src/features/challenges/challenge.tsx @@ -36,47 +36,87 @@ import { createLazyProxy } from "util/proxies"; import type { Ref, WatchStopHandle } from "vue"; import { computed, unref, watch } from "vue"; -export const ChallengeType = Symbol("ChallengeType"); +/** A symbol used to identify {@link Challenge} features. */ +export const ChallengeType = Symbol("Challenge"); +/** + * An object that configures a {@link Challenge}. + */ export interface ChallengeOptions { + /** Whether this challenge should be visible. */ visibility?: Computable; + /** Whether this challenge can be started. */ canStart?: Computable; + /** The reset function for this challenge. */ reset?: GenericReset; + /** The requirement(s) to complete this challenge. */ requirements: Requirements; + /** Whether or not completing this challenge should grant multiple completions if requirements met. Requires {@link requirements} to be a requirement or array of requirements with {@link Requirement.canMaximize} true. */ maximize?: Computable; + /** The maximum number of times the challenge can be completed. */ completionLimit?: Computable; + /** Shows a marker on the corner of the feature. */ mark?: Computable; + /** Dictionary of CSS classes to apply to this feature. */ classes?: Computable>; + /** CSS to apply to this feature. */ style?: Computable; + /** The display to use for this challenge. */ display?: Computable< | CoercableComponent | { + /** A header to appear at the top of the display. */ title?: CoercableComponent; + /** The main text that appears in the display. */ description: CoercableComponent; + /** A description of the current goal for this challenge. */ goal?: CoercableComponent; + /** A description of what will change upon completing this challenge. */ reward?: CoercableComponent; + /** A description of the current effect of this challenge. */ effectDisplay?: CoercableComponent; } >; + /** A function that is called when the challenge is completed. */ onComplete?: VoidFunction; + /** A function that is called when the challenge is exited. */ onExit?: VoidFunction; + /** A function that is called when the challenge is entered. */ onEnter?: VoidFunction; } +/** + * The properties that are added onto a processed {@link ChallengeOptions} to create a {@link Challenge}. + */ export interface BaseChallenge { + /** An auto-generated ID for identifying challenges that appear in the DOM. Will not persist between refreshes or updates. */ id: string; + /** The current amount of times this challenge can be completed. */ canComplete: Ref; + /** The current number of times this challenge has been completed. */ completions: Persistent; + /** Whether or not this challenge has been completed. */ completed: Ref; + /** Whether or not this challenge's completion count is at its limit. */ maxed: Ref; + /** Whether or not this challenge is currently active. */ active: Persistent; + /** A function to enter or leave the challenge. */ toggle: VoidFunction; + /** + * A function to complete this challenge. + * @param remainInChallenge - Optional parameter to specify if the challenge should remain active after completion. + */ complete: (remainInChallenge?: boolean) => void; + /** A symbol that helps identify features of the same type. */ type: typeof ChallengeType; + /** The Vue component used to render this feature. */ [Component]: GenericComponent; + /** A function to gather the props the vue component requires for this feature. */ [GatherProps]: () => Record; } +/** An object that represents a feature that can be entered and exited, and have one or more completions with scaling requirements. */ export type Challenge = Replace< T & BaseChallenge, { @@ -92,6 +132,7 @@ export type Challenge = Replace< } >; +/** A type that matches any valid {@link Challenge} object. */ export type GenericChallenge = Replace< Challenge, { @@ -102,6 +143,10 @@ export type GenericChallenge = Replace< } >; +/** + * Lazily creates a challenge with the given options. + * @param optionsFunc Challenge options. + */ export function createChallenge( optionsFunc: OptionsFunc ): Challenge { @@ -248,6 +293,12 @@ export function createChallenge( }); } +/** + * This will automatically complete a challenge when it's requirements are met. + * @param challenge The challenge to auto-complete + * @param autoActive Whether or not auto-completing should currently occur + * @param exitOnComplete Whether or not to exit the challenge after auto-completion + */ export function setupAutoComplete( challenge: GenericChallenge, autoActive: Computable = true, @@ -264,19 +315,27 @@ export function setupAutoComplete( ); } +/** + * Utility for taking an array of challenges where only one may be active at a time, and giving a ref to the one currently active (or null if none are active) + * @param challenges The list of challenges that are mutually exclusive + */ export function createActiveChallenge( challenges: GenericChallenge[] -): Ref { - return computed(() => challenges.find(challenge => challenge.active.value)); +): Ref { + return computed(() => challenges.find(challenge => challenge.active.value) ?? null); } +/** + * Utility for reporting if any challenge in a list is currently active. Intended for preventing entering a challenge if another is already active. + * @param challenges List of challenges that are mutually exclusive + */ export function isAnyChallengeActive( - challenges: GenericChallenge[] | Ref + challenges: GenericChallenge[] | Ref ): Ref { if (isArray(challenges)) { challenges = createActiveChallenge(challenges); } - return computed(() => (challenges as Ref).value != null); + return computed(() => (challenges as Ref).value != null); } declare module "game/settings" { From 742d2293d043d8c3e38c58ff3d844734c78e9757 Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 2 Apr 2023 23:49:51 -0500 Subject: [PATCH 31/36] Made achievements use requirements system, and document them --- src/features/achievements/achievement.tsx | 83 ++++++++++++++++------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 4776fd1..baa86ca 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -1,3 +1,4 @@ +import { isArray } from "@vue/shared"; import AchievementComponent from "features/achievements/Achievement.vue"; import { CoercableComponent, @@ -5,7 +6,6 @@ import { GatherProps, GenericComponent, getUniqueID, - isVisible, OptionsFunc, Replace, setDefault, @@ -16,6 +16,12 @@ import "game/notifications"; import type { Persistent } from "game/persistence"; import { persistent } from "game/persistence"; import player from "game/player"; +import { + createBooleanRequirement, + createVisibilityRequirement, + Requirements, + requirementsMet +} from "game/requirements"; import settings from "game/settings"; import type { Computable, @@ -31,28 +37,50 @@ import { useToast } from "vue-toastification"; const toast = useToast(); +/** A symbol used to identify {@link Achievement} features. */ export const AchievementType = Symbol("Achievement"); +/** + * An object that configures an {@link Achievement}. + */ export interface AchievementOptions { + /** Whether this achievement should be visible. */ visibility?: Computable; - shouldEarn?: () => boolean; + /** The requirement(s) to earn this achievement. Can be left null if using {@link BaseAchievement.complete}. */ + requirements?: Requirements; + /** The display to use for this achievement. */ display?: Computable; + /** Shows a marker on the corner of the feature. */ mark?: Computable; + /** An image to display as the background for this achievement. */ image?: Computable; + /** CSS to apply to this feature. */ style?: Computable; + /** Dictionary of CSS classes to apply to this feature. */ classes?: Computable>; + /** A function that is called when the achievement is completed. */ onComplete?: VoidFunction; } +/** + * The properties that are added onto a processed {@link AchievementOptions} to create an {@link Achievement}. + */ export interface BaseAchievement { + /** An auto-generated ID for identifying achievements that appear in the DOM. Will not persist between refreshes or updates. */ id: string; + /** Whether or not this achievement has been earned. */ earned: Persistent; + /** A function to complete this achievement. */ complete: VoidFunction; + /** A symbol that helps identify features of the same type. */ type: typeof AchievementType; + /** The Vue component used to render this feature. */ [Component]: GenericComponent; + /** A function to gather the props the vue component requires for this feature. */ [GatherProps]: () => Record; } +/** An object that represents a feature with that is passively earned upon meeting certain requirements. */ export type Achievement = Replace< T & BaseAchievement, { @@ -65,6 +93,7 @@ export type Achievement = Replace< } >; +/** A type that matches any valid {@link Achievement} object. */ export type GenericAchievement = Replace< Achievement, { @@ -72,6 +101,10 @@ export type GenericAchievement = Replace< } >; +/** + * Lazily creates a achievement with the given options. + * @param optionsFunc Achievement options. + */ export function createAchievement( optionsFunc?: OptionsFunc ): Achievement { @@ -85,6 +118,21 @@ export function createAchievement( achievement.earned = earned; achievement.complete = function () { earned.value = true; + const genericAchievement = achievement as GenericAchievement; + genericAchievement.onComplete?.(); + if (genericAchievement.display != null) { + const Display = coerceComponent(unref(genericAchievement.display)); + toast.info( +
+

Achievement earned!

+
+ {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore */} + +
+
+ ); + } }; processComputable(achievement as T, "visibility"); @@ -100,30 +148,19 @@ export function createAchievement( return { visibility, display, earned, image, style: unref(style), classes, mark, id }; }; - if (achievement.shouldEarn) { + if (achievement.requirements) { const genericAchievement = achievement as GenericAchievement; + const requirements = [ + createVisibilityRequirement(genericAchievement), + createBooleanRequirement(() => !genericAchievement.earned.value), + ...(isArray(achievement.requirements) + ? achievement.requirements + : [achievement.requirements]) + ]; watchEffect(() => { if (settings.active !== player.id) return; - if ( - !genericAchievement.earned.value && - isVisible(genericAchievement.visibility) && - genericAchievement.shouldEarn?.() - ) { - genericAchievement.earned.value = true; - genericAchievement.onComplete?.(); - if (genericAchievement.display != null) { - const Display = coerceComponent(unref(genericAchievement.display)); - toast.info( -
-

Achievement earned!

-
- {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - -
-
- ); - } + if (requirementsMet(requirements)) { + genericAchievement.complete(); } }); } From 7c7fb38dd802a9d63227b3118fd624688f314f9f Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Mon, 3 Apr 2023 00:34:45 -0500 Subject: [PATCH 32/36] Merge milestones and achievements Yay for removing a whole redundant feature! --- src/data/common.tsx | 36 ++-- src/features/achievements/Achievement.vue | 82 +++++++- src/features/achievements/achievement.tsx | 141 ++++++++++++- src/features/milestones/Milestone.vue | 128 ------------ src/features/milestones/milestone.tsx | 235 ---------------------- 5 files changed, 226 insertions(+), 396 deletions(-) delete mode 100644 src/features/milestones/Milestone.vue delete mode 100644 src/features/milestones/milestone.tsx diff --git a/src/data/common.tsx b/src/data/common.tsx index befb408..d163f72 100644 --- a/src/data/common.tsx +++ b/src/data/common.tsx @@ -1,10 +1,10 @@ import Collapsible from "components/layout/Collapsible.vue"; +import { GenericAchievement } from "features/achievements/achievement"; import type { Clickable, ClickableOptions, GenericClickable } from "features/clickables/clickable"; import { createClickable } from "features/clickables/clickable"; import type { GenericConversion } from "features/conversion"; import type { CoercableComponent, JSXFunction, OptionsFunc, Replace } from "features/feature"; import { jsx, setDefault } from "features/feature"; -import { GenericMilestone } from "features/milestones/milestone"; import { displayResource, Resource } from "features/resources/resource"; import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree"; import { createTreeNode } from "features/trees/tree"; @@ -384,35 +384,35 @@ export function colorText(textToColor: string, color = "var(--accent2)"): JSX.El } /** - * Creates a collapsible display of a list of milestones - * @param milestones A dictionary of the milestones to display, inserted in the order from easiest to hardest + * Creates a collapsible display of a list of achievements + * @param achievements A dictionary of the achievements to display, inserted in the order from easiest to hardest */ -export function createCollapsibleMilestones(milestones: Record) { - // Milestones are typically defined from easiest to hardest, and we want to show hardest first - const orderedMilestones = Object.values(milestones).reverse(); - const collapseMilestones = persistent(true, false); - const lockedMilestones = computed(() => - orderedMilestones.filter(m => m.earned.value === false) +export function createCollapsibleAchievements(achievements: Record) { + // Achievements are typically defined from easiest to hardest, and we want to show hardest first + const orderedAchievements = Object.values(achievements).reverse(); + const collapseAchievements = persistent(true, false); + const lockedAchievements = computed(() => + orderedAchievements.filter(m => m.earned.value === false) ); const { firstFeature, collapsedContent, hasCollapsedContent } = getFirstFeature( - orderedMilestones, + orderedAchievements, m => m.earned.value ); const display = jsx(() => { - const milestonesToDisplay = [...lockedMilestones.value]; + const achievementsToDisplay = [...lockedAchievements.value]; if (firstFeature.value) { - milestonesToDisplay.push(firstFeature.value); + achievementsToDisplay.push(firstFeature.value); } return renderColJSX( - ...milestonesToDisplay, + ...achievementsToDisplay, jsx(() => ( @@ -420,7 +420,7 @@ export function createCollapsibleMilestones(milestones: Record - + - - - diff --git a/src/features/milestones/milestone.tsx b/src/features/milestones/milestone.tsx deleted file mode 100644 index 0a973db..0000000 --- a/src/features/milestones/milestone.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import Select from "components/fields/Select.vue"; -import type { - CoercableComponent, - GenericComponent, - OptionsFunc, - Replace, - StyleValue -} from "features/feature"; -import { - Component, - GatherProps, - getUniqueID, - isVisible, - jsx, - setDefault, - Visibility -} from "features/feature"; -import MilestoneComponent from "features/milestones/Milestone.vue"; -import { globalBus } from "game/events"; -import "game/notifications"; -import type { Persistent } from "game/persistence"; -import { persistent } from "game/persistence"; -import player from "game/player"; -import settings, { registerSettingField } from "game/settings"; -import { camelToTitle } from "util/common"; -import type { - Computable, - GetComputableType, - GetComputableTypeWithDefault, - ProcessedComputable -} from "util/computed"; -import { processComputable } from "util/computed"; -import { createLazyProxy } from "util/proxies"; -import { coerceComponent, isCoercableComponent } from "util/vue"; -import { computed, unref, watchEffect } from "vue"; -import { useToast } from "vue-toastification"; - -const toast = useToast(); - -export const MilestoneType = Symbol("Milestone"); - -export enum MilestoneDisplay { - All = "all", - //Last = "last", - Configurable = "configurable", - Incomplete = "incomplete", - None = "none" -} - -export interface MilestoneOptions { - visibility?: Computable; - shouldEarn?: () => boolean; - style?: Computable; - classes?: Computable>; - display?: Computable< - | CoercableComponent - | { - requirement: CoercableComponent; - effectDisplay?: CoercableComponent; - optionsDisplay?: CoercableComponent; - } - >; - showPopups?: Computable; - onComplete?: VoidFunction; -} - -export interface BaseMilestone { - id: string; - earned: Persistent; - complete: VoidFunction; - type: typeof MilestoneType; - [Component]: GenericComponent; - [GatherProps]: () => Record; -} - -export type Milestone = Replace< - T & BaseMilestone, - { - visibility: GetComputableTypeWithDefault; - style: GetComputableType; - classes: GetComputableType; - display: GetComputableType; - showPopups: GetComputableType; - } ->; - -export type GenericMilestone = Replace< - Milestone, - { - visibility: ProcessedComputable; - } ->; - -export function createMilestone( - optionsFunc?: OptionsFunc -): Milestone { - const earned = persistent(false, false); - return createLazyProxy(() => { - const milestone = optionsFunc?.() ?? ({} as ReturnType>); - milestone.id = getUniqueID("milestone-"); - milestone.type = MilestoneType; - milestone[Component] = MilestoneComponent as GenericComponent; - - milestone.earned = earned; - milestone.complete = function () { - const genericMilestone = milestone as GenericMilestone; - earned.value = true; - genericMilestone.onComplete?.(); - if (genericMilestone.display != null && unref(genericMilestone.showPopups) === true) { - const display = unref(genericMilestone.display); - const Display = coerceComponent( - isCoercableComponent(display) ? display : display.requirement - ); - toast( - <> -

Milestone earned!

-
- {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - -
- - ); - } - }; - - processComputable(milestone as T, "visibility"); - setDefault(milestone, "visibility", Visibility.Visible); - const visibility = milestone.visibility as ProcessedComputable; - milestone.visibility = computed(() => { - const display = unref((milestone as GenericMilestone).display); - switch (settings.msDisplay) { - default: - case MilestoneDisplay.All: - return unref(visibility); - case MilestoneDisplay.Configurable: - if ( - unref(milestone.earned) && - !( - display != null && - typeof display == "object" && - "optionsDisplay" in (display as Record) - ) - ) { - return Visibility.None; - } - return unref(visibility); - case MilestoneDisplay.Incomplete: - if (unref(milestone.earned)) { - return Visibility.None; - } - return unref(visibility); - case MilestoneDisplay.None: - return Visibility.None; - } - }); - - processComputable(milestone as T, "style"); - processComputable(milestone as T, "classes"); - processComputable(milestone as T, "display"); - processComputable(milestone as T, "showPopups"); - - milestone[GatherProps] = function (this: GenericMilestone) { - const { visibility, display, style, classes, earned, id } = this; - return { visibility, display, style: unref(style), classes, earned, id }; - }; - - if (milestone.shouldEarn) { - const genericMilestone = milestone as GenericMilestone; - watchEffect(() => { - if (settings.active !== player.id) return; - if ( - !genericMilestone.earned.value && - isVisible(genericMilestone.visibility) && - genericMilestone.shouldEarn?.() - ) { - genericMilestone.earned.value = true; - genericMilestone.onComplete?.(); - if ( - genericMilestone.display != null && - unref(genericMilestone.showPopups) === true - ) { - const display = unref(genericMilestone.display); - const Display = coerceComponent( - isCoercableComponent(display) ? display : display.requirement - ); - toast( - <> -

Milestone earned!

-
- {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - -
- - ); - } - } - }); - } - - return milestone as unknown as Milestone; - }); -} - -declare module "game/settings" { - interface Settings { - msDisplay: MilestoneDisplay; - } -} - -globalBus.on("loadSettings", settings => { - setDefault(settings, "msDisplay", MilestoneDisplay.All); -}); - -const msDisplayOptions = Object.values(MilestoneDisplay).map(option => ({ - label: camelToTitle(option), - value: option -})); - -registerSettingField( - jsx(() => ( -