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,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Keywords
6
+ # CR 702.51a-d: convoke. While paying the spell's total cost, each untapped creature you control may be
7
+ # tapped to pay {1} or one mana of one of its colors (a `pay_cost` option `{type: "convoke", object: id}`).
8
+ class Convoke < Primitive
9
+ primitive :keyword, :convoke,
10
+ description: "Convoke: while paying this spell's total cost you may tap untapped creatures you " \
11
+ "control, each paying for {1} or one mana of one of its colors (pay_cost option " \
12
+ "`{type: \"convoke\", object: id}`). It isn't an alternative or additional cost.",
13
+ rules: %w[702.51a 702.51b 702.51c 702.51d],
14
+ example: { "keywords" => ["convoke"] }
15
+ end
16
+
17
+ # CR 702.126a-c: improvise. For each generic mana in the spell's total cost, an untapped artifact you control
18
+ # may be tapped instead of paying it (a `pay_cost` option `{type: "improvise", object: id}`).
19
+ class Improvise < Primitive
20
+ primitive :keyword, :improvise,
21
+ description: "Improvise: while paying this spell's total cost you may tap untapped artifacts you " \
22
+ "control, each paying for {1} of its generic part (pay_cost option `{type: " \
23
+ "\"improvise\", object: id}`). It isn't an alternative or additional cost.",
24
+ rules: %w[702.126a 702.126b 702.126c],
25
+ example: { "keywords" => ["improvise"] }
26
+ end
27
+ end
28
+
29
+ module Abilities
30
+ # "Noncreature spells you cast have improvise." (CR 702.126a, 611.3a): while this permanent is on the
31
+ # battlefield, spells its controller casts matching the card spec `spells` have the listed helper keywords
32
+ # (convoke, improvise) as their costs are paid (Engine::AltCasting#helper_keywords).
33
+ class SpellsHave < Primitive
34
+ KEYWORDS = %w[convoke improvise].freeze
35
+
36
+ primitive :ability, :spells_have,
37
+ description: "Spells you cast matching the card spec `spells` (default: every spell) have `keywords` " \
38
+ "(convoke and/or improvise) while this permanent is on the battlefield.",
39
+ rules: %w[702.126a 702.51a 611.3a 604.1],
40
+ params: { "spells" => "card spec, e.g. {\"not_types\": [\"creature\"]}",
41
+ "keywords" => "list of convoke, improvise" },
42
+ example: { "spells_have" => { "spells" => { "not_types" => ["creature"] }, "keywords" => ["improvise"] } }
43
+
44
+ attr_reader :spells, :keywords
45
+
46
+ def self.build(params, errors, path, **)
47
+ Mechanics.check_keys(params, %w[spells keywords], errors, path)
48
+ keywords = Mechanics.fetch_param(params, :keywords)
49
+ unless keywords.is_a?(Array) && !keywords.empty? && (keywords - KEYWORDS).empty?
50
+ errors << "#{path}.keywords: must be a non-empty list of #{KEYWORDS.join(", ")}"
51
+ end
52
+ raw = Mechanics.fetch_param(params, :spells)
53
+ new(raw && CardSpec.build(raw, errors, "#{path}.spells"), Array(keywords).freeze)
54
+ end
55
+
56
+ def initialize(spells, keywords)
57
+ super()
58
+ @spells = spells
59
+ @keywords = keywords
60
+ freeze
61
+ end
62
+
63
+ def kind = :spells_have
64
+ def mana_ability? = false
65
+ def allowed_types = nil
66
+ def try_effects = []
67
+
68
+ # The keywords `spell` gets from this ability of a permanent controlled by `controller`.
69
+ def keywords_for(engine, spell, controller)
70
+ spell.controller == controller && (spells.nil? || spells.match?(engine, spell)) ? keywords : []
71
+ end
72
+
73
+ def to_s = "#{spells&.to_s&.sub(/ card\z/, "")} spells you cast have #{keywords.join(", ")}".strip.capitalize
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Costs
6
+ # The cost of crew N (CR 702.122a): "Tap any number of other untapped creatures you control with total
7
+ # power N or greater." Each creature is a `choose_cost` decision and is tapped as it is chosen (cancelling
8
+ # untaps it); once their total power is N or more the player may stop (`done`) or tap more. Summoning
9
+ # sickness doesn't matter (CR 302.6). As the ability is activated each one crews the Vehicle (CR 702.122b).
10
+ class Crew < Primitive
11
+ attr_reader :power
12
+
13
+ def initialize(power)
14
+ super()
15
+ @power = power
16
+ freeze
17
+ end
18
+
19
+ def mana? = false
20
+ def pay!(_engine, _object) = nil
21
+ def choices_needed = power
22
+ def to_s = "Tap any number of other untapped creatures you control with total power #{power} or greater"
23
+
24
+ # The power a creature crews with: its power, unless it "crews Vehicles as though its power were 2 greater"
25
+ # or "using its toughness" (Abilities::TappingPower).
26
+ def crew_power(engine, creature) = Abilities::TappingPower.of(engine, creature, action)
27
+ def action = :crew
28
+
29
+ def candidates(engine, object)
30
+ engine.state.battlefield.select do |id|
31
+ other = engine.object(id)
32
+ id != object.id && other.controller == object.controller && !other.tapped && engine.chars(other).creature?
33
+ end
34
+ end
35
+
36
+ def payable?(engine, object) = candidates(engine, object).sum { crew_power(engine, engine.object(it)) } >= power
37
+ def choice_options(engine, object, _chosen) = candidates(engine, object).map { { object: it } }
38
+
39
+ def choose!(engine, option)
40
+ creature = engine.object(option[:object])
41
+ engine.tap!(creature)
42
+ { "object" => creature.id, "power" => crew_power(engine, creature), "tapped" => true }
43
+ end
44
+
45
+ def finishable?(entries) = entries.sum { it["power"] } >= power
46
+
47
+ # Chosen once the total power is enough and the player is done (or nothing is left to tap).
48
+ def chosen?(engine, object, entries, done)
49
+ finishable?(entries) && (done || candidates(engine, object).empty?)
50
+ end
51
+
52
+ # CR 702.122b: each creature tapped to pay the cost crews the Vehicle.
53
+ def pay_chosen!(engine, object, entries)
54
+ entries.each { engine.emit Events::CrewedVehicle.new(object: it["object"], vehicle: object.id) }
55
+ end
56
+ end
57
+ end
58
+
59
+ module Effects
60
+ # The effect of a crew ability (CR 702.122a, 702.122e): this permanent becomes an artifact creature until
61
+ # end of turn, and it "becomes crewed" by the creatures tapped to pay the cost.
62
+ class Crewed < Effect
63
+ def self.keys = %w[to]
64
+
65
+ def apply(engine, ctx)
66
+ vehicle = objects(engine, ctx).first or return
67
+ engine.add_continuous_effect!("add_types", ctx, [vehicle], "types" => %w[artifact creature], "supertypes" => [],
68
+ "subtypes" => [])
69
+ crew = (engine.object(ctx.object).paid || []).map { it["object"] }
70
+ engine.emit Events::BecameCrewed.new(object: vehicle, crew: crew)
71
+ end
72
+
73
+ def to_s = "this becomes an artifact creature until end of turn"
74
+ end
75
+ end
76
+
77
+ module Abilities
78
+ # Crew N (CR 702.122a): an activated ability of a Vehicle. Written {"crew": {"power": N}}.
79
+ class Crew < ActivatedAbility
80
+ primitive :ability, :crew,
81
+ description: "Crew N: tap any number of other untapped creatures you control with total power N or " \
82
+ "more (chosen one at a time; `done` once the total is enough): this permanent becomes " \
83
+ "an artifact creature until end of turn. A Vehicle has its printed power and toughness " \
84
+ "only while it's a creature.",
85
+ rules: %w[702.122a 702.122b 702.122e 301.7 301.7a 301.7b 208.3 302.6],
86
+ params: { "power" => "integer >= 1 (the N of crew N)" },
87
+ example: { "crew" => { "power" => 3 } }
88
+
89
+ def self.build(params, errors, path, **)
90
+ Mechanics.check_keys(params, %w[power], errors, path)
91
+ n = Mechanics.integer_param(params, :power, errors, path, min: 1)
92
+ new(sorcery: false, targets: [].freeze, costs: [Costs::Crew.new(n)].freeze,
93
+ effects: [Effects::Crewed.new(to: Selector.new(:self), params: {}, errors: [], path: path)].freeze)
94
+ end
95
+
96
+ def power = costs.first.power
97
+ def to_s = "Crew #{power}"
98
+ end
99
+ end
100
+
101
+ module Triggers
102
+ # "Whenever this Vehicle becomes crewed" (CR 702.122e): whenever its crew ability resolves.
103
+ class BecomesCrewed < Trigger
104
+ primitive :trigger, :becomes_crewed,
105
+ description: "A crew ability of the subject resolves (it becomes crewed).",
106
+ rules: %w[702.122e 603.2],
107
+ params: { "subject" => "\"self\" (default) or {\"each\": filter, ...}" },
108
+ example: { "becomes_crewed" => {} }
109
+
110
+ def event_class = Events::BecameCrewed
111
+
112
+ def match?(engine, event, source)
113
+ obj = engine.state.objects[event.object] or return false
114
+ @subject.match?(engine, event.object, obj.controller, obj.card, source, types: engine.chars(obj).types)
115
+ end
116
+
117
+ def occurrences(engine, event, source)
118
+ return [] unless match?(engine, event, source)
119
+
120
+ obj = engine.object(event.object)
121
+ [{ "object" => obj.id, "player" => obj.controller, "card" => obj.card, "amount" => event.crew.size }]
122
+ end
123
+
124
+ def to_s = "Whenever #{@subject} becomes crewed"
125
+ end
126
+
127
+ # "Whenever this creature crews a Vehicle" (CR 702.122b): it was tapped to pay a crew cost. The event's
128
+ # object for the effects ("that Vehicle") is the Vehicle.
129
+ class CrewsVehicle < Trigger
130
+ primitive :trigger, :crews_vehicle,
131
+ description: "The subject (a creature) crews a Vehicle: it's tapped to pay that Vehicle's crew cost. " \
132
+ "{\"triggering\": \"object\"} is the Vehicle.",
133
+ rules: %w[702.122b 603.2],
134
+ params: { "subject" => "\"self\" (default) or {\"each\": filter, ...}" },
135
+ example: { "crews_vehicle" => { "subject" => "self" } }
136
+
137
+ def event_class = Events::CrewedVehicle
138
+
139
+ def match?(engine, event, source)
140
+ obj = engine.state.objects[event.object] or return false
141
+ @subject.match?(engine, event.object, obj.controller, obj.card, source, types: engine.chars(obj).types)
142
+ end
143
+
144
+ def occurrences(engine, event, source)
145
+ return [] unless match?(engine, event, source)
146
+
147
+ vehicle = engine.state.objects[event.vehicle]
148
+ [{ "object" => event.vehicle, "player" => engine.object(event.object).controller, "card" => vehicle&.card }.compact]
149
+ end
150
+
151
+ def to_s = "Whenever #{@subject} crews a Vehicle"
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Abilities
6
+ # Cycling and typecycling (CR 702.29): activated abilities that function only while the card
7
+ # is in its owner's hand.
8
+ # {"cycling": {"cost": "{2}"}} [cost], discard this card: draw a card
9
+ # {"cycling": {"cost": "{2}", "for": {"subtypes": ["swamp"]}}} ...: search for a Swamp card (typecycling)
10
+ # {"cycling": {"cost": "{2}", "for": {"supertypes": ["basic"], "types": ["land"]}}} basic landcycling
11
+ class Cycling < ActivatedAbility
12
+ primitive :ability, :cycling,
13
+ description: "Cycling: pay `cost` and discard this card from your hand to draw a card. With `for` " \
14
+ "it is typecycling: instead search your library for a card matching `for`, reveal " \
15
+ "it, put it into your hand, then shuffle.",
16
+ rules: %w[702.29a 702.29b 702.29e 702.29f 113.6m 602.2a],
17
+ params: { "cost" => "mana cost string, e.g. \"{2}\"",
18
+ "for" => "optional card spec for typecycling (see search_library)" },
19
+ example: { "cycling" => { "cost" => "{2}", "for" => { "supertypes" => ["basic"], "types" => ["land"] } } }
20
+
21
+ attr_reader :spec
22
+
23
+ def self.build(params, errors, path, **)
24
+ Mechanics.check_keys(params, %w[cost for], errors, path)
25
+ raw = Mechanics.fetch_param(params, :cost)
26
+ mana = raw.nil? ? ((errors << "#{path}.cost: required") && "{0}") : raw
27
+ costs = [Costs::ManaPayment.build(mana, errors, "#{path}.cost"), Costs::DiscardSelf::INSTANCE].freeze
28
+ spec_data = Mechanics.fetch_param(params, :for)
29
+ spec = spec_data.nil? ? nil : CardSpec.build(spec_data, errors, "#{path}.for")
30
+ new(targets: [].freeze, effects: [effect_for(spec_data)].compact.freeze, costs: costs, sorcery: false, spec: spec)
31
+ end
32
+
33
+ # The ability's effect as card data, built like any other effect (errors in `for` are reported above).
34
+ def self.effect_for(spec_data)
35
+ data = if spec_data.nil?
36
+ { "draw" => { "amount" => 1 } }
37
+ else
38
+ { "search_library" => { "for" => spec_data, "reveal" => true } }
39
+ end
40
+ Mechanics.build_entry(:effect, data, [], "cycling")
41
+ end
42
+
43
+ def initialize(spec:, **)
44
+ @spec = spec
45
+ super(**)
46
+ end
47
+
48
+ # CR 702.29f: typecycling abilities are cycling abilities.
49
+ def cycling? = true
50
+
51
+ def to_s
52
+ name = spec ? "#{spec.to_s.delete_suffix(" card").capitalize}cycling" : "Cycling"
53
+ "#{name} #{costs.first}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "prowess"
4
+
5
+ module Enjine
6
+ module Mechanics
7
+ module Keywords
8
+ # Decayed (CR 702.147a): "This creature can't block" (Engine::Combat#eligible_blockers) and "When this
9
+ # creature attacks, sacrifice it at end of combat." A decayed counter gives it too (CR 122.1b).
10
+ class Decayed < Primitive
11
+ primitive :keyword, :decayed,
12
+ description: "This creature can't block. When it attacks, sacrifice it at end of combat.",
13
+ rules: %w[702.147a 122.1b], example: { "keywords" => ["decayed"] }
14
+
15
+ ABILITY = { "triggered" => { "when" => { "attacks" => {} },
16
+ "effects" => [{ "delayed_trigger" => {
17
+ "when" => { "step_begins" => { "step" => "end_of_combat", "whose" => "each" } },
18
+ "watch" => "self", "effects" => [{ "sacrifice" => { "to" => "watched" } }]
19
+ } }] } }.freeze
20
+
21
+ def self.intrinsic_abilities = @intrinsic_abilities ||= [Keywords.ability_from_data(ABILITY, "decayed")].freeze
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../abilities/entering_choices"
4
+
5
+ module Enjine
6
+ module Mechanics
7
+ module Abilities
8
+ # Devour N / devour [quality] N (CR 702.82a, 702.82c): "As this object enters, you may sacrifice any number
9
+ # of creatures ([quality] permanents). This permanent enters with N +1/+1 counters on it for each one
10
+ # sacrificed this way." An "as this enters" choice of any number of permanents, picked one at a time
11
+ # (enter_choice `{type: "devour", object: id}`, then `{type: "done"}`); they're sacrificed as it enters,
12
+ # and it remembers how many it devoured (memory "devoured", CR 702.82b).
13
+ class Devour < EnteringChoice
14
+ primitive :ability, :devour,
15
+ description: "Devour `amount`: as this permanent enters, you may sacrifice any number of permanents " \
16
+ "matching `each` (default \"creature\"; \"land\" for devour land) that you control, one " \
17
+ "at a time; it enters with `amount` +1/+1 counters for each.",
18
+ rules: %w[702.82a 702.82b 702.82c 614.12 701.21a],
19
+ params: { "amount" => "integer >= 1", "each" => "permanent filter (default \"creature\")" },
20
+ example: { "devour" => { "amount" => 3, "each" => "land" } }
21
+
22
+ attr_reader :amount
23
+
24
+ def self.build(params, errors, path, **)
25
+ Mechanics.check_keys(params, %w[amount each], errors, path)
26
+ amount = Mechanics.integer_param(params, :amount, errors, path, min: 1)
27
+ raw = Mechanics.fetch_param(params, :each) || "creature"
28
+ new(amount, Selector.build({ "each" => raw, "controller" => "you" }, errors, "#{path}.each", targets_count: 0))
29
+ end
30
+
31
+ def initialize(amount, selector)
32
+ super()
33
+ @amount = amount
34
+ @selector = selector
35
+ freeze
36
+ end
37
+
38
+ def to_s = "Devour #{amount} (#{@selector})"
39
+
40
+ # The permanents still to pick from, then "done" (CR 702.82a: any number, so none is fine).
41
+ def pick_options(engine, controller, chosen)
42
+ ctx = EffectContext.new(controller, nil, nil, [], [])
43
+ (@selector.resolve(engine, ctx) - chosen).map { { type: "devour", object: it } } + [{ type: "done" }]
44
+ end
45
+
46
+ def enters_for(answer)
47
+ count = Array(answer[:objects]).size
48
+ { "counters" => count.positive? ? { "+1/+1" => amount * count } : {}, "memory" => { "devoured" => count } }
49
+ end
50
+
51
+ # As it enters: the chosen permanents still there are sacrificed (CR 701.21a).
52
+ def pay!(engine, controller, answer)
53
+ Array(answer["objects"]).each do |id|
54
+ obj = engine.state.objects[id]
55
+ engine.move!(id, :graveyard, reason: "sacrifice") if obj&.zone == :battlefield && obj.controller == controller
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Keywords
6
+ # Evergreen keyword abilities. A keyword is a flag on the object's characteristics;
7
+ # the rules code asks chars(obj).keyword?(:name) at the point the rule applies.
8
+ class Evergreen < Primitive
9
+ def self.define(name, description, rules)
10
+ klass = Class.new(self)
11
+ Keywords.const_set(name.to_s.split("_").map(&:capitalize).join, klass)
12
+ klass.primitive :keyword, name, description: description, rules: rules, example: { "keywords" => [name.to_s.tr("_", " ")] }
13
+ end
14
+ end
15
+
16
+ {
17
+ flying: ["Can't be blocked except by creatures with flying or reach.", %w[702.9b]],
18
+ reach: ["Can block creatures with flying.", %w[702.17b]],
19
+ vigilance: ["Attacking doesn't cause this creature to tap.", %w[702.20b]],
20
+ menace: ["Can't be blocked except by two or more creatures.", %w[702.111b]],
21
+ defender: ["Can't attack.", %w[702.3b]],
22
+ trample: ["Excess combat damage beyond lethal damage to its blockers can be assigned to the player or " \
23
+ "planeswalker it's attacking.", %w[702.19b 702.19e]],
24
+ first_strike: ["Deals combat damage in a first combat damage step, before creatures without first strike.",
25
+ %w[702.7b 510.4]],
26
+ double_strike: ["Deals combat damage in both the first-strike and the regular combat damage step.",
27
+ %w[702.4b 510.4]],
28
+ deathtouch: ["Any damage it deals to a creature is enough to destroy it; 1 damage counts as lethal when " \
29
+ "assigning combat damage.", %w[702.2b 702.2c 704.5h]],
30
+ lifelink: ["Damage dealt by this source also causes its controller to gain that much life.", %w[702.15b]],
31
+ indestructible: ["Can't be destroyed: lethal damage, deathtouch and \"destroy\" effects don't put it into " \
32
+ "the graveyard.", %w[702.12b]],
33
+ hexproof: ["Can't be the target of spells or abilities your opponents control.", %w[702.11b]],
34
+ wither: ["Damage this source deals to creatures is dealt as that many -1/-1 counters instead of marked " \
35
+ "damage, put by its controller.", %w[702.80a 702.80c 702.80d 120.3d]]
36
+ }.each { |name, (description, rules)| Evergreen.define(name, description, rules) }
37
+
38
+ # Flash (CR 702.8): may be cast any time its controller could cast an instant.
39
+ class Flash < Primitive
40
+ primitive :keyword, :flash,
41
+ description: "You may cast this spell any time you could cast an instant.",
42
+ rules: %w[702.8a], example: { "keywords" => ["flash"] }
43
+ end
44
+
45
+ # Haste (CR 702.10): can attack and {T} the turn it comes under your control.
46
+ class Haste < Primitive
47
+ primitive :keyword, :haste,
48
+ description: "This creature can attack and use {T} abilities even if it hasn't been under your " \
49
+ "control continuously since your most recent turn began.",
50
+ rules: %w[702.10a 302.6], example: { "keywords" => ["haste"] }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Keywords
6
+ # Extort (CR 702.101a): "Whenever you cast a spell, you may pay {W/B}. If you do, each opponent loses 1 life
7
+ # and you gain life equal to the total life lost this way." Each instance triggers separately (CR 702.101b).
8
+ # The life lost inside the payment is recorded on the payment (effect 0), so the gain reads it.
9
+ class Extort < Primitive
10
+ primitive :keyword, :extort,
11
+ description: "Whenever you cast a spell, you may pay {W/B}. If you do, each opponent loses 1 life " \
12
+ "and you gain life equal to the total life lost this way.",
13
+ rules: %w[702.101a 702.101b 118.12], example: { "keywords" => ["extort"] }
14
+
15
+ ABILITY = { "triggered" => {
16
+ "when" => { "cast" => { "controller" => "you" } },
17
+ "effects" => [{ "pay" => { "mana" => "{W/B}",
18
+ "then" => [{ "lose_life" => { "amount" => 1, "to" => "each_opponent" } }] } },
19
+ { "gain_life" => { "amount" => { "this_way" => 0 } } }]
20
+ } }.freeze
21
+
22
+ def self.intrinsic_abilities = @intrinsic_abilities ||= [Keywords.ability_from_data(ABILITY, "extort")].freeze
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "evergreen"
4
+
5
+ module Enjine
6
+ module Mechanics
7
+ module Keywords
8
+ # Rules modifications that work like a keyword: a flag on the object's characteristics that the
9
+ # rules code checks, printed, handed out by a static ability (often a conditional one) or gained until end of
10
+ # turn.
11
+ {
12
+ attacks_despite_defender: ["Defender doesn't stop this creature from attacking.", %w[702.3b 508.1c]],
13
+ doesnt_untap: ["This permanent doesn't untap during its controller's untap step.", %w[502.3]],
14
+ assigns_damage_by_toughness: ["If this creature's toughness is greater than its power, it assigns combat damage " \
15
+ "equal to its toughness rather than its power.", %w[510.1a]],
16
+ keeps_damage: ["Damage isn't removed from this permanent during cleanup steps (remove_damage still removes it).",
17
+ %w[514.2 120.6]],
18
+ abilities_cant_be_activated: ["Its activated abilities, mana abilities included, can't be activated (\"Activated " \
19
+ "abilities of artifacts can't be activated\" is a static handing it out).",
20
+ %w[602.5 605.3a 101.2]]
21
+ }.each { |name, (description, rules)| Evergreen.define(name, description, rules) }
22
+
23
+ # Combat restrictions and requirements (CR 508.1c-d, 509.1b). They aren't keyword abilities
24
+ # in the CR (702), but they work exactly like one: a flag on the object's current
25
+ # characteristics that combat code checks. So a card prints them (`"keywords": ["cant_block"]`),
26
+ # a static ability hands them out ("Creatures your opponents control can't block") and the
27
+ # `grant` effect gives them until end of turn ("Target creature can't be blocked this turn").
28
+ {
29
+ cant_attack: ["Restriction: this creature can't attack.", %w[508.1c]],
30
+ cant_block: ["Restriction: this creature can't block.", %w[509.1b]],
31
+ cant_be_blocked: ["Restriction: this creature can't be blocked.", %w[509.1b]],
32
+ cant_attack_alone: ["Restriction: this creature can't attack unless another creature attacks too. It is " \
33
+ "offered as an attacker only while another creature attacks or could still be declared, " \
34
+ "and declaring attackers can't finish while it attacks alone.", %w[506.5 508.1c]],
35
+ attacks_each_combat: ["Requirement: this creature attacks each combat if able. Declaring attackers can't " \
36
+ "finish while it could still attack.", %w[508.1d]],
37
+ must_be_blocked: ["Requirement: this attacking creature must be blocked if able. Declaring blockers can't " \
38
+ "finish while it is unblocked and a creature that could block it blocks nothing.", %w[509.1c]]
39
+ }.each { |name, (description, rules)| Evergreen.define(name, description, rules) }
40
+
41
+ # "This spell can't be countered." (CR 101.2: a "can't" wins over an effect that would counter it.)
42
+ Evergreen.define(:cant_be_countered, "This spell can't be countered: counter effects leave it on the stack.",
43
+ %w[701.6a 101.2])
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "alternative_costs"
4
+
5
+ module Enjine
6
+ module Mechanics
7
+ module Abilities
8
+ # Foretell (CR 702.143a-b): any time its owner has priority during their turn, they may pay {2} and exile
9
+ # the card from their hand face down (a special action, `{type: "special_action", object: id, ability:
10
+ # ref}`, CR 116.2h). They may look at it while it remains exiled, and cast it after that turn has ended by
11
+ # paying its foretell cost rather than its mana cost (cast option `via: "alt:<ref>"`, an alternative cost).
12
+ class Foretell < AlternativeCost
13
+ primitive :ability, :foretell,
14
+ description: "Foretell: during your turn, you may pay {2} and exile this card from your hand face down " \
15
+ "(special action `{type: \"special_action\"}`). After that turn you may cast it from " \
16
+ "exile by paying `cost` rather than its mana cost (cast option `via: \"alt:<ref>\"`). " \
17
+ "`{\"cast_with\": \"foretell\"}` reads it.",
18
+ rules: %w[702.143a 702.143b 702.143c 116.2h 406.3 118.9 601.2b 601.2f 601.2h],
19
+ params: { "cost" => "mana cost string, or a list of costs" },
20
+ example: { "foretell" => { "cost" => "{2}{R}" } }
21
+
22
+ FORETELL_COST = Mana::Cost.parse("{2}")
23
+
24
+ def zone = :exile
25
+
26
+ # CR 702.143a: a card foretold on an earlier turn (the exiled card remembers the turn).
27
+ def functions?(engine, obj, _index)
28
+ turn = obj.zone == :exile && obj.memory["foretold"]
29
+ turn ? engine.state.turn > turn : false
30
+ end
31
+
32
+ # ---- the special action (Actions::SpecialAction) ----
33
+
34
+ def special_cost = FORETELL_COST
35
+ def special_allowed?(engine, obj, index) = obj.zone == :hand && engine.state.active == index
36
+
37
+ # Exiled face down; its owner may look at it (CR 702.143a, 406.3).
38
+ def special!(engine, obj, index)
39
+ fresh = engine.move!(obj.id, :exile, reason: "foretell", face_down: [index])
40
+ fresh.memory["foretold"] = engine.state.turn if fresh
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "kicker"
4
+
5
+ module Enjine
6
+ module Mechanics
7
+ module Costs
8
+ # The gift cost (CR 702.174a): "you may choose an opponent" — one `choose_cost` decision among the
9
+ # opponents. As the spell becomes cast it remembers the chosen player ("gift"), who the gift goes to.
10
+ class GiftOpponent
11
+ def mana? = false
12
+ def choices_needed = 1
13
+ def payable?(engine, object) = !opponents(engine, object).empty?
14
+ def opponents(engine, object) = engine.state.players.each_index.reject { it == object.controller }
15
+ def choice_options(engine, object, _chosen) = opponents(engine, object).map { { object: Filters.player_ref(engine, it) } }
16
+ def choose!(_engine, option) = { "object" => option[:object] }
17
+ def pay_chosen!(_engine, object, entries) = object.memory["gift"] = entries.first&.fetch("object")
18
+ def to_s = "Choose an opponent"
19
+ end
20
+ end
21
+
22
+ module Abilities
23
+ # Gift (CR 702.174a-b, k): "As an additional cost to cast this spell, you may choose an opponent." It is
24
+ # announced like kicker (the `choose_kicker` decision), then the opponent is chosen (`choose_cost`). The
25
+ # gift itself is ordinary card data: the effect, first in the spell's effects (CR 702.174j) or in an
26
+ # "enters" trigger of a permanent, with `"if": {"cast_with": "gift"}` ("if the gift was promised") and the
27
+ # selector "gift_recipient" for the chosen player. Targets only for a promised gift are `kicked_targets`
28
+ # (CR 702.174m).
29
+ class Gift < SpellCost
30
+ primitive :ability, :gift,
31
+ description: "Gift: as you cast this spell you may promise a gift to an opponent you choose (the " \
32
+ "`choose_kicker` decision, then `choose_cost` for the opponent). `{\"cast_with\": " \
33
+ "\"gift\"}` holds if you did, and the selector \"gift_recipient\" is that player; write " \
34
+ "the gift as the spell's first effect (or an enters trigger) under that condition.",
35
+ rules: %w[702.174a 702.174b 702.174j 702.174k 702.174m 601.2b],
36
+ example: "gift"
37
+
38
+ COST = Costs::GiftOpponent.new.freeze
39
+
40
+ def self.build(params, errors, path, **)
41
+ Mechanics.check_keys(params, [], errors, path)
42
+ new([COST].freeze)
43
+ end
44
+
45
+ def kind = :kicker
46
+ def to_s = "Gift"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mobilize_and_firebending"
4
+
5
+ module Enjine
6
+ module Mechanics
7
+ module Abilities
8
+ # Hideaway N (CR 702.75a): "When this permanent enters, look at the top N cards of your library. Exile one
9
+ # of them face down and put the rest on the bottom of your library in a random order." Only its controller
10
+ # may look at the exiled card. It's linked to the permanent (CR 607.2a): another of its abilities names it
11
+ # with the selector "exiled_with", e.g. `cast_free` with `play: true` ("you may play the exiled card
12
+ # without paying its mana cost").
13
+ class Hideaway < NumberedTrigger
14
+ primitive :ability, :hideaway,
15
+ description: "Hideaway `amount`: when this permanent enters, look at the top N cards of your library, " \
16
+ "exile one face down (linked: \"exiled_with\") and put the rest on the bottom in a random order.",
17
+ rules: %w[702.75a 406.3 607.2a],
18
+ params: { "amount" => "amount" },
19
+ example: { "hideaway" => { "amount" => 4 } }
20
+
21
+ def self.expansion(amount)
22
+ { "when" => { "enters" => {} },
23
+ "effects" => [{ "dig" => { "amount" => amount, "put" => "exile", "face_down" => true, "link" => true } }] }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end