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,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ # The context an effect runs in.
6
+ # `results` lists, per earlier effect of the same effect list, the objects that effect
7
+ # moved or created (CR 400.7j: "the card exiled this way"); see Selector {"result": n}.
8
+ # `amounts`: per earlier effect, how much it changed (life an earlier effect took); see Amount {"this_way": n}.
9
+ # `memory`: stands in for the resolving ability's memory while a trigger condition is checked (CR 603.4).
10
+ EffectContext = Struct.new(:controller, :source, :object, :targets, :results, :amounts, :memory)
11
+
12
+ # Data form of "who/what does this effect apply to":
13
+ # {"target": 0} the first chosen target
14
+ # "self" the source permanent, or the source card while it's still in
15
+ # the graveyard, hand or exile ("return this card from your
16
+ # graveyard", CR 113.6m; it's a new object once it moves, CR 400.7)
17
+ # "you" the controller
18
+ # "each_opponent" every opponent
19
+ # "attached" what the source is attached to ("enchanted creature", CR 303.4m)
20
+ # {"each": "creature", "controller": "you"} every matching object (you | opponent | any)
21
+ # {"result": 0} what effect 0 of this list moved or created (CR 400.7j)
22
+ # {"controller_of": selector} the controller of each referenced object ("its controller");
23
+ # one that has left the battlefield as it last existed (CR 608.2h)
24
+ # {"triggering": "object" | "player"} what the trigger event was about ("that creature",
25
+ # "that player", CR 603.2); see Triggers.event_facts
26
+ # {"triggering": "targets"} the targets of the spell a cast trigger saw that matched its
27
+ # `targets` filter ("those creatures", CR 601.2c)
28
+ # {"triggering": "ability"} the activated or triggered ability on the stack the trigger
29
+ # event was about ("copy that ability", CR 707.10)
30
+ # "watched" what a delayed triggered ability watches (CR 603.7c)
31
+ # "exiled_with" cards in exile exiled with the source (linked abilities, CR 607.2a)
32
+ # "gift_recipient" the opponent chosen for the spell's gift (CR 702.174a)
33
+ # "granter" the permanent whose static ability granted this ability ("Return
34
+ # Trusty Boomerang to its owner's hand", CR 607.1a), while it's still
35
+ # on the battlefield; the source itself for an ability it has printed
36
+ class Selector
37
+ CONTROLLERS = %w[you opponent any].freeze
38
+ TRIGGERING = %w[object player targets ability].freeze
39
+ # Where "self" finds its source (CR 113.6m); a source that changed zones is a new object (CR 400.7).
40
+ SELF_ZONES = %i[battlefield graveyard exile hand].freeze
41
+
42
+ attr_reader :kind, :value, :controller
43
+
44
+ def self.build(data, errors, path, targets_count:, results_count: 0)
45
+ case data
46
+ when "self", "you", "each_opponent", "attached", "watched", "exiled_with", "gift_recipient", "granter" then new(data.to_sym)
47
+ when Hash
48
+ data = data.transform_keys(&:to_s)
49
+ return build_result(data, errors, path, results_count) if data.key?("result")
50
+ return build_controller_of(data, errors, path, targets_count, results_count) if data.key?("controller_of")
51
+ return build_triggering(data, errors, path) if data.key?("triggering")
52
+
53
+ build_hash(data, errors, path, targets_count)
54
+ else
55
+ errors << "#{path}: must be {\"target\": n}, \"self\", \"you\", \"each_opponent\", \"attached\" or {\"each\": filter}"
56
+ nil
57
+ end
58
+ end
59
+
60
+ # {"result": n}: n must name an earlier effect of the same list (a nested list sees its parent's).
61
+ def self.build_result(data, errors, path, results_count)
62
+ Mechanics.check_keys(data, %w[result], errors, path)
63
+ index = data["result"]
64
+ unless index.is_a?(Integer) && index >= 0 && index < results_count
65
+ errors << "#{path}.result: must refer to one of the #{results_count} earlier effects"
66
+ end
67
+ new(:result, index)
68
+ end
69
+
70
+ def self.build_controller_of(data, errors, path, targets_count, results_count)
71
+ Mechanics.check_keys(data, %w[controller_of], errors, path)
72
+ inner = build(data["controller_of"], errors, "#{path}.controller_of", targets_count: targets_count,
73
+ results_count: results_count)
74
+ new(:controller_of, inner)
75
+ end
76
+
77
+ def self.build_triggering(data, errors, path)
78
+ Mechanics.check_keys(data, %w[triggering], errors, path)
79
+ errors << "#{path}.triggering: must be one of #{TRIGGERING.join(", ")}" unless TRIGGERING.include?(data["triggering"])
80
+ new(:triggering, data["triggering"])
81
+ end
82
+
83
+ def self.build_hash(data, errors, path, targets_count)
84
+ if data.key?("target")
85
+ index = data["target"]
86
+ unless index.is_a?(Integer) && index >= 0 && index < targets_count
87
+ errors << "#{path}.target: must refer to one of the #{targets_count} declared targets"
88
+ end
89
+ Mechanics.check_keys(data, %w[target], errors, path)
90
+ new(:target, index)
91
+ elsif data.key?("each")
92
+ Mechanics.check_keys(data, %w[each controller], errors, path)
93
+ filter = Mechanics.build_entry(:filter, data["each"], errors, "#{path}.each")
94
+ controller = data.fetch("controller", "any")
95
+ errors << "#{path}.controller: must be one of #{CONTROLLERS.join(", ")}" unless CONTROLLERS.include?(controller)
96
+ new(:each, filter, controller)
97
+ else
98
+ errors << "#{path}: needs \"target\" or \"each\""
99
+ nil
100
+ end
101
+ end
102
+
103
+ def initialize(kind, value = nil, controller = nil)
104
+ @kind = kind
105
+ @value = value
106
+ @controller = controller
107
+ freeze
108
+ end
109
+
110
+ def to_s
111
+ case kind
112
+ when :target then "target ##{value + 1}"
113
+ when :result then "the object(s) from effect ##{value + 1}"
114
+ when :controller_of then "the controller of #{value}"
115
+ when :self then "itself"
116
+ when :attached then "the attached permanent"
117
+ when :triggering then "the triggering #{value}"
118
+ when :watched then "the watched object(s)"
119
+ when :you then "you"
120
+ when :each_opponent then "each opponent"
121
+ when :exiled_with then "the cards exiled with it"
122
+ when :gift_recipient then "the chosen opponent"
123
+ when :granter then "the permanent granting it"
124
+ when :each then "each #{value.primitive_name.tr("_",
125
+ " ")}#{{ "you" => " you control",
126
+ "opponent" => " an opponent controls" }[controller]}"
127
+ end
128
+ end
129
+
130
+ # Resolves to a list of references (object ids / "player:<name>") at resolution time.
131
+ def resolve(engine, ctx)
132
+ case kind
133
+ when :target then Array(ctx.targets[value]).compact # a counted target's slot holds a list (CR 601.2c)
134
+ when :result then Array(ctx.results&.[](value))
135
+ when :self then SELF_ZONES.include?(engine.state.objects[ctx.source]&.zone) ? [ctx.source] : []
136
+ when :attached then attached(engine, ctx)
137
+ when :triggering then triggering(engine, ctx)
138
+ when :watched, :gift_recipient then remembered(engine, ctx)
139
+ when :you then [Filters.player_ref(engine, ctx.controller)]
140
+ when :each_opponent
141
+ engine.state.players.each_index.reject { it == ctx.controller }.map { Filters.player_ref(engine, it) }
142
+ when :each then each(engine, ctx)
143
+ when :controller_of then controllers(engine, ctx)
144
+ when :exiled_with then Linked.exiled_with(engine, ctx.source, ctx)
145
+ when :granter then granter(engine, ctx)
146
+ end
147
+ end
148
+
149
+ # What the resolving triggered ability remembers (the event facts, watched objects).
150
+ def self.memory(engine, ctx) = ctx.memory || engine.state.objects[ctx.object]&.memory || {}
151
+
152
+ # What a delayed trigger watches; the opponent chosen for a gift, kept by the spell or the permanent it
153
+ # became (CR 702.174b).
154
+ def remembered(engine, ctx)
155
+ return Array(Selector.memory(engine, ctx)["watched"]) if kind == :watched
156
+
157
+ Array(Selector.memory(engine, ctx)["gift"] || engine.state.objects[ctx.source]&.memory&.[]("gift"))
158
+ end
159
+
160
+ private
161
+
162
+ def controllers(engine, ctx)
163
+ Array(value&.resolve(engine, ctx)).filter_map do |ref|
164
+ obj = engine.state.objects[ref]
165
+ controller = if obj && %i[battlefield
166
+ stack].include?(obj.zone)
167
+ obj.controller
168
+ else
169
+ engine.resolving_last_known(ref)&.fetch("controller") ||
170
+ engine.state.objects[ctx.object]&.memory&.dig(Linked::LKI, ref) || obj&.controller
171
+ end
172
+ controller && Filters.player_ref(engine, controller)
173
+ end.uniq
174
+ end
175
+
176
+ def granter(engine, ctx)
177
+ id = engine.state.objects[ctx.object]&.memory&.[]("granter") || ctx.source
178
+ engine.state.battlefield.include?(id) ? [id] : []
179
+ end
180
+
181
+ def triggering(engine, ctx)
182
+ found = Selector.memory(engine, ctx).fetch("triggering", {})[value]
183
+ return [] if found.nil?
184
+
185
+ case value
186
+ when "player" then [Filters.player_ref(engine, found)]
187
+ when "ability" then [engine.stack_object_for(found)].compact
188
+ else Array(found)
189
+ end
190
+ end
191
+
192
+ # CR 301.5f / 303.4m: "equipped/enchanted creature" is whatever the source is attached to now.
193
+ def attached(engine, ctx)
194
+ to = engine.state.objects[ctx.source]&.attached_to
195
+ to && engine.state.battlefield.include?(to) ? [to] : []
196
+ end
197
+
198
+ def each(engine, ctx)
199
+ value.candidates_for(engine, ctx.controller, ctx.source, ctx: ctx).select do |ref|
200
+ index = Filters.player_index(engine, ref)
201
+ owner = index || engine.object(ref).controller
202
+ controller == "any" || (controller == "you") == (owner == ctx.controller)
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Subtypes
6
+ # Auras and Equipment (CR 301.5, 303.4, 701.3, 702.5, 702.6). A permanent is attached to
7
+ # another through GameObject#attached_to; "equipped/enchanted creature" abilities refer to
8
+ # whatever the source is attached to (CR 301.5f, 303.4m). The engine side (legality,
9
+ # attach!/unattach!, state-based actions 704.5m/n/p) is in Engine::Attachments.
10
+ # Shared rules of the subtypes that make a permanent attachable.
11
+ class AttachmentSubtype < Primitive
12
+ def self.validate_definition(_defn, _errors) = nil
13
+ end
14
+
15
+ # Aura (CR 303.4): an enchantment that enters attached to what its spell targeted.
16
+ class Aura < AttachmentSubtype
17
+ primitive :subtype, :aura,
18
+ description: "An enchantment that is cast targeting what its `enchant` ability allows and enters " \
19
+ "the battlefield attached to that target. If it's attached to something it can't " \
20
+ "enchant, or to nothing, it's put into its owner's graveyard (a state-based action).",
21
+ rules: %w[303.4 303.4a 303.4c 303.4d 608.3c 704.5m],
22
+ example: { "types" => ["enchantment"], "subtypes" => ["aura"], "abilities" => [{ "enchant" => "creature" }] }
23
+
24
+ def self.applies_to = [:enchantment]
25
+
26
+ # CR 303.4a: an Aura spell requires a target, defined by its enchant ability.
27
+ def self.validate_definition(defn, errors)
28
+ count = defn.abilities.count { it.kind == :enchant }
29
+ errors << "abilities: an Aura needs exactly one enchant ability (CR 303.4a, 702.5a)" unless count == 1
30
+ end
31
+ end
32
+
33
+ # Equipment (CR 301.5): an artifact that can be attached to a creature, usually by equip.
34
+ class Equipment < AttachmentSubtype
35
+ primitive :subtype, :equipment,
36
+ description: "An artifact that can be attached to a creature (it enters unattached). If the " \
37
+ "creature it equips stops being a creature or leaves, it becomes unattached and " \
38
+ "stays on the battlefield (a state-based action).",
39
+ rules: %w[301.5 301.5b 301.5c 704.5n],
40
+ example: { "types" => ["artifact"], "subtypes" => ["equipment"],
41
+ "abilities" => [{ "equip" => { "cost" => [{ "mana" => "{2}" }] } }] }
42
+
43
+ def self.applies_to = [:artifact]
44
+ end
45
+ end
46
+
47
+ module Abilities
48
+ # Enchant [object] (CR 702.5): what an Aura spell can target and what the Aura can enchant.
49
+ class Enchant < Primitive
50
+ primitive :ability, :enchant,
51
+ description: "The Aura's enchant ability: a target filter for what its spell targets and what the " \
52
+ "Aura may be attached to, e.g. \"creature\" or {\"permanent\": {\"any_of\": " \
53
+ "[\"artifact\", \"creature\"]}}. Only Auras have it.",
54
+ rules: %w[702.5a 115.1b 303.4a],
55
+ params: { "(value)" => "a target filter for a permanent" },
56
+ example: { "enchant" => "creature" }
57
+
58
+ PLAYER_FILTERS = %w[player opponent any_target].freeze
59
+
60
+ attr_reader :filter
61
+
62
+ def self.scalar_params? = true
63
+
64
+ def self.build(params, errors, path, **)
65
+ filter = Mechanics.build_entry(:filter, params, errors, path)
66
+ message = "must be a filter for permanents (Auras that enchant players are not supported yet)"
67
+ errors << "#{path}: #{message}" unless permanents?(filter)
68
+ new(filter)
69
+ end
70
+
71
+ # Whether the filter matches only permanents ("Enchant creature or Vehicle" is a one_of of two).
72
+ def self.permanents?(filter)
73
+ return filter.filters.all? { permanents?(it) } if filter.respond_to?(:filters)
74
+
75
+ base = filter.respond_to?(:base) ? filter.base : filter
76
+ base.nil? || (base.zone == :battlefield && !PLAYER_FILTERS.include?(base.primitive_name))
77
+ end
78
+
79
+ def initialize(filter)
80
+ super()
81
+ @filter = filter
82
+ freeze
83
+ end
84
+
85
+ def kind = :enchant
86
+ def mana_ability? = false
87
+ def allowed_types = [:enchantment]
88
+ def try_effects = []
89
+
90
+ def validate_definition(defn, errors, path)
91
+ errors << "#{path}: only Auras have enchant (CR 702.5a)" unless defn.subtypes.include?(:aura)
92
+ end
93
+
94
+ def to_s = "Enchant #{filter.is_a?(Class) ? filter.primitive_name.tr("_", " ") : filter}"
95
+ end
96
+
97
+ # The spell ability an Aura spell has implicitly (CR 115.1b, 303.4a): one target chosen
98
+ # by its enchant ability, no effects. Resolving attaches the Aura to it (CR 608.3c).
99
+ class AuraSpell < StackAbility
100
+ def self.for(enchant) = new(targets: [enchant.filter].freeze, effects: [].freeze)
101
+
102
+ def kind = :aura_spell
103
+ end
104
+
105
+ # Equip [cost] (CR 702.6a): "[Cost]: Attach this permanent to target creature you control.
106
+ # Activate only as a sorcery."
107
+ class Equip < ActivatedAbility
108
+ primitive :ability, :equip,
109
+ description: "Equip: pay `cost` to attach this Equipment to target creature you control (not " \
110
+ "itself). Sorcery speed. Only Equipment have it.",
111
+ rules: %w[702.6a 301.5b 301.5c 701.3a],
112
+ params: { "cost" => "list of costs, e.g. [{\"mana\": \"{2}\"}]" },
113
+ example: { "equip" => { "cost" => [{ "mana" => "{2}" }] } }
114
+
115
+ TARGET = { "creature" => { "controller" => "you", "other" => true } }.freeze
116
+ EFFECT = { "attach" => { "to" => { "target" => 0 } } }.freeze
117
+
118
+ def self.build(params, errors, path, **)
119
+ Mechanics.check_keys(params, %w[cost], errors, path)
120
+ costs = parse_costs(params, errors, path)
121
+ errors << "#{path}.cost: must be a non-empty list" if costs.empty? && errors.none? { it.start_with?("#{path}.cost") }
122
+ targets = [Mechanics.build_entry(:filter, TARGET, errors, path)].freeze
123
+ effects = [Mechanics.build_entry(:effect, EFFECT, errors, path, targets_count: 1)].freeze
124
+ new(targets: targets, effects: effects, costs: costs, sorcery: true)
125
+ end
126
+
127
+ def allowed_types = [:artifact]
128
+
129
+ def validate_definition(defn, errors, path)
130
+ errors << "#{path}: only Equipment have equip (CR 702.6a)" unless defn.subtypes.include?(:equipment)
131
+ end
132
+
133
+ def to_s = "Equip #{costs.join(", ")}"
134
+ end
135
+ end
136
+
137
+ module Effects
138
+ # Attach the source Aura or Equipment to a permanent (CR 701.3).
139
+ class Attach < Effect
140
+ primitive :effect, :attach,
141
+ description: "Attach this Aura or Equipment (the ability's source), or the `attachment` (e.g. a token " \
142
+ "an earlier effect created, {\"result\": n}), to the referenced permanent. Nothing " \
143
+ "happens if it can't legally be attached to it, if it's already attached to it, or if " \
144
+ "the attachment has left the battlefield.",
145
+ rules: %w[701.3a 701.3b 701.3c 301.5c],
146
+ params: { "to" => "selector naming one permanent: {\"target\": n}, \"self\" or {\"result\": n}",
147
+ "attachment" => "selector (default: the source)" },
148
+ example: { "attach" => { "to" => { "target" => 0 } } }
149
+
150
+ SINGLE = %i[target self result].freeze
151
+
152
+ def self.keys = %w[to attachment if]
153
+
154
+ def setup(params, errors, path)
155
+ if to && !SINGLE.include?(to.kind)
156
+ errors << "#{path}.to: must be {\"target\": n}, \"self\" or {\"result\": n} (an attachment equips one object, CR 301.5c)"
157
+ end
158
+ raw = Mechanics.fetch_param(params, :attachment)
159
+ @attachment = raw && Selector.build(raw, errors, "#{path}.attachment", targets_count: @targets_count,
160
+ results_count: @results_count)
161
+ end
162
+
163
+ def apply(engine, ctx)
164
+ target = objects(engine, ctx).first or return
165
+ ids = @attachment ? @attachment.resolve(engine, ctx) : [ctx.source]
166
+ ids.each do |id|
167
+ source = engine.state.objects[id]
168
+ engine.attach!(source, target) if source && engine.state.battlefield.include?(id)
169
+ end
170
+ end
171
+
172
+ def to_s = "attach to #{to}"
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Subtypes
6
+ # Shared behavior of the five basic land types (CR 305.6): each grants an
7
+ # intrinsic "{T}: Add <color>." ability.
8
+ class BasicLandType < Primitive
9
+ class << self
10
+ attr_reader :color
11
+
12
+ def define(name, color)
13
+ klass = Class.new(self)
14
+ klass.instance_variable_set(:@color, color)
15
+ Subtypes.const_set(name.capitalize, klass)
16
+ klass.primitive :subtype, name,
17
+ description: "Basic land type. A land with this subtype has \"{T}: Add {#{color}}.\"",
18
+ rules: %w[305.6 205.3i],
19
+ example: { "types" => ["land"], "subtypes" => [name.to_s] }
20
+ klass
21
+ end
22
+
23
+ def applies_to = [:land]
24
+
25
+ def intrinsic_abilities
26
+ @intrinsic_abilities ||= [Abilities::ManaAbility.new([Costs::Tap::INSTANCE].freeze, [color].freeze)].freeze
27
+ end
28
+ end
29
+ end
30
+
31
+ { plains: :W, island: :U, swamp: :B, mountain: :R, forest: :G }.each do |name, color|
32
+ BasicLandType.define(name, color)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../triggers/base"
4
+ require_relative "../keywords/station"
5
+
6
+ module Enjine
7
+ module Mechanics
8
+ module Effects
9
+ # The effect of a class level bar's activated ability (CR 717.2a): "This Class's level becomes N."
10
+ class GainClassLevel < Effect
11
+ def self.keys = %w[to]
12
+
13
+ attr_reader :level
14
+
15
+ def setup(params, _errors, _path) = @level = params["level"]
16
+ def apply(engine, ctx) = objects(engine, ctx).each { engine.set_class_level!(engine.object(it), level) }
17
+ def to_s = "this Class's level becomes #{level}"
18
+ end
19
+ end
20
+
21
+ module Abilities
22
+ # A class level bar "[Cost]: Level N — [Abilities]" (CR 717.2a): "[Cost]: This Class's level becomes N.
23
+ # Activate only if this Class is level N-1 and only as a sorcery" and "As long as this Class is level N or
24
+ # greater, it has [abilities]." Abilities printed in its text box section are its `abilities` and
25
+ # `keywords`; those above the first level bar are ordinary abilities (CR 717.3).
26
+ class ClassLevel < ActivatedAbility
27
+ primitive :ability, :class_level,
28
+ description: "A class level bar: `cost` (a mana cost string or a list of costs): this permanent's " \
29
+ "level becomes `level`; activate only if its level is `level` - 1, as a sorcery. While " \
30
+ "its level is `level` or greater it has `keywords` and `abilities`. A permanent " \
31
+ "without a level is level 1.",
32
+ rules: %w[717.2a 717.2b 717.2d 717.3 602.5d],
33
+ params: { "level" => "integer >= 2", "cost" => "mana cost string or list of costs",
34
+ "keywords" => "list of keywords", "abilities" => "list of abilities" },
35
+ example: { "class_level" => { "level" => 2, "cost" => "{1}{R}",
36
+ "abilities" => [{ "triggered" => {
37
+ "when" => { "discards" => {} },
38
+ "effects" => [{ "deal_damage" => { "amount" => 2, "to" => "each_opponent" } }]
39
+ } }] } }
40
+
41
+ attr_reader :level, :keywords, :nested_abilities
42
+
43
+ def self.build(params, errors, path, **)
44
+ Mechanics.check_keys(params, %w[level cost keywords abilities], errors, path)
45
+ level = Mechanics.integer_param(params, :level, errors, path, min: 2).to_i
46
+ raw = Mechanics.fetch_param(params, :cost)
47
+ costs = raw.is_a?(String) ? [Costs::ManaPayment.build(raw, errors, "#{path}.cost")] : parse_costs(params, errors, path)
48
+ errors << "#{path}.cost: required" if raw.nil?
49
+ keywords = StationSymbol.build_keywords(params, errors, path)
50
+ nested = StationSymbol.build_abilities(params, errors, path)
51
+ nested.each_with_index do |ability, i|
52
+ errors << "#{path}.abilities[#{i}]: class level bars can't be nested" if ability.is_a?(ClassLevel)
53
+ end
54
+ effect = Effects::GainClassLevel.new(to: Selector.new(:self), params: { "level" => level }, errors: errors, path: path)
55
+ new(sorcery: true, targets: [].freeze, costs: costs.compact.freeze, effects: [effect].freeze,
56
+ level: level, keywords: keywords.freeze, nested_abilities: nested.freeze)
57
+ end
58
+
59
+ def initialize(level:, keywords:, nested_abilities:, **)
60
+ @level = level
61
+ @keywords = keywords
62
+ @nested_abilities = nested_abilities
63
+ super(**)
64
+ end
65
+
66
+ # No power/toughness box (Engine::Characteristics#station_symbol_layers reads it).
67
+ def power = nil
68
+
69
+ # CR 717.2a: only if this Class is level N-1.
70
+ def restriction_met?(engine, index, source)
71
+ engine.class_level(engine.object(source)) == level - 1 && super
72
+ end
73
+
74
+ def to_s
75
+ bits = keywords.map(&:to_s) + nested_abilities.map(&:to_s)
76
+ "#{costs.join(", ")}: Level #{level}#{" — #{bits.join("; ")}" unless bits.empty?}"
77
+ end
78
+ end
79
+ end
80
+
81
+ module Triggers
82
+ # "When this Class becomes level N" (CR 717.2a): its level bar for N resolved.
83
+ class BecomesLevel < Trigger
84
+ primitive :trigger, :becomes_level,
85
+ description: "This permanent's level becomes `level` (a class level bar's ability resolves).",
86
+ rules: %w[717.2a 603.2],
87
+ params: { "level" => "integer >= 2" },
88
+ example: { "becomes_level" => { "level" => 3 } }
89
+
90
+ def self.keys = %w[level]
91
+ def event_class = Events::ClassLevelChanged
92
+
93
+ def setup(params, errors, path)
94
+ @level = Mechanics.integer_param(params, :level, errors, path, min: 2)
95
+ end
96
+
97
+ def match?(_engine, event, source) = event.object == source["id"] && event.level == @level
98
+ def to_s = "When this becomes level #{@level}"
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Subtypes
6
+ # The Saga enchantment type (CR 715).
7
+ class Saga < Primitive
8
+ primitive :subtype, :saga,
9
+ description: "Enchantment type. A Saga enters with a lore counter; as its controller's precombat " \
10
+ "main phase begins, they put another lore counter on it. Each lore counter added can " \
11
+ "trigger its chapter abilities. Once its lore counters reach its final chapter number " \
12
+ "and no chapter ability of it is waiting to resolve, its controller sacrifices it.",
13
+ rules: %w[715.3a 715.3c 505.4 715.4 704.5s],
14
+ example: { "types" => ["enchantment"], "subtypes" => ["saga"],
15
+ "abilities" => [{ "chapter" => { "chapter" => 1, "effects" => [{ "draw" => { "amount" => 1 } }] } }] }
16
+
17
+ def self.applies_to = [:enchantment]
18
+
19
+ # CR 715.3a: "This Saga enters with a lore counter on it" (a replacement effect, CR 614.1c).
20
+ def self.entering(_definition, enters)
21
+ enters["counters"]["lore"] = enters["counters"].fetch("lore", 0) + 1
22
+ end
23
+ end
24
+ end
25
+
26
+ module Triggers
27
+ # The trigger condition of a chapter ability (CR 715.2b): one or more lore counters are put on
28
+ # this Saga and their number goes from less than N to at least N. Entering with lore counters
29
+ # counts as having them put on it (CR 122.6).
30
+ class ChapterTrigger
31
+ attr_reader :chapter
32
+
33
+ def initialize(chapter)
34
+ @chapter = chapter
35
+ freeze
36
+ end
37
+
38
+ def watches?(event) = event.is_a?(Events::CountersChanged) || event.is_a?(Events::ZoneChanged)
39
+ def leaves_battlefield? = false
40
+
41
+ def match?(engine, event, source)
42
+ if event.is_a?(Events::CountersChanged)
43
+ event.object == source["id"] && event.counter == "lore" && event.from < chapter && event.to >= chapter
44
+ else
45
+ entered = event.to == "battlefield" && event.new_id == source["id"]
46
+ entered && engine.state.objects[event.new_id].counters.fetch("lore", 0) >= chapter
47
+ end
48
+ end
49
+
50
+ def to_s = "Chapter #{chapter}"
51
+ end
52
+ end
53
+
54
+ module Abilities
55
+ # A chapter ability "{rN} — [effect]" (CR 715.2): a triggered ability of a Saga. Write
56
+ # "{rN1}, {rN2} — [effect]" as one chapter ability per number (CR 715.2c).
57
+ class ChapterAbility < StackAbility
58
+ primitive :ability, :chapter,
59
+ description: "A Saga's chapter ability: triggers when lore counters put on it bring their number " \
60
+ "from below `chapter` to at least `chapter`. Its targets are chosen as it's put on the " \
61
+ "stack. A Saga's final chapter number is its greatest `chapter`.",
62
+ rules: %w[715.2 715.2b 715.2c 715.2d 122.6],
63
+ params: { "chapter" => "integer >= 1 (the chapter symbol's number)", "targets" => "list of target filters",
64
+ "effects" => "list of effects", "modes" => "optional modes, as for a spell" },
65
+ example: { "chapter" => { "chapter" => 2, "effects" => [{ "gain_life" => { "amount" => 3 } }] } }
66
+
67
+ attr_reader :chapter, :trigger
68
+
69
+ def self.build(params, errors, path, **)
70
+ Mechanics.check_keys(params, %w[chapter] + BODY_KEYS, errors, path)
71
+ chapter = Mechanics.integer_param(params, :chapter, errors, path, min: 1)
72
+ new(**parse_body(params, errors, path), chapter: chapter)
73
+ end
74
+
75
+ def initialize(chapter:, **)
76
+ @chapter = chapter
77
+ @trigger = Triggers::ChapterTrigger.new(chapter)
78
+ super(**)
79
+ end
80
+
81
+ def kind = :triggered
82
+ def allowed_types = [:enchantment]
83
+ def condition = nil
84
+ def condition_met?(*, **) = true
85
+ def to_s = "Chapter #{chapter} — #{effects_text}"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Supertypes
6
+ # Basic (CR 205.4c). Exempt from the four-copy deck limit.
7
+ class Basic < Primitive
8
+ primitive :supertype, :basic,
9
+ description: "Basic cards are exempt from the four-copies deck construction limit.",
10
+ rules: %w[205.4c 100.2a],
11
+ example: { "supertypes" => ["basic"] }
12
+
13
+ def self.deck_limit_exempt? = true
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Supertypes
6
+ # Legendary (CR 205.4d). Subject to the legend rule (CR 704.5j).
7
+ class Legendary < Primitive
8
+ primitive :supertype, :legendary,
9
+ description: "If a player controls two or more legendary permanents with the same name, they choose " \
10
+ "one and put the rest into their owners' graveyards (the legend rule).",
11
+ rules: %w[205.4d 704.5j],
12
+ example: { "supertypes" => ["legendary"] }
13
+
14
+ def self.deck_limit_exempt? = false
15
+ end
16
+ end
17
+ end
18
+ end