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,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Creates a delayed triggered ability (CR 603.7): "at the beginning of the next end step,
7
+ # return it". It is defined here, in the card data, and addressed as "delayed:<path>".
8
+ class DelayedTrigger < Effect
9
+ primitive :effect, :delayed_trigger,
10
+ description: "Creates a delayed triggered ability with the controller and source of the resolving " \
11
+ "spell or ability. It triggers only once, the next time `when` happens (e.g. " \
12
+ "{\"step_begins\": {\"step\": \"end\", \"whose\": \"each\"}} for the next end step). Its " \
13
+ "`effects` may use {\"result\": n} for what earlier effects of the creating list moved or " \
14
+ "created; those objects are affected only if they are still where they were put.",
15
+ rules: %w[603.7a 603.7b 603.7c 603.7d 603.7e],
16
+ params: { "when" => "a trigger condition", "effects" => "list of effects",
17
+ "watch" => "selector resolved now (e.g. {\"target\": 0}): the objects its trigger subject " \
18
+ "\"watched\" and its effects' selector \"watched\" mean (\"when that creature dies\")",
19
+ "this_turn" => "true: it lasts until the end of this turn and triggers each time " \
20
+ "(\"whenever ... this turn\", CR 603.7b); \"once\": only the next time " \
21
+ "this turn (\"at the beginning of the next combat this turn\")" },
22
+ example: { "delayed_trigger" => { "when" => { "step_begins" => { "step" => "end", "whose" => "each" } },
23
+ "effects" => [{ "sacrifice" => { "to" => { "result" => 0 } } }] } }
24
+
25
+ attr_reader :ref, :delayed_ability
26
+
27
+ def self.keys = %w[when effects watch this_turn]
28
+ def self.default_to = "you"
29
+
30
+ def setup(params, errors, path)
31
+ @ref = "delayed:#{path}"
32
+ raw = Mechanics.fetch_param(params, :when)
33
+ trigger = raw.nil? ? ((errors << "#{path}.when: required") && nil) : Mechanics.build_entry(:trigger, raw, errors, "#{path}.when")
34
+ effects = Abilities::StackAbility.parse_effects(params, errors, path, 0, results_count: @results_count)
35
+ errors << "#{path}.effects: needs at least one effect" if effects.empty?
36
+ @delayed_ability = Abilities::TriggeredAbility.new(targets: [].freeze, effects: effects, trigger: trigger)
37
+ raw_watch = Mechanics.fetch_param(params, :watch)
38
+ @watch = raw_watch && Selector.build(raw_watch, errors, "#{path}.watch", targets_count: @targets_count,
39
+ results_count: @results_count)
40
+ @this_turn = Mechanics.fetch_param(params, :this_turn)
41
+ errors << "#{path}.this_turn: must be true, false or \"once\"" unless [nil, true, false, "once"].include?(@this_turn)
42
+ end
43
+
44
+ def nested_effects = delayed_ability.effects
45
+ def to_s = "delayed#{" this turn" if @this_turn}: #{delayed_ability}#{" (watching #{@watch})" if @watch}"
46
+
47
+ # CR 603.7d-e: controlled by the resolving object's controller, from the same source. It
48
+ # remembers the results it may name and the objects it watches (CR 603.7c).
49
+ def apply(engine, ctx)
50
+ entry = { "controller" => ctx.controller, "source" => ctx.source, "card" => engine.object(ctx.object).card,
51
+ "ability" => ref, "results" => Snapshot.deep_copy(ctx.results || []) }
52
+ entry["watched"] = @watch.resolve(engine, ctx) if @watch
53
+ entry["turn"] = engine.state.turn if @this_turn
54
+ entry["once"] = true if @this_turn == "once" # CR 603.7b: the next time only, and only this turn
55
+ engine.create_delayed_trigger!(entry)
56
+ end
57
+ end
58
+
59
+ # A reflexive triggered ability (CR 603.12): "When you do, [effect]". Put it where the action it
60
+ # depends on is known to have happened, usually a "may"'s `then`. It triggers at once and goes on the
61
+ # stack the next time a player would receive priority, with its own targets chosen then (CR 603.3d).
62
+ class ReflexiveTrigger < Effect
63
+ primitive :effect, :reflexive_trigger,
64
+ description: "Creates a reflexive triggered ability (\"when you do, ...\") that triggers immediately, " \
65
+ "with the controller and source of the resolving spell or ability. Its `targets` are " \
66
+ "chosen as it's put on the stack; its `effects` may name the results of earlier effects " \
67
+ "of the creating list. With `if`, it triggers only when the condition holds " \
68
+ "(\"when excess damage is dealt this way\").",
69
+ rules: %w[603.12 603.12a 603.7d 603.7e 603.3d],
70
+ params: { "targets" => "list of target filters", "effects" => "list of effects", "if" => "condition",
71
+ "divide" => "amount divided among the targets as it's put on the stack (CR 601.2d)" },
72
+ example: { "reflexive_trigger" => { "targets" => ["any_target"],
73
+ "effects" => [{ "deal_damage" => { "amount" => 2,
74
+ "to" => { "target" => 0 } } }] } }
75
+
76
+ # Stands in for the trigger condition on the stack ability ("When you do").
77
+ WhenYouDo = Data.define do
78
+ def to_s = "When you do"
79
+ end
80
+
81
+ attr_reader :ref, :delayed_ability
82
+
83
+ def self.keys = %w[targets effects divide if]
84
+ def self.default_to = "you"
85
+
86
+ def setup(params, errors, path)
87
+ @ref = "reflexive:#{path}"
88
+ targets = Abilities::StackAbility.parse_targets(params, errors, path)
89
+ effects = Abilities::StackAbility.parse_effects(params, errors, path, targets.size, results_count: @results_count)
90
+ errors << "#{path}.effects: needs at least one effect" if effects.empty?
91
+ extras = Abilities::StackAbility.parse_extras(params.slice("divide"), errors, path)
92
+ Abilities::StackAbility.check_divide(extras, targets, errors, path)
93
+ @delayed_ability = Abilities::TriggeredAbility.new(targets: targets, effects: effects, trigger: WhenYouDo.new,
94
+ extras: extras)
95
+ end
96
+
97
+ def nested_effects = delayed_ability.effects
98
+ def to_s = "reflexive: #{delayed_ability}"
99
+
100
+ def apply(engine, ctx)
101
+ # Created by a replacement's follow-up (Engine::FollowUps), its spell may be long gone: its card is remembered.
102
+ memory = Selector.memory(engine, ctx)
103
+ card = engine.state.objects[ctx.object]&.card || memory["source_card"]
104
+ source = { "id" => ctx.source, "card" => card, "controller" => ctx.controller, "ability" => ref }
105
+ engine.queue_pending_trigger!(source, nil, { "results" => Snapshot.deep_copy(ctx.results || []),
106
+ "amounts" => (ctx.amounts || []).dup,
107
+ "triggering" => memory["triggering"]&.dup }.compact)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Destroy permanents (CR 701.8).
7
+ class Destroy < Effect
8
+ primitive :effect, :destroy, description: "Destroy each referenced permanent (it goes to its owner's graveyard).",
9
+ rules: %w[701.8a], params: { "to" => "selector" },
10
+ example: { "destroy" => { "to" => { "target" => 0 } } }
11
+ def self.keys = %w[to if]
12
+
13
+ # CR 702.12b: indestructible permanents aren't destroyed.
14
+ def apply(engine, ctx)
15
+ objects(engine, ctx).each do |id|
16
+ engine.move!(id, :graveyard, reason: "destroy") unless engine.chars(id).keyword?(:indestructible)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Discard N cards of the player's choice (CR 701.9), at random (CR 701.9b), or the whole hand.
7
+ class Discard < ChoiceEffect
8
+ primitive :effect, :discard, description: "Each referenced player discards `amount` cards of their choice " \
9
+ "(default: you). `random: true`: the cards are chosen at random " \
10
+ "(the game's random number generator), with no decision. " \
11
+ "`hand: true`: each discards their whole hand at once (\"discard " \
12
+ "your hand\"; how many is its amount {\"this_way\": n}).",
13
+ rules: %w[701.9a 701.9b], params: { "amount" => "integer", "to" => "selector",
14
+ "random" => "boolean",
15
+ "hand" => "true: the whole hand" },
16
+ example: { "discard" => { "amount" => 1, "to" => "each_opponent" } }
17
+ def self.keys = %w[amount to if random hand]
18
+ def self.amount_optional?(params) = Mechanics.fetch_param(params, :hand) == true
19
+ def self.default_to = "you"
20
+
21
+ def setup(params, errors, path)
22
+ @random = Mechanics.flag_param(params, :random, errors, path)
23
+ @hand = Mechanics.flag_param(params, :hand, errors, path)
24
+ errors << "#{path}: hand and random can't be combined" if @hand && @random
25
+ end
26
+
27
+ def to_s = @hand ? "discard the whole hand → #{to}" : "#{super}#{" at random" if @random}"
28
+
29
+ # CR 118.3: "you may discard a card" needs a card to discard; a hand can always be discarded.
30
+ def possible?(engine, ctx) = @hand || players(engine, ctx).all? { engine.player(it).hand.size >= amount_for(engine, ctx) }
31
+
32
+ def run(engine, ctx, locals)
33
+ return discard_at_random(engine, ctx) if @random
34
+ return discard_hands(engine, ctx) if @hand
35
+
36
+ queue = (locals["players"] ||= players(engine, ctx).flat_map { |i| [i] * amount_for(engine, ctx) })
37
+ chosen = answers(locals)
38
+ apply_answers(engine, locals, chosen)
39
+ while chosen.size < queue.size
40
+ index = queue[chosen.size]
41
+ hand = engine.player(index).hand
42
+ return ask(engine, locals, index, "Discard a card", hand.map { { "type" => "discard", "object" => it } }) unless hand.empty?
43
+
44
+ chosen << { "type" => "none" } # nothing left to discard
45
+ locals["applied"] = chosen.size
46
+ end
47
+ :done
48
+ end
49
+
50
+ # CR 701.9b: no one chooses; each card is picked by the game's RNG (so a replay picks the same ones).
51
+ def discard_at_random(engine, ctx)
52
+ return :done unless condition.nil? || condition.true?(engine, ctx)
53
+
54
+ n = amount_for(engine, ctx)
55
+ players(engine, ctx).each do |index|
56
+ hand = engine.player(index).hand
57
+ picked = engine.state.rng.shuffle!(hand.dup).first(n)
58
+ picked.each { engine.move!(it, :graveyard, reason: "discard") }
59
+ end
60
+ :done
61
+ end
62
+
63
+ # "Discard your hand" (CR 701.9a): all at once, no choice; how many were discarded is its amount.
64
+ def discard_hands(engine, ctx)
65
+ return :done unless condition.nil? || condition.true?(engine, ctx)
66
+
67
+ players(engine, ctx).each do |index|
68
+ cards = engine.player(index).hand.dup
69
+ cards.each { engine.move!(it, :graveyard, reason: "discard") }
70
+ engine.record_amount!(cards.size)
71
+ end
72
+ :done
73
+ end
74
+
75
+ # Performs the discards chosen since the last step.
76
+ def apply_answers(engine, locals, chosen)
77
+ (locals["applied"] ||= 0).upto(chosen.size - 1) do |i|
78
+ answer = chosen[i]
79
+ engine.move!(answer["object"], :graveyard, reason: "discard") if answer["type"] == "discard"
80
+ end
81
+ locals["applied"] = chosen.size
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Draw cards (CR 121).
7
+ class Draw < Effect
8
+ primitive :effect, :draw, description: "Each referenced player draws `amount` cards (default: you).",
9
+ rules: %w[121.1 121.2], params: { "amount" => "integer", "to" => "selector" },
10
+ example: { "draw" => { "amount" => 2 } }
11
+ def self.default_to = "you"
12
+ def apply(engine, ctx) = players(engine, ctx).each { |i| amount_for(engine, ctx).times { engine.draw!(i) } }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Earthbend N (CR 701.66a): "Target land you control becomes a 0/0 land creature with haste in addition to
7
+ # its other types. Put N +1/+1 counters on it. When that land dies or is put into exile, return it to the
8
+ # battlefield tapped under your control." The target is declared on the ability like any other target
9
+ # (usually `{"land": {"controller": "you"}}`) and named by `to`. The land becomes a creature for as long as
10
+ # it stays on the battlefield (no duration is stated, CR 611.2a), is the effect's result ("that land"), and
11
+ # a delayed triggered ability watches it (CR 603.7c). Creating that ability is earthbending (CR 701.66b).
12
+ class Earthbend < Effect
13
+ primitive :effect, :earthbend,
14
+ description: "Earthbend `amount`: each referenced land (the target, `to`) becomes a 0/0 creature " \
15
+ "with haste that's still a land, gets `amount` +1/+1 counters, and when it dies or is " \
16
+ "put into exile it returns to the battlefield tapped under your control. The land is " \
17
+ "the effect's result.",
18
+ rules: %w[701.66a 701.66b 603.7c 611.2a 613.1d 613.4b],
19
+ params: { "amount" => "amount", "to" => "selector (the target land, e.g. {\"target\": 0})" },
20
+ example: { "earthbend" => { "amount" => 2, "to" => { "target" => 0 } } }
21
+
22
+ # The delayed triggered ability: "When that land dies or is put into exile, return it to the battlefield
23
+ # tapped under your control" ("it" is the card in its new zone, CR 400.7).
24
+ RETURN = { "when" => { "any_of" => [{ "dies" => { "subject" => "watched" } },
25
+ { "leaves_battlefield" => { "subject" => "watched", "to" => "exile" } }] },
26
+ "effects" => [{ "return_to_battlefield" => { "to" => { "triggering" => "object" }, "tapped" => true,
27
+ "under" => "you" } }] }.freeze
28
+
29
+ attr_reader :ref, :delayed_ability
30
+
31
+ def setup(_params, errors, path)
32
+ @ref = "earthbend:#{path}"
33
+ trigger = Mechanics.build_entry(:trigger, RETURN["when"], errors, "#{path}.when")
34
+ effects = Abilities::StackAbility.parse_effects(RETURN, errors, path, 0)
35
+ @delayed_ability = Abilities::TriggeredAbility.new(targets: [].freeze, effects: effects, trigger: trigger)
36
+ end
37
+
38
+ def nested_effects = delayed_ability.effects
39
+ def to_s = "earthbend #{amount} → #{to}"
40
+
41
+ def apply(engine, ctx)
42
+ lands = objects(engine, ctx).select { engine.chars(it).land? }
43
+ return if lands.empty?
44
+
45
+ n = amount_for(engine, ctx)
46
+ become_creature!(engine, ctx, lands)
47
+ lands.each do |id|
48
+ engine.record_result!(id)
49
+ engine.add_counters!(engine.object(id), "+1/+1", n, by_player: ctx.controller)
50
+ engine.create_delayed_trigger!("controller" => ctx.controller, "source" => ctx.source,
51
+ "card" => engine.object(ctx.object).card, "ability" => ref, "watched" => [id])
52
+ engine.emit Events::KeywordAction.new(player: engine.name(ctx.controller), action: "earthbend", amount: n)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # Layer 4 (a creature in addition to its other types), layer 6 (haste) and layer 7b (base 0/0), for as
59
+ # long as it stays on the battlefield; the same continuous effects `become` creates.
60
+ def become_creature!(engine, ctx, lands)
61
+ lasting = { "duration" => "permanent" }
62
+ engine.add_continuous_effect!("add_types", ctx, lands,
63
+ { "types" => ["creature"], "supertypes" => [], "subtypes" => [] }.merge(lasting))
64
+ engine.add_continuous_effect!("grant", ctx, lands, { "keywords" => ["haste"] }.merge(lasting))
65
+ engine.add_continuous_effect!("base_power_toughness", ctx, lands, { "power" => 0, "toughness" => 0 }.merge(lasting))
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # "You get an emblem with '...'" (CR 114.1-114.4): each referenced player gets an emblem in the command
7
+ # zone with the `abilities`. They are part of this card's data, referenced "<this effect's ref>:<j>"
8
+ # (CardDefinition#effect_granted), and work from the command zone for the rest of the game (Engine::Emblems).
9
+ class CreateEmblem < Effect
10
+ KINDS = %i[triggered static player_rule].freeze
11
+
12
+ primitive :effect, :create_emblem,
13
+ description: "Each referenced player (default you) gets an emblem with the `abilities` (triggered, " \
14
+ "static or player_rule abilities). An emblem is in the command zone for the rest of the " \
15
+ "game; its abilities work there, and \"this emblem\" is the source of what they do.",
16
+ rules: %w[114.1 114.2 114.3 114.4 113.6p],
17
+ params: { "abilities" => "list of triggered, static or player_rule abilities", "to" => "selector" },
18
+ example: { "create_emblem" => { "abilities" => [
19
+ { "static" => { "affects" => { "each" => "creature", "controller" => "you" },
20
+ "pump" => { "power" => 2, "toughness" => 2 } } }
21
+ ] } }
22
+
23
+ attr_reader :ref, :granted_abilities
24
+
25
+ def self.keys = %w[abilities to if]
26
+ def self.default_to = "you"
27
+
28
+ def setup(params, errors, path)
29
+ @ref = "emblem:#{path}"
30
+ list = Mechanics.list_param(params, :abilities, errors, path)
31
+ errors << "#{path}.abilities: needs at least one ability" if list.empty?
32
+ @granted_abilities = list.each_with_index.filter_map do |entry, j|
33
+ ability = Mechanics.build_entry(:ability, entry, errors, "#{path}.abilities[#{j}]")
34
+ next ability if ability.nil? || KINDS.include?(ability.kind)
35
+
36
+ errors << "#{path}.abilities[#{j}]: an emblem's abilities are #{KINDS.join(", ")} abilities"
37
+ nil
38
+ end.freeze
39
+ end
40
+
41
+ def nested_effects = granted_abilities.flat_map(&:try_effects)
42
+
43
+ def apply(engine, ctx)
44
+ card = engine.object(ctx.object).card
45
+ players(engine, ctx).each { engine.create_emblem!(it, card, ref) }
46
+ end
47
+
48
+ def to_s = "gets an emblem with \"#{granted_abilities.join("; ")}\" → #{to}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Endure N (CR 701.63a): the permanent's controller creates an N/N white Spirit creature token unless
7
+ # they put N +1/+1 counters on that permanent. The choice is an `effect_choice` ("counters" is offered
8
+ # only while the permanent is on the battlefield). Endure 0 does nothing (CR 701.63b). N is any amount
9
+ # ("endures X", "where X is the number of counters on this creature"), fixed as the effect starts; the
10
+ # token's power and toughness are copiable values the effect gives it (CR 707.2, 111.4).
11
+ class Endure < ChoiceEffect
12
+ primitive :effect, :endure,
13
+ description: "Each referenced permanent (default: this one) endures `amount`: its controller puts " \
14
+ "that many +1/+1 counters on it, or creates an N/N white Spirit creature token.",
15
+ rules: %w[701.63a 701.63b 707.2 111.4],
16
+ params: { "amount" => "amount (2, \"x\", {\"counters\": ...})",
17
+ "to" => "selector of permanents (default self)" },
18
+ example: { "endure" => { "amount" => 2 } }
19
+
20
+ SPIRIT = { "name" => "Spirit", "types" => ["creature"], "subtypes" => ["spirit"], "colors" => ["W"],
21
+ "power" => 0, "toughness" => 0 }.freeze
22
+
23
+ def self.keys = %w[amount to if]
24
+ def self.default_to = "self"
25
+ def self.spirit = @spirit ||= CardDefinition.token(SPIRIT)
26
+
27
+ def created_tokens = [Endure.spirit]
28
+
29
+ def run(engine, ctx, locals)
30
+ unless locals.key?("queue")
31
+ return :done unless condition.nil? || condition.true?(engine, ctx)
32
+
33
+ locals["n"] = [amount_for(engine, ctx), 0].max
34
+ locals["queue"] = locals["n"].zero? ? [] : refs(engine, ctx).select { engine.state.objects.key?(it) }
35
+ end
36
+ n = locals["n"]
37
+ while (id = locals["queue"].first)
38
+ perm = engine.object(id)
39
+ index = perm.zone == :battlefield ? perm.controller : ctx.controller
40
+ answer = answers(locals).first
41
+ if answer.nil? && perm.zone == :battlefield
42
+ return ask(engine, locals, index, "Endure #{n}: put #{n} +1/+1 counters on it, or create a #{n}/#{n} Spirit?",
43
+ [{ "type" => "counters", "object" => id }, { "type" => "spirit" }])
44
+ end
45
+ answer&.[]("type") == "counters" ? engine.add_counters!(perm, "+1/+1", n, by_player: index) : spirit!(engine, index, n)
46
+ locals["queue"].shift
47
+ locals.delete("answers")
48
+ end
49
+ :done
50
+ end
51
+
52
+ def to_s = "endure #{amount} → #{to}"
53
+
54
+ private
55
+
56
+ # An N/N Spirit: N is part of the token's copiable values (Engine::Copies, "copy_except").
57
+ def spirit!(engine, index, n)
58
+ engine.create_token!(Endure.spirit, index, memory: { "copy_except" => { "power" => n, "toughness" => n } })
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Exile (CR 406, 701.13). `link`: the card is "exiled with" the source, for its linked abilities
7
+ # (CR 607.2a). `until_source_leaves`: it returns once the source leaves the battlefield (CR 610.3).
8
+ class Exile < ZoneEffect
9
+ primitive :effect, :exile,
10
+ description: "Exile each referenced permanent, spell, or card in a graveyard or hand. `link: true`: the cards " \
11
+ "are exiled with the source, for its \"exiled with\" abilities (the `exiled_with` selector). " \
12
+ "`until_source_leaves: true`: they return to the battlefield under their owners' control " \
13
+ "when the source leaves the battlefield; if it already has, nothing is exiled.",
14
+ rules: %w[406.1 701.13a 607.2a 406.6 610.3 610.3a 610.3b 610.3c],
15
+ params: { "to" => "selector", "link" => "boolean", "until_source_leaves" => "boolean" },
16
+ example: { "exile" => { "to" => { "target" => 0 } } }
17
+
18
+ attr_reader :link, :until_leaves
19
+
20
+ def self.keys = %w[to link until_source_leaves if]
21
+ def self.zone = :exile
22
+ # A spell too ("exile target spell", CR 701.13a).
23
+ def self.from_zones = %i[battlefield graveyard hand stack]
24
+
25
+ def setup(params, errors, path)
26
+ @link = Mechanics.flag_param(params, "link", errors, path)
27
+ @until_leaves = Mechanics.flag_param(params, "until_source_leaves", errors, path)
28
+ end
29
+
30
+ def to_s = "exile#{" until the source leaves" if until_leaves}#{" (linked)" if link} → #{to}"
31
+
32
+ # CR 610.3a-b: if the source has already left the battlefield, the object doesn't move.
33
+ def apply(engine, ctx)
34
+ return if until_leaves && !engine.state.battlefield.include?(ctx.source)
35
+
36
+ super
37
+ end
38
+
39
+ def move(engine, ctx, id)
40
+ fresh = super or return
41
+ engine.link_exiled!(ctx.source, fresh.id) if link || until_leaves
42
+ return unless until_leaves
43
+
44
+ (engine.object(ctx.source).memory[Linked::UNTIL_LEAVES] ||= []) << fresh.id
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # "Take an extra turn after this one." (CR 500.7)
7
+ class ExtraTurn < Effect
8
+ primitive :effect, :extra_turn,
9
+ description: "Each referenced player (default you) takes an extra turn after this one. Extra " \
10
+ "turns are taken most recently created first.",
11
+ rules: %w[500.7],
12
+ params: { "to" => "selector" },
13
+ example: { "extra_turn" => { "if" => { "began_extra_turn" => false } } }
14
+
15
+ def self.keys = %w[to if]
16
+ def self.default_to = "you"
17
+
18
+ def apply(engine, ctx) = players(engine, ctx).each { engine.add_extra_turn!(it) }
19
+ def to_s = "take an extra turn after this one → #{to}"
20
+ end
21
+
22
+ # "Target opponent skips their next X turns." (CR 614.10)
23
+ class SkipTurns < Effect
24
+ primitive :effect, :skip_turns,
25
+ description: "Each referenced player skips their next `amount` turns (extra turns included): " \
26
+ "each of those turns doesn't happen, and the next player in turn order takes theirs. " \
27
+ "Several skips add up.",
28
+ rules: %w[614.1b 614.10 614.10a],
29
+ params: { "amount" => "amount", "to" => "selector" },
30
+ example: { "skip_turns" => { "amount" => 1, "to" => { "target" => 0 } } }
31
+
32
+ def apply(engine, ctx)
33
+ n = amount_for(engine, ctx)
34
+ players(engine, ctx).each { engine.skip_turns!(it, n) }
35
+ end
36
+
37
+ def to_s = "skip the next #{amount} turn(s) → #{to}"
38
+ end
39
+
40
+ # "After this phase, there is an additional combat phase", "... followed by an additional main phase",
41
+ # "After this main phase, there are two additional combat phases." (CR 500.8)
42
+ class AdditionalPhases < Effect
43
+ primitive :effect, :additional_phases,
44
+ description: "Adds `phases` (in order; each \"combat\" or \"main\") directly after the current " \
45
+ "phase of this turn. Phases added after the same phase come most recently created " \
46
+ "first. An added main phase is a postcombat main phase. `attackers`: only creatures " \
47
+ "matching this filter can attack during the (first) added combat phase.",
48
+ rules: %w[500.8 505.1 508.1c],
49
+ params: { "phases" => "list of \"combat\" | \"main\"", "attackers" => "creature filter" },
50
+ example: { "additional_phases" => { "phases" => %w[combat main] } }
51
+
52
+ attr_reader :phases, :attackers
53
+
54
+ def self.keys = %w[phases attackers if]
55
+ def self.default_to = "you"
56
+
57
+ def setup(params, errors, path)
58
+ @attackers = Mechanics.fetch_param(params, :attackers)
59
+ Mechanics.build_entry(:filter, @attackers, errors, "#{path}.attackers") unless @attackers.nil?
60
+ @phases = Mechanics.list_param(params, :phases, errors, path)
61
+ return if !@phases.empty? && @phases.all? { Engine::ADDED_PHASES.key?(it) }
62
+
63
+ errors << "#{path}.phases: must be a non-empty list of \"combat\" | \"main\""
64
+ end
65
+
66
+ def apply(engine, _ctx) = engine.add_phases!(phases.dup, attackers: attackers)
67
+
68
+ def to_s
69
+ only = " (only #{Mechanics.build_entry(:filter, attackers, [], "")} can attack)" if attackers
70
+ "after this phase, additional #{phases.join(" and ")} phase#{"s" if phases.size > 1}#{only}"
71
+ end
72
+ end
73
+
74
+ # "End the turn." (CR 725.1)
75
+ class EndTheTurn < Effect
76
+ primitive :effect, :end_the_turn,
77
+ description: "Ends the turn: triggered abilities not yet on the stack cease to exist, every object on " \
78
+ "the stack is exiled (this spell too, so its later effects don't happen), state-based " \
79
+ "actions are checked, combat ends and the game skips to the cleanup step. Abilities " \
80
+ "that trigger meanwhile go on the stack during cleanup, followed by another cleanup step.",
81
+ rules: %w[725.1 725.1a 725.1b 725.1c 725.1d 725.1e 725.1f 514.3a],
82
+ example: "end_the_turn"
83
+
84
+ def self.keys = %w[if]
85
+ def self.default_to = "you"
86
+
87
+ def apply(engine, _ctx) = engine.end_the_turn!
88
+ def to_s = "end the turn"
89
+ end
90
+ end
91
+
92
+ class Condition
93
+ # "If you haven't begun an extra turn this game" (CR 500.7): {"began_extra_turn": false}.
94
+ class BeganExtraTurn
95
+ def initialize(wanted)
96
+ @wanted = wanted
97
+ freeze
98
+ end
99
+
100
+ def true?(engine, ctx) = engine.began_extra_turn?(ctx.controller) == @wanted
101
+ def to_s = @wanted ? "you've begun an extra turn this game" : "you haven't begun an extra turn this game"
102
+ end
103
+
104
+ # "If it's the first combat phase of the turn" (CR 500.8): {"first_combat_phase": true}.
105
+ class FirstCombatPhase
106
+ def self.build(data, errors, path)
107
+ Mechanics.check_keys(data, %w[first_combat_phase], errors, path)
108
+ errors << "#{path}.first_combat_phase: must be true or false" unless [true, false].include?(data["first_combat_phase"])
109
+ new(data["first_combat_phase"] != false)
110
+ end
111
+
112
+ def initialize(wanted)
113
+ @wanted = wanted
114
+ freeze
115
+ end
116
+
117
+ def true?(engine, _ctx) = engine.first_combat_phase? == @wanted
118
+ def to_s = @wanted ? "it's the first combat phase of the turn" : "it isn't the first combat phase of the turn"
119
+ end
120
+ end
121
+ end
122
+ end