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,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Transform permanents (CR 701.27): turn each referenced double-faced permanent over (see
7
+ # Engine::Faces#transform!).
8
+ class Transform < Effect
9
+ primitive :effect, :transform,
10
+ description: "Transform each referenced permanent (default: this one): turn it over so its other " \
11
+ "face is up. It stays the same object, and effects on it keep applying. Nothing " \
12
+ "happens to a permanent that isn't a double-faced card or token, or whose other face " \
13
+ "is an instant or sorcery. An ability of the permanent transforms it only if it hasn't " \
14
+ "transformed since the ability was put on the stack.",
15
+ rules: %w[701.27a 701.27c 701.27d 701.27e 701.27g 712.15],
16
+ params: { "to" => "selector (default \"self\")" },
17
+ example: { "transform" => { "to" => "self" } }
18
+
19
+ def self.keys = %w[to if]
20
+ def self.default_to = "self"
21
+
22
+ def apply(engine, ctx)
23
+ objects(engine, ctx).each do |id|
24
+ obj = engine.object(id)
25
+ engine.transform!(obj) if engine.transform_still_allowed?(obj, ctx)
26
+ end
27
+ end
28
+ end
29
+
30
+ # The prepared designation (CR 723.3a-b).
31
+ class Prepare < Effect
32
+ primitive :effect, :prepare,
33
+ description: "Each referenced permanent (default: this one) becomes prepared, if it has a prepare " \
34
+ "spell and isn't prepared already: its controller creates a copy of the prepare spell " \
35
+ "in exile and may cast that copy while the permanent stays prepared. Casting it " \
36
+ "unprepares the permanent.",
37
+ rules: %w[723.3a 723.3c],
38
+ params: { "to" => "selector (default \"self\")" },
39
+ example: { "prepare" => { "to" => "self" } }
40
+
41
+ def self.keys = %w[to if]
42
+ def self.default_to = "self"
43
+
44
+ def apply(engine, ctx) = objects(engine, ctx).each { engine.prepare!(engine.object(it)) }
45
+ end
46
+
47
+ # CR 723.3b: "becomes unprepared".
48
+ class Unprepare < Effect
49
+ primitive :effect, :unprepare,
50
+ description: "Each referenced permanent (default: this one) loses the prepared designation; the " \
51
+ "copy of its prepare spell in exile ceases to exist.",
52
+ rules: %w[723.3b 723.3c],
53
+ params: { "to" => "selector (default \"self\")" },
54
+ example: { "unprepare" => { "to" => "self" } }
55
+
56
+ def self.keys = %w[to if]
57
+ def self.default_to = "self"
58
+
59
+ def apply(engine, ctx) = objects(engine, ctx).each { engine.unprepare!(engine.object(it)) }
60
+ end
61
+ end
62
+
63
+ module Abilities
64
+ # "This creature enters prepared." (CR 723.3a): a self-replacement effect on its own entering.
65
+ class EntersPrepared < Primitive
66
+ primitive :ability, :enters_prepared,
67
+ description: "This permanent enters the battlefield prepared (it needs a prepare spell, see " \
68
+ "`layout: \"prepare\"`).",
69
+ rules: %w[723.3a 614.1c],
70
+ example: "enters_prepared"
71
+
72
+ INSTANCE = new.freeze
73
+
74
+ def self.build(params, errors, path, **)
75
+ Mechanics.check_keys(params, [], errors, path)
76
+ INSTANCE
77
+ end
78
+
79
+ def kind = :enters_prepared
80
+ def mana_ability? = false
81
+ def allowed_types = nil
82
+ def to_s = "enters prepared"
83
+ def try_effects = []
84
+
85
+ def validate_definition(defn, errors, path)
86
+ errors << "#{path}: only a card with a prepare spell (layout \"prepare\") enters prepared" unless defn.layout == "prepare"
87
+ end
88
+
89
+ def modify_entering(enters) = enters["prepared"] = true
90
+ end
91
+ end
92
+
93
+ module Triggers
94
+ # "Whenever this creature transforms into [this face]" (CR 701.27f): a self trigger is an ability of the
95
+ # face it transformed into, so it triggers only on transforming into that face.
96
+ class Transforms < Trigger
97
+ primitive :trigger, :transforms,
98
+ description: "The subject transforms. For the default subject (this permanent), the ability is on " \
99
+ "the face that's up afterward, so it means \"whenever this transforms into [this face]\".",
100
+ rules: %w[701.27f 603.2],
101
+ params: { "subject" => "\"self\" (default) or {\"each\": filter, ...}" },
102
+ example: { "transforms" => {} }
103
+
104
+ def event_class = Events::Transformed
105
+
106
+ def match?(engine, event, source)
107
+ obj = engine.state.objects[event.object] or return false
108
+ @subject.match?(engine, event.object, obj.controller, obj.card, source, types: engine.chars(obj).types)
109
+ end
110
+
111
+ def to_s = "whenever #{@subject} transforms"
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Base for effects with a second selector besides `to` (`with` / `from`).
7
+ class PairEffect < Effect
8
+ def self.other_key = raise(NotImplementedError)
9
+ def self.keys = ["to", other_key, "if"]
10
+
11
+ attr_reader :other
12
+
13
+ def setup(params, errors, path)
14
+ key = self.class.other_key
15
+ raw = Mechanics.fetch_param(params, key)
16
+ @other = if raw.nil?
17
+ (errors << "#{path}.#{key}: required") && nil
18
+ else
19
+ Selector.build(raw, errors, "#{path}.#{key}", targets_count: @targets_count, results_count: @results_count)
20
+ end
21
+ end
22
+
23
+ def other_creatures(engine, ctx)
24
+ other.resolve(engine, ctx).select { engine.state.battlefield.include?(it) && engine.chars(it).creature? }
25
+ end
26
+
27
+ def creatures(engine, ctx) = objects(engine, ctx).select { engine.chars(it).creature? }
28
+ end
29
+
30
+ # "Target creature you control fights target creature an opponent controls." (CR 701.14)
31
+ class Fight < PairEffect
32
+ primitive :effect, :fight,
33
+ description: "The creature referenced by `to` fights the one referenced by `with`: each deals " \
34
+ "damage equal to its power to the other at the same time (not combat damage). If " \
35
+ "either is gone, no longer a creature or an illegal target, neither deals damage. " \
36
+ "{\"this_way\": n} naming it is the excess damage dealt to the `with` creature.",
37
+ rules: %w[701.14a 701.14b 701.14c 701.14d 120.4a],
38
+ params: { "to" => "selector (one creature)", "with" => "selector (one creature)" },
39
+ example: { "fight" => { "to" => { "target" => 0 }, "with" => { "target" => 1 } } }
40
+
41
+ def self.other_key = "with"
42
+
43
+ def apply(engine, ctx)
44
+ one = refs(engine, ctx).first
45
+ other_one = other.resolve(engine, ctx).first
46
+ # Nothing happens unless both are creatures on the battlefield. The excess damage dealt to the
47
+ # `with` creature is what {"this_way": n} reads (CR 120.4a).
48
+ engine.record_amount!(engine.fight!(one, other_one))
49
+ end
50
+
51
+ def to_s = "#{to} fights #{other}"
52
+ end
53
+
54
+ # "Target creatures you control each deal damage equal to their power to target creature." ("bite")
55
+ class Bite < PairEffect
56
+ primitive :effect, :bite,
57
+ description: "Each creature referenced by `from` deals damage equal to its power to each creature, " \
58
+ "planeswalker or player referenced by `to` (one damage event per source; not combat " \
59
+ "damage, and nothing is dealt back).",
60
+ rules: %w[120.3 120.4 608.2b],
61
+ params: { "from" => "selector (the creatures dealing damage)", "to" => "selector" },
62
+ example: { "bite" => { "from" => { "target" => 0 }, "to" => { "target" => 1 } } }
63
+
64
+ def self.other_key = "from"
65
+
66
+ def apply(engine, ctx)
67
+ sources = other_creatures(engine, ctx)
68
+ powers = sources.to_h { [it, engine.power(engine.object(it))] } # determined before any is dealt
69
+ sources.each { |source| refs(engine, ctx).each { engine.deal_damage!(source, it, powers[source]) } }
70
+ end
71
+
72
+ def to_s = "#{other} each deal damage equal to their power → #{to}"
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../abilities/turn_rules"
4
+
5
+ module Enjine
6
+ module Mechanics
7
+ module Effects
8
+ # Flipping coins (CR 705). With `count: "wins"` (default) the flipper calls each coin, heads or tails,
9
+ # and wins the flips whose result matches the call (CR 705.2); with `count: "heads"` the effect only
10
+ # cares how many coins come up heads and nobody wins or loses. The number counted is the effect's
11
+ # amount for later effects ({"this_way": n}): "if you win the flip, ..." is an `if` comparing it.
12
+ class FlipCoins < ChoiceEffect
13
+ COUNTS = %w[wins heads].freeze
14
+ CALLS = [{ "type" => "call", "call" => "heads" }, { "type" => "call", "call" => "tails" }].freeze
15
+
16
+ primitive :effect, :flip_coins,
17
+ description: "You flip `amount` coins (default 1), each coming up heads or tails at random (the " \
18
+ "game's random number generator). `count`: \"wins\" (default; you call each flip and " \
19
+ "win it if the call matches) or \"heads\" (no one wins or loses). How many were " \
20
+ "counted is this effect's amount: {\"this_way\": n} in later effects and conditions.",
21
+ rules: %w[705.1 705.2 705.3 608.2c],
22
+ params: { "amount" => "amount (default 1)", "count" => COUNTS.join(" | ") },
23
+ example: { "flip_coins" => {} }
24
+
25
+ def self.keys = %w[amount count if]
26
+ def self.default_to = "you"
27
+
28
+ def initialize(to:, params:, errors:, path:, targets_count: 0, results_count: 0)
29
+ params = params.transform_keys(&:to_s)
30
+ params["amount"] = 1 unless params.key?("amount")
31
+ super
32
+ end
33
+
34
+ def setup(params, errors, path)
35
+ @count = Mechanics.fetch_param(params, :count) || "wins"
36
+ errors << "#{path}.count: must be one of #{COUNTS.join(", ")}" unless COUNTS.include?(@count)
37
+ end
38
+
39
+ def to_s = "flip #{amount == 1 ? "a coin" : "#{amount} coins"} (counting #{@count})"
40
+
41
+ # A call is asked before each flip (CR 705.2); the flips happen as they are called.
42
+ def run(engine, ctx, locals)
43
+ unless locals["left"]
44
+ return :done unless condition.nil? || condition.true?(engine, ctx)
45
+
46
+ locals["left"] = amount_for(engine, ctx)
47
+ locals["fixed"] = true if Abilities::FirstFlipsWin.applies?(engine, ctx.controller) # CR 705.3
48
+ end
49
+ calls = @count == "wins" && !locals["fixed"] # a flip with a stated result needs no call (CR 705.3)
50
+ while locals["left"].positive?
51
+ call = calls ? answers(locals).shift&.fetch("call") : nil
52
+ return ask(engine, locals, ctx.controller, "Call the coin flip", CALLS) if calls && call.nil?
53
+
54
+ flip!(engine, ctx, call, fixed: locals["fixed"])
55
+ locals["left"] -= 1
56
+ end
57
+ :done
58
+ end
59
+
60
+ private
61
+
62
+ # CR 705.3: a flip with a stated result (heads, and won) ignores the actual result.
63
+ def flip!(engine, ctx, call, fixed: false)
64
+ heads = fixed || engine.state.rng.rand(2).zero?
65
+ won = fixed ? @count == "wins" || nil : call && (call == "heads") == heads
66
+ call = "heads" if fixed && @count == "wins"
67
+ engine.emit Events::CoinFlipped.new(player: engine.name(ctx.controller), heads: heads, won: won)
68
+ engine.record_amount!(1) if call ? won : heads
69
+ end
70
+ end
71
+ end
72
+
73
+ module Abilities
74
+ # "The first time you flip one or more coins each turn, those coins come up heads and you win those flips"
75
+ # (CR 705.3): the flips of the first flip_coins effect its controller performs each turn have that result
76
+ # (no call is asked). A flip is remembered in the turn's history (Engine::TurnHistory "coin_flips").
77
+ class FirstFlipsWin < TurnRule
78
+ primitive :ability, :first_flips_win,
79
+ description: "The first time each turn its controller flips one or more coins, those coins come up " \
80
+ "heads and they win those flips.",
81
+ rules: %w[705.3 604.1], example: "first_flips_win"
82
+
83
+ INSTANCE = new.freeze
84
+
85
+ def kind = :first_flips_win
86
+ def to_s = "The first time you flip one or more coins each turn, those coins come up heads and you win those flips"
87
+
88
+ def self.applies?(engine, index)
89
+ engine.turn_log(index).fetch("coin_flips", 0).zero? &&
90
+ engine.state.battlefield.any? do |id|
91
+ engine.object(id).controller == index && engine.chars(id).abilities.any? { it.ability.kind == :first_flips_win }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # "Gain control of target permanent" / "... until end of turn" / "Target opponent gains control of
7
+ # target permanent you control" (CR 613.1b, 108.4, 506.4, 302.6).
8
+ class GainControl < Effect
9
+ DURATIONS = %w[permanent end_of_turn while_source_on_battlefield while_source_tapped while_you_control_source].freeze
10
+
11
+ primitive :effect, :gain_control,
12
+ description: "The player referenced by `gains` (default you) gains control of each referenced " \
13
+ "permanent: indefinitely (`duration: \"permanent\"`, default), until end of turn, or for " \
14
+ "as long as this effect's source remains on the battlefield / tapped / under your control (it does " \
15
+ "nothing if that is already over). " \
16
+ "A permanent that changes control leaves combat and is newly under its controller's " \
17
+ "control (it can't attack or {T} without haste).",
18
+ rules: %w[613.1b 108.4 506.4 302.6 514.2 611.2b],
19
+ params: { "to" => "selector (permanents)", "gains" => "selector (a player; default \"you\")",
20
+ "duration" => DURATIONS.join(" | ") },
21
+ example: { "gain_control" => { "to" => { "target" => 0 }, "duration" => "end_of_turn" } }
22
+
23
+ attr_reader :gains, :duration
24
+
25
+ def self.keys = %w[to gains duration if]
26
+
27
+ def setup(params, errors, path)
28
+ @gains = Selector.build(Mechanics.fetch_param(params, :gains) || "you", errors, "#{path}.gains",
29
+ targets_count: @targets_count, results_count: @results_count)
30
+ @duration = Mechanics.fetch_param(params, :duration) || "permanent"
31
+ errors << "#{path}.duration: must be one of #{DURATIONS.join(", ")}" unless DURATIONS.include?(@duration)
32
+ end
33
+
34
+ def apply(engine, ctx)
35
+ player = gains.resolve(engine, ctx).filter_map { Filters.player_index(engine, it) }.first or return
36
+ return unless engine.source_duration_holds?(duration, ctx.source, ctx.controller)
37
+
38
+ objects(engine, ctx).each do |id|
39
+ obj = engine.object(id)
40
+ next engine.gain_control!(obj, player) if duration == "permanent"
41
+
42
+ engine.gain_control_for!(obj, player, ctx, duration: duration,
43
+ source: duration == "end_of_turn" ? nil : ctx.source)
44
+ end
45
+ end
46
+
47
+ def to_s = "#{gains} gains control#{" until end of turn" if duration == "end_of_turn"} → #{to}"
48
+ end
49
+
50
+ # "Exchange control of target artifact you control and target artifact an opponent controls" (CR 701.12a-b):
51
+ # if both permanents are still there and controlled by different players, each player gains control of
52
+ # the other one (indefinitely); otherwise nothing happens.
53
+ class ExchangeControl < Effect
54
+ primitive :effect, :exchange_control,
55
+ description: "The permanent `to` references and the one `with` references exchange controllers. " \
56
+ "If either is gone or both have the same controller, nothing happens.",
57
+ rules: %w[701.12a 701.12b 613.1b],
58
+ params: { "to" => "selector (a permanent)", "with" => "selector (a permanent)" },
59
+ example: { "exchange_control" => { "to" => { "target" => 0 }, "with" => { "target" => 1 } } }
60
+
61
+ attr_reader :with
62
+
63
+ def self.keys = %w[to with if]
64
+
65
+ def setup(params, errors, path)
66
+ @with = Exchange.with(params, errors, path, @targets_count, @results_count)
67
+ end
68
+
69
+ def apply(engine, ctx)
70
+ first, second = [to, with].map { |sel| sel.resolve(engine, ctx).find { engine.state.battlefield.include?(it) } }
71
+ return if first.nil? || second.nil? || first == second
72
+
73
+ a = engine.object(first)
74
+ b = engine.object(second)
75
+ return if a.controller == b.controller
76
+
77
+ timestamp = engine.next_timestamp!
78
+ [[a, b.controller], [b, a.controller]].each { |obj, controller| engine.gain_control!(obj, controller, timestamp) }
79
+ end
80
+
81
+ def to_s = "exchange control of #{to} and #{with}"
82
+ end
83
+
84
+ # The `with` side of an exchange (CR 701.12a): a required selector.
85
+ module Exchange
86
+ def self.with(params, errors, path, targets_count, results_count)
87
+ raw = Mechanics.fetch_param(params, :with)
88
+ errors << "#{path}.with: required" if raw.nil?
89
+ Selector.build(raw || "you", errors, "#{path}.with", targets_count: targets_count, results_count: results_count)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ module Mechanics
5
+ module Effects
6
+ # Base for effects that may end the game: once it's over, nothing else in the list happens.
7
+ class GameEndEffect < Effect
8
+ def self.keys = %w[to if]
9
+ def self.default_to = "you"
10
+
11
+ def run(engine, ctx, locals)
12
+ super
13
+ engine.state.over? ? :wait : :done # the game ended: resolution stops here
14
+ end
15
+ end
16
+
17
+ # "You win the game." (CR 104.2b)
18
+ class WinGame < GameEndEffect
19
+ primitive :effect, :win_game,
20
+ description: "Each referenced player (default you) wins the game: every opponent loses it.",
21
+ rules: %w[104.2a 104.2b 104.3f],
22
+ params: { "to" => "selector" },
23
+ example: "win_game"
24
+
25
+ def apply(engine, ctx) = players(engine, ctx).each { engine.win_game!(it) }
26
+ def to_s = "win the game → #{to}"
27
+ end
28
+
29
+ # "You lose the game." (CR 104.3e)
30
+ class LoseGame < GameEndEffect
31
+ primitive :effect, :lose_game,
32
+ description: "Each referenced player (default you) loses the game.",
33
+ rules: %w[104.3e 104.4a],
34
+ params: { "to" => "selector" },
35
+ example: "lose_game"
36
+
37
+ def apply(engine, ctx) = players(engine, ctx).each { engine.lose_game!(it) }
38
+ def to_s = "lose the game → #{to}"
39
+ end
40
+ end
41
+ end
42
+ end