mana-scribe 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.c8rc.json +10 -0
  2. package/.github/workflows/tests.yml +37 -0
  3. package/LICENSE +7 -0
  4. package/README.md +79 -0
  5. package/jasmine.json +8 -0
  6. package/package.json +35 -0
  7. package/src/costs/ActivationCost.js +32 -0
  8. package/src/costs/Cost.js +48 -0
  9. package/src/costs/ManaCost.js +38 -0
  10. package/src/customErrors.js +6 -0
  11. package/src/index.js +4 -0
  12. package/src/symbols/ColoredManaSymbol.js +20 -0
  13. package/src/symbols/ColoredPhyrexianManaSymbol.js +20 -0
  14. package/src/symbols/ColorlessManaSymbol.js +20 -0
  15. package/src/symbols/ColorlessPhyrexianManaSymbol.js +12 -0
  16. package/src/symbols/EnergySymbol.js +12 -0
  17. package/src/symbols/FiveColorHybridManaSymbol.js +12 -0
  18. package/src/symbols/FourColorHybridManaSymbol.js +12 -0
  19. package/src/symbols/GenericHybridManaSymbol.js +20 -0
  20. package/src/symbols/GenericManaSymbol.js +16 -0
  21. package/src/symbols/HalfColoredManaSymbol.js +21 -0
  22. package/src/symbols/HybridManaSymbol.js +21 -0
  23. package/src/symbols/InfiniteManaSymbol.js +16 -0
  24. package/src/symbols/NonManaSymbol.js +11 -0
  25. package/src/symbols/SnowManaSymbol.js +20 -0
  26. package/src/symbols/Symbol.js +44 -0
  27. package/src/symbols/TapSymbol.js +12 -0
  28. package/src/symbols/ThreeColorHybridManaSymbol.js +12 -0
  29. package/src/symbols/TwoColorHybridManaSymbol.js +12 -0
  30. package/src/symbols/UntapSymbol.js +12 -0
  31. package/src/symbols/VariableManaSymbol.js +16 -0
  32. package/tests/costs/ActivationCost.test.js +65 -0
  33. package/tests/costs/Cost.test.js +27 -0
  34. package/tests/costs/ManaCost.test.js +124 -0
  35. package/tests/realWorld.test.js +80 -0
  36. package/tests/symbols/ColoredManaSymbol.js +62 -0
  37. package/tests/symbols/ColoredPhyrexianManaSymbol.test.js +69 -0
  38. package/tests/symbols/ColorlessManaSymbol.test.js +56 -0
  39. package/tests/symbols/ColorlessPhyrexianManaSymbol.test.js +61 -0
  40. package/tests/symbols/EnergySymbol.test.js +75 -0
  41. package/tests/symbols/FiveColorHybridManaSymbol.test.js +70 -0
  42. package/tests/symbols/FourColorHybridManaSymbol.test.js +70 -0
  43. package/tests/symbols/GenericHybridManaSymbol.test.js +77 -0
  44. package/tests/symbols/GenericManaSymbol.test.js +68 -0
  45. package/tests/symbols/HalfColoredManaSymbol.test.js +80 -0
  46. package/tests/symbols/InfiniteManaSymbol.test.js +70 -0
  47. package/tests/symbols/SnowManaSymbol.test.js +70 -0
  48. package/tests/symbols/Symbol.test.js +41 -0
  49. package/tests/symbols/TapSymbol.test.js +74 -0
  50. package/tests/symbols/ThreeColorHybridManaSymbol.test.js +68 -0
  51. package/tests/symbols/TwoColorHybridManaSymbol.test.js +68 -0
  52. package/tests/symbols/UntapSymbol.test.js +74 -0
  53. package/tests/symbols/VariableManaSymbol.test.js +72 -0
