enjine 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +201 -0
- data/SUPPORTED.md +387 -0
- data/docs/ARCHITECTURE.md +141 -0
- data/docs/DESIGN.md +139 -0
- data/docs/MECHANICS_COVERAGE.md +670 -0
- data/docs/guides/adding-a-mechanic.md +60 -0
- data/docs/guides/api.md +614 -0
- data/docs/guides/card-data-format.md +1472 -0
- data/docs/guides/deck-builder.md +157 -0
- data/docs/plans/standard.md +371 -0
- data/docs/reference/abilities.md +1680 -0
- data/docs/reference/costs.md +339 -0
- data/docs/reference/counters.md +121 -0
- data/docs/reference/decisions.md +102 -0
- data/docs/reference/effects.md +2340 -0
- data/docs/reference/filters.md +172 -0
- data/docs/reference/keywords.md +647 -0
- data/docs/reference/subtypes.md +169 -0
- data/docs/reference/supertypes.md +31 -0
- data/docs/reference/triggers.md +797 -0
- data/docs/reference/types.md +216 -0
- data/exe/enjine +7 -0
- data/lib/enjine/actions/activate_mana_ability.rb +24 -0
- data/lib/enjine/actions/cast.rb +57 -0
- data/lib/enjine/actions/pass.rb +18 -0
- data/lib/enjine/actions/play_land.rb +52 -0
- data/lib/enjine/actions/special_action.rb +37 -0
- data/lib/enjine/actions.rb +20 -0
- data/lib/enjine/agents/busy.rb +27 -0
- data/lib/enjine/agents/random.rb +28 -0
- data/lib/enjine/card_definition.rb +433 -0
- data/lib/enjine/card_faces.rb +116 -0
- data/lib/enjine/catalog.rb +56 -0
- data/lib/enjine/characteristics.rb +39 -0
- data/lib/enjine/cli/text.rb +295 -0
- data/lib/enjine/cli.rb +312 -0
- data/lib/enjine/decisions/bottom.rb +29 -0
- data/lib/enjine/decisions/casting.rb +188 -0
- data/lib/enjine/decisions/combat.rb +75 -0
- data/lib/enjine/decisions/discard.rb +29 -0
- data/lib/enjine/decisions/effect_choice.rb +21 -0
- data/lib/enjine/decisions/enter_choice.rb +26 -0
- data/lib/enjine/decisions/legend_rule.rb +21 -0
- data/lib/enjine/decisions/mulligan.rb +26 -0
- data/lib/enjine/decisions/order_triggers.rb +21 -0
- data/lib/enjine/decisions/play_or_draw.rb +19 -0
- data/lib/enjine/decisions/priority.rb +22 -0
- data/lib/enjine/decisions/untap_choice.rb +35 -0
- data/lib/enjine/decisions.rb +22 -0
- data/lib/enjine/deck.rb +151 -0
- data/lib/enjine/deck_builder.rb +454 -0
- data/lib/enjine/docs.rb +90 -0
- data/lib/enjine/engine/ability_layers.rb +118 -0
- data/lib/enjine/engine/attachments.rb +86 -0
- data/lib/enjine/engine/casting/alt_casting.rb +254 -0
- data/lib/enjine/engine/casting/cast_choices.rb +292 -0
- data/lib/enjine/engine/casting/casting.rb +326 -0
- data/lib/enjine/engine/casting/cost_options.rb +99 -0
- data/lib/enjine/engine/casting/costs.rb +117 -0
- data/lib/enjine/engine/casting/payment.rb +353 -0
- data/lib/enjine/engine/casting/permissions.rb +412 -0
- data/lib/enjine/engine/casting/target_counts.rb +113 -0
- data/lib/enjine/engine/casting/waterbend.rb +90 -0
- data/lib/enjine/engine/characteristics.rb +379 -0
- data/lib/enjine/engine/class_levels.rb +25 -0
- data/lib/enjine/engine/combat.rb +315 -0
- data/lib/enjine/engine/copies.rb +208 -0
- data/lib/enjine/engine/damage.rb +196 -0
- data/lib/enjine/engine/designations.rb +46 -0
- data/lib/enjine/engine/emblems.rb +37 -0
- data/lib/enjine/engine/entering.rb +156 -0
- data/lib/enjine/engine/face_down.rb +30 -0
- data/lib/enjine/engine/faces.rb +222 -0
- data/lib/enjine/engine/follow_ups.rb +27 -0
- data/lib/enjine/engine/mana_sources.rb +230 -0
- data/lib/enjine/engine/phasing.rb +74 -0
- data/lib/enjine/engine/pregame.rb +95 -0
- data/lib/enjine/engine/priority.rb +47 -0
- data/lib/enjine/engine/resolution.rb +168 -0
- data/lib/enjine/engine/rules.rb +287 -0
- data/lib/enjine/engine/sagas.rb +41 -0
- data/lib/enjine/engine/speed.rb +104 -0
- data/lib/enjine/engine/spell_grants.rb +46 -0
- data/lib/enjine/engine/state_based_actions.rb +157 -0
- data/lib/enjine/engine/triggers.rb +364 -0
- data/lib/enjine/engine/turn_history.rb +240 -0
- data/lib/enjine/engine/turn_structure.rb +224 -0
- data/lib/enjine/engine/zones.rb +278 -0
- data/lib/enjine/engine.rb +55 -0
- data/lib/enjine/errors.rb +62 -0
- data/lib/enjine/events.rb +161 -0
- data/lib/enjine/frames.rb +84 -0
- data/lib/enjine/fuzz.rb +112 -0
- data/lib/enjine/game.rb +204 -0
- data/lib/enjine/history.rb +26 -0
- data/lib/enjine/mana.rb +142 -0
- data/lib/enjine/mechanics/abilities/additional_triggers.rb +86 -0
- data/lib/enjine/mechanics/abilities/chosen_values.rb +213 -0
- data/lib/enjine/mechanics/abilities/cost_changes.rb +154 -0
- data/lib/enjine/mechanics/abilities/damage_replacements.rb +569 -0
- data/lib/enjine/mechanics/abilities/double_counters.rb +71 -0
- data/lib/enjine/mechanics/abilities/entering_choices.rb +91 -0
- data/lib/enjine/mechanics/abilities/entering_copy.rb +69 -0
- data/lib/enjine/mechanics/abilities/enters_tapped.rb +159 -0
- data/lib/enjine/mechanics/abilities/enters_with_counters.rb +116 -0
- data/lib/enjine/mechanics/abilities/mana_ability.rb +209 -0
- data/lib/enjine/mechanics/abilities/play_permissions.rb +527 -0
- data/lib/enjine/mechanics/abilities/player_rules.rb +169 -0
- data/lib/enjine/mechanics/abilities/replacement_effects.rb +368 -0
- data/lib/enjine/mechanics/abilities/restrictions.rb +144 -0
- data/lib/enjine/mechanics/abilities/spell_keywords.rb +55 -0
- data/lib/enjine/mechanics/abilities/stack_abilities.rb +542 -0
- data/lib/enjine/mechanics/abilities/static.rb +365 -0
- data/lib/enjine/mechanics/abilities/tapping_power.rb +71 -0
- data/lib/enjine/mechanics/abilities/turn_rules.rb +49 -0
- data/lib/enjine/mechanics/card_spec.rb +177 -0
- data/lib/enjine/mechanics/copy_except.rb +73 -0
- data/lib/enjine/mechanics/costs/base.rb +115 -0
- data/lib/enjine/mechanics/costs/behold.rb +76 -0
- data/lib/enjine/mechanics/costs/discard.rb +65 -0
- data/lib/enjine/mechanics/costs/exert.rb +29 -0
- data/lib/enjine/mechanics/costs/exile_cards.rb +39 -0
- data/lib/enjine/mechanics/costs/from_graveyard.rb +45 -0
- data/lib/enjine/mechanics/costs/get_counters.rb +39 -0
- data/lib/enjine/mechanics/costs/mana.rb +37 -0
- data/lib/enjine/mechanics/costs/one_of.rb +49 -0
- data/lib/enjine/mechanics/costs/pay_life.rb +50 -0
- data/lib/enjine/mechanics/costs/remove_counters.rb +51 -0
- data/lib/enjine/mechanics/costs/sacrifice.rb +59 -0
- data/lib/enjine/mechanics/costs/tap.rb +56 -0
- data/lib/enjine/mechanics/costs/waterbend.rb +29 -0
- data/lib/enjine/mechanics/counters/kinds.rb +190 -0
- data/lib/enjine/mechanics/effects/add_subtypes.rb +30 -0
- data/lib/enjine/mechanics/effects/airbend.rb +41 -0
- data/lib/enjine/mechanics/effects/amass.rb +124 -0
- data/lib/enjine/mechanics/effects/assimilate.rb +72 -0
- data/lib/enjine/mechanics/effects/base.rb +148 -0
- data/lib/enjine/mechanics/effects/become.rb +182 -0
- data/lib/enjine/mechanics/effects/behold.rb +48 -0
- data/lib/enjine/mechanics/effects/blight.rb +94 -0
- data/lib/enjine/mechanics/effects/choose_cards.rb +168 -0
- data/lib/enjine/mechanics/effects/copy.rb +370 -0
- data/lib/enjine/mechanics/effects/counter_spell.rb +21 -0
- data/lib/enjine/mechanics/effects/counters.rb +161 -0
- data/lib/enjine/mechanics/effects/damage.rb +72 -0
- data/lib/enjine/mechanics/effects/delayed_triggers.rb +112 -0
- data/lib/enjine/mechanics/effects/destroy.rb +22 -0
- data/lib/enjine/mechanics/effects/discard.rb +86 -0
- data/lib/enjine/mechanics/effects/draw.rb +16 -0
- data/lib/enjine/mechanics/effects/earthbend.rb +70 -0
- data/lib/enjine/mechanics/effects/emblem.rb +52 -0
- data/lib/enjine/mechanics/effects/endure.rb +63 -0
- data/lib/enjine/mechanics/effects/exile.rb +49 -0
- data/lib/enjine/mechanics/effects/extra_turn.rb +122 -0
- data/lib/enjine/mechanics/effects/faces.rb +115 -0
- data/lib/enjine/mechanics/effects/fight.rb +76 -0
- data/lib/enjine/mechanics/effects/flip_coins.rb +97 -0
- data/lib/enjine/mechanics/effects/gain_control.rb +94 -0
- data/lib/enjine/mechanics/effects/game_end.rb +42 -0
- data/lib/enjine/mechanics/effects/library.rb +327 -0
- data/lib/enjine/mechanics/effects/life.rb +83 -0
- data/lib/enjine/mechanics/effects/look_at_hand.rb +17 -0
- data/lib/enjine/mechanics/effects/loot.rb +118 -0
- data/lib/enjine/mechanics/effects/mana.rb +96 -0
- data/lib/enjine/mechanics/effects/may.rb +82 -0
- data/lib/enjine/mechanics/effects/meld.rb +77 -0
- data/lib/enjine/mechanics/effects/mill.rb +22 -0
- data/lib/enjine/mechanics/effects/pay.rb +231 -0
- data/lib/enjine/mechanics/effects/phasing.rb +51 -0
- data/lib/enjine/mechanics/effects/proliferate.rb +72 -0
- data/lib/enjine/mechanics/effects/pump.rb +60 -0
- data/lib/enjine/mechanics/effects/put_in_library.rb +98 -0
- data/lib/enjine/mechanics/effects/return_choice.rb +29 -0
- data/lib/enjine/mechanics/effects/return_to_battlefield.rb +80 -0
- data/lib/enjine/mechanics/effects/return_to_hand.rb +20 -0
- data/lib/enjine/mechanics/effects/roll_dice.rb +45 -0
- data/lib/enjine/mechanics/effects/sacrifice.rb +118 -0
- data/lib/enjine/mechanics/effects/scry_and_surveil.rb +74 -0
- data/lib/enjine/mechanics/effects/search.rb +156 -0
- data/lib/enjine/mechanics/effects/tap_untap.rb +23 -0
- data/lib/enjine/mechanics/effects/tokens.rb +76 -0
- data/lib/enjine/mechanics/filters/any_target.rb +15 -0
- data/lib/enjine/mechanics/filters/base.rb +386 -0
- data/lib/enjine/mechanics/filters/graveyard.rb +23 -0
- data/lib/enjine/mechanics/filters/one_of.rb +55 -0
- data/lib/enjine/mechanics/filters/permanents.rb +52 -0
- data/lib/enjine/mechanics/filters/players.rb +20 -0
- data/lib/enjine/mechanics/filters/stack.rb +97 -0
- data/lib/enjine/mechanics/follow_up.rb +46 -0
- data/lib/enjine/mechanics/keywords/alternative_costs.rb +361 -0
- data/lib/enjine/mechanics/keywords/beam_me_up.rb +78 -0
- data/lib/enjine/mechanics/keywords/changeling.rb +58 -0
- data/lib/enjine/mechanics/keywords/convoke.rb +77 -0
- data/lib/enjine/mechanics/keywords/crew.rb +155 -0
- data/lib/enjine/mechanics/keywords/cycling.rb +58 -0
- data/lib/enjine/mechanics/keywords/decayed.rb +25 -0
- data/lib/enjine/mechanics/keywords/devour.rb +61 -0
- data/lib/enjine/mechanics/keywords/evergreen.rb +54 -0
- data/lib/enjine/mechanics/keywords/extort.rb +26 -0
- data/lib/enjine/mechanics/keywords/flags.rb +46 -0
- data/lib/enjine/mechanics/keywords/foretell.rb +45 -0
- data/lib/enjine/mechanics/keywords/gift.rb +50 -0
- data/lib/enjine/mechanics/keywords/hideaway.rb +28 -0
- data/lib/enjine/mechanics/keywords/increment.rb +31 -0
- data/lib/enjine/mechanics/keywords/job_select.rb +29 -0
- data/lib/enjine/mechanics/keywords/kicker.rb +92 -0
- data/lib/enjine/mechanics/keywords/mobilize_and_firebending.rb +84 -0
- data/lib/enjine/mechanics/keywords/paradigm.rb +91 -0
- data/lib/enjine/mechanics/keywords/power_up.rb +48 -0
- data/lib/enjine/mechanics/keywords/protection.rb +111 -0
- data/lib/enjine/mechanics/keywords/prowess.rb +30 -0
- data/lib/enjine/mechanics/keywords/riot.rb +35 -0
- data/lib/enjine/mechanics/keywords/saddle.rb +115 -0
- data/lib/enjine/mechanics/keywords/speed.rb +89 -0
- data/lib/enjine/mechanics/keywords/station.rb +163 -0
- data/lib/enjine/mechanics/keywords/storied.rb +41 -0
- data/lib/enjine/mechanics/keywords/storm_and_cascade.rb +86 -0
- data/lib/enjine/mechanics/keywords/sunburst.rb +29 -0
- data/lib/enjine/mechanics/keywords/suspend.rb +191 -0
- data/lib/enjine/mechanics/keywords/teamwork.rb +45 -0
- data/lib/enjine/mechanics/keywords/ward.rb +67 -0
- data/lib/enjine/mechanics/linked.rb +67 -0
- data/lib/enjine/mechanics/mana_spent.rb +99 -0
- data/lib/enjine/mechanics/selector.rb +207 -0
- data/lib/enjine/mechanics/subtypes/auras_and_equipment.rb +176 -0
- data/lib/enjine/mechanics/subtypes/basic_land_types.rb +36 -0
- data/lib/enjine/mechanics/subtypes/class_levels.rb +102 -0
- data/lib/enjine/mechanics/subtypes/saga.rb +89 -0
- data/lib/enjine/mechanics/supertypes/basic.rb +17 -0
- data/lib/enjine/mechanics/supertypes/legendary.rb +18 -0
- data/lib/enjine/mechanics/triggers/ability_triggers.rb +51 -0
- data/lib/enjine/mechanics/triggers/any_of.rb +43 -0
- data/lib/enjine/mechanics/triggers/base.rb +228 -0
- data/lib/enjine/mechanics/triggers/casting.rb +219 -0
- data/lib/enjine/mechanics/triggers/combat.rb +123 -0
- data/lib/enjine/mechanics/triggers/damage.rb +130 -0
- data/lib/enjine/mechanics/triggers/dilemma.rb +37 -0
- data/lib/enjine/mechanics/triggers/draw_and_discard.rb +85 -0
- data/lib/enjine/mechanics/triggers/keyword_actions.rb +55 -0
- data/lib/enjine/mechanics/triggers/life.rb +35 -0
- data/lib/enjine/mechanics/triggers/steps.rb +39 -0
- data/lib/enjine/mechanics/triggers/tap_and_counters.rb +111 -0
- data/lib/enjine/mechanics/triggers/zone_changes.rb +249 -0
- data/lib/enjine/mechanics/types/kindred.rb +30 -0
- data/lib/enjine/mechanics/types/land.rb +24 -0
- data/lib/enjine/mechanics/types/permanent_types.rb +67 -0
- data/lib/enjine/mechanics/types/spell_types.rb +38 -0
- data/lib/enjine/mechanics/values.rb +675 -0
- data/lib/enjine/mechanics.rb +74 -0
- data/lib/enjine/player.rb +30 -0
- data/lib/enjine/primitive.rb +61 -0
- data/lib/enjine/replacements.rb +191 -0
- data/lib/enjine/rng.rb +57 -0
- data/lib/enjine/rules_monitor.rb +404 -0
- data/lib/enjine/setup.rb +45 -0
- data/lib/enjine/snapshot/player_checks.rb +86 -0
- data/lib/enjine/snapshot/turn_checks.rb +91 -0
- data/lib/enjine/snapshot.rb +307 -0
- data/lib/enjine/state.rb +173 -0
- data/lib/enjine/steps.rb +117 -0
- data/lib/enjine/tokens.rb +25 -0
- data/lib/enjine/verify.rb +233 -0
- data/lib/enjine/version.rb +7 -0
- data/lib/enjine/view.rb +162 -0
- data/lib/enjine.rb +46 -0
- metadata +315 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Random deck builder
|
|
2
|
+
|
|
3
|
+
`Enjine::DeckBuilder` builds random, legal 60-card decks from a card pool, in the
|
|
4
|
+
colors you ask for. It knows nothing of sets: the pool is whatever cards you give
|
|
5
|
+
it, from one set, several, or your own collection. It doesn't look for synergy. It
|
|
6
|
+
gives every deck the shape constructed decks share: about 24 lands, a creature
|
|
7
|
+
count in a typical range, a curve that leans on cheap cards, and mostly 4-ofs.
|
|
8
|
+
Those are ranges, not fixed numbers, and now and then a deck lands outside them on
|
|
9
|
+
purpose.
|
|
10
|
+
|
|
11
|
+
Create one builder and keep it: it holds the pool in memory, validated once, and
|
|
12
|
+
builds as many decks as you ask for, each with its own colors, size and seed.
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require "enjine/deck_builder" # opt-in
|
|
16
|
+
|
|
17
|
+
builder = Enjine::DeckBuilder.new(cards) # card data blocks and/or CardDefinitions
|
|
18
|
+
deck = builder.build(colors: %w[W U], seed: 7) # explicit colors
|
|
19
|
+
deck = builder.build(colors: 2, seed: 8) # two random colors
|
|
20
|
+
deck = builder.build(seed: 9) # 1-3 random colors (see random_colors)
|
|
21
|
+
|
|
22
|
+
player = Enjine::Player.new(name: "alice", deck: deck)
|
|
23
|
+
player.ready? # => true
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The result is a deck document (see the [card data format](card-data-format.md#6-decks)):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{ "name": "Random WU deck",
|
|
30
|
+
"description": "random deck: colors=WU seed=7 — 24 lands, 18 creatures, 18 other spells, 17 distinct cards",
|
|
31
|
+
"cards": [ { "name": "Plains", ... }, ... ],
|
|
32
|
+
"main": [ { "card": "Plains", "count": 12 }, ... ] }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The same pool, arguments and seed always give the same deck, whatever the order
|
|
36
|
+
of the pool. The `description` records the colors (including ones picked at
|
|
37
|
+
random) and the seed, so you can rebuild any deck.
|
|
38
|
+
|
|
39
|
+
## The pool
|
|
40
|
+
|
|
41
|
+
Each entry is a card data block (the [card DSL](card-data-format.md), optionally
|
|
42
|
+
with `rarity` and `printing`) or a `CardDefinition`. To build only from cards you
|
|
43
|
+
own, give counts:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
mine = Enjine::DeckBuilder.new([
|
|
47
|
+
{ "card" => { "name" => "Lightning Bolt", ... }, "count" => 3 }, # you own 3
|
|
48
|
+
{ "card" => { "name" => "Grizzly Bears", ... }, "count" => 9 }, # 9, but a deck holds at most 4
|
|
49
|
+
{ "name" => "Hill Giant", ... } # no count: as many as a deck may hold
|
|
50
|
+
])
|
|
51
|
+
mine.count("Lightning Bolt") # => 3 (nil: uncapped, 0: not in the pool)
|
|
52
|
+
mine.cards # the pool's CardDefinitions, sorted by id
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Entries for the same card add up. Two different definitions under one name raise
|
|
56
|
+
`Enjine::CatalogConflict`; the same card with a different `rarity` or `printing` (a
|
|
57
|
+
reprint) is the same card. A deck never holds more copies than the pool has, and
|
|
58
|
+
basic lands are always available, owned or not.
|
|
59
|
+
|
|
60
|
+
## Arguments to `build`
|
|
61
|
+
|
|
62
|
+
| Argument | Default | Meaning |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `seed:` | required | Seed for `Enjine::Rng`. Same seed and pool, same deck. |
|
|
65
|
+
| `colors:` | `:any` | `%w[W U]`, `"WU"`, a number `1`-`5` (that many random colors), or `:any` (a random count from `random_colors`). |
|
|
66
|
+
| `size:` | `60` | Deck size, at least 60. Every count in the profile scales with it. |
|
|
67
|
+
| `profile:` | `{}` | Overrides for `DEFAULT_PROFILE`, merged key by key. |
|
|
68
|
+
| `including:` | `[]` | Pool cards (names or ids) the deck must hold, as many copies as allowed up to 4. Each must fit the colors (a land: make only deck colors). A deck with included cards may name colors the pool has no other card of; those bring only basics. |
|
|
69
|
+
|
|
70
|
+
Random colors are drawn uniformly from the color combinations the pool can fill:
|
|
71
|
+
each color needs at least one card, and there must be enough cards in total. If no
|
|
72
|
+
combination works, the builder takes the one closest to working, and the error it
|
|
73
|
+
raises then tells you what's missing.
|
|
74
|
+
|
|
75
|
+
## Which cards fit
|
|
76
|
+
|
|
77
|
+
- **Spells.** A card fits when the colored symbols in its mana cost (its colors,
|
|
78
|
+
CR 105.2, 202.2) and in its activation costs are all deck colors. Colorless
|
|
79
|
+
cards fit every deck. Cards without a mana cost aren't used, because they can't
|
|
80
|
+
be cast.
|
|
81
|
+
- **Non-basic lands.** A land fits when all the colored mana it can produce is in
|
|
82
|
+
the deck colors (colorless is fine). A land that produces no mana isn't used.
|
|
83
|
+
Lands that produce two deck colors are picked more often.
|
|
84
|
+
- **Basic lands.** Always available, whatever the pool holds. If the pool has its own
|
|
85
|
+
basic lands, the builder uses them; otherwise it uses built-in ones. Basics are split by the deck's colored mana
|
|
86
|
+
symbols (mana cost pips times copies). Every color with at least one symbol gets
|
|
87
|
+
at least one basic, and the rest are split proportionally.
|
|
88
|
+
- **The four-copy rule** (CR 100.2a) applies to every card except basic lands, and an
|
|
89
|
+
owned count caps it further.
|
|
90
|
+
|
|
91
|
+
## The default profile
|
|
92
|
+
|
|
93
|
+
All counts are for 60 cards (`Enjine::DeckBuilder::DEFAULT_PROFILE`).
|
|
94
|
+
|
|
95
|
+
| Key | Default | Why |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `lands` | `range: 22..26, center: 24, outliers: 20..27` | Most 60-card decks play 22-26 lands, and 24 is the classic baseline. Very low curves go down to about 20, and heavy control decks go up to about 27. |
|
|
98
|
+
| `creatures` | `range: 14..24, center: 18, outliers: 10..28` | Creature counts vary the most between archetypes. |
|
|
99
|
+
| `outlier_chance` | `10` | The percent chance that a count is drawn uniformly from `outliers` instead of the triangular distribution over `range` peaking at `center`. |
|
|
100
|
+
| `curve` | `{1=>16, 2=>28, 3=>24, 4=>16, 5=>9, 6=>7}` | The weight of each mana value when picking the next card. 0 counts as 1, and 6 means 6 or more. |
|
|
101
|
+
| `copies` | `{4=>50, 3=>20, 2=>20, 1=>10}` | How many copies of each card: mostly 4-ofs, some 2-ofs and 3-ofs, a few 1-ofs. |
|
|
102
|
+
| `nonbasic_lands` | `mono: 0..4, multi: 2..8` | How many non-basic land copies to include, if any fit. Never more than half the lands. |
|
|
103
|
+
| `random_colors` | `{1=>30, 2=>50, 3=>20}` | How many colors `colors: :any` picks. |
|
|
104
|
+
|
|
105
|
+
Non-creature spells (instants, sorceries, artifacts, enchantments and
|
|
106
|
+
planeswalkers) fill whatever slots are left.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
aggro = { lands: { range: 20..23, center: 21 }, creatures: { range: 22..28, center: 25 },
|
|
110
|
+
curve: { 1 => 30, 2 => 35, 3 => 20, 4 => 10, 5 => 5, 6 => 0 } }
|
|
111
|
+
builder.build(colors: "R", seed: 1, profile: aggro)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## When the pool is too small
|
|
115
|
+
|
|
116
|
+
A small pool in one color may not have enough cards. The builder works through
|
|
117
|
+
these steps in order:
|
|
118
|
+
|
|
119
|
+
1. It adds more copies of the cards it already picked, up to 4 each (or the count
|
|
120
|
+
owned).
|
|
121
|
+
2. It shifts slots between creatures and other spells. A pool with few creatures
|
|
122
|
+
gets more non-creature spells, and the reverse.
|
|
123
|
+
3. It plays more lands, up to the profile's land `outliers` maximum.
|
|
124
|
+
4. If the deck still isn't full, it raises `Enjine::DeckBuilder::NotEnoughCards`.
|
|
125
|
+
The message names the colors, the pool's size, the number of playable cards it
|
|
126
|
+
found and how many cards were missing. `#details` returns the same facts as a Hash
|
|
127
|
+
(`pool`, `colors`, `size`, `colors_without_cards`, ...).
|
|
128
|
+
|
|
129
|
+
Every deck color needs at least one playable card: asking for `"WG"` from a pool
|
|
130
|
+
without green cards raises `NotEnoughCards` naming the missing color. It never pads
|
|
131
|
+
the deck with extra basics beyond the land maximum.
|
|
132
|
+
|
|
133
|
+
## Command line
|
|
134
|
+
|
|
135
|
+
`--pool FILE` is a JSON array of pool entries, loaded once for the whole run.
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
bundle exec exe/enjine --pool examples/pool.json --random-deck colors=WU seed=7 # prints the deck as JSON
|
|
139
|
+
bundle exec exe/enjine --pool examples/pool.json --random-deck "colors=2 seed=7" # quoted works too
|
|
140
|
+
bundle exec exe/enjine --pool examples/pool.json --random-deck colors=any,seed=3 # comma-separated works too
|
|
141
|
+
bundle exec exe/enjine --pool examples/pool.json --deck1 random:colors=WU --deck2 random --p2 random # play with them
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Spec keys are `colors` (letters, a count, or `any`), `seed` and `size`. Without
|
|
145
|
+
`seed`, `--random-deck` uses `--seed`, and `--deck1`/`--deck2` use the game seed
|
|
146
|
+
plus the seat index. The game intro prints each deck's recipe. A game saved with
|
|
147
|
+
`s FILE` carries its cards, so `--load` resumes it without the pool.
|
|
148
|
+
|
|
149
|
+
## Color balance
|
|
150
|
+
|
|
151
|
+
In multicolor decks, each new card is drawn from the least represented chosen
|
|
152
|
+
color with probability `color_balance` (default 75%), measured in colored mana
|
|
153
|
+
symbols. This keeps a two-color deck a real two-color deck: across 400 sample
|
|
154
|
+
decks, the weaker color makes up at least 36% of the colored symbols (median
|
|
155
|
+
47%). Set it to `0` for unbalanced splashes, or to `100` for strict balance.
|
|
156
|
+
Cards outside the chosen colors are never used, and neither are lands that make
|
|
157
|
+
off-color mana.
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Program: every Standard mechanic implemented
|
|
2
|
+
|
|
3
|
+
**Goal.** The engine supports every mechanic used by cards of the Standard-legal
|
|
4
|
+
sets that stay legal past the next rotation, newest set first. **Cards are written
|
|
5
|
+
by hand** in the [card DSL](../guides/card-data-format.md). The engine never reads
|
|
6
|
+
card text, and the library ships no card data and no sets: cards reach it as data,
|
|
7
|
+
in decks or a `DeckBuilder` pool.
|
|
8
|
+
|
|
9
|
+
For each set:
|
|
10
|
+
1. List the mechanics its cards need (keywords, keyword actions, set mechanics,
|
|
11
|
+
ability and effect shapes) and compare that list with `SUPPORTED.md`.
|
|
12
|
+
2. Implement each missing mechanic as a primitive, cite its Comprehensive Rules
|
|
13
|
+
numbers, and test it at every level with **invented** cards (unit, integration,
|
|
14
|
+
and fuzz through `spec/fixtures/cards/`).
|
|
15
|
+
3. Mark the set's mechanics **supported** in the table below.
|
|
16
|
+
|
|
17
|
+
## Progress (as of 2026-11-10)
|
|
18
|
+
|
|
19
|
+
This is the project's single progress document: where the Standard program
|
|
20
|
+
stands and what is pending.
|
|
21
|
+
|
|
22
|
+
**Done and on `main`:**
|
|
23
|
+
- Mechanics round 1 (6 builders) and round 2 (6 builders) are merged: 62 groups of
|
|
24
|
+
mechanics and card shapes, from earthbend and saddle to Paradigm, Meld, gift,
|
|
25
|
+
divided damage and ending the turn. See "Closed since the census" below.
|
|
26
|
+
- 591 real Standard cards are written in the DSL in `spec/fixtures/cards/real/`
|
|
27
|
+
(all 14 sets, every card type), each with a scenario spec, and they pass
|
|
28
|
+
`enjine --verify`.
|
|
29
|
+
- `enjine --verify DIR` (`Enjine::Verify`) checks any folder of card JSON:
|
|
30
|
+
validation, rules-monitored play, and which cards were used.
|
|
31
|
+
- The caller API guide (`docs/guides/api.md`); the CLI passes priority for you when
|
|
32
|
+
passing is the only choice and shows whose turn and which step it is.
|
|
33
|
+
- Gate: 1888 specs, ~99.5% line / ~91.5% branch coverage, RuboCop clean, fuzz
|
|
34
|
+
and CI green (CI uses the latest Ruby 4.0.x).
|
|
35
|
+
- By the census, Star Trek, Reality Fracture, Avatar, Spider-Man and Aetherdrift
|
|
36
|
+
have no known gaps; the other nine sets have 14 single-card gaps between them
|
|
37
|
+
(tables below).
|
|
38
|
+
|
|
39
|
+
**In progress: round 3 ("known gaps").** Implemented on unmerged branches, not
|
|
40
|
+
yet gated or merged:
|
|
41
|
+
|
|
42
|
+
| Area | Covers |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| Casting and permissions | Raubahn, Nita, Bilbo (Thief in the Night), Gwen Stacy, Hama, Molten Exhale, Celestial Reunion, Key to the Side-Door, Ninja Teen, Baron Zemo, the gift trigger |
|
|
45
|
+
| Zones and loops | "repeat this process" (Sin), exile until total mana value (Improvisation Capstone), "exiled with" targets (The Darkness Crystal, Borg Cube), change the target (Redirect Lightning), Nth-counter trigger (Death to Our Enemies), "if it was attacking" (Desculpting Blast), meld mana value, hideaway, earthbend cleanup |
|
|
46
|
+
| Statics and combat (least complete) | granter tap cost (Fishing Pole), chosen-color hexproof (Mondo Gecko), "attacks alone", sized tokens, "can't become untapped"; still to do: Spider-Punk's granted riot, Ghalta, Storm, Chandra, Flameshaper, Elspeth, Storm Slayer |
|
|
47
|
+
| Rules correctness | entering attacking a planeswalker (508.4), choosing the order of damage replacements (616.1), prevention follow-ups for unpreventable damage (615.12), new targets firing ward and becomes-target triggers, revalidating divided spells, "a source of your choice" (609.7), ability last known information |
|
|
48
|
+
|
|
49
|
+
To finish it: complete and gate each area, merge them one at a time, run the
|
|
50
|
+
full gate plus fuzz and `--verify`, and recount the tables below.
|
|
51
|
+
|
|
52
|
+
**Pending after round 3:**
|
|
53
|
+
1. **Per-set verification.** The census checked one example card per gap per set,
|
|
54
|
+
not every card. For each set, write every card in the DSL outside the
|
|
55
|
+
repository and run `enjine --verify` on it; any card that can't be written is a
|
|
56
|
+
new gap and goes into another round. Then fill the README "Set support" table
|
|
57
|
+
with ✓.
|
|
58
|
+
2. **Docs cleanup.** Reorganize `docs/guides/card-data-format.md` by concept
|
|
59
|
+
(sections 4.19 to 4.29 grew one per merge); make `SUPPORTED.md` a compact
|
|
60
|
+
index; shorten the README; check DESIGN, ARCHITECTURE and the guides against
|
|
61
|
+
the code; decide whether the informational `rules_text` field stays in the DSL,
|
|
62
|
+
the views and the example cards.
|
|
63
|
+
3. The remaining "Still open" items at the end of this file that no Standard card
|
|
64
|
+
needs (lure, blocking several creatures, rad counters, 613.8 dependency loops)
|
|
65
|
+
stay deferred until a card needs them.
|
|
66
|
+
|
|
67
|
+
## Definition of "supported"
|
|
68
|
+
|
|
69
|
+
A set's mechanics are supported when:
|
|
70
|
+
- every mechanic it uses is listed in `SUPPORTED.md`, or is expressible with
|
|
71
|
+
listed primitives, and is documented in `docs/guides/card-data-format.md`;
|
|
72
|
+
- every new mechanic has unit and integration specs plus at least one invented
|
|
73
|
+
fuzz card, and `bundle exec rake` is green;
|
|
74
|
+
- `rake fuzz` (invariants, rules monitor, export/import round-trips) passes.
|
|
75
|
+
|
|
76
|
+
## Sets, in work order
|
|
77
|
+
|
|
78
|
+
Release dates and rotation years are from Wizards' published schedule as
|
|
79
|
+
summarized at draftsim.com/mtg-standard-rotation (retrieved 2026-11-08). From 2027,
|
|
80
|
+
rotation happens at the first set release of the year. Legal-until dates for 2028
|
|
81
|
+
and later are **estimates** (January 1 of the rotation year) until that year's
|
|
82
|
+
first release is dated.
|
|
83
|
+
|
|
84
|
+
| # | Set | Released | Leaves Standard | Status |
|
|
85
|
+
| ---: | --- | --- | --- | --- |
|
|
86
|
+
| 1 | Star Trek | 2026-11-13 | 2029 (est.) | supported |
|
|
87
|
+
| 2 | Reality Fracture | 2026-10-02 | 2029 (est.) | supported |
|
|
88
|
+
| 3 | The Hobbit | 2026-08-14 | 2029 (est.) | 2 gaps |
|
|
89
|
+
| 4 | Marvel Super Heroes | 2026-06-26 | 2029 (est.) | 2 gaps |
|
|
90
|
+
| 5 | Secrets of Strixhaven | 2026-04-24 | 2029 (est.) | 2 gaps |
|
|
91
|
+
| 6 | Teenage Mutant Ninja Turtles | 2026-03-06 | 2029 (est.) | 1 gap |
|
|
92
|
+
| 7 | Lorwyn Eclipsed | 2026-01-23 | 2029 (est.) | 1 gap |
|
|
93
|
+
| 8 | Avatar: The Last Airbender | 2025-11-21 | 2028 (est.) | supported |
|
|
94
|
+
| 9 | Spider-Man | 2025-09-26 | 2028 (est.) | supported |
|
|
95
|
+
| 10 | Edge of Eternities | 2025-08-01 | 2028 (est.) | 1 gap |
|
|
96
|
+
| 11 | Final Fantasy | 2025-06-13 | 2028 (est.) | 2 gaps |
|
|
97
|
+
| 12 | Tarkir: Dragonstorm | 2025-04-11 | 2028 (est.) | 2 gaps |
|
|
98
|
+
| 13 | Aetherdrift | 2025-02-14 | 2028 (est.) | supported |
|
|
99
|
+
| 14 | Foundations | 2024-11-15 | 2029 (est.) | 1 gap |
|
|
100
|
+
|
|
101
|
+
**Out of scope:** sets that are no longer Standard-legal, or that rotate out at the
|
|
102
|
+
next announced rotation, are out of scope. On 2027-02-05 Wilds of Eldraine, The
|
|
103
|
+
Lost Caverns of Ixalan, Murders at Karlov Manor, Outlaws of Thunder Junction,
|
|
104
|
+
Bloomburrow and Duskmourn leave Standard, so they're skipped (the maintainer
|
|
105
|
+
decided this on 2026-11-08). When a new set releases, add it at the top. When a
|
|
106
|
+
rotation is announced, drop the rotating sets from this table.
|
|
107
|
+
|
|
108
|
+
## Mechanics gaps
|
|
109
|
+
|
|
110
|
+
Census taken 2026-11-09 against `SUPPORTED.md` at this commit. It lists what can't
|
|
111
|
+
yet be written in the card DSL. Card lists came from the Scryfall API (main sets,
|
|
112
|
+
`unique=cards`, no bonus sheets) and were checked outside the repository; nothing
|
|
113
|
+
derived from card text is stored here.
|
|
114
|
+
|
|
115
|
+
- **Star Trek** was checked card by card: each of its 311 cards was written mentally
|
|
116
|
+
in the DSL.
|
|
117
|
+
- **The other sets** were checked by keyword ability and keyword action (Scryfall's
|
|
118
|
+
keyword lists, matched against CR 701/702), by card layout, and by a
|
|
119
|
+
phrase classifier for recurring ability and effect shapes, calibrated by reading
|
|
120
|
+
samples of each set's unflagged cards. Their counts are approximate (`~Cards` is
|
|
121
|
+
how many cards show the shape), and rare one-off shapes may be missing. Seen
|
|
122
|
+
once or twice in samples and not listed below: controlling another player's
|
|
123
|
+
turn (CR 724), granted loyalty abilities, "attacks alone" triggers and pile
|
|
124
|
+
splitting. (Deck-construction exceptions, CR 100.2a, odd/even choices and ward
|
|
125
|
+
with a non-mana cost were also seen and are now supported.)
|
|
126
|
+
- Ability words (landfall, threshold, raid, …) are not gaps in themselves; only
|
|
127
|
+
what they need is listed. Sagas, Spacecraft/station, Planets, Equipment, Auras,
|
|
128
|
+
modal spells, hybrid `{R/G}` mana, planeswalkers and legendary permanents
|
|
129
|
+
are supported.
|
|
130
|
+
- A set's **Status** in the table above is its number of distinct gaps, or
|
|
131
|
+
"supported" when none is left. Gaps closed since the census are listed under
|
|
132
|
+
"Closed since the census" at the end of this section.
|
|
133
|
+
|
|
134
|
+
**Gaps left across all sets** (each now needed by about one card):
|
|
135
|
+
|
|
136
|
+
| Mechanic | CR | Sets | Example |
|
|
137
|
+
| --- | --- | --- | --- |
|
|
138
|
+
| Discard cost matched against your permanents ("same name as a legendary permanent you control") | 118.3, 701.9a | hob | Key to the Side-Door |
|
|
139
|
+
| Casting a spell from your graveyard as an effect ("you may cast an artifact, instant, or sorcery spell from your graveyard") | 601.2a, 608.2g | hob | Bilbo, Thief in the Night |
|
|
140
|
+
| Trigger on the Nth counter of a kind ("when the fourth plan counter is put on this") | 122.6, 603.2 | msh | Death to Our Enemies |
|
|
141
|
+
| Exile any number of graveyard cards as a cost, by mana symbols among them, then cast copies | 118.3, 707.12 | msh | Baron Helmut Zemo |
|
|
142
|
+
| Exile cards until their total mana value is N or greater | 406.1, 608.2c | sos | Improvisation Capstone |
|
|
143
|
+
| Cast trigger on a spell you don't own | 603.2, 108.3 | sos | Nita, Forum Conciliator |
|
|
144
|
+
| Hexproof from a color chosen on resolution | 702.11d | tmt | Mondo Gecko |
|
|
145
|
+
| A creature type chosen as part of an optional behold cost | 701.4, 601.2b | ecl | Celestial Reunion |
|
|
146
|
+
| Condition on a returned permanent's last known combat status ("if it was attacking") | 608.2h, 506.4 | eoe | Desculpting Blast |
|
|
147
|
+
| "Repeat this process" loops | 608.2c | fin | Sin, Spira's Punishment |
|
|
148
|
+
| Target filter: a card exiled with this permanent | 607.2a, 115.1 | fin | The Darkness Crystal |
|
|
149
|
+
| Flash that depends on an optional cost ("as though it had flash if you behold a Dragon") | 601.3c, 702.8 | tdm | Molten Exhale |
|
|
150
|
+
| A granted ability's cost that taps the granting permanent ("Tap Fishing Pole") | 602.2b, 607.1 | fdn | Fishing Pole |
|
|
151
|
+
|
|
152
|
+
### Star Trek (trk) — supported
|
|
153
|
+
|
|
154
|
+
311 cards.
|
|
155
|
+
|
|
156
|
+
No listed gap remains.
|
|
157
|
+
|
|
158
|
+
### Reality Fracture (fra) — supported
|
|
159
|
+
|
|
160
|
+
285 cards. No listed gap remains: last turn's history, behold or pay, restrictions on casting
|
|
161
|
+
the spell itself and type changes with a base P/T are written as in 4.24, 4.25, 4.27 and 4.28.
|
|
162
|
+
|
|
163
|
+
### The Hobbit (hob) — 2 gaps
|
|
164
|
+
|
|
165
|
+
193 cards.
|
|
166
|
+
|
|
167
|
+
| Missing mechanic | CR | ~Cards | Example |
|
|
168
|
+
| --- | --- | ---: | --- |
|
|
169
|
+
| Discard cost matched against your permanents ("same name as a legendary permanent you control") | 118.3, 701.9a | 1 | Key to the Side-Door |
|
|
170
|
+
| Casting a spell from your graveyard as an effect (the exile-instead rider itself is supported) | 601.2a, 608.2g | 1 | Bilbo, Thief in the Night |
|
|
171
|
+
|
|
172
|
+
### Marvel Super Heroes (msh) — 2 gaps
|
|
173
|
+
|
|
174
|
+
281 cards.
|
|
175
|
+
|
|
176
|
+
| Missing mechanic | CR | ~Cards | Example |
|
|
177
|
+
| --- | --- | ---: | --- |
|
|
178
|
+
| Trigger on the Nth counter of a kind (divided damage on the reflexive trigger is supported) | 122.6, 603.2 | 1 | Death to Our Enemies |
|
|
179
|
+
| Exile any number of graveyard cards as a cost, by mana symbols among them, then cast copies | 118.3, 707.12 | 1 | Baron Helmut Zemo |
|
|
180
|
+
|
|
181
|
+
### Secrets of Strixhaven (sos) — 2 gaps
|
|
182
|
+
|
|
183
|
+
271 cards.
|
|
184
|
+
|
|
185
|
+
| Missing mechanic | CR | ~Cards | Example |
|
|
186
|
+
| --- | --- | ---: | --- |
|
|
187
|
+
| Exile cards until their total mana value is N or greater (paradigm itself is supported) | 406.1, 608.2c | 1 | Improvisation Capstone |
|
|
188
|
+
| Cast trigger on a spell you don't own (the exile-instead rider is supported) | 603.2, 108.3 | 1 | Nita, Forum Conciliator |
|
|
189
|
+
|
|
190
|
+
### Teenage Mutant Ninja Turtles (tmt) — 1 gap
|
|
191
|
+
|
|
192
|
+
195 cards.
|
|
193
|
+
|
|
194
|
+
| Missing mechanic | CR | ~Cards | Example |
|
|
195
|
+
| --- | --- | ---: | --- |
|
|
196
|
+
| Hexproof from a color chosen on resolution | 702.11d | 1 | Mondo Gecko |
|
|
197
|
+
|
|
198
|
+
### Lorwyn Eclipsed (ecl) — 1 gap
|
|
199
|
+
|
|
200
|
+
273 cards.
|
|
201
|
+
|
|
202
|
+
| Missing mechanic | CR | ~Cards | Example |
|
|
203
|
+
| --- | --- | ---: | --- |
|
|
204
|
+
| A creature type chosen as part of an optional behold cost (behold or pay is supported) | 701.4, 601.2b | 1 | Celestial Reunion |
|
|
205
|
+
|
|
206
|
+
### Avatar: The Last Airbender (tla) — supported
|
|
207
|
+
|
|
208
|
+
286 cards. No listed gap remains (Hama, the Bloodbender's cast permission is under "Still open" below).
|
|
209
|
+
|
|
210
|
+
### Spider-Man (spm) — supported
|
|
211
|
+
|
|
212
|
+
193 cards. No listed gap remains (Spider-Man 2099 is written with `cast_restriction`, `turns_taken`
|
|
213
|
+
and `lands_played_this_turn`).
|
|
214
|
+
|
|
215
|
+
### Edge of Eternities (eoe) — 1 gap
|
|
216
|
+
|
|
217
|
+
266 cards.
|
|
218
|
+
|
|
219
|
+
| Missing mechanic | CR | ~Cards | Example |
|
|
220
|
+
| --- | --- | ---: | --- |
|
|
221
|
+
| Condition on a returned permanent's last known combat status (`can_block_only` is supported) | 608.2h, 506.4 | 1 | Desculpting Blast |
|
|
222
|
+
|
|
223
|
+
### Final Fantasy (fin) — 2 gaps
|
|
224
|
+
|
|
225
|
+
312 cards.
|
|
226
|
+
|
|
227
|
+
| Missing mechanic | CR | ~Cards | Example |
|
|
228
|
+
| --- | --- | ---: | --- |
|
|
229
|
+
| "Repeat this process" loops | 608.2c | 1 | Sin, Spira's Punishment |
|
|
230
|
+
| Target filter: a card exiled with this permanent (the dies-to-exile replacement is supported) | 607.2a, 115.1 | 1 | The Darkness Crystal |
|
|
231
|
+
|
|
232
|
+
### Tarkir: Dragonstorm (tdm) — 2 gaps
|
|
233
|
+
|
|
234
|
+
276 cards. The census listed Stalwart Successor under suspend; it has no suspend (Taigam, Master
|
|
235
|
+
Opportunist is the set's suspend card, and is supported).
|
|
236
|
+
|
|
237
|
+
| Missing mechanic | CR | ~Cards | Example |
|
|
238
|
+
| --- | --- | ---: | --- |
|
|
239
|
+
| Flash that depends on an optional cost ("as though it had flash if you behold a Dragon") | 601.3c, 702.8 | 1 | Molten Exhale |
|
|
240
|
+
| Keywords granted "until your next turn" (`grant` and `become` last until end of turn or longer, not until your next turn) | 611.2a | 1 | Elspeth, Storm Slayer |
|
|
241
|
+
|
|
242
|
+
### Aetherdrift (dft) — supported
|
|
243
|
+
|
|
244
|
+
276 cards. No listed gap remains.
|
|
245
|
+
|
|
246
|
+
### Foundations (fdn) — 1 gap
|
|
247
|
+
|
|
248
|
+
517 cards.
|
|
249
|
+
|
|
250
|
+
| Missing mechanic | CR | ~Cards | Example |
|
|
251
|
+
| --- | --- | ---: | --- |
|
|
252
|
+
| A granted ability's cost that taps the granting permanent (the `granter` selector covers effects) | 602.2b, 607.1 | 1 | Fishing Pole |
|
|
253
|
+
|
|
254
|
+
### Closed since the census
|
|
255
|
+
|
|
256
|
+
These gaps were removed from the per-set tables above (their primitives are in
|
|
257
|
+
`SUPPORTED.md` and `docs/guides/card-data-format.md`). A row that was only partly
|
|
258
|
+
closed stays under a narrower name with the cards known to need it.
|
|
259
|
+
|
|
260
|
+
| Gap | Rules | Now written as (card-data-format section) |
|
|
261
|
+
| --- | --- | --- |
|
|
262
|
+
| Variable number of targets (X targets, any number of targets) | 601.2c, 115.3 | target spec `"count": "x" \| n \| amount \| "any"` (+ `"optional"` for "up to"); decision `choose_target_count` (4.2) |
|
|
263
|
+
| Filters on keyword abilities ("creature with flying") | 702.1, 115.1 | filter `"keywords"` / `"not_keywords"` (4.4) |
|
|
264
|
+
| Choosing a color, creature type, card type or named option (as it enters or on resolution) | 614.12, 607.2d, 607.2f | `choose_color` / `choose_creature_type` / `choose_card_type` / `choose_option` (abilities and effects); filters `"…": "chosen"`; condition `{"chosen": word}` (4.15) |
|
|
265
|
+
| "A deck can have any number of cards named ~" | 100.2a | card field `"deck_limit": "any"` (or a number) (2) |
|
|
266
|
+
| Vehicles / crew | 702.122, 301.7 | ability `crew`; triggers `becomes_crewed`, `crews_vehicle`; printed P/T on a Vehicle (4.16) |
|
|
267
|
+
| Type-changing effects (becomes a creature, gains a card type) | 613.1d, 205.1b | effect `become` (types, subtypes, colors, keywords, abilities, base P/T); static `types` (4.16) |
|
|
268
|
+
| Exhaust (activate only once) | 702.177 | `"exhaust": true` on an activated ability; trigger filter `activates_ability` `exhaust` (4.16) |
|
|
269
|
+
| Start your engines! / max speed | 702.179, 702.178 | keyword `start_your_engines`; ability `max_speed`; amount `speed`; condition `max_speed` (4.16) |
|
|
270
|
+
| Double-faced cards (transform / modal DFC) | 712, 701.27 | `layout` + `faces`; effect `transform`; trigger `transforms`; `return_to_battlefield` `transformed` (4.17) |
|
|
271
|
+
| Adventurer cards, Omen cards | 716, 721 | layouts `adventure`, `omen` (4.17) |
|
|
272
|
+
| Preparation cards / prepared | 723 | layout `prepare`; effects `prepare` / `unprepare`; ability `enters_prepared` (4.17) |
|
|
273
|
+
| Flashback, harmonize, mayhem, warp, evoke, web-slinging, sneak | 702.34, 702.180, 702.187, 702.185, 702.74, 702.188, 702.190 | abilities of the same names with a `cost`; conditions `cast_with`, `cast_from`, `cast_with_this_turn` (4.10) |
|
|
274
|
+
| Convoke | 702.51 | keyword `convoke` (4.10) |
|
|
275
|
+
| Turn-history conditions (you attacked, a permanent left the battlefield, cards drawn / sacrificed / exiled this turn) | 508.1, 400.7 | condition `you_attacked_this_turn`; amounts `left_this_turn`, `sacrificed_this_turn`, `attackers_this_turn`, `exiled_this_turn`, `cards_drawn_this_turn`, `cards_discarded_this_turn` (4.11) |
|
|
276
|
+
| Trigger: leaves the battlefield; more trigger events (discard, cycle, sacrifice, scry/surveil, put into a zone, becomes untapped) | 603.2, 603.6c, 603.10a | triggers `leaves_battlefield`, `sacrifices`, `discards`, `cycles`, `scries`, `surveils`, `becomes_untapped`, `put_into`, with `batched` "one or more" (4.5b) |
|
|
277
|
+
| Mana spent to cast a spell (converge, "if N or more mana was spent") | 601.2h, 207.2c | amount `mana_spent` (4.18) |
|
|
278
|
+
| Prowess, job select | 702.108, 702.182 | keywords `prowess`, `job_select` (4.19) |
|
|
279
|
+
| Mobilize, firebending | 702.181, 702.189 | abilities `mobilize`, `firebending` (4.19) |
|
|
280
|
+
| Blight, connive, recruit, amass, empower Jace, endure | 701.68, 701.50, 701.70, 701.47, 701.71, 701.63 | effects of the same names; cost `blight` (4.19) |
|
|
281
|
+
| Kindred card type | 308 | `"types": ["kindred", …]` (4.19) |
|
|
282
|
+
| Earthbend, waterbend, airbend | 701.66, 701.67, 701.65 | effects `earthbend`, `airbend`; cost `waterbend` (also in `pay`, `counter_unless_pays`, `ward`); triggers `earthbends`, `waterbends`, `airbends`, `firebends` (4.19) |
|
|
283
|
+
| Saddle (Mounts), exert | 702.171, 701.43 | ability `saddle`; effect `become_saddled`; filter `saddled`; triggers `becomes_saddled`, `saddles_mount`; cost `exert` (4.19) |
|
|
284
|
+
| Decayed, wither | 702.147, 702.80 | keywords `decayed` (and its counter), `wither` (4.19) |
|
|
285
|
+
| Power-up, boast | 702.193, 702.142 | `"power_up": true` / `"boast": true` on an activated ability (4.20) |
|
|
286
|
+
| Teamwork | 702.194 | ability `teamwork`; condition `{"cast_with": "teamwork"}` (4.20) |
|
|
287
|
+
| Tiered | 702.183 | a `cost` on each mode of a modal spell (4.20) |
|
|
288
|
+
| Class cards | 717 | ability `class_level`; trigger `becomes_level` (4.20) |
|
|
289
|
+
| Increment, extort, riot, storm, cascade | 702.191, 702.101, 702.136, 702.40, 702.85 | keywords of the same names (4.20) |
|
|
290
|
+
| Storied (enduring story designation) | 702.195 | keyword `storied`; condition `{"designation": "enduring_story"}` (4.20) |
|
|
291
|
+
| Colors among permanents, domain | 105.2, 305.6 | amounts `distinct_colors`, `distinct_land_types` (4.20) |
|
|
292
|
+
| Discard a card / pay life / exile cards from your graveyard as a cost (also as a spell's additional cost) | 701.9, 119.4, 118.3 | costs `discard` (`count`, `card`, `random`), `pay_life` (N or `"X"`), `exile_from_graveyard` (4.2) |
|
|
293
|
+
| Behold | 701.4 | cost `behold` (`exile`: linked `exiled_with`, also after leaving); effect `behold` under `may` (4.2) |
|
|
294
|
+
| Monocolored hybrid mana {2/W} | 107.4e, 202.3f | mana cost symbol `{2/W}` (2) |
|
|
295
|
+
| Improvise; mana of the chosen color; activate only once | 702.126, 607.2d, 602.5b | keyword `improvise` (4.10); mana `color: "chosen"` (3); `limit: "once"` (4.2) |
|
|
296
|
+
| Ability removal (loses all abilities) | 613.1f | `"loses_abilities": true` on `become` and `static`; static `colors`, `base_power_toughness` (4.21) |
|
|
297
|
+
| More player/game restrictions (can't cast, can't gain life, spells can't be countered, ...) | 101.2, 604.1 | `player_rule` rules `cant_cast` (`spells`), `cant_cast_except_from_hand`, `spells_cant_be_countered`, `cant_gain_life`, `cant_activate`, `damage_cant_be_prevented`, `combat_damage_cant_be_prevented`; `if` (4.21) |
|
|
298
|
+
| Hexproof from / protection from | 702.11d, 702.16 | abilities `hexproof_from` / `protection_from` with a card spec (`colors`, `color_count`) or `"everything"` (4.21) |
|
|
299
|
+
| Granting activated abilities | 613.1f, 602.1 | `static` / `become` `abilities` with `activated` (4.21) |
|
|
300
|
+
| Emblems | 114 | effect `create_emblem`; the command zone (4.21) |
|
|
301
|
+
| Exchange (control, life totals), set life total | 701.12, 119.5 | effects `exchange_control`, `exchange_life_totals`, `set_life` (4.21) |
|
|
302
|
+
| Player counters (poison, experience) | 122.1, 122.1f, 704.5c | `add_counters` on players; amount `counters` of players (4.21; rad counters' trigger, CR 729, not yet) |
|
|
303
|
+
| Skip turns | 614.10 | effect `skip_turns` (4.21) |
|
|
304
|
+
| A chosen type added to other types; choosing a basic land type | 614.12, 607.2 | static `"subtypes": "chosen"`; ability `choose_basic_land_type` (4.21) |
|
|
305
|
+
| Control "for as long as" | 611.2b | `gain_control` durations `while_source_on_battlefield`, `while_source_tapped` (4.21) |
|
|
306
|
+
| Damage modification and redirection (double, plus N, "dealt to this creature instead") | 614.1a, 614.9, 616.1 | static `damage_replacement` (`by`, `to`, `combat`; `multiply`, `plus`, `redirect`) (4.22) |
|
|
307
|
+
| Damage prevention; damage can't be prevented | 615, 615.7, 615.12 | static and effect `prevent_damage` (`next`: a shield), `damage_unpreventable`; also `player_rule` `damage_cant_be_prevented` (4.21, 4.22) |
|
|
308
|
+
| Trigger: is dealt damage; trigger: blocks; noncombat damage | 120.2b, 509.3a, 603.2 | triggers `dealt_damage`, `blocks`; `deals_damage` `noncombat`; amount `noncombat_damage_this_turn` (4.22) |
|
|
309
|
+
| Excess damage | 120.4a, 120.10 | `{"this_way": n}` of a `deal_damage` effect (4.22) |
|
|
310
|
+
| Must be blocked; assigns combat damage by toughness | 509.1c, 510.1a | keyword flags `must_be_blocked`, `assigns_damage_by_toughness` (4.22) |
|
|
311
|
+
| Additional combat (and main) phases | 500.8 | effect `additional_phases`; condition `first_combat_phase` (4.22) |
|
|
312
|
+
| Static dies-to-exile replacement; token doubling | 614.1a, 614.5 | statics `exile_if_dies`, `double_tokens` (4.22) |
|
|
313
|
+
| Cast trigger filtered on the spell's targets ("a spell that targets ~ / a creature you control") | 603.2, 115.1 | trigger `cast` with `"targets": filter \| "self"` (4.23) |
|
|
314
|
+
| Spells cast from anywhere other than your hand: trigger and cost filter | 601.2a, 603.2 | `"from"` / `"not_from"` on trigger `cast` and on `cost_modifier` (4.23) |
|
|
315
|
+
| Copy activated or triggered abilities on the stack | 707.10 | `copy_spell` of a `stack_ability` target (`kind`, `controller`, `source`) (4.23) |
|
|
316
|
+
| Random choices (discard at random, a card at random from a graveyard) | 701.9b | `"random": true` on `discard` and `choose_cards` (4.23) |
|
|
317
|
+
| Coin flips | 705 | effect `flip_coins` (`count`: wins \| heads), amount `{ "this_way": n }` (4.23) |
|
|
318
|
+
| Proliferate | 701.34a | effect `proliferate` (`amount`) (4.23) |
|
|
319
|
+
| Choosing between costs ("behold a Goblin or pay {2}"), optional additional costs, paying costs while resolving, ward with a non-mana cost, gift, "you may behold a Jace; if you don't, it enters tapped" | 601.2b, 118.8b, 118.12, 702.21a, 702.174 | cost `one_of`, decision `choose_cost_option`; `additional_cost` `optional`; `pay` / `counter_unless_pays` `cost`; `ward` `cost`; cost `get_counters`; ability `gift`; `enters_tapped_unless_behold`; `spells_have` (4.24) |
|
|
320
|
+
| Divide damage among targets; "can't cast this spell unless"; which sources paid the mana ("mana from a Treasure"), mana spent compared with an amount; mana spendable only on spells not cast from hand; exile-instead replacement for spells cast via an effect; "Activate only if" on mana abilities | 601.2d, 601.3, 601.2h, 106.6, 614.1a, 602.5b | `divide` + effect `distribute_damage`; ability `cast_restriction`; amounts `turns_taken`, `mana_value`, `greatest_toughness`; `mana_spent` `from`; `spend_only` `from` / `not_from`; `exile_instead`; mana `only_if` (4.25) |
|
|
321
|
+
| Block restrictions; attack triggers on total power and "for the first time each turn"; attack restriction for an added combat; damage modification for sources that entered this turn or tied to one player; damage isn't removed during cleanup; prevention with a follow-up, a chosen source or a duration; dies-to-exile with a follow-up; excess damage in a fight; ending the turn | 509.1b, 508.1c, 616, 514.2, 615.5, 615.8, 611.2b, 614.1a, 120.4a, 725 | `can_block_only`; triggers `attacks` / `player_attacks`; `additional_phases` `attackers`; filter `entered_this_turn`; effect `damage_replacement`; keyword `keeps_damage`; `prevent_damage` `then` / `chosen_source` / `next_time` / `duration`; `exile_if_dies` `then`; effect `end_the_turn` (4.26) |
|
|
322
|
+
| Statics that set card types or the name; protection from each opponent; station, crew and saddle using toughness or "as though its power were 2 greater"; "for as long as you control this"; "activated abilities can't be activated" including mana abilities; granted abilities that refer to the granting permanent; two "as this enters" choices; type changes depending on each object | 205.1a, 612.8, 702.16k, 702.184a, 611.2b, 602.5, 607.1a, 614.12, 613.7e | static `set_types`, `name`; `protection_from` `"each_opponent"`; ability `tapping_power`; duration `while_you_control_source`; keyword flag `abilities_cant_be_activated`; selector `"granter"` (4.27) |
|
|
323
|
+
| What happened last turn; playing a land from anywhere (trigger, amount); "enters from exile"; amount "this way" through a "may"; "those creatures"; copying the ability that triggered; boast and teamwork-tap triggers; choosing new targets; granting cascade to spells; exchange of targets that share a card type; searching for different names; coin-flip replacements; dice | 120.2b, 305.1, 608.2c, 707.10, 702.142b, 115.7d, 701.12b, 705.3, 706 | amount `noncombat_damage_last_turn`; trigger `plays_land`, `lands_played_this_turn`; selectors `{"triggering": "targets" \| "ability"}`; trigger `ability_triggers`; effect `choose_new_targets`; ability `spell_keywords`; `shares_type_with`; `different_names`; `first_flips_win`; effect `roll_dice` (4.28) |
|
|
324
|
+
| Paradigm, foretell, suspend, hideaway, devour, meld, endure with a computed N, proliferate X times by planeswalker types | 702.192, 702.143, 702.62, 702.75, 702.82, 701.42, 713, 701.63 | keywords and abilities of the same names; effects `meld`, `suspend`; amount `distinct_planeswalker_types` (4.29) |
|
|
325
|
+
|
|
326
|
+
Still open in these areas (not yet counted as gaps above, since the census didn't
|
|
327
|
+
separate them):
|
|
328
|
+
- Costs: "Ward—Pay life equal to its power" (a computed `pay_life`, Raubahn), "whenever a player gives a gift"
|
|
329
|
+
(CR 702.174c), more than one optional cost announced per spell (teamwork still marks the spell kicked), and
|
|
330
|
+
"change the target of target spell" (Redirect Lightning; `choose_new_targets` lets the player keep it).
|
|
331
|
+
- "Can't crew Vehicles" (CR 702.122d); granting `tapping_power` through `become`.
|
|
332
|
+
- Casting a card "by waterbending {X}" rather than paying its mana cost, and cast permissions that last "for as
|
|
333
|
+
long as you control" a source (Hama, the Bloodbender; Gwen Stacy).
|
|
334
|
+
- Granting flashback, harmonize, mayhem, web-slinging or sneak to other cards (Ninja Teen); "cost {N} less"
|
|
335
|
+
for spells cast from anywhere other than your hand (the remaining "Spells cast from anywhere other than
|
|
336
|
+
hand" rows); "as though exhaust abilities haven't been activated".
|
|
337
|
+
- Amass or empower by another player, and triggers on blighting, firebending or conniving.
|
|
338
|
+
- Casting a card transformed (disturb, CR 712.8a), convert (CR 701.28), day/night (CR 702.145), the mana value
|
|
339
|
+
of a transformed back face (CR 712.6d) and of a melded permanent (CR 713.5c).
|
|
340
|
+
- A type change by a resolved effect that replaces the other types or lasts "until your next turn".
|
|
341
|
+
- "Whenever you cycle or discard a card" triggering only once for a cycled card
|
|
342
|
+
(CR 702.29d); amounts from another leaving object's last known counters; "a card put
|
|
343
|
+
into your graveyard from anywhere this turn" as a target filter.
|
|
344
|
+
- A card spec (library search) matching the chosen type, and a static that grants
|
|
345
|
+
keywords to creatures filtered by keywords (layer-6 dependency, CR 613.8).
|
|
346
|
+
- Set mechanics: granting riot to other permanents (Spider-Punk: statics that would grant an "as this enters"
|
|
347
|
+
choice are not read before it enters, CR 614.12) and the choice of hybrid half when a power-up cost is
|
|
348
|
+
reduced (CR 118.7e: the half the cost needs is taken).
|
|
349
|
+
- Rules and statics: rad counters' inherent trigger (CR 729); skipping steps or phases (only whole turns);
|
|
350
|
+
"can't draw more than one card / cast more than one spell each turn"; "can't become untapped"
|
|
351
|
+
(Spider-Woman); dependency ordering and loops (CR 613.8).
|
|
352
|
+
- Damage and combat: blocking an additional creature or any number; lure ("all creatures able to block it do
|
|
353
|
+
so"); "becomes blocked by" a filter; a source chosen from the command zone (CR 609.7a); damage choices
|
|
354
|
+
follow a fixed order (redirect, prevent, multiply, add; statics before resolved effects) instead of the
|
|
355
|
+
affected player's choice (CR 616.1); prevention follow-ups when the damage can't be prevented (CR 615.12).
|
|
356
|
+
|
|
357
|
+
## Card data rules
|
|
358
|
+
|
|
359
|
+
- Cards are authored by hand in the DSL. Nothing in the repository derives card
|
|
360
|
+
data from rules text.
|
|
361
|
+
- **Printing identifier:** set code + collector number (`"printing": {"set", "number"}`),
|
|
362
|
+
which is vendor-neutral.
|
|
363
|
+
- **Card identity across sets:** a card's name is unique in Magic. The catalog
|
|
364
|
+
raises `CatalogConflict` if two definitions with one name disagree.
|
|
365
|
+
|
|
366
|
+
## Rules version
|
|
367
|
+
|
|
368
|
+
The engine is pinned to the 2026-11-06 CR (`Enjine::RULES_VERSION`). When a newer
|
|
369
|
+
CR comes out, re-pin: diff the section headings, renumber what moved, and run
|
|
370
|
+
`bundle exec rake "cr_citations[path/to/cr.txt]"` to check that every cited rule
|
|
371
|
+
number exists in the new text (see docs/MECHANICS_COVERAGE.md).
|