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,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Abilities
6
+ # Base for restriction abilities a permanent has on itself (CR 509.1b, 115.4): no stack, no effects.
7
+ class Restriction < Primitive
8
+ def mana_ability? = false
9
+ def allowed_types = nil
10
+ def try_effects = []
11
+ end
12
+
13
+ # "This creature can't be blocked by creatures with power 2 or less" / "... by artifact creatures".
14
+ class CantBeBlockedBy < Restriction
15
+ primitive :ability, :cant_be_blocked_by,
16
+ description: "This creature can't be blocked by creatures matching `filter` (checked as blockers " \
17
+ "are declared, with their current characteristics).",
18
+ rules: %w[509.1b 509.1a],
19
+ params: { "filter" => "a creature filter, e.g. {\"creature\": {\"power\": {\"at_most\": 2}}}" },
20
+ example: { "cant_be_blocked_by" => { "filter" => { "creature" => { "power" => { "at_most" => 2 } } } } }
21
+
22
+ attr_reader :filter
23
+
24
+ def self.build(params, errors, path, **)
25
+ Mechanics.check_keys(params, %w[filter], errors, path)
26
+ raw = Mechanics.fetch_param(params, :filter)
27
+ errors << "#{path}.filter: required" if raw.nil?
28
+ filter = raw && Mechanics.build_entry(:filter, raw, errors, "#{path}.filter")
29
+ new(filter)
30
+ end
31
+
32
+ def initialize(filter)
33
+ super()
34
+ @filter = filter
35
+ freeze
36
+ end
37
+
38
+ def kind = :cant_be_blocked_by
39
+ def allowed_types = [:creature]
40
+ def to_s = "can't be blocked by #{filter.is_a?(Class) ? filter.primitive_name : filter}"
41
+
42
+ # Whether `blocker` may not block this creature (controller-relative filter keys use the blocker's).
43
+ def forbids?(engine, blocker) = filter.match?(engine, blocker, engine.object(blocker).controller)
44
+ end
45
+
46
+ # "This token can block only creatures with flying" / "This creature can't block non-Spirit creatures".
47
+ class CanBlockOnly < Restriction
48
+ primitive :ability, :can_block_only,
49
+ description: "This creature can block only attacking creatures matching `filter` (checked as " \
50
+ "blockers are declared, with their current characteristics; controller-relative " \
51
+ "filter keys are seen from this creature's controller).",
52
+ rules: %w[509.1b 509.1a],
53
+ params: { "filter" => "a creature filter, e.g. {\"creature\": {\"keywords\": [\"flying\"]}}" },
54
+ example: { "can_block_only" => { "filter" => { "creature" => { "keywords" => ["flying"] } } } }
55
+
56
+ attr_reader :filter
57
+
58
+ def self.build(params, errors, path, **)
59
+ Mechanics.check_keys(params, %w[filter], errors, path)
60
+ raw = Mechanics.fetch_param(params, :filter)
61
+ errors << "#{path}.filter: required" if raw.nil?
62
+ new(raw && Mechanics.build_entry(:filter, raw, errors, "#{path}.filter"))
63
+ end
64
+
65
+ def initialize(filter)
66
+ super()
67
+ @filter = filter
68
+ freeze
69
+ end
70
+
71
+ def kind = :can_block_only
72
+ def allowed_types = [:creature]
73
+ def to_s = "can block only #{filter.is_a?(Class) ? filter.primitive_name : filter}"
74
+
75
+ # Whether this creature (`blocker`) may block `attacker`.
76
+ def allows?(engine, blocker, attacker) = filter.match?(engine, attacker, engine.object(blocker).controller)
77
+ end
78
+
79
+ # "This creature can't be the target of abilities from artifact sources your opponents control."
80
+ class CantBeTargetedBy < Restriction
81
+ primitive :ability, :cant_be_targeted_by,
82
+ description: "This permanent can't be the target of abilities its controller's opponents control " \
83
+ "whose source matches `abilities_from` (a card spec, e.g. {\"types\": [\"artifact\"]}).",
84
+ rules: %w[115.4 113.7 702.11d],
85
+ params: { "abilities_from" => "card spec of the ability's source" },
86
+ example: { "cant_be_targeted_by" => { "abilities_from" => { "types" => ["artifact"] } } }
87
+
88
+ attr_reader :spec
89
+
90
+ def self.build(params, errors, path, **)
91
+ Mechanics.check_keys(params, %w[abilities_from], errors, path)
92
+ raw = Mechanics.fetch_param(params, :abilities_from)
93
+ errors << "#{path}.abilities_from: required" if raw.nil?
94
+ new(CardSpec.build(raw, errors, "#{path}.abilities_from"))
95
+ end
96
+
97
+ def initialize(spec)
98
+ super()
99
+ @spec = spec
100
+ freeze
101
+ end
102
+
103
+ def kind = :cant_be_targeted_by
104
+ def to_s = "can't be the target of abilities from #{spec.to_s.delete_suffix(" card")} sources opponents control"
105
+
106
+ # Whether an ability whose source is `source` (a card object) may not target this permanent.
107
+ def forbids?(engine, source) = spec.match?(engine, source)
108
+ end
109
+
110
+ # "You can't cast this spell unless there are seven or more cards in your graveyard" / "You can't cast this
111
+ # spell during your first, second, or third turns of the game" (CR 601.3): the card can be cast, from any
112
+ # zone and by any permission, only while the condition holds for the player who would cast it.
113
+ class CastRestriction < Restriction
114
+ primitive :ability, :cast_restriction,
115
+ description: "This card can be cast only while `only_if` (a condition, checked for the player who " \
116
+ "would cast it) holds, whatever lets them cast it.",
117
+ rules: %w[601.3 101.2],
118
+ params: { "only_if" => "condition" },
119
+ example: { "cast_restriction" => { "only_if" => { "amount" => { "graveyard_size" => "you" }, "at_least" => 7 } } }
120
+
121
+ attr_reader :condition
122
+
123
+ def self.build(params, errors, path, **)
124
+ Mechanics.check_keys(params, %w[only_if], errors, path)
125
+ raw = Mechanics.fetch_param(params, :only_if)
126
+ errors << "#{path}.only_if: required" if raw.nil?
127
+ new(raw && Condition.build(raw, errors, "#{path}.only_if"))
128
+ end
129
+
130
+ def initialize(condition)
131
+ super()
132
+ @condition = condition
133
+ freeze
134
+ end
135
+
136
+ def kind = :cast_restriction
137
+ def to_s = "You can't cast this spell unless #{condition}."
138
+
139
+ # Whether player `index` may begin to cast `obj` now.
140
+ def allows?(engine, index, obj) = condition.nil? || condition.true?(engine, EffectContext.new(index, obj.id, obj.id, nil, []))
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Abilities
6
+ # "Instant and sorcery spells you cast from your hand have cascade" (CR 604.1, 611.3a): a static ability of a
7
+ # permanent giving keywords to the spells its controller controls on the stack that match `spells` (and were
8
+ # cast `from` / `not_from` a zone). See Engine::SpellGrants.
9
+ class SpellKeywords < Primitive
10
+ primitive :ability, :spell_keywords,
11
+ description: "Spells you control on the stack matching `spells` (a spell filter; default every " \
12
+ "spell), cast `from` / `not_from` a zone if given, have the `keywords` (e.g. cascade, " \
13
+ "which then triggers as such a spell is cast).",
14
+ rules: %w[604.1 611.3a 702.85a],
15
+ params: { "spells" => "spell filter", "from" => "zone", "not_from" => "zone",
16
+ "keywords" => "list of keywords" },
17
+ example: { "spell_keywords" => { "spells" => { "spell" => { "any_of" => %w[instant sorcery] } },
18
+ "from" => "hand", "keywords" => ["cascade"] } }
19
+
20
+ attr_reader :keywords
21
+
22
+ def self.build(params, errors, path, **)
23
+ Mechanics.check_keys(params, %w[spells from not_from keywords], errors, path)
24
+ filter = params["spells"] && Mechanics.build_entry(:filter, params["spells"], errors, "#{path}.spells")
25
+ keywords = Array(params["keywords"]).map(&:to_s)
26
+ errors << "#{path}.keywords: needs at least one keyword" if keywords.empty?
27
+ keywords.each { errors << "#{path}.keywords: unknown keyword #{it.inspect}" unless Enjine.registry.fetch(:keyword, it) }
28
+ new(filter, CastFrom.build(params, errors, path), keywords.map(&:to_sym).freeze)
29
+ end
30
+
31
+ def initialize(filter, zones, keywords)
32
+ super()
33
+ @filter = filter
34
+ @zones = zones
35
+ @keywords = keywords
36
+ freeze
37
+ end
38
+
39
+ def kind = :spell_keywords
40
+ def mana_ability? = false
41
+ def allowed_types = nil
42
+ def try_effects = []
43
+
44
+ # `spell`: a spell on the stack controlled by the controller of `source`.
45
+ def applies_to?(engine, spell, source)
46
+ return false if spell.ability? || !@zones.match?(engine.cast_origin(spell))
47
+
48
+ @filter.nil? || @filter.match_for?(engine, spell.id, source.controller, source.id)
49
+ end
50
+
51
+ def to_s = "#{@filter || "Spells"} you cast#{@zones} have #{keywords.join(", ")}"
52
+ end
53
+ end
54
+ end
55
+ end