package/.c8rc.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "reporter": ["lcov", "text"],
3
+ "report-dir": "./coverage",
4
+ "exclude": [
5
+ "tests/**",
6
+ "node_modules/**"
7
+ ],
8
+ "check-coverage": false,
9
+ "skip_empty": true
10
+ }
@@ -0,0 +1,37 @@
1
+ name: Node.js Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ matrix:
15
+ node-version: [18.x, 24.x]
16
+
17
+ steps:
18
+ - name: Checkout repo
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: ${{ matrix.node-version }}
25
+ cache: 'npm'
26
+
27
+ - name: Install dependencies
28
+ run: npm ci
29
+
30
+ - name: Run tests with coverage
31
+ run: npm run coverage
32
+
33
+ - name: Upload coverage to Codecov
34
+ uses: codecov/codecov-action@v4
35
+ with:
36
+ token: ${{ secrets.CODECOV_TOKEN }}
37
+ files: ./coverage/lcov.info
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Colin A
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # mana-scribe
2
+ [![Tests](https://github.com/matortheeternal/mana-scribe/actions/workflows/tests.yml/badge.svg)](https://github.com/matortheeternal/mana-scribe/actions/workflows/tests.yml) [![codecov](https://codecov.io/github/matortheeternal/mana-scribe/graph/badge.svg?token=Z81O4KMEOH)](https://codecov.io/github/matortheeternal/mana-scribe)
3
+
4
+ Mana Scribe is a utility for working with Magic: The Gathering mana costs and activation costs.
5
+
6
+ Supports both brace `{3}{R/U}` and shortform `3R/U` notation.
7
+
8
+ ## Features
9
+
10
+ - Parse MTG mana costs into structured objects
11
+ - Supports all major symbols: generic, colored, hybrid, phyrexian, snow, energy, tap/untap, variable
12
+ - Works with both Scryfall braces and shortform notation
13
+ - Compute:
14
+ - Converted mana cost
15
+ - Color identity
16
+ - Devotion
17
+ - Extensible design — add new symbol types by subclassing
18
+
19
+ Note: When the parser encounters an unrecognized symbol it stops parsing and returns the symbols it parsed so far. You can access the unparsed string component through the property `remainingStr`.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install mana-scribe
25
+ ```
26
+
27
+ ## Usage
28
+ ```js
29
+ import { ManaCost } from 'mana-scribe';
30
+
31
+ const cost = ManaCost.parse('{3}{R}{R}{R}');
32
+ console.log(cost.cmc); // 6
33
+ console.log(cost.colors); // ['R']
34
+ console.log(cost.colorIdentity); // ['R']
35
+ console.log(cost.getDevotionTo('R')); // 3
36
+
37
+ console.log(cost.toString(true)); // "{3}{R}{R}{R}"
38
+ console.log(cost.toString(false)); // "3RRR"
39
+ ```
40
+ ### Shortform example
41
+ ```js
42
+ const cost = ManaCost.parse('2WU/B');
43
+ console.log(cost.cmc); // 3
44
+ console.log(cost.colors); // ['W','U','B']
45
+ ```
46
+
47
+ ## Activation costs
48
+ `ActivationCost` offers the same functionality as the `ManaCost` class, but supports additional symbols such as Tap, Untap, and Energy.
49
+
50
+ ```js
51
+ import { ActivationCost } from 'mana-scribe';
52
+
53
+ const cost = ActivationCost.parse('{1}{G}{T}');
54
+ console.log(cost.symbols.map(s => s.type)); // ["generic", "colored", "tap"]
55
+ console.log(cost.toString(true)); // "{1}{G}{T}"
56
+ ```
57
+
58
+ ## Extending
59
+
60
+ The library is class-based. Each symbol type is a subclass of a base Symbol class and implements:
61
+
62
+ - `static match(str)` → returns a regex match object if it applies
63
+ - `get colors()` → returns an array of the colors associated with the symbol
64
+ - `cmcValue()` → returns an integer corresponding to how much this symbol contributes to a card's overall converted mana cost.
65
+
66
+ This makes it easy to add custom symbols or patch existing ones in your own project.
67
+
68
+ ## Project Status
69
+
70
+ This is an early but complete implementation.
71
+
72
+ Current priorities:
73
+ - [x] Support core MTG symbols
74
+ - [x] Add test coverage
75
+ - [ ] Other improvements? TBD
76
+
77
+ ## License
78
+
79
+ This project is licensed under the MIT License. See LICENSE for more info.
package/jasmine.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "spec_dir": ".",
3
+ "spec_files": [
4
+ "tests/**/*.test.js"
5
+ ],
6
+ "random": false,
7
+ "stopSpecOnExpectationFailure": false
8
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "mana-scribe",
3
+ "version": "1.0.0",
4
+ "description": "Library for parsing and querying Magic: The Gathering mana costs — with support for CMC, devotion, and color identity.",
5
+ "keywords": [
6
+ "mtg",
7
+ "magic the gathering",
8
+ "mana",
9
+ "cost",
10
+ "parser"
11
+ ],
12
+ "homepage": "https://github.com/matortheeternal/mana-scribe#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/matortheeternal/mana-scribe/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/matortheeternal/mana-scribe.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "mator",
22
+ "type": "module",
23
+ "main": "./src/index.js",
24
+ "exports": {
25
+ ".": "./src/index.js"
26
+ },
27
+ "devDependencies": {
28
+ "c8": "^10.1.3",
29
+ "jasmine": "^5.12.0"
30
+ },
31
+ "scripts": {
32
+ "test": "jasmine --config=jasmine.json",
33
+ "coverage": "c8 npm test"
34
+ }
35
+ }
@@ -0,0 +1,32 @@
1
+ import Cost from './Cost.js';
2
+ import ColoredManaSymbol from '../symbols/ColoredManaSymbol.js';
3
+ import ColoredPhyrexianManaSymbol from '../symbols/ColoredPhyrexianManaSymbol.js';
4
+ import ColorlessManaSymbol from '../symbols/ColorlessManaSymbol.js';
5
+ import ColorlessPhyrexianManaSymbol from '../symbols/ColorlessPhyrexianManaSymbol.js';
6
+ import EnergySymbol from '../symbols/EnergySymbol.js';
7
+ import FiveColorHybridManaSymbol from '../symbols/FiveColorHybridManaSymbol.js';
8
+ import FourColorHybridManaSymbol from '../symbols/FourColorHybridManaSymbol.js';
9
+ import GenericHybridManaSymbol from '../symbols/GenericHybridManaSymbol.js';
10
+ import GenericManaSymbol from '../symbols/GenericManaSymbol.js';
11
+ import HalfColoredManaSymbol from '../symbols/HalfColoredManaSymbol.js';
12
+ import InfiniteManaSymbol from '../symbols/InfiniteManaSymbol.js';
13
+ import SnowManaSymbol from '../symbols/SnowManaSymbol.js';
14
+ import TapSymbol from '../symbols/TapSymbol.js';
15
+ import ThreeColorHybridManaSymbol from '../symbols/ThreeColorHybridManaSymbol.js';
16
+ import TwoColorHybridManaSymbol from '../symbols/TwoColorHybridManaSymbol.js';
17
+ import UntapSymbol from '../symbols/UntapSymbol.js';
18
+ import VariableManaSymbol from '../symbols/VariableManaSymbol.js';
19
+
20
+ export default class ActivationCost extends Cost {
21
+ static get allowedSymbols() {
22
+ return [
23
+ ColoredPhyrexianManaSymbol, GenericHybridManaSymbol, FiveColorHybridManaSymbol,
24
+ FourColorHybridManaSymbol, ThreeColorHybridManaSymbol, TwoColorHybridManaSymbol,
25
+ ColorlessPhyrexianManaSymbol, HalfColoredManaSymbol,
26
+ GenericManaSymbol, VariableManaSymbol,
27
+ InfiniteManaSymbol, SnowManaSymbol,
28
+ ColoredManaSymbol, ColorlessManaSymbol,
29
+ EnergySymbol, TapSymbol, UntapSymbol,
30
+ ];
31
+ }
32
+ }
@@ -0,0 +1,48 @@
1
+ import { NotImplementedError } from '../customErrors.js';
2
+
3
+ export default class Cost {
4
+ static get allowedSymbols() {
5
+ throw new NotImplementedError('allowedSymbols');
6
+ }
7
+
8
+ static parse(str) {
9
+ const cost = new this();
10
+ cost.parseSymbols(str);
11
+ return cost;
12
+ }
13
+
14
+ constructor(symbols = []) {
15
+ this.symbols = symbols;
16
+ }
17
+
18
+ get colors() {
19
+ const set = new Set(this.symbols.flatMap(sym => sym.colors));
20
+ return [...set];
21
+ }
22
+
23
+ parseSymbols(str) {
24
+ let remainingStr = str.trim();
25
+ while (remainingStr.length) {
26
+ const symbol = this.parseNextSymbol(remainingStr);
27
+ if (!symbol) break;
28
+ symbol.apply(this.symbols);
29
+ if (remainingStr === symbol.remainingStr) break;
30
+ remainingStr = symbol.remainingStr;
31
+ }
32
+ this.remainingStr = remainingStr;
33
+ }
34
+
35
+ parseNextSymbol(str) {
36
+ for (const symbol of this.constructor.allowedSymbols) {
37
+ const match = symbol.match(str);
38
+ if (!match) continue;
39
+ return symbol.parse(match, str);
40
+ }
41
+ }
42
+
43
+ toString(useBraces = false) {
44
+ return this.symbols.map(sym => {
45
+ return sym.toString(useBraces);
46
+ }).join('');
47
+ }
48
+ }
@@ -0,0 +1,38 @@
1
+ import Cost from './Cost.js';
2
+ import ColoredManaSymbol from '../symbols/ColoredManaSymbol.js';
3
+ import ColoredPhyrexianManaSymbol from '../symbols/ColoredPhyrexianManaSymbol.js';
4
+ import ColorlessManaSymbol from '../symbols/ColorlessManaSymbol.js';
5
+ import ColorlessPhyrexianManaSymbol from '../symbols/ColorlessPhyrexianManaSymbol.js';
6
+ import FiveColorHybridManaSymbol from '../symbols/FiveColorHybridManaSymbol.js';
7
+ import FourColorHybridManaSymbol from '../symbols/FourColorHybridManaSymbol.js';
8
+ import GenericHybridManaSymbol from '../symbols/GenericHybridManaSymbol.js';
9
+ import GenericManaSymbol from '../symbols/GenericManaSymbol.js';
10
+ import HalfColoredManaSymbol from '../symbols/HalfColoredManaSymbol.js';
11
+ import InfiniteManaSymbol from '../symbols/InfiniteManaSymbol.js';
12
+ import SnowManaSymbol from '../symbols/SnowManaSymbol.js';
13
+ import ThreeColorHybridManaSymbol from '../symbols/ThreeColorHybridManaSymbol.js';
14
+ import TwoColorHybridManaSymbol from '../symbols/TwoColorHybridManaSymbol.js';
15
+ import VariableManaSymbol from '../symbols/VariableManaSymbol.js';
16
+
17
+ export default class ManaCost extends Cost {
18
+ static get allowedSymbols() {
19
+ return [
20
+ ColoredPhyrexianManaSymbol, GenericHybridManaSymbol, FiveColorHybridManaSymbol,
21
+ FourColorHybridManaSymbol, ThreeColorHybridManaSymbol, TwoColorHybridManaSymbol,
22
+ ColorlessPhyrexianManaSymbol, HalfColoredManaSymbol,
23
+ GenericManaSymbol, VariableManaSymbol,
24
+ InfiniteManaSymbol, SnowManaSymbol,
25
+ ColoredManaSymbol, ColorlessManaSymbol
26
+ ];
27
+ }
28
+
29
+ get cmc() {
30
+ return this.symbols.reduce((sum, sym) => sum + sym.cmcValue(), 0);
31
+ }
32
+
33
+ getDevotionTo(color) {
34
+ return this.symbols.reduce((devotion, sym) => {
35
+ return devotion + (sym.colors.includes(color) ? 1 : 0);
36
+ }, 0);
37
+ }
38
+ }
@@ -0,0 +1,6 @@
1
+ export class NotImplementedError extends Error {
2
+ constructor(methodName) {
3
+ super(`Method "${methodName}" not implemented`);
4
+ this.name = 'NotImplementedError';
5
+ }
6
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import ActivationCost from './costs/ActivationCost.js';
2
+ import ManaCost from './costs/ManaCost.js';
3
+
4
+ export { ActivationCost, ManaCost };
@@ -0,0 +1,20 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ export default class ColoredManaSymbol extends Symbol {
4
+ static match(str) {
5
+ return str.match(/^{[WUBRG]}/i)
6
+ || str.match(/^[WUBRG]/i);
7
+ }
8
+
9
+ get colors() {
10
+ return [this.raw];
11
+ }
12
+
13
+ get type() {
14
+ return 'coloredMana';
15
+ }
16
+
17
+ cmcValue() {
18
+ return 1;
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ export default class ColoredPhyrexianManaSymbol extends Symbol {
4
+ static match(str) {
5
+ return str.match(/^{(h\/[wubrg]|[wubrg]\/h)}/i)
6
+ || str.match(/^(h\/[wubrg]|[wubrg]\/h)/i);
7
+ }
8
+
9
+ get colors() {
10
+ return this.raw.split('/').filter(c => 'WUBRG'.includes(c));
11
+ }
12
+
13
+ get type() {
14
+ return 'coloredPhyrexianMana';
15
+ }
16
+
17
+ cmcValue() {
18
+ return 1;
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ export default class ColorlessManaSymbol extends Symbol {
4
+ static match(str) {
5
+ return str.match(/^{C}/i)
6
+ || str.match(/^C/i);
7
+ }
8
+
9
+ get colors() {
10
+ return [];
11
+ }
12
+
13
+ get type() {
14
+ return 'colorlessMana';
15
+ }
16
+
17
+ cmcValue() {
18
+ return 1;
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ import ColorlessManaSymbol from './ColorlessManaSymbol.js';
2
+
3
+ export default class ColorlessPhyrexianManaSymbol extends ColorlessManaSymbol {
4
+ static match(str) {
5
+ return str.match(/^{(h\/c|c\/h|h)}/i)
6
+ || str.match(/^(h\/c|c\/h|h)/i);
7
+ }
8
+
9
+ get type() {
10
+ return 'colorlessPhyrexianMana';
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import NonManaSymbol from './NonManaSymbol.js';
2
+
3
+ export default class EnergySymbol extends NonManaSymbol {
4
+ static match(str) {
5
+ return str.match(/^{E}/i)
6
+ || str.match(/^E/i);
7
+ }
8
+
9
+ get type() {
10
+ return 'energy';
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import HybridManaSymbol from './HybridManaSymbol.js';
2
+
3
+ export default class FiveColorHybridManaSymbol extends HybridManaSymbol {
4
+ static match(str) {
5
+ return super.match(str, 5, /^{[WUBRG](\/[WUBRG]){4}}/i)
6
+ || super.match(str, 5, /^[WUBRG](\/[WUBRG]){4}/i);
7
+ }
8
+
9
+ get type() {
10
+ return 'fiveColorHybridMana';
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import HybridManaSymbol from './HybridManaSymbol.js';
2
+
3
+ export default class FourColorHybridManaSymbol extends HybridManaSymbol {
4
+ static match(str) {
5
+ return super.match(str, 4, /^{[WUBRG](\/[WUBRG]){3}}/i)
6
+ || super.match(str, 4, /^[WUBRG](\/[WUBRG]){3}/i);
7
+ }
8
+
9
+ get type() {
10
+ return 'fourColorHybridMana';
11
+ }
12
+ }
@@ -0,0 +1,20 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ export default class GenericHybridManaSymbol extends Symbol {
4
+ static match(str) {
5
+ return str.match(/^{\d\/([WUBRGC])}/i)
6
+ || str.match(/^\d\/([WUBRGC])/i)
7
+ }
8
+
9
+ get colors() {
10
+ return this.raw.split('/').filter(c => 'WUBRG'.includes(c));
11
+ }
12
+
13
+ get type() {
14
+ return 'genericHybridMana';
15
+ }
16
+
17
+ cmcValue() {
18
+ return 1;
19
+ }
20
+ }
@@ -0,0 +1,16 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ export default class GenericManaSymbol extends Symbol {
4
+ static match(str) {
5
+ return str.match(/^{[0-9][0-9]?}/)
6
+ || str.match(/^[0-9][0-9]?/);
7
+ }
8
+
9
+ get type() {
10
+ return 'genericMana';
11
+ }
12
+
13
+ cmcValue() {
14
+ return parseInt(this.raw);
15
+ }
16
+ }
@@ -0,0 +1,21 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ export default class HalfColoredManaSymbol extends Symbol {
4
+ static match(str) {
5
+ return str.match(/^{\|[WUBRGS]}/i)
6
+ || str.match(/^\|[WUBRGS]/i);
7
+ }
8
+
9
+ get colors() {
10
+ const c = this.raw[1];
11
+ return c === 'S' ? [] : [c];
12
+ }
13
+
14
+ get type() {
15
+ return 'halfColoredMana';
16
+ }
17
+
18
+ cmcValue() {
19
+ return 0.5;
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ function getHybridCount(str) {
4
+ return new Set(str.split('/')).size;
5
+ }
6
+
7
+ export default class HybridManaSymbol extends Symbol {
8
+ static match(str, hybridCount, expr) {
9
+ const matchData = str.match(expr);
10
+ if (!matchData || getHybridCount(matchData[0]) !== hybridCount) return;
11
+ return matchData;
12
+ }
13
+
14
+ get colors() {
15
+ return this.raw.split('/');
16
+ }
17
+
18
+ cmcValue() {
19
+ return 1;
20
+ }
21
+ }
@@ -0,0 +1,16 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ export default class InfiniteManaSymbol extends Symbol {
4
+ static match(str) {
5
+ return str.match(/^{I}/i)
6
+ || str.match(/^I/i)
7
+ }
8
+
9
+ get type() {
10
+ return 'infiniteMana';
11
+ }
12
+
13
+ cmcValue() {
14
+ return Infinity;
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ export default class NonManaSymbol extends Symbol {
4
+ get colors() {
5
+ return [];
6
+ }
7
+
8
+ cmcValue() {
9
+ return 0;
10
+ }
11
+ }
@@ -0,0 +1,20 @@
1
+ import Symbol from './Symbol.js';
2
+
3
+ export default class SnowManaSymbol extends Symbol {
4
+ static match(str) {
5
+ return str.match(/^{S}/i)
6
+ || str.match(/^S/i);
7
+ }
8
+
9
+ get colors() {
10
+ return [];
11
+ }
12
+
13
+ get type() {
14
+ return 'snowMana';
15
+ }
16
+
17
+ cmcValue() {
18
+ return 1;
19
+ }
20
+ }
@@ -0,0 +1,44 @@
1
+ import { NotImplementedError } from '../customErrors.js';
2
+
3
+ export default class Symbol {
4
+ static match(str) {
5
+ throw new NotImplementedError('match');
6
+ }
7
+
8
+ static fromString(str) {
9
+ const matchData = this.match(str);
10
+ if (!matchData)
11
+ throw new Error(`Invalid ${this.name} input: ${str}`);
12
+ return this.parse(matchData, str);
13
+ }
14
+
15
+ static parse(match, str) {
16
+ return new this(match, str);
17
+ }
18
+
19
+ constructor(match, str) {
20
+ const raw = match[0];
21
+ this.raw = (raw[0] === '{' ? raw.slice(1, -1) : raw).toUpperCase();
22
+ this.remainingStr = str.slice(match[0].length);
23
+ }
24
+
25
+ get type() {
26
+ throw new NotImplementedError('type');
27
+ }
28
+
29
+ get cmcValue() {
30
+ throw new NotImplementedError('cmcValue');
31
+ }
32
+
33
+ get colors() {
34
+ return [];
35
+ }
36
+
37
+ apply(symbols) {
38
+ symbols.push(this);
39
+ }
40
+
41
+ toString(useBraces = false) {
42
+ return useBraces ? `{${this.raw}}` : this.raw;
43
+ }
44
+ }
@@ -0,0 +1,12 @@
1
+ import NonManaSymbol from './NonManaSymbol.js';
2
+
3
+ export default class TapSymbol extends NonManaSymbol {
4
+ static match(str) {
5
+ return str.match(/^{T}/i)
6
+ || str.match(/^T/i);
7
+ }
8
+
9
+ get type() {
10
+ return 'tap';
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import HybridManaSymbol from './HybridManaSymbol.js';
2
+
3
+ export default class ThreeColorHybridManaSymbol extends HybridManaSymbol {
4
+ static match(str) {
5
+ return super.match(str, 3, /^{[WUBRG](\/[WUBRG]){2}}/i)
6
+ || super.match(str, 3, /^[WUBRG](\/[WUBRG]){2}/i);
7
+ }
8
+
9
+ get type() {
10
+ return 'threeColorHybridMana';
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import HybridManaSymbol from './HybridManaSymbol.js';
2
+
3
+ export default class TwoColorHybridManaSymbol extends HybridManaSymbol {
4
+ static match(str) {
5
+ return super.match(str, 2, /^{[WUBRG]\/[WUBRG]}/i)
6
+ || super.match(str, 2, /^[WUBRG]\/[WUBRG]/i);
7
+ }
8
+
9
+ get type() {
10
+ return 'twoColorHybridMana';
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ import NonManaSymbol from './NonManaSymbol.js';
2
+
3
+ export default class UntapSymbol extends NonManaSymbol {
4
+ static match(str) {
5
+ return str.match(/^{Q}/i)
6
+ || str.match(/^Q/i);
7
+ }
8
+
9
+ get type() {
10
+ return 'untap';
11
+ }
12
+ }