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.
Files changed (269) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +201 -0
  5. data/SUPPORTED.md +387 -0
  6. data/docs/ARCHITECTURE.md +141 -0
  7. data/docs/DESIGN.md +139 -0
  8. data/docs/MECHANICS_COVERAGE.md +670 -0
  9. data/docs/guides/adding-a-mechanic.md +60 -0
  10. data/docs/guides/api.md +614 -0
  11. data/docs/guides/card-data-format.md +1472 -0
  12. data/docs/guides/deck-builder.md +157 -0
  13. data/docs/plans/standard.md +371 -0
  14. data/docs/reference/abilities.md +1680 -0
  15. data/docs/reference/costs.md +339 -0
  16. data/docs/reference/counters.md +121 -0
  17. data/docs/reference/decisions.md +102 -0
  18. data/docs/reference/effects.md +2340 -0
  19. data/docs/reference/filters.md +172 -0
  20. data/docs/reference/keywords.md +647 -0
  21. data/docs/reference/subtypes.md +169 -0
  22. data/docs/reference/supertypes.md +31 -0
  23. data/docs/reference/triggers.md +797 -0
  24. data/docs/reference/types.md +216 -0
  25. data/exe/enjine +7 -0
  26. data/lib/enjine/actions/activate_mana_ability.rb +24 -0
  27. data/lib/enjine/actions/cast.rb +57 -0
  28. data/lib/enjine/actions/pass.rb +18 -0
  29. data/lib/enjine/actions/play_land.rb +52 -0
  30. data/lib/enjine/actions/special_action.rb +37 -0
  31. data/lib/enjine/actions.rb +20 -0
  32. data/lib/enjine/agents/busy.rb +27 -0
  33. data/lib/enjine/agents/random.rb +28 -0
  34. data/lib/enjine/card_definition.rb +433 -0
  35. data/lib/enjine/card_faces.rb +116 -0
  36. data/lib/enjine/catalog.rb +56 -0
  37. data/lib/enjine/characteristics.rb +39 -0
  38. data/lib/enjine/cli/text.rb +295 -0
  39. data/lib/enjine/cli.rb +312 -0
  40. data/lib/enjine/decisions/bottom.rb +29 -0
  41. data/lib/enjine/decisions/casting.rb +188 -0
  42. data/lib/enjine/decisions/combat.rb +75 -0
  43. data/lib/enjine/decisions/discard.rb +29 -0
  44. data/lib/enjine/decisions/effect_choice.rb +21 -0
  45. data/lib/enjine/decisions/enter_choice.rb +26 -0
  46. data/lib/enjine/decisions/legend_rule.rb +21 -0
  47. data/lib/enjine/decisions/mulligan.rb +26 -0
  48. data/lib/enjine/decisions/order_triggers.rb +21 -0
  49. data/lib/enjine/decisions/play_or_draw.rb +19 -0
  50. data/lib/enjine/decisions/priority.rb +22 -0
  51. data/lib/enjine/decisions/untap_choice.rb +35 -0
  52. data/lib/enjine/decisions.rb +22 -0
  53. data/lib/enjine/deck.rb +151 -0
  54. data/lib/enjine/deck_builder.rb +454 -0
  55. data/lib/enjine/docs.rb +90 -0
  56. data/lib/enjine/engine/ability_layers.rb +118 -0
  57. data/lib/enjine/engine/attachments.rb +86 -0
  58. data/lib/enjine/engine/casting/alt_casting.rb +254 -0
  59. data/lib/enjine/engine/casting/cast_choices.rb +292 -0
  60. data/lib/enjine/engine/casting/casting.rb +326 -0
  61. data/lib/enjine/engine/casting/cost_options.rb +99 -0
  62. data/lib/enjine/engine/casting/costs.rb +117 -0
  63. data/lib/enjine/engine/casting/payment.rb +353 -0
  64. data/lib/enjine/engine/casting/permissions.rb +412 -0
  65. data/lib/enjine/engine/casting/target_counts.rb +113 -0
  66. data/lib/enjine/engine/casting/waterbend.rb +90 -0
  67. data/lib/enjine/engine/characteristics.rb +379 -0
  68. data/lib/enjine/engine/class_levels.rb +25 -0
  69. data/lib/enjine/engine/combat.rb +315 -0
  70. data/lib/enjine/engine/copies.rb +208 -0
  71. data/lib/enjine/engine/damage.rb +196 -0
  72. data/lib/enjine/engine/designations.rb +46 -0
  73. data/lib/enjine/engine/emblems.rb +37 -0
  74. data/lib/enjine/engine/entering.rb +156 -0
  75. data/lib/enjine/engine/face_down.rb +30 -0
  76. data/lib/enjine/engine/faces.rb +222 -0
  77. data/lib/enjine/engine/follow_ups.rb +27 -0
  78. data/lib/enjine/engine/mana_sources.rb +230 -0
  79. data/lib/enjine/engine/phasing.rb +74 -0
  80. data/lib/enjine/engine/pregame.rb +95 -0
  81. data/lib/enjine/engine/priority.rb +47 -0
  82. data/lib/enjine/engine/resolution.rb +168 -0
  83. data/lib/enjine/engine/rules.rb +287 -0
  84. data/lib/enjine/engine/sagas.rb +41 -0
  85. data/lib/enjine/engine/speed.rb +104 -0
  86. data/lib/enjine/engine/spell_grants.rb +46 -0
  87. data/lib/enjine/engine/state_based_actions.rb +157 -0
  88. data/lib/enjine/engine/triggers.rb +364 -0
  89. data/lib/enjine/engine/turn_history.rb +240 -0
  90. data/lib/enjine/engine/turn_structure.rb +224 -0
  91. data/lib/enjine/engine/zones.rb +278 -0
  92. data/lib/enjine/engine.rb +55 -0
  93. data/lib/enjine/errors.rb +62 -0
  94. data/lib/enjine/events.rb +161 -0
  95. data/lib/enjine/frames.rb +84 -0
  96. data/lib/enjine/fuzz.rb +112 -0
  97. data/lib/enjine/game.rb +204 -0
  98. data/lib/enjine/history.rb +26 -0
  99. data/lib/enjine/mana.rb +142 -0
  100. data/lib/enjine/mechanics/abilities/additional_triggers.rb +86 -0
  101. data/lib/enjine/mechanics/abilities/chosen_values.rb +213 -0
  102. data/lib/enjine/mechanics/abilities/cost_changes.rb +154 -0
  103. data/lib/enjine/mechanics/abilities/damage_replacements.rb +569 -0
  104. data/lib/enjine/mechanics/abilities/double_counters.rb +71 -0
  105. data/lib/enjine/mechanics/abilities/entering_choices.rb +91 -0
  106. data/lib/enjine/mechanics/abilities/entering_copy.rb +69 -0
  107. data/lib/enjine/mechanics/abilities/enters_tapped.rb +159 -0
  108. data/lib/enjine/mechanics/abilities/enters_with_counters.rb +116 -0
  109. data/lib/enjine/mechanics/abilities/mana_ability.rb +209 -0
  110. data/lib/enjine/mechanics/abilities/play_permissions.rb +527 -0
  111. data/lib/enjine/mechanics/abilities/player_rules.rb +169 -0
  112. data/lib/enjine/mechanics/abilities/replacement_effects.rb +368 -0
  113. data/lib/enjine/mechanics/abilities/restrictions.rb +144 -0
  114. data/lib/enjine/mechanics/abilities/spell_keywords.rb +55 -0
  115. data/lib/enjine/mechanics/abilities/stack_abilities.rb +542 -0
  116. data/lib/enjine/mechanics/abilities/static.rb +365 -0
  117. data/lib/enjine/mechanics/abilities/tapping_power.rb +71 -0
  118. data/lib/enjine/mechanics/abilities/turn_rules.rb +49 -0
  119. data/lib/enjine/mechanics/card_spec.rb +177 -0
  120. data/lib/enjine/mechanics/copy_except.rb +73 -0
  121. data/lib/enjine/mechanics/costs/base.rb +115 -0
  122. data/lib/enjine/mechanics/costs/behold.rb +76 -0
  123. data/lib/enjine/mechanics/costs/discard.rb +65 -0
  124. data/lib/enjine/mechanics/costs/exert.rb +29 -0
  125. data/lib/enjine/mechanics/costs/exile_cards.rb +39 -0
  126. data/lib/enjine/mechanics/costs/from_graveyard.rb +45 -0
  127. data/lib/enjine/mechanics/costs/get_counters.rb +39 -0
  128. data/lib/enjine/mechanics/costs/mana.rb +37 -0
  129. data/lib/enjine/mechanics/costs/one_of.rb +49 -0
  130. data/lib/enjine/mechanics/costs/pay_life.rb +50 -0
  131. data/lib/enjine/mechanics/costs/remove_counters.rb +51 -0
  132. data/lib/enjine/mechanics/costs/sacrifice.rb +59 -0
  133. data/lib/enjine/mechanics/costs/tap.rb +56 -0
  134. data/lib/enjine/mechanics/costs/waterbend.rb +29 -0
  135. data/lib/enjine/mechanics/counters/kinds.rb +190 -0
  136. data/lib/enjine/mechanics/effects/add_subtypes.rb +30 -0
  137. data/lib/enjine/mechanics/effects/airbend.rb +41 -0
  138. data/lib/enjine/mechanics/effects/amass.rb +124 -0
  139. data/lib/enjine/mechanics/effects/assimilate.rb +72 -0
  140. data/lib/enjine/mechanics/effects/base.rb +148 -0
  141. data/lib/enjine/mechanics/effects/become.rb +182 -0
  142. data/lib/enjine/mechanics/effects/behold.rb +48 -0
  143. data/lib/enjine/mechanics/effects/blight.rb +94 -0
  144. data/lib/enjine/mechanics/effects/choose_cards.rb +168 -0
  145. data/lib/enjine/mechanics/effects/copy.rb +370 -0
  146. data/lib/enjine/mechanics/effects/counter_spell.rb +21 -0
  147. data/lib/enjine/mechanics/effects/counters.rb +161 -0
  148. data/lib/enjine/mechanics/effects/damage.rb +72 -0
  149. data/lib/enjine/mechanics/effects/delayed_triggers.rb +112 -0
  150. data/lib/enjine/mechanics/effects/destroy.rb +22 -0
  151. data/lib/enjine/mechanics/effects/discard.rb +86 -0
  152. data/lib/enjine/mechanics/effects/draw.rb +16 -0
  153. data/lib/enjine/mechanics/effects/earthbend.rb +70 -0
  154. data/lib/enjine/mechanics/effects/emblem.rb +52 -0
  155. data/lib/enjine/mechanics/effects/endure.rb +63 -0
  156. data/lib/enjine/mechanics/effects/exile.rb +49 -0
  157. data/lib/enjine/mechanics/effects/extra_turn.rb +122 -0
  158. data/lib/enjine/mechanics/effects/faces.rb +115 -0
  159. data/lib/enjine/mechanics/effects/fight.rb +76 -0
  160. data/lib/enjine/mechanics/effects/flip_coins.rb +97 -0
  161. data/lib/enjine/mechanics/effects/gain_control.rb +94 -0
  162. data/lib/enjine/mechanics/effects/game_end.rb +42 -0
  163. data/lib/enjine/mechanics/effects/library.rb +327 -0
  164. data/lib/enjine/mechanics/effects/life.rb +83 -0
  165. data/lib/enjine/mechanics/effects/look_at_hand.rb +17 -0
  166. data/lib/enjine/mechanics/effects/loot.rb +118 -0
  167. data/lib/enjine/mechanics/effects/mana.rb +96 -0
  168. data/lib/enjine/mechanics/effects/may.rb +82 -0
  169. data/lib/enjine/mechanics/effects/meld.rb +77 -0
  170. data/lib/enjine/mechanics/effects/mill.rb +22 -0
  171. data/lib/enjine/mechanics/effects/pay.rb +231 -0
  172. data/lib/enjine/mechanics/effects/phasing.rb +51 -0
  173. data/lib/enjine/mechanics/effects/proliferate.rb +72 -0
  174. data/lib/enjine/mechanics/effects/pump.rb +60 -0
  175. data/lib/enjine/mechanics/effects/put_in_library.rb +98 -0
  176. data/lib/enjine/mechanics/effects/return_choice.rb +29 -0
  177. data/lib/enjine/mechanics/effects/return_to_battlefield.rb +80 -0
  178. data/lib/enjine/mechanics/effects/return_to_hand.rb +20 -0
  179. data/lib/enjine/mechanics/effects/roll_dice.rb +45 -0
  180. data/lib/enjine/mechanics/effects/sacrifice.rb +118 -0
  181. data/lib/enjine/mechanics/effects/scry_and_surveil.rb +74 -0
  182. data/lib/enjine/mechanics/effects/search.rb +156 -0
  183. data/lib/enjine/mechanics/effects/tap_untap.rb +23 -0
  184. data/lib/enjine/mechanics/effects/tokens.rb +76 -0
  185. data/lib/enjine/mechanics/filters/any_target.rb +15 -0
  186. data/lib/enjine/mechanics/filters/base.rb +386 -0
  187. data/lib/enjine/mechanics/filters/graveyard.rb +23 -0
  188. data/lib/enjine/mechanics/filters/one_of.rb +55 -0
  189. data/lib/enjine/mechanics/filters/permanents.rb +52 -0
  190. data/lib/enjine/mechanics/filters/players.rb +20 -0
  191. data/lib/enjine/mechanics/filters/stack.rb +97 -0
  192. data/lib/enjine/mechanics/follow_up.rb +46 -0
  193. data/lib/enjine/mechanics/keywords/alternative_costs.rb +361 -0
  194. data/lib/enjine/mechanics/keywords/beam_me_up.rb +78 -0
  195. data/lib/enjine/mechanics/keywords/changeling.rb +58 -0
  196. data/lib/enjine/mechanics/keywords/convoke.rb +77 -0
  197. data/lib/enjine/mechanics/keywords/crew.rb +155 -0
  198. data/lib/enjine/mechanics/keywords/cycling.rb +58 -0
  199. data/lib/enjine/mechanics/keywords/decayed.rb +25 -0
  200. data/lib/enjine/mechanics/keywords/devour.rb +61 -0
  201. data/lib/enjine/mechanics/keywords/evergreen.rb +54 -0
  202. data/lib/enjine/mechanics/keywords/extort.rb +26 -0
  203. data/lib/enjine/mechanics/keywords/flags.rb +46 -0
  204. data/lib/enjine/mechanics/keywords/foretell.rb +45 -0
  205. data/lib/enjine/mechanics/keywords/gift.rb +50 -0
  206. data/lib/enjine/mechanics/keywords/hideaway.rb +28 -0
  207. data/lib/enjine/mechanics/keywords/increment.rb +31 -0
  208. data/lib/enjine/mechanics/keywords/job_select.rb +29 -0
  209. data/lib/enjine/mechanics/keywords/kicker.rb +92 -0
  210. data/lib/enjine/mechanics/keywords/mobilize_and_firebending.rb +84 -0
  211. data/lib/enjine/mechanics/keywords/paradigm.rb +91 -0
  212. data/lib/enjine/mechanics/keywords/power_up.rb +48 -0
  213. data/lib/enjine/mechanics/keywords/protection.rb +111 -0
  214. data/lib/enjine/mechanics/keywords/prowess.rb +30 -0
  215. data/lib/enjine/mechanics/keywords/riot.rb +35 -0
  216. data/lib/enjine/mechanics/keywords/saddle.rb +115 -0
  217. data/lib/enjine/mechanics/keywords/speed.rb +89 -0
  218. data/lib/enjine/mechanics/keywords/station.rb +163 -0
  219. data/lib/enjine/mechanics/keywords/storied.rb +41 -0
  220. data/lib/enjine/mechanics/keywords/storm_and_cascade.rb +86 -0
  221. data/lib/enjine/mechanics/keywords/sunburst.rb +29 -0
  222. data/lib/enjine/mechanics/keywords/suspend.rb +191 -0
  223. data/lib/enjine/mechanics/keywords/teamwork.rb +45 -0
  224. data/lib/enjine/mechanics/keywords/ward.rb +67 -0
  225. data/lib/enjine/mechanics/linked.rb +67 -0
  226. data/lib/enjine/mechanics/mana_spent.rb +99 -0
  227. data/lib/enjine/mechanics/selector.rb +207 -0
  228. data/lib/enjine/mechanics/subtypes/auras_and_equipment.rb +176 -0
  229. data/lib/enjine/mechanics/subtypes/basic_land_types.rb +36 -0
  230. data/lib/enjine/mechanics/subtypes/class_levels.rb +102 -0
  231. data/lib/enjine/mechanics/subtypes/saga.rb +89 -0
  232. data/lib/enjine/mechanics/supertypes/basic.rb +17 -0
  233. data/lib/enjine/mechanics/supertypes/legendary.rb +18 -0
  234. data/lib/enjine/mechanics/triggers/ability_triggers.rb +51 -0
  235. data/lib/enjine/mechanics/triggers/any_of.rb +43 -0
  236. data/lib/enjine/mechanics/triggers/base.rb +228 -0
  237. data/lib/enjine/mechanics/triggers/casting.rb +219 -0
  238. data/lib/enjine/mechanics/triggers/combat.rb +123 -0
  239. data/lib/enjine/mechanics/triggers/damage.rb +130 -0
  240. data/lib/enjine/mechanics/triggers/dilemma.rb +37 -0
  241. data/lib/enjine/mechanics/triggers/draw_and_discard.rb +85 -0
  242. data/lib/enjine/mechanics/triggers/keyword_actions.rb +55 -0
  243. data/lib/enjine/mechanics/triggers/life.rb +35 -0
  244. data/lib/enjine/mechanics/triggers/steps.rb +39 -0
  245. data/lib/enjine/mechanics/triggers/tap_and_counters.rb +111 -0
  246. data/lib/enjine/mechanics/triggers/zone_changes.rb +249 -0
  247. data/lib/enjine/mechanics/types/kindred.rb +30 -0
  248. data/lib/enjine/mechanics/types/land.rb +24 -0
  249. data/lib/enjine/mechanics/types/permanent_types.rb +67 -0
  250. data/lib/enjine/mechanics/types/spell_types.rb +38 -0
  251. data/lib/enjine/mechanics/values.rb +675 -0
  252. data/lib/enjine/mechanics.rb +74 -0
  253. data/lib/enjine/player.rb +30 -0
  254. data/lib/enjine/primitive.rb +61 -0
  255. data/lib/enjine/replacements.rb +191 -0
  256. data/lib/enjine/rng.rb +57 -0
  257. data/lib/enjine/rules_monitor.rb +404 -0
  258. data/lib/enjine/setup.rb +45 -0
  259. data/lib/enjine/snapshot/player_checks.rb +86 -0
  260. data/lib/enjine/snapshot/turn_checks.rb +91 -0
  261. data/lib/enjine/snapshot.rb +307 -0
  262. data/lib/enjine/state.rb +173 -0
  263. data/lib/enjine/steps.rb +117 -0
  264. data/lib/enjine/tokens.rb +25 -0
  265. data/lib/enjine/verify.rb +233 -0
  266. data/lib/enjine/version.rb +7 -0
  267. data/lib/enjine/view.rb +162 -0
  268. data/lib/enjine.rb +46 -0
  269. 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).